mirror of
https://github.com/9p4/jellyfin-plugin-sso.git
synced 2026-09-19 13:12:19 +00:00
Compare commits
24
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 | ||
|
|
c5e6bf96f9 | ||
|
|
fd59b83a0e |
@@ -0,0 +1,39 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
dotnet-version:
|
||||
required: false
|
||||
default: "6.0.x"
|
||||
description: "The .NET version to setup for the build"
|
||||
type: string
|
||||
dotnet-target:
|
||||
required: false
|
||||
default: "net6.0"
|
||||
description: "The .NET target to set for JPRM"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "${{ inputs.dotnet-version }}"
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
uses: oddstr13/jellyfin-plugin-repository-manager@b9e92867a6aa279d611a5ea80cf61f6358838c39
|
||||
id: jprm
|
||||
with:
|
||||
dotnet-target: "${{ inputs.dotnet-target }}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
|
||||
with:
|
||||
name: build-artifact
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
path: ${{ steps.jprm.outputs.artifact }}
|
||||
@@ -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
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@30fd723cc3bafc4457a4a85be3cdbee2f3e2013b
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
dotnet-version: "6.0.*"
|
||||
dotnet-target: "net6.0"
|
||||
@@ -28,11 +28,16 @@ jobs:
|
||||
sha256sum ${file#./} >> ${file%.*}.sha256
|
||||
done
|
||||
ls -l
|
||||
- name: Upload GitHub Release assets
|
||||
uses: shogo82148/[email protected]
|
||||
- name: Publish output artifacts
|
||||
id: publish-assets
|
||||
uses: softprops/action-gh-release@50195ba7f6f93d1ac97ba8332a178e008ad176aa
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: ./*
|
||||
prerelease: false
|
||||
fail_on_unmatched_files: true
|
||||
tag_name: ${{ github.event.release.tag_name }}
|
||||
files: ./*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
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.
|
||||
- `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.
|
||||
- `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.
|
||||
- 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.
|
||||
- `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/Get`: Lists the configurations currently available.
|
||||
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel.OidcClient;
|
||||
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -34,6 +36,7 @@ public class SSOController : ControllerBase
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly ILogger<SSOController> _logger;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
||||
|
||||
/// <summary>
|
||||
@@ -43,11 +46,13 @@ public class SSOController : ControllerBase
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> 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;
|
||||
_userManager = userManager;
|
||||
_authContext = authContext;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_logger = logger;
|
||||
_logger.LogInformation("SSO Controller initialized");
|
||||
}
|
||||
@@ -85,6 +90,7 @@ public class SSOController : ControllerBase
|
||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||
};
|
||||
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 currentState = StateManager[state].State;
|
||||
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).Result;
|
||||
@@ -104,7 +110,7 @@ public class SSOController : ControllerBase
|
||||
|
||||
foreach (var claim in result.User.Claims)
|
||||
{
|
||||
if (claim.Type == "preferred_username")
|
||||
if (claim.Type == (config.DefaultUsernameClaim ?? "preferred_username"))
|
||||
{
|
||||
StateManager[state].Username = claim.Value;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
try
|
||||
{
|
||||
@@ -637,18 +659,7 @@ public class SSOController : ControllerBase
|
||||
if (userId == Guid.Empty)
|
||||
{
|
||||
_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;
|
||||
|
||||
CreateCanonicalLink(mode, provider, userId, canonicalName);
|
||||
}
|
||||
|
||||
@@ -948,7 +959,19 @@ public class SSOController : ControllerBase
|
||||
|
||||
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)
|
||||
|
||||
@@ -217,6 +217,16 @@ public class OidConfig
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -437,6 +437,25 @@
|
||||
</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
|
||||
id="SaveProvider"
|
||||
is="emby-button"
|
||||
|
||||
Reference in New Issue
Block a user