forked from mirrors/jellyfin-plugin-sso
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7331e2ffe3 | |||
| cef4fe5784 | |||
| 0d2952423d | |||
| 98ddb9e352 | |||
| 46eb00bf43 | |||
| 5723cd718d | |||
| 96b14f7a5a | |||
| 320551bc18 | |||
| 9f8626bdf7 | |||
| d5925fc1de | |||
| facf45058f | |||
| f3d0497801 | |||
| 672fce8189 | |||
| c2f50e1e4c | |||
| a90e0359e4 | |||
| 332f62d76e | |||
| 47db5cb504 | |||
| fe1fdad0b0 | |||
| d06f680407 | |||
| 976a816d8c | |||
| f7ce40fe5e | |||
| 8666818dfa |
@@ -0,0 +1,11 @@
|
|||||||
|
name: Lint Commit Messages
|
||||||
|
on: [pull_request, push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commitlint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: wagoid/commitlint-github-action@4caf21aed4a778f940d0b17eb109942ef167bb27
|
||||||
@@ -28,11 +28,16 @@ jobs:
|
|||||||
sha256sum ${file#./} >> ${file%.*}.sha256
|
sha256sum ${file#./} >> ${file%.*}.sha256
|
||||||
done
|
done
|
||||||
ls -l
|
ls -l
|
||||||
- name: Upload GitHub Release assets
|
- name: Publish output artifacts
|
||||||
uses: shogo82148/actions-upload-release-asset@v1.5.0
|
id: publish-assets
|
||||||
|
uses: softprops/action-gh-release@50195ba7f6f93d1ac97ba8332a178e008ad176aa
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
prerelease: false
|
||||||
asset_path: ./*
|
fail_on_unmatched_files: true
|
||||||
|
tag_name: ${{ github.event.release.tag_name }}
|
||||||
|
files: ./*
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
generate:
|
generate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -192,10 +192,12 @@ These all require authorization. Append an API key to the end of the request: `c
|
|||||||
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
|
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
|
||||||
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
|
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
|
||||||
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_access.roles` by default. The first element is the claim type, the subsequent values are to parse the JSON of the claim value. Use a "\\." to denote a literal ".". This expects a list of strings from the OIDC server.
|
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_access.roles` by default. The first element is the claim type, the subsequent values are to parse the JSON of the claim value. Use a "\\." to denote a literal ".". This expects a list of strings from the OIDC server.
|
||||||
- `oidScopes` : array of strings. each containing an additional scope name to include in the OIDC request.
|
- `oidScopes` : array of strings. Each contains an additional scope name to include in the OIDC request.
|
||||||
- For some OIDC providers (For example, [authelia](https://github.com/9p4/jellyfin-plugin-sso/issues/23#issuecomment-1112237616)), additional scopes may be required in order to validate group membership in role claim.
|
- For some OIDC providers (For example, [authelia](https://github.com/9p4/jellyfin-plugin-sso/issues/23#issuecomment-1112237616)), additional scopes may be required in order to validate group membership in role claim.
|
||||||
- Leave empty to only request the default scopes.
|
- Leave empty to only request the default scopes.
|
||||||
- `defaultProvider`: string. The set provider then gets assigned to the user after they have logged in. If it is not set, nothing is changed. With this, a user can login with SSO but is still able to log in via other providers later. See the `Unregister` endpoint.
|
- `defaultProvider`: string. The set provider then gets assigned to the user after they have logged in. If it is not set, nothing is changed. With this, a user can login with SSO but is still able to log in via other providers later. See the `Unregister` endpoint.
|
||||||
|
- `defaultUsernameClaim`: string. The provider will use the claim to create the users' usernames. If not set, it fallbacks to `preferred_username`.
|
||||||
|
- `requireHttps`: boolean. Determines whether the OpenID discovery endpoint requires HTTP. It is advised NOT to set this to false. If not set, it fallbacks to `true`.
|
||||||
- GET `OID/Del/PROVIDER_NAME`: This removes a configuration for OpenID for a given provider name.
|
- GET `OID/Del/PROVIDER_NAME`: This removes a configuration for OpenID for a given provider name.
|
||||||
- GET `OID/Get`: Lists the configurations currently available.
|
- GET `OID/Get`: Lists the configurations currently available.
|
||||||
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel.OidcClient;
|
using IdentityModel.OidcClient;
|
||||||
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Authentication;
|
|||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
|
using MediaBrowser.Model.Cryptography;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -34,6 +36,7 @@ public class SSOController : ControllerBase
|
|||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
private readonly ILogger<SSOController> _logger;
|
private readonly ILogger<SSOController> _logger;
|
||||||
|
private readonly ICryptoProvider _cryptoProvider;
|
||||||
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,11 +46,13 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public SSOController(ILogger<SSOController> logger, ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext)
|
/// <param name="cryptoProvider">Instance of the <see cref="ICryptoProvider"/> interface.</param>
|
||||||
|
public SSOController(ILogger<SSOController> logger, ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, ICryptoProvider cryptoProvider)
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_authContext = authContext;
|
_authContext = authContext;
|
||||||
|
_cryptoProvider = cryptoProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_logger.LogInformation("SSO Controller initialized");
|
_logger.LogInformation("SSO Controller initialized");
|
||||||
}
|
}
|
||||||
@@ -85,6 +90,7 @@ public class SSOController : ControllerBase
|
|||||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||||
};
|
};
|
||||||
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
|
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
|
||||||
|
options.Policy.Discovery.RequireHttps = config.RequireHttps || true;
|
||||||
var oidcClient = new OidcClient(options);
|
var oidcClient = new OidcClient(options);
|
||||||
var currentState = StateManager[state].State;
|
var currentState = StateManager[state].State;
|
||||||
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).Result;
|
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).Result;
|
||||||
@@ -104,7 +110,7 @@ public class SSOController : ControllerBase
|
|||||||
|
|
||||||
foreach (var claim in result.User.Claims)
|
foreach (var claim in result.User.Claims)
|
||||||
{
|
{
|
||||||
if (claim.Type == "preferred_username")
|
if (claim.Type == (config.DefaultUsernameClaim ?? "preferred_username"))
|
||||||
{
|
{
|
||||||
StateManager[state].Username = claim.Value;
|
StateManager[state].Username = claim.Value;
|
||||||
if (config.Roles.Length == 0)
|
if (config.Roles.Length == 0)
|
||||||
@@ -186,7 +192,7 @@ public class SSOController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the provider doesn't support preferred_username, then use sub
|
// If the provider doesn't support the preferred username claim, then use the sub claim
|
||||||
if (!StateManager[state].Valid)
|
if (!StateManager[state].Valid)
|
||||||
{
|
{
|
||||||
foreach (var claim in result.User.Claims)
|
foreach (var claim in result.User.Claims)
|
||||||
@@ -624,6 +630,22 @@ public class SSOController : ControllerBase
|
|||||||
|
|
||||||
private async Task<Guid> CreateCanonicalLinkAndUserIfNotExist(string mode, string provider, string canonicalName)
|
private async Task<Guid> CreateCanonicalLinkAndUserIfNotExist(string mode, string provider, string canonicalName)
|
||||||
{
|
{
|
||||||
|
User user = null;
|
||||||
|
user = _userManager.GetUserByName(canonicalName);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"SSO user {canonicalName} doesn't exist, creating...");
|
||||||
|
user = await _userManager.CreateUserAsync(canonicalName).ConfigureAwait(false);
|
||||||
|
user.AuthenticationProviderId = GetType().FullName;
|
||||||
|
// https://jonathancrozier.com/blog/how-to-generate-a-cryptographically-secure-random-string-in-dot-net-with-c-sharp
|
||||||
|
user.Password = _cryptoProvider.CreatePasswordHash(Convert.ToBase64String(RandomNumberGenerator.GetBytes(64))).ToString();
|
||||||
|
|
||||||
|
// Make sure there aren't any trailing existing links
|
||||||
|
var links = GetCanonicalLinks(mode, provider);
|
||||||
|
links.Remove(canonicalName);
|
||||||
|
UpdateCanonicalLinkConfig(links, mode, provider);
|
||||||
|
}
|
||||||
|
|
||||||
Guid userId = Guid.Empty;
|
Guid userId = Guid.Empty;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -637,18 +659,7 @@ public class SSOController : ControllerBase
|
|||||||
if (userId == Guid.Empty)
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("SSO user link doesn't exist, creating...");
|
_logger.LogInformation("SSO user link doesn't exist, creating...");
|
||||||
User user = null;
|
|
||||||
user = _userManager.GetUserByName(canonicalName);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"SSO user {canonicalName} doesn't exist, creating...");
|
|
||||||
user = await _userManager.CreateUserAsync(canonicalName).ConfigureAwait(false);
|
|
||||||
user.AuthenticationProviderId = GetType().FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
userId = user.Id;
|
userId = user.Id;
|
||||||
|
|
||||||
CreateCanonicalLink(mode, provider, userId, canonicalName);
|
CreateCanonicalLink(mode, provider, userId, canonicalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +959,19 @@ public class SSOController : ControllerBase
|
|||||||
|
|
||||||
private string GetRequestBase()
|
private string GetRequestBase()
|
||||||
{
|
{
|
||||||
return Request.Scheme + "://" + Request.Host + Request.PathBase;
|
int requestPort = Request.Host.Port ?? -1;
|
||||||
|
if ((requestPort == 80 && string.Equals(Request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(Request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
requestPort = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UriBuilder
|
||||||
|
{
|
||||||
|
Scheme = Request.Scheme,
|
||||||
|
Host = Request.Host.Host,
|
||||||
|
Port = requestPort,
|
||||||
|
Path = Request.PathBase
|
||||||
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContentResult ReturnError(int code, string message)
|
private ContentResult ReturnError(int code, string message)
|
||||||
|
|||||||
@@ -217,6 +217,16 @@ public class OidConfig
|
|||||||
}
|
}
|
||||||
set => _canonicalLinks = value;
|
set => _canonicalLinks = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default username claim when creating new accounts.
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultUsernameClaim { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether HTTPS in the discovery endpoint is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireHttps { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -437,6 +437,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label
|
||||||
|
class="inputLabel inputLabelUnfocused"
|
||||||
|
for="DefaultUsernameClaim"
|
||||||
|
>Set default username claim:</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
is="emby-input"
|
||||||
|
id="DefaultUsernameClaim"
|
||||||
|
type="text"
|
||||||
|
class="sso-text"
|
||||||
|
/>
|
||||||
|
<div class="fieldDescription">
|
||||||
|
The default username claim to use from OpenID by default. If
|
||||||
|
it is not set, it defaults to
|
||||||
|
<code>preferred_username</code>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="SaveProvider"
|
id="SaveProvider"
|
||||||
is="emby-button"
|
is="emby-button"
|
||||||
|
|||||||
Reference in New Issue
Block a user