Compare commits

..

35 Commits

Author SHA1 Message Date
Ersei Saggi 42fba8656a Make CI happy 2024-05-13 09:32:52 -04:00
Ersei Saggi 618bcdbd8a Update GH workflows to use net8.0 2024-05-13 09:31:05 -04:00
Ersei Saggi 2e8c19d631 Update for Jellyfin 10.9 2024-05-13 09:17:42 -04:00
9p4 19a6b49afa Merge pull request #190 from tbelway/main
Update providers.md to include android app keycloak OIDC redirect URI
2024-05-13 08:31:54 -04:00
tbelway 5b26808f0e Update providers.md to include android app keycloak redirect URI 2024-05-12 20:24:20 -04:00
9p4 2724d64de8 Merge pull request #170 from hendrik1120/main
update authelia example to authelia v4.38
2024-03-14 16:40:11 -04:00
Hendrik Sievers 63ec46db4a keep authelia example configuration prior to v4.38 2024-03-14 21:29:49 +01:00
Hendrik Sievers 4c891d8ef0 update authelia example to authelia v4.38
fix warnings in authelia
fix authelia using client_secret_basic instead of client_secret_post
2024-03-14 20:50:39 +01:00
Jade Lovelace 29189a9af0 Update version in SSO-Auth.csproj 2024-02-22 20:23:49 -05:00
Ersei Saggi 3894048168 Prepare for v3.5.2.3 2024-02-21 17:09:51 -05:00
Ersei Saggi d51e5069d6 feat: allow for better endpoint validation 2024-02-20 09:11:48 -05:00
Sergii Bogomolov 5149cef625 Remove jellyfin_credentials before loading iframe
Due to Safari's aggressive caching updated jellyfin_credentials were not
used. They were overwritten with the old credentials instead. This fix
removes jellyfin_credentials first, then let's iframe load and create
new jellyfin_credentials with no access token. After this we update it
and login succeeds.
2024-02-10 13:09:05 -05:00
Ersei Saggi 9be3e8e9c4 Update flake.lock 2024-02-07 10:26:00 -05:00
Ersei Saggi db203808f2 Prettier README linting 2024-02-04 17:40:43 -05:00
Ersei Saggi ec82bf7fbc Update README to use form-based login button 2024-02-04 17:38:11 -05:00
Ersei Saggi 87edc5f90a Prepare for 3.5.2.2 2024-01-22 16:38:06 -05:00
Ersei Saggi 304170e214 Update flake.lock 2024-01-11 09:57:16 -05:00
Ersei Saggi 9321c25f20 feat: allow linking to work with new paths 2024-01-11 09:56:45 -05:00
Ersei Saggi ebf7eef83e docs: mention quick connect in README 2023-12-09 16:15:43 -05:00
Ersei Saggi 065ea8b5cd docs: mention to have both paths in redirects 2023-11-24 17:59:46 -05:00
Ersei Saggi 0476f817a6 v.3.5.2.1 2023-10-18 01:11:26 -04:00
Ersei Saggi 3bb241b6d2 Clarify docs 2023-10-18 01:11:10 -04:00
Ersei Saggi cb26ba9852 Add yet another null check in SAML 2023-10-18 01:09:33 -04:00
Ersei Saggi 1b6761bf73 fix: add another null check in SAML for the default provider 2023-09-01 15:59:13 -04:00
Ersei Saggi eeabc3cc46 Merge branch 'main' of github.com:9p4/jellyfin-plugin-sso 2023-08-29 13:06:15 -04:00
Ersei Saggi e65c5add95 fix: a few more null checks 2023-08-29 13:06:02 -04:00
Nathan Easton fce636bddd Added a section in the authentik provider (#137)
Add note about error processing request from issue #136
2023-08-22 13:29:50 -04:00
Ersei Saggi 94d1c7a95a Prepare for 3.5.2.0 2023-08-07 13:15:04 -04:00
Ersei Saggi a1a355591c docs: add Nix flakes blurb 2023-08-07 12:26:51 -04:00
Ersei Saggi 71802d954c docs: fix SAML paths in docs 2023-08-07 12:24:04 -04:00
Ersei Saggi f37e8dd3bb fix: handle empty and invalid strings in the schemeOverride 2023-08-07 12:14:52 -04:00
Ersei Saggi 9b667b3175 feat: allow overriding redirect scheme 2023-08-07 12:09:51 -04:00
Ersei Saggi 2c3fc4d8e1 Prepare for 3.5.1.1 2023-08-07 08:42:10 -04:00
Ersei Saggi f897704100 fix: change iframe URL to point to the web UI instead of the root 2023-08-07 08:23:34 -04:00
Ersei Saggi d0b9b10a36 fix: remove the commit lint check (it's really useless) 2023-08-06 16:36:24 -04:00
18 changed files with 234 additions and 132 deletions
+3 -3
View File
@@ -3,12 +3,12 @@ on:
inputs:
dotnet-version:
required: false
default: "6.0.x"
default: "8.0.x"
description: "The .NET version to setup for the build"
type: string
dotnet-target:
required: false
default: "net6.0"
default: "net8.0"
description: "The .NET target to set for JPRM"
type: string
@@ -36,4 +36,4 @@ jobs:
name: build-artifact
retention-days: 30
if-no-files-found: error
path: ${{ steps.jprm.outputs.artifact }}
path: ${{ steps.jprm.outputs.artifact }}
-11
View File
@@ -1,11 +0,0 @@
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
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build Dotnet
@@ -35,7 +35,7 @@ jobs:
version: "0.0.0.9000"
verbosity: debug
path: .
dotnet-target: "net6.0"
dotnet-target: "net8.0"
output: _dist
- name: Prepare GitHub Release assets
run: |-
+2 -2
View File
@@ -10,8 +10,8 @@ jobs:
build:
uses: ./.github/workflows/build.yml
with:
dotnet-version: "6.0.*"
dotnet-target: "net6.0"
dotnet-version: "8.0.*"
dotnet-target: "net8.0"
upload:
runs-on: ubuntu-latest
needs:
+17 -9
View File
@@ -34,7 +34,7 @@ This is 100% alpha software! PRs are welcome to improve the code.
~~There is NO admin configuration! You must use the API to configure the program!~~ Added by [strazto](https://github.com/strazto) in PR [#18](https://github.com/9p4/jellyfin-plugin-sso/pull/18) and [#27](https://github.com/9p4/jellyfin-plugin-sso/pull/27).
**[This is for Jellyfin 10.8](https://github.com/9p4/jellyfin-plugin-sso/issues/3) and only on the Web UI!**
**[This is for Jellyfin 10.8](https://github.com/9p4/jellyfin-plugin-sso/issues/3) and only on the Web UI and clients supporting [Quick Connect](https://jellyfin.org/docs/general/server/quick-connect)**
**This README reflects the branch it is currently on! Switch tags to view version-specific documentation!**
@@ -91,16 +91,18 @@ The nightly build may have new features unavailable in other builds, but **be wa
## Examples
**Note that you should add both "/r/" and "/redirect/" paths to your SSO provider's configuration!**
### Creating A Login Button On The Main Page
In the Jellyfin administration UI, under "General", there is a "Branding" section. In that section, add the following code in the "Login disclaimer" block (replacing `PROVIDER_NAME` and the domain):
```html
<a
href="https://jellyfin.example.com/sso/OID/start/PROVIDER_NAME"
class="raised cancel block emby-button"
>Sign in with SSO</a
>
<form action="https://jellyfin.example.com/sso/OID/start/PROVIDER_NAME">
<button class="raised block emby-button button-submit">
Sign in with SSO
</button>
</form>
```
Then, add the following code in the "Custom CSS code" section:
@@ -133,7 +135,7 @@ The SAML provider must have the following configuration (I am using Keycloak, an
- Sign Documents on
- Sign Assertions off
- Client Signature Required off
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/post/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
@@ -180,7 +182,7 @@ These all require authorization. Append an API key to the end of the request: `c
- `samlClientId`: string. The SAML client ID.
- `samlCertificate`: string. The base64 encoded SAML certificate.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. If disabled, then the permissions of users will not be modified and the Jellyfin defaults will be used instead.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the SAML response against the `Role` attribute. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking.
@@ -193,6 +195,7 @@ These all require authorization. Append an API key to the end of the request: `c
- `enableLiveTv`: boolean. Whether to allow Live TV by default. This applies even if `enableLiveTvRoles` is enabled.
- `enableLiveTvManagement`: boolean. Whether to allow Live TV management by default. This applies even if `enableLiveTvRoles` is enabled.
- `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.
- `schemeOverride`: string. Sets the scheme for URLs used. Can be useful if the plugin refuses to use HTTPS URLs.
- GET `SAML/Del/PROVIDER_NAME`: This removes a configuration for SAML for a given provider name.
- GET `SAML/Get`: Lists the configurations currently available.
@@ -218,7 +221,7 @@ These all require authorization. Append an API key to the end of the request: `c
- `oidClientId`: string. The OpenID client ID.
- `oidSecret`: string. The OpenID secret.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. If disabled, then the permissions of users will not be modified and the Jellyfin defaults will be used instead.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
@@ -239,6 +242,7 @@ These all require authorization. Append an API key to the end of the request: `c
- `disableHttps`: boolean. Determines whether the OpenID discovery endpoint requires HTTPS.
- `doNotValidateEndpoints`: boolean. Determines whether the OpenID discovery process will validate endpoints. This may be required for Google.
- `doNotValidateIssuerName`: boolean. Determines whether the OpenID discovery process will validate the OpenID issuer name.
- `schemeOverride`: string. Sets the scheme for URLs used. Can be useful if the plugin refuses to use HTTPS URLs.
- 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.
@@ -263,6 +267,10 @@ There is also no logout callback. Logging out of Jellyfin will log you out of Je
# Contributing
## Dependencies
This project uses Nix flakes to manage development dependencies. Run `nix develop` to use the same toolchain versions.
## Building
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll`, the `IdentityModel.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
+58 -27
View File
@@ -10,6 +10,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.SSO_Auth.Config;
using Jellyfin.Plugin.SSO_Auth.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -82,14 +83,17 @@ public class SSOController : ControllerBase
if (config.Enabled)
{
var scopes = config.OidScopes == null ? new string[2] : config.OidScopes;
var options = new OidcClientOptions
{
Authority = config.OidEndpoint?.Trim(),
ClientId = config.OidClientId?.Trim(),
ClientSecret = config.OidSecret?.Trim(),
RedirectUri = GetRequestBase() + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
RedirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
Scope = string.Join(" ", scopes.Prepend("openid profile")),
};
var oidEndpointUri = new Uri(config.OidEndpoint?.Trim());
options.Policy.Discovery.AdditionalEndpointBaseAddresses.Add(oidEndpointUri.GetLeftPart(UriPartial.Authority));
options.Policy.Discovery.ValidateEndpoints = !config.DoNotValidateEndpoints; // For Google and other providers with different endpoints
options.Policy.Discovery.RequireHttps = !config.DisableHttps;
options.Policy.Discovery.ValidateIssuerName = !config.DoNotValidateIssuerName;
@@ -101,7 +105,7 @@ public class SSOController : ControllerBase
return ReturnError(StatusCodes.Status400BadRequest, result.Error + " Try logging in again.");
}
if (!config.EnableFolderRoles)
if (!config.EnableFolderRoles && config.EnabledFolders != null)
{
StateManager[state].Folders = new List<string>(config.EnabledFolders);
}
@@ -246,7 +250,7 @@ public class SSOController : ControllerBase
if (StateManager[state].Valid)
{
_logger.LogInformation($"Is request linking: {isLinking}");
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(config.SchemeOverride), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
}
else
{
@@ -287,15 +291,26 @@ public class SSOController : ControllerBase
if (config.Enabled)
{
bool newPath = config.NewPath;
if (!isLinking)
{
newPath = Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase);
config.NewPath = newPath;
}
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(newPath ? "redirect" : "r")}/" + provider;
var options = new OidcClientOptions
{
Authority = config.OidEndpoint?.Trim(),
ClientId = config.OidClientId?.Trim(),
ClientSecret = config.OidSecret?.Trim(),
RedirectUri = GetRequestBase() + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
RedirectUri = redirectUri,
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
var oidEndpointUri = new Uri(config.OidEndpoint?.Trim());
options.Policy.Discovery.AdditionalEndpointBaseAddresses.Add(oidEndpointUri.GetLeftPart(UriPartial.Authority));
options.Policy.Discovery.ValidateEndpoints = !config.DoNotValidateEndpoints; // For Google and other providers with different endpoints
var oidcClient = new OidcClient(options);
var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
@@ -313,7 +328,7 @@ public class SSOController : ControllerBase
/// </summary>
/// <param name="provider">The name of the provider to add.</param>
/// <param name="config">The OID configuration (deserialized from a JSON post).</param>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpPost("OID/Add/{provider}")]
public void OidAdd(string provider, [FromBody] OidConfig config)
{
@@ -326,7 +341,7 @@ public class SSOController : ControllerBase
/// Deletes an OpenID provider.
/// </summary>
/// <param name="provider">Name of provider to delete.</param>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("OID/Del/{provider}")]
public void OidDel(string provider)
{
@@ -339,7 +354,7 @@ public class SSOController : ControllerBase
/// Lists the OpenID providers configured. Requires administrator privileges.
/// </summary>
/// <returns>The list of OpenID configurations.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("OID/Get")]
public ActionResult OidProviders()
{
@@ -370,7 +385,7 @@ public class SSOController : ControllerBase
/// This is a debug endpoint to list all running OpenID flows. Requires administrator privileges.
/// </summary>
/// <returns>The list of OpenID flows in progress.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("OID/States")]
public ActionResult OidStates()
{
@@ -474,7 +489,7 @@ public class SSOController : ControllerBase
WebResponse.Generator(
data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)),
provider: provider,
baseUrl: GetRequestBase(),
baseUrl: GetRequestBase(config.SchemeOverride),
mode: "SAML",
isLinking: isLinking),
MediaTypeNames.Text.Html);
@@ -513,6 +528,14 @@ public class SSOController : ControllerBase
if (config.Enabled)
{
bool newPath = config.NewPath;
if (!isLinking)
{
newPath = Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase);
config.NewPath = newPath;
}
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/SAML/{(newPath ? "post" : "p")}/" + provider;
string relayState = null;
if (isLinking)
{
@@ -521,7 +544,7 @@ public class SSOController : ControllerBase
var request = new AuthRequest(
config.SamlClientId.Trim(),
GetRequestBase() + $"/sso/SAML/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "post" : "p")}/" + provider);
redirectUri);
return Redirect(request.GetRedirectUrl(config.SamlEndpoint.Trim(), relayState));
}
@@ -535,7 +558,7 @@ public class SSOController : ControllerBase
/// <param name="provider">The provider name to add.</param>
/// <param name="newConfig">The SAML configuration object (deserialized) from JSON.</param>
/// <returns>The success result.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpPost("SAML/Add/{provider}")]
public OkResult SamlAdd(string provider, [FromBody] SamlConfig newConfig)
{
@@ -550,7 +573,7 @@ public class SSOController : ControllerBase
/// </summary>
/// <param name="provider">The ID of the provider to delete.</param>
/// <returns>The success result.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("SAML/Del/{provider}")]
public OkResult SamlDel(string provider)
{
@@ -564,7 +587,7 @@ public class SSOController : ControllerBase
/// Returns a list of all SAML providers configured. Requires administrator privileges.
/// </summary>
/// <returns>A list of all of the Saml providers available.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("SAML/Get")]
public ActionResult SamlProviders()
{
@@ -599,7 +622,7 @@ public class SSOController : ControllerBase
bool liveTvManagement = config.EnableLiveTvManagement;
var samlResponse = new Response(config.SamlCertificate, response.Data);
List<string> folders;
if (!config.EnableFolderRoles)
if (!config.EnableFolderRoles && config.EnabledFolders != null)
{
folders = new List<string>(config.EnabledFolders);
}
@@ -623,11 +646,14 @@ public class SSOController : ControllerBase
if (config.EnableFolderRoles)
{
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
if (config.FolderRoleMapping != null)
{
if (folderRoleMap.Role.Equals(role))
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
folders.AddRange(folderRoleMap.Folders);
if (folderRoleMap.Role.Equals(role))
{
folders.AddRange(folderRoleMap.Folders);
}
}
}
}
@@ -660,7 +686,7 @@ public class SSOController : ControllerBase
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("saml", provider, samlResponse.GetNameID());
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), liveTv, liveTvManagement, response, config.DefaultProvider.Trim())
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), liveTv, liveTvManagement, response, config.DefaultProvider?.Trim())
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -674,7 +700,7 @@ public class SSOController : ControllerBase
/// <param name="username">The username to switch to the new provider.</param>
/// <param name="provider">The new provider to switch to.</param>
/// <returns>Whether this API endpoint succeeded.</returns>
[Authorize(Policy = "RequiresElevation")]
[Authorize(Policy = Policies.RequiresElevation)]
[HttpPost("Unregister/{username}")]
public ActionResult Unregister(string username, [FromBody] string provider)
{
@@ -766,7 +792,7 @@ public class SSOController : ControllerBase
/// <param name="jellyfinUserId">The user ID within jellyfin to link to the provider.</param>
/// <param name="authResponse">The client information to authenticate the user with.</param>
/// <returns>Whether this API endpoint succeeded.</returns>
[Authorize(Policy = "DefaultAuthorization")]
[Authorize]
[HttpPost("{mode}/Link/{provider}/{jellyfinUserId}")]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
@@ -796,7 +822,7 @@ public class SSOController : ControllerBase
/// <param name="jellyfinUserId">The user ID within jellyfin to unlink from the provider.</param>
/// <param name="canonicalName">The user ID within jellyfin to unlink.</param>
/// <returns>Whether this API endpoint succeeded.</returns>
[Authorize(Policy = "DefaultAuthorization")]
[Authorize]
[HttpDelete("{mode}/Link/{provider}/{jellyfinUserId}/{canonicalName}")]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
@@ -826,7 +852,7 @@ public class SSOController : ControllerBase
/// </summary>
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
/// <returns>A dictionary of provider : link mappings.</returns>
[Authorize(Policy = "DefaultAuthorization")]
[Authorize]
[HttpGet("saml/links/{jellyfinUserId}")]
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetSamlLinksByUser(Guid jellyfinUserId)
@@ -854,7 +880,7 @@ public class SSOController : ControllerBase
/// </summary>
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
/// <returns>A dictionary of provider : link mappings.</returns>
[Authorize(Policy = "DefaultAuthorization")]
[Authorize]
[HttpGet("oid/links/{jellyfinUserId}")]
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetOidLinksByUser(Guid jellyfinUserId)
@@ -1042,7 +1068,7 @@ public class SSOController : ControllerBase
}
}
private string GetRequestBase()
private string GetRequestBase(string schemeOverride = null)
{
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)))
@@ -1050,9 +1076,14 @@ public class SSOController : ControllerBase
requestPort = -1;
}
if (schemeOverride != "http" && schemeOverride != "https")
{
schemeOverride = null;
}
return new UriBuilder
{
Scheme = Request.Scheme,
Scheme = schemeOverride ?? Request.Scheme,
Host = Request.Host.Host,
Port = requestPort,
Path = Request.PathBase
+20
View File
@@ -126,6 +126,16 @@ public class SamlConfig
/// </summary>
public string DefaultProvider { get; set; }
/// <summary>
/// Gets or sets the redirect scheme override.
/// </summary>
public string SchemeOverride { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
/// </summary>
public bool NewPath { get; set; }
/// <summary>
/// Gets or sets a mapping of canonical names from the provider to jellyfin user ids.
/// </summary>
@@ -250,6 +260,16 @@ public class OidConfig
/// </summary>
public string DefaultProvider { get; set; }
/// <summary>
/// Gets or sets the redirect scheme override.
/// </summary>
public string SchemeOverride { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
/// </summary>
public bool NewPath { get; set; }
/// <summary>
/// Gets or sets a mapping of canonical names from the provider to jellyfin user ids.
/// </summary>
+16
View File
@@ -603,6 +603,22 @@
</label>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
>Scheme Override</label
>
<input
is="emby-input"
id="SchemeOverride"
type="text"
class="sso-text"
/>
<div class="fieldDescription">
If the plugin is redirecting to an insecure URL, set this to
"https"
</div>
</div>
<button
id="SaveProvider"
is="emby-button"
+7 -7
View File
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
<AssemblyVersion>3.3.0.0</AssemblyVersion>
<FileVersion>3.3.0.0</FileVersion>
<AssemblyVersion>3.5.2.4</AssemblyVersion>
<FileVersion>3.5.2.4</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
@@ -29,16 +29,16 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
@@ -83,4 +83,4 @@ public class SSOViewsController : ControllerBase
{
return ServeView(viewName);
}
}
}
+4 -1
View File
@@ -453,6 +453,9 @@ async function link(request) {
}
async function main() {
localStorage.removeItem('jellyfin_credentials');
document.getElementById('iframe-main').src = '" + baseUrl + @"/web/index.html';
var data = '" + data + @"';
while (localStorage.getItem(""_deviceId2"") == null ||
localStorage.getItem(""jellyfin_credentials"") == null ||
@@ -501,6 +504,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
// https://stackoverflow.com/a/25435165
</script><iframe class='docs-texteventtarget-iframe' sandbox='allow-same-origin allow-forms allow-scripts' src='" + baseUrl + "' style='position: absolute;width:0;height:0;border:0;'></iframe></body></html>";
</script><iframe id='iframe-main' class='docs-texteventtarget-iframe' sandbox='allow-same-origin allow-forms allow-scripts' src='' style='position: absolute;width:0;height:0;border:0;'></iframe></body></html>";
}
}
+9 -3
View File
@@ -1,9 +1,9 @@
name: "SSO Authentication"
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
version: "3.5.1.0"
targetAbi: "10.8.0.0"
framework: "net6.0"
version: "3.5.2.4"
targetAbi: "10.9.0.0"
framework: "net8.0"
owner: "9p4"
overview: "Authenticate users against an SSO provider."
description: |
@@ -15,6 +15,12 @@ artifacts:
- "IdentityModel.OidcClient.dll"
- "IdentityModel.dll"
changelog: |
3.5.2.4: Updates for Jellyfin 10.9
3.5.2.3: Improve OpenID discovery policy security rules, fix iOS login bugs related to cache
3.5.2.2: Fix linking page when using new paths
3.5.2.1: Hotfix for SAML null checks
3.5.2.0: Allow overriding the scheme used for generating URLs.
3.5.1.1: Change iframe URL to point to the web UI instead of the root
3.5.1.0: Improved paths! No more obscure "p" versus "r" URLs! Improve final redirect for automatic authentication. Add more configuration options for OpenID discovery.
3.5.0.0: Add support for Live TV authentication. Fix various null pointer bugs.
3.4.0.0: Add user self-service for linking existing accounts + managing existing links. Allow IDP accounts to be linked to jellyfin accounts with a different display-name.
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1691218994,
"narHash": "sha256-46GJ5vLf9H+Oh7Jii2gJI9GATJHGbx2iQpon5nUSFPI=",
"lastModified": 1715499532,
"narHash": "sha256-9UJLb8rdi2VokYcfOBQHUzP3iNxOPNWcbK++ENElpk0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0d2fb29f5071a12d7983319c2c2576be6a130582",
"rev": "af8b9db5c00f1a8e4b83578acc578ff7d823b786",
"type": "github"
},
"original": {
+1 -1
View File
@@ -5,6 +5,6 @@
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShell.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.prettier pkgs.dotnet-sdk ]; };
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.prettier pkgs.dotnet-sdk_8 ]; };
};
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 107 KiB

+68 -61
View File
@@ -1,68 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<!-- disable warning SA1404: Code analysis suppression should have justification -->
<Rule Id="SA1404" Action="Info" />
<RuleSet Name="Rules for Jellyfin.Server"
Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<!-- disable warning SA1404: Code analysis suppression should have justification -->
<Rule Id="SA1404" Action="Info" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1402: File may only contain a single type -->
<Rule Id="SA1402" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the
file -->
<Rule Id="SA1633" Action="None" />
<!-- disable warning SA1649: File name should match first type name -->
<Rule Id="SA1649" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as
static -->
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
<Rule Id="CA1055" Action="None" />
<!-- disable warning CA1056: URI properties should not be strings -->
<Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
</Rules>
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
<Rule Id="CA1055" Action="None" />
<!-- disable warning CA1056: URI properties should not be strings -->
<Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
</Rules>
</RuleSet>
+22
View File
@@ -42,6 +42,25 @@ Authelia is simple to configure, and RBAC is straightforward.
Below is the `identity_providers` section of an Authelia config:
### Authelia v4.38 and above
```yaml
identity_providers:
oidc:
# hmac secret and private key given by env variables
clients:
- client_id: jellyfin
client_name: My media server
# Client secret should be randomly generated
client_secret: <redacted>
token_endpoint_auth_method: client_secret_post
authorization_policy: one_factor
redirect_uris:
- https://jellyfin.example.com/sso/OID/redirect/authelia
```
### Authelia v4.37 and below
```yaml
identity_providers:
oidc:
@@ -134,6 +153,8 @@ authentik:
OidScopes: ["groups"]
```
If you recieve the error `Error processing request.` from Jellyfin when attempting to login and the Jellyfin logs show `Error loading discovery document: Endpoint belongs to different authority` try setting `Do not validate endpoints` in the plugin settings.
## Keycloak OIDC
Keycloak in general is a little more complicated than other providers. Ensure that you have a realm created and have some usable users.
@@ -147,6 +168,7 @@ Ensure that the following configuration options are set:
- Access Type: Confidential
- Standard Flow Enabled
- Redirect URI: https://myjellyfin.example.com/sso/OID/redirect/PROVIDER_NAME
- Redirect URI (for Android app): org.jellyfin.mobile://login-callback
- Base URL: https://myjellyfin.example.com
Press the "Save" button at the bottom of the page and open the "Credentials" tab. Note down the secret.