mirror of
https://github.com/9p4/jellyfin-plugin-sso.git
synced 2026-09-19 13:12:19 +00:00
Compare commits
89
Commits
cache-buster
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1e157bac2 | ||
|
|
83deeb253c | ||
|
|
ca187a7b5e | ||
|
|
05213dc1ee | ||
|
|
f7a0c94452 | ||
|
|
475048a32e | ||
|
|
781e07b2ce | ||
|
|
fed7e764a8 | ||
|
|
f0def6fd8d | ||
|
|
8e128932c4 | ||
|
|
5b3d70d328 | ||
|
|
be26670e1f | ||
|
|
547eabf55f | ||
|
|
48d75325b5 | ||
|
|
cdce0e583d | ||
|
|
0e897f922f | ||
|
|
afbab1073e | ||
|
|
87425aae36 | ||
|
|
9be9a1fed8 | ||
|
|
3b47851131 | ||
|
|
337ea0ba04 | ||
|
|
c37e3e3a71 | ||
|
|
8f86ce5101 | ||
|
|
d2c77db404 | ||
|
|
fc3976dcc8 | ||
|
|
fc58b8e80f | ||
|
|
da21ebffa3 | ||
|
|
0d18ebae03 | ||
|
|
77e2a229f2 | ||
|
|
71bb9a2f14 | ||
|
|
8baa922eea | ||
|
|
0738ad85f7 | ||
|
|
8d049705c4 | ||
|
|
3459baffdf | ||
|
|
de6cd5d5c2 | ||
|
|
299d3436ec | ||
|
|
5808f34064 | ||
|
|
5ed4c8bbcc | ||
|
|
f78a0f3108 | ||
|
|
aa0e361265 | ||
|
|
efc997c39e | ||
|
|
caab66b653 | ||
|
|
79ac628c4c | ||
|
|
fe3ca6337d | ||
|
|
f761dea356 | ||
|
|
55f58ecedd | ||
|
|
556feba864 | ||
|
|
1f08ccd213 | ||
|
|
f1f57723c3 | ||
|
|
216908acd5 | ||
|
|
9365ca2a33 | ||
|
|
ae0ee62f21 | ||
|
|
728a994728 | ||
|
|
9c0597a5bc | ||
|
|
fb9335d908 | ||
|
|
cbbafc976b | ||
|
|
f7bde6f1a0 | ||
|
|
d7351ef596 | ||
|
|
07315a124c | ||
|
|
9e96bd5218 | ||
|
|
6f77735248 | ||
|
|
73b6c17518 | ||
|
|
86dcd4c579 | ||
|
|
553c88873e | ||
|
|
cc86549c2e | ||
|
|
a2064c1f17 | ||
|
|
2cf4230bd6 | ||
|
|
91c57b18f5 | ||
|
|
feb56ebdd6 | ||
|
|
2d6e2fc938 | ||
|
|
ef085e9fa3 | ||
|
|
95d2c36e2c | ||
|
|
b8e56cefab | ||
|
|
e65f358c79 | ||
|
|
cd35ef45ae | ||
|
|
b5068f53a6 | ||
|
|
6f584597f6 | ||
|
|
42fba8656a | ||
|
|
618bcdbd8a | ||
|
|
2e8c19d631 | ||
|
|
19a6b49afa | ||
|
|
5b26808f0e | ||
|
|
2724d64de8 | ||
|
|
63ec46db4a | ||
|
|
4c891d8ef0 | ||
|
|
29189a9af0 | ||
|
|
3894048168 | ||
|
|
d51e5069d6 | ||
|
|
5149cef625 |
@@ -3,12 +3,12 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
dotnet-version:
|
dotnet-version:
|
||||||
required: false
|
required: false
|
||||||
default: "6.0.x"
|
default: "9.0.x"
|
||||||
description: "The .NET version to setup for the build"
|
description: "The .NET version to setup for the build"
|
||||||
type: string
|
type: string
|
||||||
dotnet-target:
|
dotnet-target:
|
||||||
required: false
|
required: false
|
||||||
default: "net6.0"
|
default: "net9.0"
|
||||||
description: "The .NET target to set for JPRM"
|
description: "The .NET target to set for JPRM"
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -25,15 +25,15 @@ jobs:
|
|||||||
dotnet-version: "${{ inputs.dotnet-version }}"
|
dotnet-version: "${{ inputs.dotnet-version }}"
|
||||||
|
|
||||||
- name: Build Jellyfin Plugin
|
- name: Build Jellyfin Plugin
|
||||||
uses: oddstr13/jellyfin-plugin-repository-manager@b9e92867a6aa279d611a5ea80cf61f6358838c39
|
uses: oddstr13/jellyfin-plugin-repository-manager@9497a0a499416cc572ed2e07a391d9f943a37b4d # v1.1.1
|
||||||
id: jprm
|
id: jprm
|
||||||
with:
|
with:
|
||||||
dotnet-target: "${{ inputs.dotnet-target }}"
|
dotnet-target: "${{ inputs.dotnet-target }}"
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2
|
||||||
with:
|
with:
|
||||||
name: build-artifact
|
name: build-artifact
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: ${{ steps.jprm.outputs.artifact }}
|
path: ${{ steps.jprm.outputs.artifact }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 6.0.x
|
dotnet-version: 9.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 6.0.x
|
dotnet-version: 9.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build Dotnet
|
- name: Build Dotnet
|
||||||
@@ -30,12 +30,12 @@ jobs:
|
|||||||
updateFile: true
|
updateFile: true
|
||||||
- name: "JPRM: Build"
|
- name: "JPRM: Build"
|
||||||
id: jrpm
|
id: jrpm
|
||||||
uses: oddstr13/jellyfin-plugin-repository-manager@b9e92867a6aa279d611a5ea80cf61f6358838c39
|
uses: oddstr13/jellyfin-plugin-repository-manager@9497a0a499416cc572ed2e07a391d9f943a37b4d # v1.1.1
|
||||||
with:
|
with:
|
||||||
version: "0.0.0.9000"
|
version: "0.0.0.9000"
|
||||||
verbosity: debug
|
verbosity: debug
|
||||||
path: .
|
path: .
|
||||||
dotnet-target: "net6.0"
|
dotnet-target: "net9.0"
|
||||||
output: _dist
|
output: _dist
|
||||||
- name: Prepare GitHub Release assets
|
- name: Prepare GitHub Release assets
|
||||||
run: |-
|
run: |-
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
with:
|
with:
|
||||||
dotnet-version: "6.0.*"
|
dotnet-version: "9.0.*"
|
||||||
dotnet-target: "net6.0"
|
dotnet-target: "net9.0"
|
||||||
upload:
|
upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v2.1.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build-artifact
|
name: build-artifact
|
||||||
- name: Prepare GitHub Release assets
|
- name: Prepare GitHub Release assets
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
Project archived because I'm tired of working on this after all the years.
|
||||||
|
|
||||||
This plugin allows users to sign in through an SSO provider (such as Google, Microsoft, or your own provider). This enables one-click signin.
|
This plugin allows users to sign in through an SSO provider (such as Google, Microsoft, or your own provider). This enables one-click signin.
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-a909-e5414878a864.mp4
|
https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-a909-e5414878a864.mp4
|
||||||
@@ -34,7 +36,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).
|
~~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 and clients supporting [Quick Connect](https://jellyfin.org/docs/general/server/quick-connect)**
|
**[This is for Jellyfin >=10.8](https://github.com/9p4/jellyfin-plugin-sso/issues/3) and only on the Web UI or 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!**
|
**This README reflects the branch it is currently on! Switch tags to view version-specific documentation!**
|
||||||
|
|
||||||
@@ -46,11 +48,13 @@ This is 100% alpha software! PRs are welcome to improve the code.
|
|||||||
- authentik
|
- authentik
|
||||||
- Keycloak
|
- Keycloak
|
||||||
- OIDC & SAML
|
- OIDC & SAML
|
||||||
|
- Pocket ID
|
||||||
|
- Kanidm
|
||||||
- Google OpenID: Works, but usernames are all numeric
|
- Google OpenID: Works, but usernames are all numeric
|
||||||
|
|
||||||
## Supported Protocols
|
## Supported Protocols
|
||||||
|
|
||||||
- [OpenID](https://openid.net/what-is-openid/)
|
- [OpenID](https://openid.net/developers/how-connect-works/)
|
||||||
- [SAML](https://www.cloudflare.com/learning/access-management/what-is-saml/)
|
- [SAML](https://www.cloudflare.com/learning/access-management/what-is-saml/)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
@@ -91,8 +95,6 @@ The nightly build may have new features unavailable in other builds, but **be wa
|
|||||||
|
|
||||||
## Examples
|
## 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
|
### 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):
|
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):
|
||||||
@@ -239,6 +241,7 @@ These all require authorization. Append an API key to the end of the request: `c
|
|||||||
- 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`.
|
- `defaultUsernameClaim`: string. The provider will use the claim to create the users' usernames. If not set, it fallbacks to `preferred_username`.
|
||||||
|
- `avatarUrlFormat`: string. The URL format for the users avatars. OIDC claims can be used by using the `@{claim_type}` syntax. If not set, the avatars won't change.
|
||||||
- `disableHttps`: boolean. Determines whether the OpenID discovery endpoint requires HTTPS.
|
- `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.
|
- `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.
|
- `doNotValidateIssuerName`: boolean. Determines whether the OpenID discovery process will validate the OpenID issuer name.
|
||||||
@@ -310,7 +313,7 @@ Much thanks to the [Jellyfin LDAP plugin](https://github.com/jellyfin/jellyfin-p
|
|||||||
|
|
||||||
I use the [AspNet SAML](https://github.com/jitbit/AspNetSaml/) library for the SAML side of things (patched to work with Base64 on non-Windows machines).
|
I use the [AspNet SAML](https://github.com/jitbit/AspNetSaml/) library for the SAML side of things (patched to work with Base64 on non-Windows machines).
|
||||||
|
|
||||||
I use the [IdentityModel OIDC Client](https://github.com/IdentityModel/IdentityModel.OidcClient/) library for the OpenID side of things.
|
I use the [Duende IdentityModel OIDC Client](https://github.com/DuendeSoftware/foss) library for the OpenID side of things.
|
||||||
|
|
||||||
Thanks to these projects, without which I would have been pulling my hair out implementing these protocols from scratch.
|
Thanks to these projects, without which I would have been pulling my hair out implementing these protocols from scratch.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Please email all security vulnerabilities and issues found to the email "contact at ersei dot net". If using LLMs/AI to find the issues, first verify the issue exists manually. Please do not publicly disclose security vulnerabilities until after a stable release for the fix has been released for 30 days.
|
||||||
|
|
||||||
|
The latest released version is the only supported version.
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 16.0.30114.105
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO-Auth", "SSO-Auth\SSO-Auth.csproj", "{C30A5CFB-B27E-4E83-9E96-1E0362B36748}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO-Auth", "SSO-Auth\SSO-Auth.csproj", "{C30A5CFB-B27E-4E83-9E96-1E0362B36748}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
|||||||
+288
-55
@@ -1,27 +1,34 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel.OidcClient;
|
using Duende.IdentityModel.OidcClient;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Plugin.SSO_Auth.Config;
|
using Jellyfin.Plugin.SSO_Auth.Config;
|
||||||
using Jellyfin.Plugin.SSO_Auth.Helpers;
|
using Jellyfin.Plugin.SSO_Auth.Helpers;
|
||||||
|
using MediaBrowser.Common.Api;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Cryptography;
|
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;
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SSO_Auth.Lib;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SSO_Auth.Api;
|
namespace Jellyfin.Plugin.SSO_Auth.Api;
|
||||||
|
|
||||||
@@ -36,24 +43,45 @@ 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 ILoggerFactory _loggerFactory;
|
||||||
private readonly ICryptoProvider _cryptoProvider;
|
private readonly ICryptoProvider _cryptoProvider;
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SSOController"/> class.
|
/// Initializes a new instance of the <see cref="SSOController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{SSOController}"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{SSOController}"/> interface.</param>
|
||||||
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
/// <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>
|
||||||
/// <param name="cryptoProvider">Instance of the <see cref="ICryptoProvider"/> interface.</param>
|
/// <param name="cryptoProvider">Instance of the <see cref="ICryptoProvider"/> interface.</param>
|
||||||
public SSOController(ILogger<SSOController> logger, ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, ICryptoProvider cryptoProvider)
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
|
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public SSOController(
|
||||||
|
ILogger<SSOController> logger,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IAuthorizationContext authContext,
|
||||||
|
ICryptoProvider cryptoProvider,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_authContext = authContext;
|
_authContext = authContext;
|
||||||
_cryptoProvider = cryptoProvider;
|
_cryptoProvider = cryptoProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
_logger.LogInformation("SSO Controller initialized");
|
_logger.LogInformation("SSO Controller initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,46 +110,78 @@ public class SSOController : ControllerBase
|
|||||||
|
|
||||||
if (config.Enabled)
|
if (config.Enabled)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(state))
|
||||||
|
{
|
||||||
|
return BadRequest("Missing state");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StateManager.TryGetValue(state, out var timedState))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid or expired state");
|
||||||
|
}
|
||||||
|
|
||||||
var scopes = config.OidScopes == null ? new string[2] : config.OidScopes;
|
var scopes = config.OidScopes == null ? new string[2] : config.OidScopes;
|
||||||
var options = new OidcClientOptions
|
var options = new OidcClientOptions
|
||||||
{
|
{
|
||||||
Authority = config.OidEndpoint?.Trim(),
|
Authority = config.OidEndpoint?.Trim(),
|
||||||
ClientId = config.OidClientId?.Trim(),
|
ClientId = config.OidClientId?.Trim(),
|
||||||
ClientSecret = config.OidSecret?.Trim(),
|
ClientSecret = config.OidSecret?.Trim(),
|
||||||
RedirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
|
RedirectUri = GetRequestBase(config.SchemeOverride, config.PortOverride) + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
|
||||||
Scope = string.Join(" ", scopes.Prepend("openid profile")),
|
Scope = string.Join(" ", scopes.Prepend("openid profile")),
|
||||||
|
DisablePushedAuthorization = config.DisablePushedAuthorization,
|
||||||
|
LoggerFactory = _loggerFactory,
|
||||||
|
LoadProfile = !config.DoNotLoadProfile,
|
||||||
|
HttpClientFactory = o =>
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||||
|
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||||
|
string version = fvi.FileVersion;
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
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.ValidateEndpoints = !config.DoNotValidateEndpoints; // For Google and other providers with different endpoints
|
||||||
options.Policy.Discovery.RequireHttps = !config.DisableHttps;
|
options.Policy.Discovery.RequireHttps = !config.DisableHttps;
|
||||||
options.Policy.Discovery.ValidateIssuerName = !config.DoNotValidateIssuerName;
|
options.Policy.Discovery.ValidateIssuerName = !config.DoNotValidateIssuerName;
|
||||||
var oidcClient = new OidcClient(options);
|
var oidcClient = new OidcClient(options);
|
||||||
var currentState = StateManager[state].State;
|
var currentState = timedState.State;
|
||||||
var result = await oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).ConfigureAwait(false);
|
var result = await oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).ConfigureAwait(false);
|
||||||
|
|
||||||
if (result.IsError)
|
if (result.IsError)
|
||||||
{
|
{
|
||||||
return ReturnError(StatusCodes.Status400BadRequest, result.Error + " Try logging in again.");
|
return ReturnError(StatusCodes.Status400BadRequest, $"Error logging in: {result.Error} - {result.ErrorDescription}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||||
{
|
{
|
||||||
StateManager[state].Folders = new List<string>(config.EnabledFolders);
|
timedState.Folders = new List<string>(config.EnabledFolders);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
StateManager[state].Folders = new List<string>();
|
timedState.Folders = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateManager[state].EnableLiveTv = config.EnableLiveTv;
|
timedState.EnableLiveTv = config.EnableLiveTv;
|
||||||
StateManager[state].EnableLiveTvManagement = config.EnableLiveTvManagement;
|
timedState.EnableLiveTvManagement = config.EnableLiveTvManagement;
|
||||||
|
|
||||||
|
if (config.AvatarUrlFormat is not null)
|
||||||
|
{
|
||||||
|
timedState.AvatarURL = result.User.Claims.Aggregate(
|
||||||
|
config.AvatarUrlFormat,
|
||||||
|
(s, claim) => s.Contains($"@{{{claim.Type}}}") ? s.Replace($"@{{{claim.Type}}}", claim.Value) : s);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var claim in result.User.Claims)
|
foreach (var claim in result.User.Claims)
|
||||||
{
|
{
|
||||||
if (claim.Type == (config.DefaultUsernameClaim?.Trim() ?? "preferred_username"))
|
if (claim.Type == (config.DefaultUsernameClaim?.Trim() ?? "preferred_username"))
|
||||||
{
|
{
|
||||||
StateManager[state].Username = claim.Value;
|
timedState.Username = claim.Value;
|
||||||
if (config.Roles == null || config.Roles.Length == 0)
|
if (config.Roles == null || config.Roles.Length == 0)
|
||||||
{
|
{
|
||||||
StateManager[state].Valid = true;
|
timedState.Valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +207,40 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
// We recursively traverse through the JSON data for the roles and parse it
|
// We recursively traverse through the JSON data for the roles and parse it
|
||||||
var json = JsonConvert.DeserializeObject<IDictionary<string, object>>(claim.Value);
|
var json = JsonConvert.DeserializeObject<IDictionary<string, object>>(claim.Value);
|
||||||
for (int i = 1; i < segments.Length - 1; i++)
|
if (json is null)
|
||||||
{
|
{
|
||||||
var segment = segments[i];
|
roles = new List<string>();
|
||||||
json = (json[segment] as JObject).ToObject<IDictionary<string, object>>();
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool missingSegment = false;
|
||||||
|
for (int i = 1; i < segments.Length - 1; i++)
|
||||||
|
{
|
||||||
|
var segment = segments[i];
|
||||||
|
if (!json.TryGetValue(segment, out var nextToken) || nextToken is not JObject nextObject)
|
||||||
|
{
|
||||||
|
missingSegment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// The final step is to take the JSON and turn it from a dictionary into a string
|
json = nextObject.ToObject<IDictionary<string, object>>();
|
||||||
roles = (json[segments[^1]] as JArray).ToObject<List<string>>();
|
if (json is null)
|
||||||
|
{
|
||||||
|
missingSegment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingSegment || !json.TryGetValue(segments[^1], out var rolesToken) || rolesToken is not JArray rolesArray)
|
||||||
|
{
|
||||||
|
roles = new List<string>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The final step is to take the JSON and turn it from a dictionary into a string
|
||||||
|
roles = rolesArray.ToObject<List<string>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string role in roles)
|
foreach (string role in roles)
|
||||||
@@ -166,7 +252,7 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (role.Equals(validRoles))
|
if (role.Equals(validRoles))
|
||||||
{
|
{
|
||||||
StateManager[state].Valid = true;
|
timedState.Valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +264,7 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (role.Equals(validAdminRoles))
|
if (role.Equals(validAdminRoles))
|
||||||
{
|
{
|
||||||
StateManager[state].Admin = true;
|
timedState.Admin = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +276,7 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (role.Equals(folderRoleMap.Role?.Trim()))
|
if (role.Equals(folderRoleMap.Role?.Trim()))
|
||||||
{
|
{
|
||||||
StateManager[state].Folders.AddRange(folderRoleMap.Folders);
|
timedState.Folders.AddRange(folderRoleMap.Folders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +290,7 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (role.Equals(validLiveTvRoles))
|
if (role.Equals(validLiveTvRoles))
|
||||||
{
|
{
|
||||||
StateManager[state].EnableLiveTv = true;
|
timedState.EnableLiveTv = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +302,7 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (role.Equals(validLiveTvManagementRoles))
|
if (role.Equals(validLiveTvManagementRoles))
|
||||||
{
|
{
|
||||||
StateManager[state].EnableLiveTvManagement = true;
|
timedState.EnableLiveTvManagement = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,33 +313,33 @@ public class SSOController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the provider doesn't support the preferred username claim, then use the sub claim
|
// If the provider doesn't support the preferred username claim, then use the sub claim
|
||||||
if (!StateManager[state].Valid)
|
if (!timedState.Valid)
|
||||||
{
|
{
|
||||||
foreach (var claim in result.User.Claims)
|
foreach (var claim in result.User.Claims)
|
||||||
{
|
{
|
||||||
if (claim.Type == "sub")
|
if (claim.Type == "sub")
|
||||||
{
|
{
|
||||||
StateManager[state].Username = claim.Value;
|
timedState.Username = claim.Value;
|
||||||
if (config.Roles.Length == 0)
|
if (config.Roles.Length == 0)
|
||||||
{
|
{
|
||||||
StateManager[state].Valid = true;
|
timedState.Valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLinking = StateManager[state].IsLinking;
|
bool isLinking = timedState.IsLinking;
|
||||||
|
|
||||||
if (StateManager[state].Valid)
|
if (timedState.Valid)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Is request linking: {isLinking}");
|
_logger.LogInformation($"Is request linking: {isLinking}");
|
||||||
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(config.SchemeOverride), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
|
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(config.SchemeOverride, config.PortOverride), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
"OpenID user {Username} has one or more incorrect role claims: {@Claims}. Expected any one of: {@ExpectedClaims}",
|
"OpenID user {Username} has one or more incorrect role claims: {@Claims}. Expected any one of: {@ExpectedClaims}",
|
||||||
StateManager[state].Username,
|
timedState.Username,
|
||||||
result.User.Claims.Select(o => new { o.Type, o.Value }),
|
result.User.Claims.Select(o => new { o.Type, o.Value }),
|
||||||
config.Roles);
|
config.Roles);
|
||||||
|
|
||||||
@@ -295,7 +381,7 @@ public class SSOController : ControllerBase
|
|||||||
config.NewPath = newPath;
|
config.NewPath = newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(newPath ? "redirect" : "r")}/" + provider;
|
string redirectUri = GetRequestBase(config.SchemeOverride, config.PortOverride) + $"/sso/OID/{(newPath ? "redirect" : "r")}/" + provider;
|
||||||
|
|
||||||
var options = new OidcClientOptions
|
var options = new OidcClientOptions
|
||||||
{
|
{
|
||||||
@@ -304,10 +390,33 @@ public class SSOController : ControllerBase
|
|||||||
ClientSecret = config.OidSecret?.Trim(),
|
ClientSecret = config.OidSecret?.Trim(),
|
||||||
RedirectUri = redirectUri,
|
RedirectUri = redirectUri,
|
||||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||||
|
DisablePushedAuthorization = config.DisablePushedAuthorization,
|
||||||
|
LoggerFactory = _loggerFactory,
|
||||||
|
LoadProfile = !config.DoNotLoadProfile,
|
||||||
|
HttpClientFactory = o =>
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||||
|
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||||
|
string version = fvi.FileVersion;
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
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
|
||||||
|
options.Policy.Discovery.RequireHttps = !config.DisableHttps;
|
||||||
|
options.Policy.Discovery.ValidateIssuerName = !config.DoNotValidateIssuerName;
|
||||||
var oidcClient = new OidcClient(options);
|
var oidcClient = new OidcClient(options);
|
||||||
var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
|
var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (state.IsError)
|
||||||
|
{
|
||||||
|
return ReturnError(StatusCodes.Status400BadRequest, $"Error preparing login: {state.Error} - {state.ErrorDescription}");
|
||||||
|
}
|
||||||
|
|
||||||
StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
|
StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
|
||||||
|
|
||||||
// Track whether this is a linking request or not.
|
// Track whether this is a linking request or not.
|
||||||
@@ -323,7 +432,7 @@ public class SSOController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">The name of the provider to add.</param>
|
/// <param name="provider">The name of the provider to add.</param>
|
||||||
/// <param name="config">The OID configuration (deserialized from a JSON post).</param>
|
/// <param name="config">The OID configuration (deserialized from a JSON post).</param>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpPost("OID/Add/{provider}")]
|
[HttpPost("OID/Add/{provider}")]
|
||||||
public void OidAdd(string provider, [FromBody] OidConfig config)
|
public void OidAdd(string provider, [FromBody] OidConfig config)
|
||||||
{
|
{
|
||||||
@@ -336,7 +445,7 @@ public class SSOController : ControllerBase
|
|||||||
/// Deletes an OpenID provider.
|
/// Deletes an OpenID provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">Name of provider to delete.</param>
|
/// <param name="provider">Name of provider to delete.</param>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpGet("OID/Del/{provider}")]
|
[HttpGet("OID/Del/{provider}")]
|
||||||
public void OidDel(string provider)
|
public void OidDel(string provider)
|
||||||
{
|
{
|
||||||
@@ -349,7 +458,7 @@ public class SSOController : ControllerBase
|
|||||||
/// Lists the OpenID providers configured. Requires administrator privileges.
|
/// Lists the OpenID providers configured. Requires administrator privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The list of OpenID configurations.</returns>
|
/// <returns>The list of OpenID configurations.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpGet("OID/Get")]
|
[HttpGet("OID/Get")]
|
||||||
public ActionResult OidProviders()
|
public ActionResult OidProviders()
|
||||||
{
|
{
|
||||||
@@ -380,7 +489,7 @@ public class SSOController : ControllerBase
|
|||||||
/// This is a debug endpoint to list all running OpenID flows. Requires administrator privileges.
|
/// This is a debug endpoint to list all running OpenID flows. Requires administrator privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The list of OpenID flows in progress.</returns>
|
/// <returns>The list of OpenID flows in progress.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpGet("OID/States")]
|
[HttpGet("OID/States")]
|
||||||
public ActionResult OidStates()
|
public ActionResult OidStates()
|
||||||
{
|
{
|
||||||
@@ -416,8 +525,19 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("oid", provider, kvp.Value.Username);
|
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("oid", provider, kvp.Value.Username);
|
||||||
|
|
||||||
var authenticationResult = await Authenticate(userId, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), kvp.Value.EnableLiveTv, kvp.Value.EnableLiveTvManagement, response, config.DefaultProvider?.Trim())
|
var authenticationResult = await Authenticate(
|
||||||
|
userId,
|
||||||
|
kvp.Value.Admin,
|
||||||
|
config.EnableAuthorization,
|
||||||
|
config.EnableAllFolders,
|
||||||
|
kvp.Value.Folders.ToArray(),
|
||||||
|
kvp.Value.EnableLiveTv,
|
||||||
|
kvp.Value.EnableLiveTvManagement,
|
||||||
|
response,
|
||||||
|
config.DefaultProvider?.Trim(),
|
||||||
|
kvp.Value.AvatarURL)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
StateManager.Remove(kvp.Key);
|
||||||
return Ok(authenticationResult);
|
return Ok(authenticationResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,6 +578,11 @@ public class SSOController : ControllerBase
|
|||||||
{
|
{
|
||||||
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
|
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
|
||||||
|
|
||||||
|
if (!samlResponse.IsValid())
|
||||||
|
{
|
||||||
|
return Problem("Invalid SAML signature");
|
||||||
|
}
|
||||||
|
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
||||||
// If no roles are configured, don't use RBAC
|
// If no roles are configured, don't use RBAC
|
||||||
@@ -484,7 +609,7 @@ public class SSOController : ControllerBase
|
|||||||
WebResponse.Generator(
|
WebResponse.Generator(
|
||||||
data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)),
|
data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)),
|
||||||
provider: provider,
|
provider: provider,
|
||||||
baseUrl: GetRequestBase(config.SchemeOverride),
|
baseUrl: GetRequestBase(config.SchemeOverride, config.PortOverride),
|
||||||
mode: "SAML",
|
mode: "SAML",
|
||||||
isLinking: isLinking),
|
isLinking: isLinking),
|
||||||
MediaTypeNames.Text.Html);
|
MediaTypeNames.Text.Html);
|
||||||
@@ -530,7 +655,7 @@ public class SSOController : ControllerBase
|
|||||||
config.NewPath = newPath;
|
config.NewPath = newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/SAML/{(newPath ? "post" : "p")}/" + provider;
|
string redirectUri = GetRequestBase(config.SchemeOverride, config.PortOverride) + $"/sso/SAML/{(newPath ? "post" : "p")}/" + provider;
|
||||||
string relayState = null;
|
string relayState = null;
|
||||||
if (isLinking)
|
if (isLinking)
|
||||||
{
|
{
|
||||||
@@ -553,7 +678,7 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="provider">The provider name to add.</param>
|
/// <param name="provider">The provider name to add.</param>
|
||||||
/// <param name="newConfig">The SAML configuration object (deserialized) from JSON.</param>
|
/// <param name="newConfig">The SAML configuration object (deserialized) from JSON.</param>
|
||||||
/// <returns>The success result.</returns>
|
/// <returns>The success result.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpPost("SAML/Add/{provider}")]
|
[HttpPost("SAML/Add/{provider}")]
|
||||||
public OkResult SamlAdd(string provider, [FromBody] SamlConfig newConfig)
|
public OkResult SamlAdd(string provider, [FromBody] SamlConfig newConfig)
|
||||||
{
|
{
|
||||||
@@ -568,7 +693,7 @@ public class SSOController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">The ID of the provider to delete.</param>
|
/// <param name="provider">The ID of the provider to delete.</param>
|
||||||
/// <returns>The success result.</returns>
|
/// <returns>The success result.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpGet("SAML/Del/{provider}")]
|
[HttpGet("SAML/Del/{provider}")]
|
||||||
public OkResult SamlDel(string provider)
|
public OkResult SamlDel(string provider)
|
||||||
{
|
{
|
||||||
@@ -582,7 +707,7 @@ public class SSOController : ControllerBase
|
|||||||
/// Returns a list of all SAML providers configured. Requires administrator privileges.
|
/// Returns a list of all SAML providers configured. Requires administrator privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of all of the Saml providers available.</returns>
|
/// <returns>A list of all of the Saml providers available.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpGet("SAML/Get")]
|
[HttpGet("SAML/Get")]
|
||||||
public ActionResult SamlProviders()
|
public ActionResult SamlProviders()
|
||||||
{
|
{
|
||||||
@@ -616,6 +741,12 @@ public class SSOController : ControllerBase
|
|||||||
bool liveTv = config.EnableLiveTv;
|
bool liveTv = config.EnableLiveTv;
|
||||||
bool liveTvManagement = config.EnableLiveTvManagement;
|
bool liveTvManagement = config.EnableLiveTvManagement;
|
||||||
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
||||||
|
|
||||||
|
if (!samlResponse.IsValid())
|
||||||
|
{
|
||||||
|
return Problem("Invalid SAML signature");
|
||||||
|
}
|
||||||
|
|
||||||
List<string> folders;
|
List<string> folders;
|
||||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||||
{
|
{
|
||||||
@@ -681,7 +812,17 @@ public class SSOController : ControllerBase
|
|||||||
|
|
||||||
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("saml", provider, samlResponse.GetNameID());
|
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(),
|
||||||
|
null)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return Ok(authenticationResult);
|
return Ok(authenticationResult);
|
||||||
}
|
}
|
||||||
@@ -695,7 +836,7 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="username">The username to switch to the new provider.</param>
|
/// <param name="username">The username to switch to the new provider.</param>
|
||||||
/// <param name="provider">The new provider to switch to.</param>
|
/// <param name="provider">The new provider to switch to.</param>
|
||||||
/// <returns>Whether this API endpoint succeeded.</returns>
|
/// <returns>Whether this API endpoint succeeded.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[HttpPost("Unregister/{username}")]
|
[HttpPost("Unregister/{username}")]
|
||||||
public ActionResult Unregister(string username, [FromBody] string provider)
|
public ActionResult Unregister(string username, [FromBody] string provider)
|
||||||
{
|
{
|
||||||
@@ -732,7 +873,28 @@ 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 user = null;
|
||||||
user = _userManager.GetUserByName(canonicalName);
|
|
||||||
|
// First try to get the user by its id in case it was already registered before
|
||||||
|
Guid userId = Guid.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userId = GetCanonicalLink(mode, provider, canonicalName);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
userId = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No userId found? Let's try and find the user by name instead
|
||||||
|
if (userId == Guid.Empty)
|
||||||
|
{
|
||||||
|
user = _userManager.GetUserByName(canonicalName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user = _userManager.GetUserById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"SSO user {canonicalName} doesn't exist, creating...");
|
_logger.LogInformation($"SSO user {canonicalName} doesn't exist, creating...");
|
||||||
@@ -747,7 +909,7 @@ public class SSOController : ControllerBase
|
|||||||
UpdateCanonicalLinkConfig(links, mode, provider);
|
UpdateCanonicalLinkConfig(links, mode, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid userId = Guid.Empty;
|
userId = Guid.Empty;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
userId = GetCanonicalLink(mode, provider, canonicalName);
|
userId = GetCanonicalLink(mode, provider, canonicalName);
|
||||||
@@ -787,7 +949,7 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="jellyfinUserId">The user ID within jellyfin to link to the provider.</param>
|
/// <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>
|
/// <param name="authResponse">The client information to authenticate the user with.</param>
|
||||||
/// <returns>Whether this API endpoint succeeded.</returns>
|
/// <returns>Whether this API endpoint succeeded.</returns>
|
||||||
[Authorize(Policy = "DefaultAuthorization")]
|
[Authorize]
|
||||||
[HttpPost("{mode}/Link/{provider}/{jellyfinUserId}")]
|
[HttpPost("{mode}/Link/{provider}/{jellyfinUserId}")]
|
||||||
[Consumes(MediaTypeNames.Application.Json)]
|
[Consumes(MediaTypeNames.Application.Json)]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
@@ -817,7 +979,7 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="jellyfinUserId">The user ID within jellyfin to unlink from the provider.</param>
|
/// <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>
|
/// <param name="canonicalName">The user ID within jellyfin to unlink.</param>
|
||||||
/// <returns>Whether this API endpoint succeeded.</returns>
|
/// <returns>Whether this API endpoint succeeded.</returns>
|
||||||
[Authorize(Policy = "DefaultAuthorization")]
|
[Authorize]
|
||||||
[HttpDelete("{mode}/Link/{provider}/{jellyfinUserId}/{canonicalName}")]
|
[HttpDelete("{mode}/Link/{provider}/{jellyfinUserId}/{canonicalName}")]
|
||||||
[Consumes(MediaTypeNames.Application.Json)]
|
[Consumes(MediaTypeNames.Application.Json)]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
@@ -847,7 +1009,7 @@ public class SSOController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
|
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
|
||||||
/// <returns>A dictionary of provider : link mappings.</returns>
|
/// <returns>A dictionary of provider : link mappings.</returns>
|
||||||
[Authorize(Policy = "DefaultAuthorization")]
|
[Authorize]
|
||||||
[HttpGet("saml/links/{jellyfinUserId}")]
|
[HttpGet("saml/links/{jellyfinUserId}")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetSamlLinksByUser(Guid jellyfinUserId)
|
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetSamlLinksByUser(Guid jellyfinUserId)
|
||||||
@@ -875,7 +1037,7 @@ public class SSOController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
|
/// <param name="jellyfinUserId">The user ID within jellyfin for which to return the links.</param>
|
||||||
/// <returns>A dictionary of provider : link mappings.</returns>
|
/// <returns>A dictionary of provider : link mappings.</returns>
|
||||||
[Authorize(Policy = "DefaultAuthorization")]
|
[Authorize]
|
||||||
[HttpGet("oid/links/{jellyfinUserId}")]
|
[HttpGet("oid/links/{jellyfinUserId}")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetOidLinksByUser(Guid jellyfinUserId)
|
public async Task<ActionResult<SerializableDictionary<string, IEnumerable<string>>>> GetOidLinksByUser(Guid jellyfinUserId)
|
||||||
@@ -923,7 +1085,11 @@ public class SSOController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
||||||
// TODO: Does saml response require further validation?
|
|
||||||
|
if (!samlResponse.IsValid())
|
||||||
|
{
|
||||||
|
return Problem("Invalid SAML signature");
|
||||||
|
}
|
||||||
|
|
||||||
string providerUserId = samlResponse.GetNameID();
|
string providerUserId = samlResponse.GetNameID();
|
||||||
|
|
||||||
@@ -1015,7 +1181,8 @@ public class SSOController : ControllerBase
|
|||||||
/// <param name="enableLiveTvAdmin">Determines whether live TV can be managed by this user.</param>
|
/// <param name="enableLiveTvAdmin">Determines whether live TV can be managed by this user.</param>
|
||||||
/// <param name="authResponse">The client information to authenticate the user with.</param>
|
/// <param name="authResponse">The client information to authenticate the user with.</param>
|
||||||
/// <param name="defaultProvider">The default provider of the user to be set after logging in.</param>
|
/// <param name="defaultProvider">The default provider of the user to be set after logging in.</param>
|
||||||
private async Task<AuthenticationResult> Authenticate(Guid userId, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, bool enableLiveTv, bool enableLiveTvAdmin, AuthResponse authResponse, string defaultProvider)
|
/// <param name="avatarUrl">The new avatar url for the user.</param>
|
||||||
|
private async Task<AuthenticationResult> Authenticate(Guid userId, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, bool enableLiveTv, bool enableLiveTvAdmin, AuthResponse authResponse, string defaultProvider, string avatarUrl)
|
||||||
{
|
{
|
||||||
User user = _userManager.GetUserById(userId);
|
User user = _userManager.GetUserById(userId);
|
||||||
if (enableAuthorization)
|
if (enableAuthorization)
|
||||||
@@ -1028,6 +1195,56 @@ public class SSOController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatarUrl is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||||
|
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||||
|
string version = fvi.FileVersion;
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||||
|
|
||||||
|
var avatarResponse = await client.GetAsync(avatarUrl);
|
||||||
|
|
||||||
|
if (!avatarResponse.Content.Headers.TryGetValues("content-type", out var contentTypeList))
|
||||||
|
{
|
||||||
|
throw new Exception("Cannot get Content-Type of image : " + avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType = contentTypeList.First();
|
||||||
|
if (!contentType.StartsWith("image"))
|
||||||
|
{
|
||||||
|
throw new Exception("Content type of avatar URL is not an image, got : " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = contentType.Split("/").Last();
|
||||||
|
var stream = await avatarResponse.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var userDataPath =
|
||||||
|
Path.Combine(
|
||||||
|
_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath,
|
||||||
|
user.Username);
|
||||||
|
if (user.ProfileImage is not null)
|
||||||
|
{
|
||||||
|
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ProfileImage = new ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||||
|
|
||||||
|
await _providerManager.SaveImage(stream, contentType, user.ProfileImage.Path)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.SetPermission(PermissionKind.EnableLiveTvAccess, enableLiveTv);
|
user.SetPermission(PermissionKind.EnableLiveTvAccess, enableLiveTv);
|
||||||
user.SetPermission(PermissionKind.EnableLiveTvManagement, enableLiveTvAdmin);
|
user.SetPermission(PermissionKind.EnableLiveTvManagement, enableLiveTvAdmin);
|
||||||
|
|
||||||
@@ -1063,9 +1280,19 @@ public class SSOController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetRequestBase(string schemeOverride = null)
|
private string GetRequestBase(string schemeOverride = null, int? portOverride = null)
|
||||||
{
|
{
|
||||||
int requestPort = Request.Host.Port ?? -1;
|
int requestPort;
|
||||||
|
|
||||||
|
if (portOverride != null)
|
||||||
|
{
|
||||||
|
requestPort = portOverride.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
requestPort = Request.Host.Port ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
if ((requestPort == 80 && string.Equals(Request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(Request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
if ((requestPort == 80 && string.Equals(Request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(Request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
requestPort = -1;
|
requestPort = -1;
|
||||||
@@ -1145,6 +1372,7 @@ public class TimedAuthorizeState
|
|||||||
IsLinking = false;
|
IsLinking = false;
|
||||||
EnableLiveTv = false;
|
EnableLiveTv = false;
|
||||||
EnableLiveTvManagement = false;
|
EnableLiveTvManagement = false;
|
||||||
|
AvatarURL = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1192,4 +1420,9 @@ public class TimedAuthorizeState
|
|||||||
/// Gets or sets a value indicating whether the user is allowed to manage live TV.
|
/// Gets or sets a value indicating whether the user is allowed to manage live TV.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableLiveTvManagement { get; set; }
|
public bool EnableLiveTvManagement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user avatar url.
|
||||||
|
/// </summary>
|
||||||
|
public string AvatarURL { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ public class SamlConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SchemeOverride { get; set; }
|
public string SchemeOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the redirect port override.
|
||||||
|
/// </summary>
|
||||||
|
public int? PortOverride { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -265,6 +270,11 @@ public class OidConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SchemeOverride { get; set; }
|
public string SchemeOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the redirect port override.
|
||||||
|
/// </summary>
|
||||||
|
public int? PortOverride { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -293,10 +303,20 @@ public class OidConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string DefaultUsernameClaim { get; set; }
|
public string DefaultUsernameClaim { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL format of the new user avatar.
|
||||||
|
/// </summary>
|
||||||
|
public string AvatarUrlFormat { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether HTTPS in the discovery endpoint is required.
|
/// Gets or sets a value indicating whether HTTPS in the discovery endpoint is required.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DisableHttps { get; set; }
|
public bool DisableHttps { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether pushed authorization is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisablePushedAuthorization { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the OpenID endpoints are validated.
|
/// Gets or sets a value indicating whether the OpenID endpoints are validated.
|
||||||
@@ -307,6 +327,11 @@ public class OidConfig
|
|||||||
/// Gets or sets a value indicating whether the OpenID issuer name is validated.
|
/// Gets or sets a value indicating whether the OpenID issuer name is validated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DoNotValidateIssuerName { get; set; }
|
public bool DoNotValidateIssuerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the UserInfo endpoint is used to get profile data.
|
||||||
|
/// </summary>
|
||||||
|
public bool DoNotLoadProfile { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -301,12 +301,20 @@ const ssoConfigurationPage = {
|
|||||||
|
|
||||||
form_elements.text_fields.forEach((id) => {
|
form_elements.text_fields.forEach((id) => {
|
||||||
const value = page.querySelector("#" + id).value;
|
const value = page.querySelector("#" + id).value;
|
||||||
if (value) current_config[id] = page.querySelector("#" + id).value;
|
if (value) {
|
||||||
|
current_config[id] = page.querySelector("#" + id).value;
|
||||||
|
} else {
|
||||||
|
current_config[id] = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
form_elements.json_fields.forEach((id) => {
|
form_elements.json_fields.forEach((id) => {
|
||||||
const value = page.querySelector("#" + id).value;
|
const value = page.querySelector("#" + id).value;
|
||||||
if (value) current_config[id] = JSON.parse(value);
|
if (value) {
|
||||||
|
current_config[id] = JSON.parse(value);
|
||||||
|
} else {
|
||||||
|
current_config[id] = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
form_elements.check_fields.forEach((id) => {
|
form_elements.check_fields.forEach((id) => {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<div class="collapseContent">
|
<div class="collapseContent">
|
||||||
<div class="selectContainer">
|
<div class="selectContainer">
|
||||||
<label class="selectLabel" for="selectProvider"
|
<label class="selectLabel" for="selectProvider"
|
||||||
>Name of OID Provider:
|
>Name of OpenID Provider:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
is="emby-select"
|
is="emby-select"
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<label
|
<label
|
||||||
class="inputLabel inputLabelUnfocused"
|
class="inputLabel inputLabelUnfocused"
|
||||||
for="OidProviderName"
|
for="OidProviderName"
|
||||||
>Name of OID Provider:</label
|
>Name of OpenID Provider:</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
is="emby-input"
|
is="emby-input"
|
||||||
@@ -133,20 +133,20 @@
|
|||||||
class="sso-text"
|
class="sso-text"
|
||||||
/>
|
/>
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
The name used by Jellyfin to identify the OID provider.
|
The name used by Jellyfin to identify the OpenID provider.
|
||||||
<br />
|
<br />
|
||||||
If an OID provider with a matching name does not exist, a
|
If an OpenID provider with a matching name does not exist, a
|
||||||
new provider with this name will be created.
|
new provider with this name will be created.
|
||||||
<br />
|
<br />
|
||||||
If an OID provider with a matching name already exists, the
|
If an OpenID provider with a matching name already exists,
|
||||||
settings for that provider will be updated.
|
the settings for that provider will be updated.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label
|
<label
|
||||||
class="inputLabel inputLabelUnfocused"
|
class="inputLabel inputLabelUnfocused"
|
||||||
for="OidEndpoint"
|
for="OidEndpoint"
|
||||||
>OID Endpoint:</label
|
>OpenID Endpoint:</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
is="emby-input"
|
is="emby-input"
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="OidSecret"
|
<label class="inputLabel inputLabelUnfocused" for="OidSecret"
|
||||||
>OID Secret:</label
|
>OpenID client secret:</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
is="emby-input"
|
is="emby-input"
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
class="sso-text"
|
class="sso-text"
|
||||||
/>
|
/>
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
The OpenID secret. Randomly generated & shared.
|
The OpenID client secret. Randomly generated & shared.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -480,7 +480,9 @@
|
|||||||
list of strings from the OIDC server.
|
list of strings from the OIDC server.
|
||||||
<br />
|
<br />
|
||||||
For Keycloak, it is <code>realm_access.roles</code> by
|
For Keycloak, it is <code>realm_access.roles</code> by
|
||||||
default.
|
default for realm roles. For client roles, it is
|
||||||
|
<code>resource_access.>clientId<.roles</code>
|
||||||
|
(e.g. resource_access.jellyfin.roles)
|
||||||
<br />
|
<br />
|
||||||
For Authelia, it is <code>groups</code>
|
For Authelia, it is <code>groups</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -559,6 +561,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label
|
||||||
|
class="inputLabel inputLabelUnfocused"
|
||||||
|
for="AvatarUrlFormat"
|
||||||
|
>Set avatar url format</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
is="emby-input"
|
||||||
|
id="AvatarUrlFormat"
|
||||||
|
type="text"
|
||||||
|
class="sso-text"
|
||||||
|
/>
|
||||||
|
<div class="fieldDescription">
|
||||||
|
The url of the avatar with sso variable format: example :
|
||||||
|
<code>https://example.com/@{user_id}.png</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer">
|
<div class="checkboxContainer">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@@ -573,6 +593,23 @@
|
|||||||
<div class="fieldDescription checkboxFieldDescription"></div>
|
<div class="fieldDescription checkboxFieldDescription"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
is="emby-checkbox"
|
||||||
|
id="DisablePushedAuthorization"
|
||||||
|
name="DisablePushedAuthorization"
|
||||||
|
type="checkbox"
|
||||||
|
class="sso-toggle"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>Disable Pushed Authorization (Insecure). May be needed
|
||||||
|
for Authelia.</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="checkboxContainer checkboxContainer-withDescription"
|
class="checkboxContainer checkboxContainer-withDescription"
|
||||||
>
|
>
|
||||||
@@ -602,6 +639,23 @@
|
|||||||
<span>Do Not Validate OpenID Issuer Name (Insecure)</span>
|
<span>Do Not Validate OpenID Issuer Name (Insecure)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="checkboxContainer checkboxContainer-withDescription"
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
is="emby-checkbox"
|
||||||
|
id="DoNotLoadProfile"
|
||||||
|
name="DoNotLoadProfile"
|
||||||
|
type="checkbox"
|
||||||
|
class="sso-toggle"
|
||||||
|
/>
|
||||||
|
<span>Do Not Load Profile Information</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">
|
||||||
|
May be required for Cloudflare OpenID
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
|
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
|
||||||
@@ -619,6 +673,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
|
||||||
|
>Port Override</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
is="emby-input"
|
||||||
|
id="PortOverride"
|
||||||
|
type="text"
|
||||||
|
class="sso-text"
|
||||||
|
/>
|
||||||
|
<div class="fieldDescription">
|
||||||
|
If the plugin is redirecting to an incorrect port, set this
|
||||||
|
to the appropiate port
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="SaveProvider"
|
id="SaveProvider"
|
||||||
is="emby-button"
|
is="emby-button"
|
||||||
|
|||||||
@@ -61,14 +61,14 @@
|
|||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
href="https://github.com/9p4/jellyfin-plugin-sso"
|
href="https://github.com/9p4/jellyfin-plugin-sso"
|
||||||
class="button-link"
|
class="button-link"
|
||||||
>help page</a
|
>homepage</a
|
||||||
>
|
>
|
||||||
and
|
and
|
||||||
<a
|
<a
|
||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
href="https://github.com/9p4/jellyfin-plugin-sso/projects/1"
|
href="https://github.com/9p4/jellyfin-plugin-sso/issues"
|
||||||
class="button-link"
|
class="button-link"
|
||||||
>roadmap
|
>issue tracker
|
||||||
</a>
|
</a>
|
||||||
for more information.
|
for more information.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RootNamespace>SSO_Auth</RootNamespace>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Library.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace SSO_Auth.Lib
|
||||||
|
|
||||||
|
module Say =
|
||||||
|
let hello name =
|
||||||
|
printfn "Hello %s" name
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
|
||||||
<AssemblyVersion>3.3.0.0</AssemblyVersion>
|
<AssemblyVersion>4.0.0.4</AssemblyVersion>
|
||||||
<FileVersion>3.3.0.0</FileVersion>
|
<FileVersion>4.0.0.4</FileVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -29,19 +29,23 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
|
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="6.0.1" />
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.11.0" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
|
<PackageReference Include="Jellyfin.Model" Version="10.11.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.0" />
|
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="Lib\Lib.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ public class Response
|
|||||||
/// <param name="certificateBytes">The certificate formatted as an array of bytes.</param>
|
/// <param name="certificateBytes">The certificate formatted as an array of bytes.</param>
|
||||||
public Response(byte[] certificateBytes)
|
public Response(byte[] certificateBytes)
|
||||||
{
|
{
|
||||||
_certificate = new X509Certificate2(certificateBytes);
|
_certificate = X509CertificateLoader.LoadCertificate(certificateBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -83,4 +83,4 @@ public class SSOViewsController : ControllerBase
|
|||||||
{
|
{
|
||||||
return ServeView(viewName);
|
return ServeView(viewName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,36 +240,42 @@ Emby Button
|
|||||||
/* fonts.scss */
|
/* fonts.scss */
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR",
|
font-family:
|
||||||
"Noto Sans SC", "Noto Sans TC", sans-serif;
|
"Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC",
|
||||||
|
"Noto Sans TC", sans-serif;
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang|="ja"] {
|
html[lang|="ja"] {
|
||||||
font-family: "Noto Sans", "Noto Sans JP", "Noto Sans HK", "Noto Sans KR",
|
font-family:
|
||||||
"Noto Sans SC", "Noto Sans TC", sans-serif;
|
"Noto Sans", "Noto Sans JP", "Noto Sans HK", "Noto Sans KR", "Noto Sans SC",
|
||||||
|
"Noto Sans TC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang|="ko"] {
|
html[lang|="ko"] {
|
||||||
font-family: "Noto Sans", "Noto Sans KR", "Noto Sans HK", "Noto Sans JP",
|
font-family:
|
||||||
"Noto Sans SC", "Noto Sans TC", sans-serif;
|
"Noto Sans", "Noto Sans KR", "Noto Sans HK", "Noto Sans JP", "Noto Sans SC",
|
||||||
|
"Noto Sans TC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang|="zh-CN"] {
|
html[lang|="zh-CN"] {
|
||||||
font-family: "Noto Sans", "Noto Sans SC", "Noto Sans HK", "Noto Sans JP",
|
font-family:
|
||||||
"Noto Sans KR", "Noto Sans TC", sans-serif;
|
"Noto Sans", "Noto Sans SC", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR",
|
||||||
|
"Noto Sans TC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang|="zh-TW"] {
|
html[lang|="zh-TW"] {
|
||||||
font-family: "Noto Sans", "Noto Sans TC", "Noto Sans HK", "Noto Sans JP",
|
font-family:
|
||||||
"Noto Sans KR", "Noto Sans SC", sans-serif;
|
"Noto Sans", "Noto Sans TC", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR",
|
||||||
|
"Noto Sans SC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang|="zh-HK"] {
|
html[lang|="zh-HK"] {
|
||||||
font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR",
|
font-family:
|
||||||
"Noto Sans SC", "Noto Sans TC", sans-serif;
|
"Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC",
|
||||||
|
"Noto Sans TC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-tv {
|
.layout-tv {
|
||||||
|
|||||||
+101
-91
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SSO_Auth;
|
namespace Jellyfin.Plugin.SSO_Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -9,7 +11,16 @@ public static class WebResponse
|
|||||||
/// The shared HTML between all of the responses.
|
/// The shared HTML between all of the responses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string Base = @"<!DOCTYPE html>
|
public static readonly string Base = @"<!DOCTYPE html>
|
||||||
<html><head></head><body>
|
<html><head>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #101010;
|
||||||
|
color: #d1cfce;
|
||||||
|
font-family: Noto Sans, Noto Sans HK, Noto Sans JP, Noto Sans KR, Noto Sans SC, Noto Sans TC, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
<p>Logging in...</p>
|
<p>Logging in...</p>
|
||||||
<noscript>Please enable Javascript to complete the login</noscript>
|
<noscript>Please enable Javascript to complete the login</noscript>
|
||||||
<script>
|
<script>
|
||||||
@@ -35,25 +46,14 @@ function isTv() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWeb0s()) {
|
return isWeb0s();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWeb0s() {
|
function isWeb0s() {
|
||||||
const userAgent = navigator.userAgent.toLowerCase();
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
|
||||||
if (userAgent.indexOf('netcast') !== -1) {
|
return userAgent.indexOf('netcast') !== -1
|
||||||
return true;
|
|| userAgent.indexOf('web0s') !== -1;
|
||||||
}
|
|
||||||
|
|
||||||
if (userAgent.indexOf('web0s') !== -1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMobile(userAgent) {
|
function isMobile(userAgent) {
|
||||||
@@ -100,11 +100,7 @@ function hasKeyboard(browser) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browser.tv) {
|
return !!browser.tv;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function iOSversion() {
|
function iOSversion() {
|
||||||
@@ -147,8 +143,12 @@ function web0sVersion(browser) {
|
|||||||
|
|
||||||
// The next is only valid for the app
|
// The next is only valid for the app
|
||||||
|
|
||||||
if (browser.versionMajor >= 79) {
|
if (browser.versionMajor >= 94) {
|
||||||
return 6;
|
return 23;
|
||||||
|
} else if (browser.versionMajor >= 87) {
|
||||||
|
return 22;
|
||||||
|
} else if (browser.versionMajor >= 79) {
|
||||||
|
return 6;
|
||||||
} else if (browser.versionMajor >= 68) {
|
} else if (browser.versionMajor >= 68) {
|
||||||
return 5;
|
return 5;
|
||||||
} else if (browser.versionMajor >= 53) {
|
} else if (browser.versionMajor >= 53) {
|
||||||
@@ -179,14 +179,11 @@ let _supportsCssAnimation;
|
|||||||
let _supportsCssAnimationWithPrefix;
|
let _supportsCssAnimationWithPrefix;
|
||||||
function supportsCssAnimation(allowPrefix) {
|
function supportsCssAnimation(allowPrefix) {
|
||||||
// TODO: Assess if this is still needed, as all of our targets should natively support CSS animations.
|
// TODO: Assess if this is still needed, as all of our targets should natively support CSS animations.
|
||||||
if (allowPrefix) {
|
if (allowPrefix && (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false)) {
|
||||||
if (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false) {
|
return _supportsCssAnimationWithPrefix;
|
||||||
return _supportsCssAnimationWithPrefix;
|
}
|
||||||
}
|
if (_supportsCssAnimation === true || _supportsCssAnimation === false) {
|
||||||
} else {
|
return _supportsCssAnimation;
|
||||||
if (_supportsCssAnimation === true || _supportsCssAnimation === false) {
|
|
||||||
return _supportsCssAnimation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let animation = false;
|
let animation = false;
|
||||||
@@ -198,8 +195,8 @@ function supportsCssAnimation(allowPrefix) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (animation === false && allowPrefix) {
|
if (animation === false && allowPrefix) {
|
||||||
for (let i = 0; i < domPrefixes.length; i++) {
|
for (const domPrefix of domPrefixes) {
|
||||||
if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
|
if (elm.style[domPrefix + 'AnimationName'] !== undefined) {
|
||||||
animation = true;
|
animation = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -218,25 +215,25 @@ function supportsCssAnimation(allowPrefix) {
|
|||||||
const uaMatch = function (ua) {
|
const uaMatch = function (ua) {
|
||||||
ua = ua.toLowerCase();
|
ua = ua.toLowerCase();
|
||||||
|
|
||||||
const match = /(edg)[ /]([\w.]+)/.exec(ua) ||
|
const match = /(chrome)[ /]([\w.]+)/.exec(ua)
|
||||||
/(edga)[ /]([\w.]+)/.exec(ua) ||
|
|| /(edg)[ /]([\w.]+)/.exec(ua)
|
||||||
/(edgios)[ /]([\w.]+)/.exec(ua) ||
|
|| /(edga)[ /]([\w.]+)/.exec(ua)
|
||||||
/(edge)[ /]([\w.]+)/.exec(ua) ||
|
|| /(edgios)[ /]([\w.]+)/.exec(ua)
|
||||||
/(opera)[ /]([\w.]+)/.exec(ua) ||
|
|| /(edge)[ /]([\w.]+)/.exec(ua)
|
||||||
/(opr)[ /]([\w.]+)/.exec(ua) ||
|
|| /(opera)[ /]([\w.]+)/.exec(ua)
|
||||||
/(chrome)[ /]([\w.]+)/.exec(ua) ||
|
|| /(opr)[ /]([\w.]+)/.exec(ua)
|
||||||
/(safari)[ /]([\w.]+)/.exec(ua) ||
|
|| /(safari)[ /]([\w.]+)/.exec(ua)
|
||||||
/(firefox)[ /]([\w.]+)/.exec(ua) ||
|
|| /(firefox)[ /]([\w.]+)/.exec(ua)
|
||||||
ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
|| ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua)
|
||||||
[];
|
|| [];
|
||||||
|
|
||||||
const versionMatch = /(version)[ /]([\w.]+)/.exec(ua);
|
const versionMatch = /(version)[ /]([\w.]+)/.exec(ua);
|
||||||
|
|
||||||
let platform_match = /(ipad)/.exec(ua) ||
|
let platform_match = /(ipad)/.exec(ua)
|
||||||
/(iphone)/.exec(ua) ||
|
|| /(iphone)/.exec(ua)
|
||||||
/(windows)/.exec(ua) ||
|
|| /(windows)/.exec(ua)
|
||||||
/(android)/.exec(ua) ||
|
|| /(android)/.exec(ua)
|
||||||
[];
|
|| [];
|
||||||
|
|
||||||
let browser = match[1] || '';
|
let browser = match[1] || '';
|
||||||
|
|
||||||
@@ -255,7 +252,7 @@ const uaMatch = function (ua) {
|
|||||||
|
|
||||||
version = version || match[2] || '0';
|
version = version || match[2] || '0';
|
||||||
|
|
||||||
let versionMajor = parseInt(version.split('.')[0]);
|
let versionMajor = parseInt(version.split('.')[0], 10);
|
||||||
|
|
||||||
if (isNaN(versionMajor)) {
|
if (isNaN(versionMajor)) {
|
||||||
versionMajor = 0;
|
versionMajor = 0;
|
||||||
@@ -313,7 +310,9 @@ if (userAgent.toLowerCase().indexOf('xbox') !== -1) {
|
|||||||
browser.tv = true;
|
browser.tv = true;
|
||||||
}
|
}
|
||||||
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
|
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
|
||||||
|
browser.hisense = userAgent.toLowerCase().includes('hisense');
|
||||||
browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || window.tizen != null;
|
browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || window.tizen != null;
|
||||||
|
browser.vidaa = userAgent.toLowerCase().includes('vidaa');
|
||||||
browser.web0s = isWeb0s();
|
browser.web0s = isWeb0s();
|
||||||
browser.edgeUwp = browser.edge && (userAgent.toLowerCase().indexOf('msapphost') !== -1 || userAgent.toLowerCase().indexOf('webview') !== -1);
|
browser.edgeUwp = browser.edge && (userAgent.toLowerCase().indexOf('msapphost') !== -1 || userAgent.toLowerCase().indexOf('webview') !== -1);
|
||||||
|
|
||||||
@@ -324,7 +323,7 @@ if (browser.web0s) {
|
|||||||
delete browser.safari;
|
delete browser.safari;
|
||||||
|
|
||||||
const v = (navigator.appVersion).match(/Tizen (\d+).(\d+)/);
|
const v = (navigator.appVersion).match(/Tizen (\d+).(\d+)/);
|
||||||
browser.tizenVersion = parseInt(v[1]);
|
browser.tizenVersion = parseInt(v[1], 10);
|
||||||
} else {
|
} else {
|
||||||
browser.orsay = userAgent.toLowerCase().indexOf('smarthub') !== -1;
|
browser.orsay = userAgent.toLowerCase().indexOf('smarthub') !== -1;
|
||||||
}
|
}
|
||||||
@@ -340,11 +339,9 @@ if (browser.mobile || browser.tv) {
|
|||||||
browser.slow = true;
|
browser.slow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof document !== 'undefined') {
|
/* eslint-disable-next-line compat/compat */
|
||||||
/* eslint-disable-next-line compat/compat */
|
if (typeof document !== 'undefined' && ('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) {
|
||||||
if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) {
|
browser.touch = true;
|
||||||
browser.touch = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.keyboard = hasKeyboard(browser);
|
browser.keyboard = hasKeyboard(browser);
|
||||||
@@ -361,39 +358,41 @@ if (browser.iOS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceName() {
|
function getDeviceName() {
|
||||||
var deviceName = '';
|
var deviceName = '';
|
||||||
if (browser.tizen) {
|
if (!deviceName) {
|
||||||
deviceName = 'Samsung Smart TV';
|
if (browser.tizen) {
|
||||||
} else if (browser.web0s) {
|
deviceName = 'Samsung Smart TV';
|
||||||
deviceName = 'LG Smart TV';
|
} else if (browser.web0s) {
|
||||||
} else if (browser.operaTv) {
|
deviceName = 'LG Smart TV';
|
||||||
deviceName = 'Opera TV';
|
} else if (browser.operaTv) {
|
||||||
} else if (browser.xboxOne) {
|
deviceName = 'Opera TV';
|
||||||
deviceName = 'Xbox One';
|
} else if (browser.xboxOne) {
|
||||||
} else if (browser.ps4) {
|
deviceName = 'Xbox One';
|
||||||
deviceName = 'Sony PS4';
|
} else if (browser.ps4) {
|
||||||
} else if (browser.chrome) {
|
deviceName = 'Sony PS4';
|
||||||
deviceName = 'Chrome';
|
} else if (browser.chrome) {
|
||||||
} else if (browser.edgeChromium) {
|
deviceName = 'Chrome';
|
||||||
deviceName = 'Edge Chromium';
|
} else if (browser.edgeChromium) {
|
||||||
} else if (browser.edge) {
|
deviceName = 'Edge Chromium';
|
||||||
deviceName = 'Edge';
|
} else if (browser.edge) {
|
||||||
} else if (browser.firefox) {
|
deviceName = 'Edge';
|
||||||
deviceName = 'Firefox';
|
} else if (browser.firefox) {
|
||||||
} else if (browser.opera) {
|
deviceName = 'Firefox';
|
||||||
deviceName = 'Opera';
|
} else if (browser.opera) {
|
||||||
} else if (browser.safari) {
|
deviceName = 'Opera';
|
||||||
deviceName = 'Safari';
|
} else if (browser.safari) {
|
||||||
} else {
|
deviceName = 'Safari';
|
||||||
deviceName = 'Web Browser';
|
} else {
|
||||||
}
|
deviceName = 'Web Browser';
|
||||||
|
}
|
||||||
|
|
||||||
if (browser.ipad) {
|
if (browser.ipad) {
|
||||||
deviceName += ' iPad';
|
deviceName += ' iPad';
|
||||||
} else if (browser.iphone) {
|
} else if (browser.iphone) {
|
||||||
deviceName += ' iPhone';
|
deviceName += ' iPhone';
|
||||||
} else if (browser.android) {
|
} else if (browser.android) {
|
||||||
deviceName += ' Android';
|
deviceName += ' Android';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deviceName;
|
return deviceName;
|
||||||
@@ -416,6 +415,14 @@ const sleep = (milliseconds) => {
|
|||||||
/// <returns>A string with the HTML to serve to the client.</returns>
|
/// <returns>A string with the HTML to serve to the client.</returns>
|
||||||
public static string Generator(string data, string provider, string baseUrl, string mode, bool isLinking = false)
|
public static string Generator(string data, string provider, string baseUrl, string mode, bool isLinking = false)
|
||||||
{
|
{
|
||||||
|
// Strip out the protocol (http:// or https://) and convert the domain to Punycode
|
||||||
|
var idnMapping = new IdnMapping();
|
||||||
|
var protocolSeparatorIndex = baseUrl.IndexOf("//");
|
||||||
|
var protocol = baseUrl.Substring(0, protocolSeparatorIndex + 2);
|
||||||
|
var domain = baseUrl.Substring(protocolSeparatorIndex + 2);
|
||||||
|
var punycodeDomain = idnMapping.GetAscii(domain);
|
||||||
|
var punycodeBaseUrl = protocol + punycodeDomain;
|
||||||
|
|
||||||
return Base + @"
|
return Base + @"
|
||||||
async function link(request) {
|
async function link(request) {
|
||||||
const jfCredentialsString = localStorage.getItem(""jellyfin_credentials"");
|
const jfCredentialsString = localStorage.getItem(""jellyfin_credentials"");
|
||||||
@@ -429,7 +436,7 @@ async function link(request) {
|
|||||||
if (jfUser == null) return;
|
if (jfUser == null) return;
|
||||||
if (jfToken == null) return;
|
if (jfToken == null) return;
|
||||||
|
|
||||||
const url = '" + $"{baseUrl}/sso/{mode}/Link/{provider}/" + @"' + jfUser;
|
const url = '" + $"{punycodeBaseUrl}/sso/{mode}/Link/{provider}/" + @"' + jfUser;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
@@ -453,6 +460,9 @@ async function link(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
localStorage.removeItem('jellyfin_credentials');
|
||||||
|
document.getElementById('iframe-main').src = '" + punycodeBaseUrl + @"/web/index.html';
|
||||||
|
|
||||||
var data = '" + data + @"';
|
var data = '" + data + @"';
|
||||||
while (localStorage.getItem(""_deviceId2"") == null ||
|
while (localStorage.getItem(""_deviceId2"") == null ||
|
||||||
localStorage.getItem(""jellyfin_credentials"") == null ||
|
localStorage.getItem(""jellyfin_credentials"") == null ||
|
||||||
@@ -469,7 +479,7 @@ async function main() {
|
|||||||
|
|
||||||
if (" + $"{isLinking}".ToLower() + @") await link(request);
|
if (" + $"{isLinking}".ToLower() + @") await link(request);
|
||||||
|
|
||||||
var url = '" + baseUrl + "/sso/" + mode + "/Auth/" + provider + @"';
|
var url = '" + punycodeBaseUrl + "/sso/" + mode + "/Auth/" + provider + @"';
|
||||||
|
|
||||||
let response = await new Promise(resolve => {
|
let response = await new Promise(resolve => {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
@@ -493,7 +503,7 @@ async function main() {
|
|||||||
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
||||||
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
||||||
localStorage.setItem('enableAutoLogin', 'true');
|
localStorage.setItem('enableAutoLogin', 'true');
|
||||||
window.location.replace('" + baseUrl + @"/web/index.html');
|
window.location.replace('" + punycodeBaseUrl + @"/web/index.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -501,6 +511,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://stackoverflow.com/a/25435165
|
// https://stackoverflow.com/a/25435165
|
||||||
</script><iframe class='docs-texteventtarget-iframe' sandbox='allow-same-origin allow-forms allow-scripts' src='" + baseUrl + "/web/index.html' 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>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -1,9 +1,9 @@
|
|||||||
name: "SSO Authentication"
|
name: "SSO Authentication"
|
||||||
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
|
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
|
||||||
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
|
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
|
||||||
version: "3.5.2.2"
|
version: "4.0.0.4"
|
||||||
targetAbi: "10.8.0.0"
|
targetAbi: "10.11.0.0"
|
||||||
framework: "net6.0"
|
framework: "net9.0"
|
||||||
owner: "9p4"
|
owner: "9p4"
|
||||||
overview: "Authenticate users against an SSO provider."
|
overview: "Authenticate users against an SSO provider."
|
||||||
description: |
|
description: |
|
||||||
@@ -12,9 +12,14 @@ description: |
|
|||||||
category: "Authentication"
|
category: "Authentication"
|
||||||
artifacts:
|
artifacts:
|
||||||
- "SSO-Auth.dll"
|
- "SSO-Auth.dll"
|
||||||
- "IdentityModel.OidcClient.dll"
|
- "Duende.IdentityModel.OidcClient.dll"
|
||||||
- "IdentityModel.dll"
|
- "Duende.IdentityModel.dll"
|
||||||
changelog: |
|
changelog: |
|
||||||
|
4.0.0.4: Fix security issue in SAML
|
||||||
|
4.0.0.0: Jellyfin 10.11
|
||||||
|
3.5.3.0: Allow for OID-provided avatars, various bugfixes and workarounds
|
||||||
|
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.2: Fix linking page when using new paths
|
||||||
3.5.2.1: Hotfix for SAML null checks
|
3.5.2.1: Hotfix for SAML null checks
|
||||||
3.5.2.0: Allow overriding the scheme used for generating URLs.
|
3.5.2.0: Allow overriding the scheme used for generating URLs.
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707205916,
|
"lastModified": 1760934318,
|
||||||
"narHash": "sha256-fmRJilYGlB7VCt3XsdYxrA0u8e/K84O5xYucerUY0iM=",
|
"narHash": "sha256-/oUYsC0lUCBory65VK+UHqCCsCspbL1Vgfcf1KUYqVw=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8cc79aa39bbc6eaedaf286ae655b224c71e02907",
|
"rev": "87848bf0cc4f87717fc813a4575f07330c3e743c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
in {
|
in {
|
||||||
devShell.x86_64-linux =
|
devShell.x86_64-linux =
|
||||||
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.prettier pkgs.dotnet-sdk ]; };
|
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.prettier pkgs.dotnet-sdk_9 ]; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-61
@@ -1,68 +1,75 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
|
<RuleSet Name="Rules for Jellyfin.Server"
|
||||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
|
||||||
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||||
<Rule Id="SA1202" Action="Info" />
|
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
|
||||||
<!-- disable warning SA1204: Static members must appear before non-static members -->
|
<Rule Id="SA1202" Action="Info" />
|
||||||
<Rule Id="SA1204" Action="Info" />
|
<!-- disable warning SA1204: Static members must appear before non-static members -->
|
||||||
<!-- disable warning SA1404: Code analysis suppression should have justification -->
|
<Rule Id="SA1204" Action="Info" />
|
||||||
<Rule Id="SA1404" 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. -->
|
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
|
||||||
<Rule Id="SA1009" Action="None" />
|
<Rule Id="SA1009" Action="None" />
|
||||||
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
|
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
|
||||||
<Rule Id="SA1101" Action="None" />
|
<Rule Id="SA1101" Action="None" />
|
||||||
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
|
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
|
||||||
<Rule Id="SA1108" Action="None" />
|
<Rule Id="SA1108" Action="None" />
|
||||||
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
|
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
|
||||||
<Rule Id="SA1128" Action="None" />
|
<Rule Id="SA1128" Action="None" />
|
||||||
<!-- disable warning SA1130: Use lambda syntax -->
|
<!-- disable warning SA1130: Use lambda syntax -->
|
||||||
<Rule Id="SA1130" Action="None" />
|
<Rule Id="SA1130" Action="None" />
|
||||||
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
|
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
|
||||||
<Rule Id="SA1200" Action="None" />
|
<Rule Id="SA1200" Action="None" />
|
||||||
<!-- disable warning SA1309: Fields must not begin with an underscore -->
|
<!-- disable warning SA1309: Fields must not begin with an underscore -->
|
||||||
<Rule Id="SA1309" Action="None" />
|
<Rule Id="SA1309" Action="None" />
|
||||||
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
|
<!-- disable warning SA1402: File may only contain a single type -->
|
||||||
<Rule Id="SA1413" Action="None" />
|
<Rule Id="SA1402" Action="None" />
|
||||||
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
|
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
|
||||||
<Rule Id="SA1512" Action="None" />
|
<Rule Id="SA1413" Action="None" />
|
||||||
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
|
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
|
||||||
<Rule Id="SA1515" Action="None" />
|
<Rule Id="SA1512" Action="None" />
|
||||||
<!-- disable warning SA1600: Elements should be documented -->
|
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
|
||||||
<Rule Id="SA1600" Action="None" />
|
<Rule Id="SA1515" Action="None" />
|
||||||
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
|
<!-- disable warning SA1600: Elements should be documented -->
|
||||||
<Rule Id="SA1633" Action="None" />
|
<Rule Id="SA1600" Action="None" />
|
||||||
</Rules>
|
<!-- 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">
|
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
|
||||||
<!-- disable warning CA1031: Do not catch general exception types -->
|
<!-- disable warning CA1031: Do not catch general exception types -->
|
||||||
<Rule Id="CA1031" Action="Info" />
|
<Rule Id="CA1031" Action="Info" />
|
||||||
<!-- disable warning CA1032: Implement standard exception constructors -->
|
<!-- disable warning CA1032: Implement standard exception constructors -->
|
||||||
<Rule Id="CA1032" Action="Info" />
|
<Rule Id="CA1032" Action="Info" />
|
||||||
<!-- disable warning CA1062: Validate arguments of public methods -->
|
<!-- disable warning CA1062: Validate arguments of public methods -->
|
||||||
<Rule Id="CA1062" Action="Info" />
|
<Rule Id="CA1062" Action="Info" />
|
||||||
<!-- disable warning CA1716: Identifiers should not match keywords -->
|
<!-- disable warning CA1716: Identifiers should not match keywords -->
|
||||||
<Rule Id="CA1716" Action="Info" />
|
<Rule Id="CA1716" Action="Info" />
|
||||||
<!-- disable warning CA1720: Identifiers should not contain type names -->
|
<!-- disable warning CA1720: Identifiers should not contain type names -->
|
||||||
<Rule Id="CA1720" Action="Info" />
|
<Rule Id="CA1720" Action="Info" />
|
||||||
<!-- disable warning CA1812: internal class that is apparently never instantiated.
|
<!-- disable warning CA1812: internal class that is apparently never instantiated.
|
||||||
If so, remove the code from the assembly.
|
If so, remove the code from the assembly.
|
||||||
If this class is intended to contain only static members, make it static -->
|
If this class is intended to contain only static members, make it static -->
|
||||||
<Rule Id="CA1812" Action="Info" />
|
<Rule Id="CA1812" Action="Info" />
|
||||||
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
|
<!-- disable warning CA1822: Member does not access instance data and can be marked as
|
||||||
<Rule Id="CA1822" Action="Info" />
|
static -->
|
||||||
<!-- disable warning CA2000: Dispose objects before losing scope -->
|
<Rule Id="CA1822" Action="Info" />
|
||||||
<Rule Id="CA2000" 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 -->
|
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
|
||||||
<Rule Id="CA1054" Action="None" />
|
<Rule Id="CA1054" Action="None" />
|
||||||
<!-- disable warning CA1055: URI return values should not be strings -->
|
<!-- disable warning CA1055: URI return values should not be strings -->
|
||||||
<Rule Id="CA1055" Action="None" />
|
<Rule Id="CA1055" Action="None" />
|
||||||
<!-- disable warning CA1056: URI properties should not be strings -->
|
<!-- disable warning CA1056: URI properties should not be strings -->
|
||||||
<Rule Id="CA1056" Action="None" />
|
<Rule Id="CA1056" Action="None" />
|
||||||
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
|
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
|
||||||
<Rule Id="CA1303" Action="None" />
|
<Rule Id="CA1303" Action="None" />
|
||||||
<!-- disable warning CA1308: Normalize strings to uppercase -->
|
<!-- disable warning CA1308: Normalize strings to uppercase -->
|
||||||
<Rule Id="CA1308" Action="None" />
|
<Rule Id="CA1308" Action="None" />
|
||||||
</Rules>
|
</Rules>
|
||||||
</RuleSet>
|
</RuleSet>
|
||||||
|
|||||||
+102
-1
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This plugin has been tested to work against various providers, though not all providers provide support for all of this plugins' features.
|
This plugin has been tested to work against various providers, though not all providers provide support for all of this plugins' features.
|
||||||
|
|
||||||
|
❗ Before you proceed, make sure you have another admin account if you are going to link SSO provider to the only admin account on the server, permission might get overwritten (see [#212](https://github.com/9p4/jellyfin-plugin-sso/issues/212)).
|
||||||
|
|
||||||
## TOC / Tested Providers:
|
## TOC / Tested Providers:
|
||||||
|
|
||||||
This section is broken into providers that support Role-Based Access Control (RBAC), and those that do not
|
This section is broken into providers that support Role-Based Access Control (RBAC), and those that do not
|
||||||
@@ -10,8 +12,9 @@ This section is broken into providers that support Role-Based Access Control (RB
|
|||||||
|
|
||||||
- ✅ [Authelia](#authelia)
|
- ✅ [Authelia](#authelia)
|
||||||
- ✅ [authentik](#authentik)
|
- ✅ [authentik](#authentik)
|
||||||
- [✅ Keycloak](#keycloak-oidc)
|
- ✅ [Keycloak](#keycloak-oidc)
|
||||||
- Both [OIDC](#keycloak-oidc) & [SAML](#keycloak-saml)
|
- Both [OIDC](#keycloak-oidc) & [SAML](#keycloak-saml)
|
||||||
|
- ✅ [Pocket ID](#pocket-id)
|
||||||
|
|
||||||
### No RBAC Support
|
### No RBAC Support
|
||||||
|
|
||||||
@@ -42,6 +45,25 @@ Authelia is simple to configure, and RBAC is straightforward.
|
|||||||
|
|
||||||
Below is the `identity_providers` section of an Authelia config:
|
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
|
```yaml
|
||||||
identity_providers:
|
identity_providers:
|
||||||
oidc:
|
oidc:
|
||||||
@@ -69,6 +91,7 @@ authelia:
|
|||||||
OidSecret: <redacted>
|
OidSecret: <redacted>
|
||||||
RoleClaim: groups
|
RoleClaim: groups
|
||||||
OidScopes: ["groups"]
|
OidScopes: ["groups"]
|
||||||
|
DisablePushedAuthorization: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## authentik
|
## authentik
|
||||||
@@ -149,6 +172,7 @@ Ensure that the following configuration options are set:
|
|||||||
- Access Type: Confidential
|
- Access Type: Confidential
|
||||||
- Standard Flow Enabled
|
- Standard Flow Enabled
|
||||||
- Redirect URI: https://myjellyfin.example.com/sso/OID/redirect/PROVIDER_NAME
|
- 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
|
- 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.
|
Press the "Save" button at the bottom of the page and open the "Credentials" tab. Note down the secret.
|
||||||
@@ -202,3 +226,80 @@ keycloak:
|
|||||||
SamlClientId: <same-as-in-keycloak>
|
SamlClientId: <same-as-in-keycloak>
|
||||||
SamlCertificate: <copied-from-xml-file>
|
SamlCertificate: <copied-from-xml-file>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Pocket ID
|
||||||
|
|
||||||
|
A simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.
|
||||||
|
|
||||||
|
### Pocket ID Config
|
||||||
|
|
||||||
|
1. Login to you Pocket ID admin account
|
||||||
|
1. Go to `Administration -> OCID Clients`
|
||||||
|
1. Click `Add OCID Client`
|
||||||
|
1. Give the client a name e.g. `Jellyfin`
|
||||||
|
1. Set the `Clent Launch URL` to your Jellyfin endpoint
|
||||||
|
1. Set the callbak url to `https://jellyfin.example.com/sso/OID/redirect/pocketid`. The `pocketid` part must match the `Name of OpenID Provider` in the Jellyfin SSO provider
|
||||||
|
1. (optional) Enable PKCE if Jellyfin is an https endpoint
|
||||||
|
1. (optional) Set a logo
|
||||||
|
1. (optional) Set `Allowed User Groups`
|
||||||
|
|
||||||
|
### Jellyfin's Config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pocketid:
|
||||||
|
OidEndpoint: https://pocketid.example.com/.well-known/openid-configuration
|
||||||
|
OidClientId: <pocket-id-client-id>
|
||||||
|
OidSecret: <pocket-id-secret>
|
||||||
|
EnableAuthorization: true # (optional) If you want Jellyfin to read group permissions from pocket id
|
||||||
|
RoleClaim: groups # (optional) If you want Jellyfin to be able to read group assignments from pocket id
|
||||||
|
AdminRoles: admin # (optional) The pocket id group which will give a user Jellyfin admin privilges
|
||||||
|
Roles: users # (optional) The pocket id group which will give a user Jellyfin access
|
||||||
|
AvatarUrlFormat: @{picture} # (optional) This will pull each users pocket id photo into Jellyfin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kanidm
|
||||||
|
|
||||||
|
Kanidm is a modern and simple identity management platform written in rust.
|
||||||
|
|
||||||
|
### Kanidm Config
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kanidm system oauth2 create jellyfin "Jellyfin" https://jellyfin.example.com/
|
||||||
|
|
||||||
|
# Set this to drop the trailing @idm.example.com in usernames
|
||||||
|
kanidm system oauth2 prefer-short-username jellyfin
|
||||||
|
|
||||||
|
kanidm system oauth2 add-redirect-url jellyfin https://jellyfin.example.com/sso/OID/redirect/kanidm
|
||||||
|
kanidm system oauth2 add-redirect-url jellyfin https://jellyfin.example.com/sso/OID/r/kanidm
|
||||||
|
|
||||||
|
# Optionally setup groups for Jellyfin
|
||||||
|
kanidm group create jellyfin_admins
|
||||||
|
kanidm group create jellyfin_users
|
||||||
|
|
||||||
|
kanidm system oauth2 update-scope-map jellyfin jellyfin_admins openid profile groups
|
||||||
|
kanidm system oauth2 update-scope-map jellyfin jellyfin_users openid profile groups
|
||||||
|
```
|
||||||
|
|
||||||
|
Get the secret used in the Jellyfin config with `kanidm system oauth2 show-basic-secret jellyfin`.
|
||||||
|
|
||||||
|
### Jellyfin's Config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
kanidm:
|
||||||
|
OidEndpoint: https://idm.example.com/oauth2/openid/jellyfin/
|
||||||
|
OidClientId: jellyfin
|
||||||
|
OidSecret: <kanidm-secret>
|
||||||
|
# (optional) If you want Jellyfin to read group permissions from kanidm
|
||||||
|
EnableAuthorization: true
|
||||||
|
OidScopes:
|
||||||
|
- groups
|
||||||
|
RoleClaim: groups
|
||||||
|
AdminsRoles:
|
||||||
|
- jellyfin_admins@idm.example.com
|
||||||
|
Roles:
|
||||||
|
- jellyfin_users@idm.example.com
|
||||||
|
# If in your setup admin accounts aren't members of the users group you need to add the admins group to roles as well
|
||||||
|
- jellyfin_admins@idm.example.com
|
||||||
|
# (optional) If you want the name attribute instead of the spn attribute as username
|
||||||
|
DefaultUsernameClaim: preferred_username
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user