Compare commits

...
Author SHA1 Message Date
Ersei Saggi 172f22e19f release: prepare for v3.5.1.0 2023-08-06 16:34:57 -04:00
Ersei Saggi 5777c8038e fix: improve final redirect (#109) 2023-08-06 16:29:44 -04:00
Ersei Saggi 95776b427b style: fix configPage style (prettier) 2023-08-06 16:27:01 -04:00
Ersei Saggi b82a3867e4 docs: detail Google requiring disabling of OpenID endpoint validation 2023-08-06 16:24:05 -04:00
Ersei Saggi 657f90ef13 fix: make SAML paths better; improve and add OID discovery defaults 2023-08-06 16:19:53 -04:00
Ersei Saggi dd01e729b8 docs: clarify plugin resetting permissions 2023-08-06 14:47:56 -04:00
Ersei Saggi f2452103bf style: fix README.md style 2023-08-06 14:44:23 -04:00
Ersei Saggi 076cb1b893 docs: document how to set login disclaimer (#16) 2023-08-06 14:43:09 -04:00
Ersei Saggi defab9c4e8 feat: clarify redirect paths (#84) 2023-08-06 14:32:05 -04:00
Ersei Saggi 2ead846ed8 fix: Remove funding 2023-08-05 18:58:04 -04:00
Ersei Saggi 8b755361b4 feat: 3.5.0.0 2023-08-05 18:51:31 -04:00
Ersei Saggi 340eb2c6c3 feat: add flakes, fix prettier 2023-08-05 18:48:33 -04:00
Ersei Saggi 08d24ba0db fix: try to fix prettier action 2023-08-05 18:26:46 -04:00
Ersei Saggi 0d19a3376f feat: Allow Live TV management 2023-08-05 18:04:28 -04:00
Ersei Saggi 1de72effca fix: Check for Roles being null in configuration for openid 2023-08-05 17:31:06 -04:00
Adam Zvolanek 698fbce81b add authentik redirect uri step (#127)
So users do not have to analyze the code for this line https://github.com/9p4/jellyfin-plugin-sso/blob/c6cac45004a32a7d8cf5863102ef947f792ed021/SSO-Auth/Api/SSOController.cs#L67
2023-07-10 12:35:22 -04:00
Esmond c6cac45004 fix: error in logs for #81 (#100) 2023-01-01 16:21:58 -05:00
Esmond dfd519ae9d fix: null check before trim() (#99)
* null check before trim()

* fix: #97 null check before trim()

* fix: apply null check to all trims
2022-11-06 21:53:55 -05:00
9p4 40120dd127 fix: #83 Trim whitespace in user provided strings (#89) 2022-11-03 22:42:56 -04:00
9p4 d78e8c88a3 fix: change matthewstrasiotto to strazto 2022-11-03 22:42:21 -04:00
17 changed files with 521 additions and 141 deletions
-13
View File
@@ -1,13 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: a055
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+5 -2
View File
@@ -12,9 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# Make sure the actual branch is checked out when running on pull requests
ref: ${{ github.head_ref }}
- name: Prettify code
uses: creyD/prettier_action@v4.2
uses: creyD/prettier_action@v4.3
with:
dry: True
prettier_options: '--check **/*.{js,html,md,css,scss}'
+58 -15
View File
@@ -32,7 +32,7 @@ Existing users may link new SSO accounts, or remove existing links using self-se
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 [matthewstrasiotto](https://github.com/matthewstrasiotto) 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!**
@@ -91,6 +91,35 @@ The nightly build may have new features unavailable in other builds, but **be wa
## Examples
### Creating A Login Button On The Main Page
In the Jellyfin administration UI, under "General", there is a "Branding" section. In that section, add the following code in the "Login disclaimer" block (replacing `PROVIDER_NAME` and the domain):
```html
<a
href="https://jellyfin.example.com/sso/OID/start/PROVIDER_NAME"
class="raised cancel block emby-button"
>Sign in with SSO</a
>
```
Then, add the following code in the "Custom CSS code" section:
```css
a.raised.emby-button {
padding: 0.9em 1em;
color: inherit !important;
}
.disclaimerContainer {
display: block;
}
```
![screenshot of the configuration page with the same code](img/custom-button.png)
For more information, refer to [issue #16](https://github.com/9p4/jellyfin-plugin-sso/issues/16).
### SAML
Example for adding a SAML configuration with the API using [curl](https://curl.se/):
@@ -104,9 +133,9 @@ The SAML provider must have the following configuration (I am using Keycloak, an
- Sign Documents on
- Sign Assertions off
- Client Signature Required off
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME](https://myjellyfin.example.com/sso/OID/p/PROVIDER_NAME)
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
Make sure that `clientid` is replaced with the actual client ID and `PROVIDER_NAME` is replaced with the chosen provider name!
@@ -120,7 +149,7 @@ The OpenID provider must have the following configuration (again, I am using Key
- Access Type: Confidential
- Standard Flow Enabled
- Redirect URI: [https://myjellyfin.example.com/sso/OID/r/PROVIDER_NAME](https://myjellyfin.example.com/sso/OID/r/PROVIDER_NAME)
- Redirect URI: [https://myjellyfin.example.com/sso/OID/redirect/PROVIDER_NAME](https://myjellyfin.example.com/sso/OID/redirect/PROVIDER_NAME)
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
Make sure that `clientid` is replaced with the actual client ID and `PROVIDER_NAME` is replaced with the chosen provider name!
@@ -133,8 +162,8 @@ The API is all done from a base URL of `/sso/`
#### Flow
- POST `SAML/p/PROVIDER_NAME`: This is the SAML POST endpoint. It accepts a form response from the SAML provider and returns HTML and JavaScript for the client to login with a given provider name.
- GET `SAML/p/PROVIDER_NAME`: This is the SAML initiator: it will begin the authorization flow for SAML with a given provider name.
- POST `SAML/start/PROVIDER_NAME`: This is the SAML POST endpoint. It accepts a form response from the SAML provider and returns HTML and JavaScript for the client to login with a given provider name.
- GET `SAML/start/PROVIDER_NAME`: This is the SAML initiator: it will begin the authorization flow for SAML with a given provider name.
- POST `SAML/Auth/PROVIDER_NAME`: This is the SAML client-side API: the HTML and JavaScript client will call this endpoint to receive Jellyfin credentials given a provider name. Post format is in JSON with the following keys:
- `deviceId`: string. Device ID.
- `deviceName`: string. Device name.
@@ -158,6 +187,11 @@ These all require authorization. Append an API key to the end of the request: `c
- `adminRoles`: array of strings. This uses SAML response's `Role` attributes. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- `enableLiveTvRoles`: boolean. Determines if role-based Live TV access should be used.
- `liveTvRoles`: array of strings. If `enableLiveTvRoles` is enabled, then the user's roles will be checked against these. If the user is granted permission, then the user will be able to view Live TV.
- `liveTvManagementRoles`: array of strings. If `enableLiveTvRoles` is enabled, then the user's roles will be checked against these. If the user is granted permission, then the user will be able to manage Live TV.
- `enableLiveTv`: boolean. Whether to allow Live TV by default. This applies even if `enableLiveTvRoles` is enabled.
- `enableLiveTvManagement`: boolean. Whether to allow Live TV management by default. This applies even if `enableLiveTvRoles` is enabled.
- `defaultProvider`: string. The set provider then gets assigned to the user after they have logged in. If it is not set, nothing is changed. With this, a user can login with SSO but is still able to log in via other providers later. See the `Unregister` endpoint.
- GET `SAML/Del/PROVIDER_NAME`: This removes a configuration for SAML for a given provider name.
- GET `SAML/Get`: Lists the configurations currently available.
@@ -166,8 +200,8 @@ These all require authorization. Append an API key to the end of the request: `c
#### Flow
- GET `OID/r/PROVIDER_NAME`: This is the OpenID callback path. This will return HTML and JavaScript for the client to login with a given provider name.
- GET `OID/p/PROVIDER_NAME`: This is the OpenID initiator: it will begin the authorization flow for OpenID with a given provider name.
- GET `OID/redirect/PROVIDER_NAME`: This is the OpenID callback path. This will return HTML and JavaScript for the client to login with a given provider name.
- GET `OID/start/PROVIDER_NAME`: This is the OpenID initiator: it will begin the authorization flow for OpenID with a given provider name.
- POST `OID/Auth/PROVIDER_NAME`: This is the OpenID client-side API: the HTML and JavaScript client will call this endpoint to receive Jellyfin credentials for a given provider name. Post format is in JSON with the following keys:
- `deviceId`: string. Device ID.
- `deviceName`: string. Device name.
@@ -191,13 +225,20 @@ These all require authorization. Append an API key to the end of the request: `c
- `adminRoles`: array of strings. This uses the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- `enableLiveTvRoles`: boolean. Determines if role-based Live TV access should be used.
- `liveTvRoles`: array of strings. If `enableLiveTvRoles` is enabled, then the user's roles will be checked against these. If the user is granted permission, then the user will be able to view Live TV.
- `liveTvManagementRoles`: array of strings. If `enableLiveTvRoles` is enabled, then the user's roles will be checked against these. If the user is granted permission, then the user will be able to manage Live TV.
- `enableLiveTv`: boolean. Whether to allow Live TV by default. This applies even if `enableLiveTvRoles` is enabled.
- `enableLiveTvManagement`: boolean. Whether to allow Live TV management by default. This applies even if `enableLiveTvRoles` is enabled.
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_access.roles` by default. The first element is the claim type, the subsequent values are to parse the JSON of the claim value. Use a "\\." to denote a literal ".". This expects a list of strings from the OIDC server.
- `oidScopes` : array of strings. Each contains an additional scope name to include in the OIDC request.
- For some OIDC providers (For example, [authelia](https://github.com/9p4/jellyfin-plugin-sso/issues/23#issuecomment-1112237616)), additional scopes may be required in order to validate group membership in role claim.
- Leave empty to only request the default scopes.
- `defaultProvider`: string. The set provider then gets assigned to the user after they have logged in. If it is not set, nothing is changed. With this, a user can login with SSO but is still able to log in via other providers later. See the `Unregister` endpoint.
- `defaultUsernameClaim`: string. The provider will use the claim to create the users' usernames. If not set, it fallbacks to `preferred_username`.
- `requireHttps`: boolean. Determines whether the OpenID discovery endpoint requires HTTP. It is advised NOT to set this to false. If not set, it fallbacks to `true`.
- `disableHttps`: boolean. Determines whether the OpenID discovery endpoint requires HTTPS.
- `doNotValidateEndpoints`: boolean. Determines whether the OpenID discovery process will validate endpoints. This may be required for Google.
- `doNotValidateIssuerName`: boolean. Determines whether the OpenID discovery process will validate the OpenID issuer name.
- GET `OID/Del/PROVIDER_NAME`: This removes a configuration for OpenID for a given provider name.
- GET `OID/Get`: Lists the configurations currently available.
- GET `OID/States`: Lists currently active OpenID flows in progress.
@@ -208,13 +249,15 @@ These all require authorization. Append an API key to the end of the request: `c
## Limitations
There is no GUI to sign in. You have to make it yourself! The buttons should redirect to something like this: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid) replacing `clientid` with the provider client ID and `SAML` with the auth scheme (either `SAML` or `OID`).
Logging in with an SSO account that has the same username as an existing Jellyfin account will override the permissions for the user. Use caution when overriding the administrator account!
~~Furthermore, there is no functional admin page (yet). PRs for this are welcome. In the meantime, you have to interact with the API to add or remove configurations.~~ Added by [matthewstrasiotto](https://github.com/matthewstrasiotto) 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 GUI to sign in. You have to make it yourself! The buttons should redirect to something like this: [https://myjellyfin.example.com/sso/SAML/start/clientid](https://myjellyfin.example.com/sso/SAML/start/clientid) replacing `clientid` with the provider client ID and `SAML` with the auth scheme (either `SAML` or `OID`).~~
~~Furthermore, there is no functional admin page (yet). PRs for this are welcome. In the meantime, you have to interact with the API to add or remove configurations.~~ 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 also no logout callback. Logging out of Jellyfin will log you out of Jellyfin only, instead of the SSO provider as well.
~~This only supports Jellyfin on it's own domain (for now). This is because I'm using string concatenation for generating some URLs. A PR is welcome to patch this.~~ Fixed in [PR #1](https://github.com/9p4/jellyfin-plugin-sso/pull/1).
~~This only supports Jellyfin on its own domain (for now). This is because I'm using string concatenation for generating some URLs. A PR is welcome to patch this.~~ Fixed in [PR #1](https://github.com/9p4/jellyfin-plugin-sso/pull/1).
**This only works on the web UI**. ~~The user must open the Jellyfin web UI BEFORE using the SSO program to populate some values in the localStorage.~~ Fixed by implementing a comment by [Pfuenzle](https://github.com/Pfuenzle) in [Issue #5](https://github.com/9p4/jellyfin-plugin-sso/issues/5#issuecomment-1041864820).
@@ -226,14 +269,14 @@ This is built with .NET 6.0. Build with `dotnet publish .` for the debug release
### VSCode Workflow
An example `.vscode` configuration may be found at [matthewstrasiotto/jellyfin-plugin-sso-vscode](https://github.com/matthewstrasiotto/jellyfin-plugin-sso-vscode).
An example `.vscode` configuration may be found at [strazto/jellyfin-plugin-sso-vscode](https://github.com/strazto/jellyfin-plugin-sso-vscode).
From the root of this repo, you may clone that to `.vscode`
```bash
# From repo root
git clone https://github.com/matthewstrasiotto/jellyfin-plugin-sso-vscode .vscode
git clone https://github.com/strazto/jellyfin-plugin-sso-vscode .vscode
```
## Releasing
@@ -250,7 +293,7 @@ Anything tagged/released as a formal Github release will also be built and publi
If you wish to use releases from your own fork, refer to
[Installing](#installing), however, you will need to change the url to the
manifest file, https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json
manifest file, `https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json`
so that it refers to your fork.
## Credits and Thanks
+166 -69
View File
@@ -65,7 +65,8 @@ public class SSOController : ControllerBase
/// <returns>A webpage that will complete the client-side flow.</returns>
// Actually a GET: https://github.com/IdentityModel/IdentityModel.OidcClient/issues/325
[HttpGet("OID/r/{provider}")]
public ActionResult OidPost(
[HttpGet("OID/redirect/{provider}")]
public async Task<ActionResult> OidPost(
[FromRoute] string provider,
[FromQuery] string state) // Although this is a GET function, this function is called `Post` for consistency with SAML
{
@@ -83,17 +84,18 @@ public class SSOController : ControllerBase
{
var options = new OidcClientOptions
{
Authority = config.OidEndpoint,
ClientId = config.OidClientId,
ClientSecret = config.OidSecret,
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
Authority = config.OidEndpoint?.Trim(),
ClientId = config.OidClientId?.Trim(),
ClientSecret = config.OidSecret?.Trim(),
RedirectUri = GetRequestBase() + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
options.Policy.Discovery.RequireHttps = config.RequireHttps || true;
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 currentState = StateManager[state].State;
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).Result;
var result = await oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).ConfigureAwait(false);
if (result.IsError)
{
return ReturnError(StatusCodes.Status400BadRequest, result.Error + " Try logging in again.");
@@ -108,12 +110,15 @@ public class SSOController : ControllerBase
StateManager[state].Folders = new List<string>();
}
StateManager[state].EnableLiveTv = config.EnableLiveTv;
StateManager[state].EnableLiveTvManagement = config.EnableLiveTvManagement;
foreach (var claim in result.User.Claims)
{
if (claim.Type == (config.DefaultUsernameClaim ?? "preferred_username"))
if (claim.Type == (config.DefaultUsernameClaim?.Trim() ?? "preferred_username"))
{
StateManager[state].Username = claim.Value;
if (config.Roles.Length == 0)
if (config.Roles == null || config.Roles.Length == 0)
{
StateManager[state].Valid = true;
}
@@ -122,69 +127,97 @@ public class SSOController : ControllerBase
// Role processing
// The regex matches any "." not preceded by a "\": a.b.c will be split into a, b, and c, but a.b\.c will be split into a, b.c (after processing the escaped dots)
// We have to first process the RoleClaim string
string[] segments = Regex.Split(config.RoleClaim, "(?<!\\\\)\\.");
// Now we make sure that any escaped "."s ("\.") are replaced with "."
for (int i = 0; i < segments.Length; i++)
{
segments[i] = segments[i].Replace("\\.", ".");
}
string[] segments = string.IsNullOrEmpty(config.RoleClaim) ? Array.Empty<string>() : Regex.Split(config.RoleClaim.Trim(), "(?<!\\\\)\\.");
if (claim.Type == segments[0])
if (segments.Any())
{
List<string> roles;
// If we are not using JSON values, just use the raw info from the claim value
if (segments.Length == 1)
// Now we make sure that any escaped "."s ("\.") are replaced with "."
segments = segments.Select(i => i.Replace("\\.", ".")).ToArray();
if (claim.Type == segments[0])
{
roles = new List<string> { claim.Value };
}
else
{
// We recursively traverse through the JSON data for the roles and parse it
var json = JsonConvert.DeserializeObject<IDictionary<string, object>>(claim.Value);
for (int i = 1; i < segments.Length - 1; i++)
List<string> roles;
// If we are not using JSON values, just use the raw info from the claim value
if (segments.Length == 1)
{
var segment = segments[i];
json = (json[segment] as JObject).ToObject<IDictionary<string, object>>();
roles = new List<string> { claim.Value };
}
else
{
// We recursively traverse through the JSON data for the roles and parse it
var json = JsonConvert.DeserializeObject<IDictionary<string, object>>(claim.Value);
for (int i = 1; i < segments.Length - 1; i++)
{
var segment = segments[i];
json = (json[segment] as JObject).ToObject<IDictionary<string, object>>();
}
// The final step is to take the JSON and turn it from a dictionary into a string
roles = (json[segments[^1]] as JArray).ToObject<List<string>>();
}
// The final step is to take the JSON and turn it from a dictionary into a string
roles = (json[segments[^1]] as JArray).ToObject<List<string>>();
}
foreach (string role in roles)
{
// Check if allowed to login based on roles
if (config.Roles.Length != 0)
foreach (string role in roles)
{
foreach (string validRoles in config.Roles)
// Check if allowed to login based on roles
if (config.Roles != null && config.Roles.Any())
{
if (role.Equals(validRoles))
foreach (string validRoles in config.Roles)
{
StateManager[state].Valid = true;
if (role.Equals(validRoles))
{
StateManager[state].Valid = true;
}
}
}
}
// Check if admin based on roles
if (config.AdminRoles.Length != 0)
{
foreach (string validAdminRoles in config.AdminRoles)
// Check if admin based on roles
if (config.AdminRoles != null && config.AdminRoles.Any())
{
if (role.Equals(validAdminRoles))
foreach (string validAdminRoles in config.AdminRoles)
{
StateManager[state].Admin = true;
if (role.Equals(validAdminRoles))
{
StateManager[state].Admin = true;
}
}
}
}
// Get allowed folders from roles
if (config.EnableFolderRoles)
{
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
// Get allowed folders from roles
if (config.EnableFolderRoles)
{
if (role.Equals(folderRoleMap.Role))
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
StateManager[state].Folders.AddRange(folderRoleMap.Folders);
if (role.Equals(folderRoleMap.Role?.Trim()))
{
StateManager[state].Folders.AddRange(folderRoleMap.Folders);
}
}
}
if (config.EnableLiveTvRoles)
{
// Check if allowed Live TV based on roles
if (config.LiveTvRoles != null && config.LiveTvRoles.Any())
{
foreach (string validLiveTvRoles in config.LiveTvRoles)
{
if (role.Equals(validLiveTvRoles))
{
StateManager[state].EnableLiveTv = true;
}
}
}
// Check if allowed Live TV management based on roles
if (config.LiveTvManagementRoles != null && config.LiveTvManagementRoles.Any())
{
foreach (string validLiveTvManagementRoles in config.LiveTvManagementRoles)
{
if (role.Equals(validLiveTvManagementRoles))
{
StateManager[state].EnableLiveTvManagement = true;
}
}
}
}
}
@@ -238,6 +271,7 @@ public class SSOController : ControllerBase
/// <param name="isLinking">Whether or not this request is to link accounts (Rather than authenticate).</param>
/// <returns>An asynchronous result for the authentication.</returns>
[HttpGet("OID/p/{provider}")]
[HttpGet("OID/start/{provider}")]
public async Task<ActionResult> OidChallenge(string provider, [FromQuery] bool isLinking = false)
{
Invalidate();
@@ -255,10 +289,10 @@ public class SSOController : ControllerBase
{
var options = new OidcClientOptions
{
Authority = config.OidEndpoint,
ClientId = config.OidClientId,
ClientSecret = config.OidSecret,
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
Authority = config.OidEndpoint?.Trim(),
ClientId = config.OidClientId?.Trim(),
ClientSecret = config.OidSecret?.Trim(),
RedirectUri = GetRequestBase() + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
@@ -372,7 +406,7 @@ public class SSOController : ControllerBase
{
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(), response, config.DefaultProvider)
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())
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -392,6 +426,7 @@ public class SSOController : ControllerBase
/// </param>
/// <returns>A webpage that will complete the client-side flow.</returns>
[HttpPost("SAML/p/{provider}")]
[HttpPost("SAML/post/{provider}")]
public ActionResult SamlPost(string provider, [FromQuery] string relayState = null)
{
SamlConfig config;
@@ -413,10 +448,12 @@ public class SSOController : ControllerBase
{
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
bool valid = false;
// If no roles are configured, don't use RBAC
if (config.Roles.Length == 0)
{
return Content(WebResponse.Generator(data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase(), mode: "SAML", isLinking: isLinking), MediaTypeNames.Text.Html);
valid = true;
}
// Check if user is allowed to log in based on roles
@@ -426,11 +463,23 @@ public class SSOController : ControllerBase
{
if (allowedRole.Equals(role))
{
return Content(WebResponse.Generator(data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase(), mode: "SAML", isLinking: isLinking), MediaTypeNames.Text.Html);
valid = true;
}
}
}
if (valid)
{
return Content(
WebResponse.Generator(
data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)),
provider: provider,
baseUrl: GetRequestBase(),
mode: "SAML",
isLinking: isLinking),
MediaTypeNames.Text.Html);
}
_logger.LogWarning(
"SAML user: {UserId} has insufficient roles: {@Roles}. Expected any one of: {@ExpectedRoles}",
samlResponse.GetNameID(),
@@ -449,6 +498,7 @@ public class SSOController : ControllerBase
/// <param name="isLinking">Whether this flow intends to link an account, or initiate auth.</param>
/// <returns>A redirect to the SAML provider's auth page.</returns>
[HttpGet("SAML/p/{provider}")]
[HttpGet("SAML/start/{provider}")]
public RedirectResult SamlChallenge(string provider, [FromQuery] bool isLinking = false)
{
SamlConfig config;
@@ -470,10 +520,10 @@ public class SSOController : ControllerBase
}
var request = new AuthRequest(
config.SamlClientId,
GetRequestBase() + "/sso/SAML/p/" + provider);
config.SamlClientId.Trim(),
GetRequestBase() + $"/sso/SAML/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "post" : "p")}/" + provider);
return Redirect(request.GetRedirectUrl(config.SamlEndpoint, relayState));
return Redirect(request.GetRedirectUrl(config.SamlEndpoint.Trim(), relayState));
}
throw new ArgumentException("Provider does not exist");
@@ -545,6 +595,8 @@ public class SSOController : ControllerBase
if (config.Enabled)
{
bool isAdmin = false;
bool liveTv = config.EnableLiveTv;
bool liveTvManagement = config.EnableLiveTvManagement;
var samlResponse = new Response(config.SamlCertificate, response.Data);
List<string> folders;
if (!config.EnableFolderRoles)
@@ -558,11 +610,14 @@ public class SSOController : ControllerBase
foreach (string role in samlResponse.GetCustomAttributes("Role"))
{
foreach (string allowedRole in config.AdminRoles)
if (config.AdminRoles != null)
{
if (allowedRole.Equals(role))
foreach (string allowedRole in config.AdminRoles)
{
isAdmin = true;
if (allowedRole.Equals(role))
{
isAdmin = true;
}
}
}
@@ -576,11 +631,36 @@ public class SSOController : ControllerBase
}
}
}
if (config.EnableLiveTvRoles)
{
if (config.LiveTvRoles != null)
{
foreach (string allowedLiveTvRole in config.LiveTvRoles)
{
if (allowedLiveTvRole.Equals(role))
{
liveTv = true;
}
}
}
if (config.LiveTvManagementRoles != null)
{
foreach (string allowedLiveTvManagementRole in config.LiveTvManagementRoles)
{
if (allowedLiveTvManagementRole.Equals(role))
{
liveTvManagement = true;
}
}
}
}
}
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("saml", provider, samlResponse.GetNameID());
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), response, config.DefaultProvider)
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), liveTv, liveTvManagement, response, config.DefaultProvider.Trim())
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -910,9 +990,11 @@ public class SSOController : ControllerBase
/// <param name="enableAuthorization">Determines whether RBAC is used for this user.</param>
/// <param name="enableAllFolders">Determines whether all folders are enabled.</param>
/// <param name="enabledFolders">Determines which folders should be enabled for this client.</param>
/// <param name="enableLiveTv">Determines whether live TV access is allowed for 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="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, AuthResponse authResponse, string defaultProvider)
private async Task<AuthenticationResult> Authenticate(Guid userId, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, bool enableLiveTv, bool enableLiveTvAdmin, AuthResponse authResponse, string defaultProvider)
{
User user = _userManager.GetUserById(userId);
if (enableAuthorization)
@@ -925,6 +1007,9 @@ public class SSOController : ControllerBase
}
}
user.SetPermission(PermissionKind.EnableLiveTvAccess, enableLiveTv);
user.SetPermission(PermissionKind.EnableLiveTvManagement, enableLiveTvAdmin);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
var authRequest = new AuthenticationRequest();
@@ -1032,6 +1117,8 @@ public class TimedAuthorizeState
Valid = false;
Admin = false;
IsLinking = false;
EnableLiveTv = false;
EnableLiveTvManagement = false;
}
/// <summary>
@@ -1069,4 +1156,14 @@ public class TimedAuthorizeState
/// Gets or sets the folders the user is allowed access to.
/// </summary>
public List<string> Folders { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is allowed to view live TV.
/// </summary>
public bool EnableLiveTv { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is allowed to manage live TV.
/// </summary>
public bool EnableLiveTvManagement { get; set; }
}
+61 -1
View File
@@ -89,6 +89,31 @@ public class SamlConfig
/// </summary>
public bool EnableFolderRoles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether RBAC is used to manage Live TV access.
/// </summary>
public bool EnableLiveTvRoles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Live TV is enabled by default.
/// </summary>
public bool EnableLiveTv { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Live TV is allowed to be managed by default.
/// </summary>
public bool EnableLiveTvManagement { get; set; }
/// <summary>
/// Gets or sets the roles that are checked to determine whether the user is allowed to view Live TV.
/// </summary>
public string[] LiveTvRoles { get; set; }
/// <summary>
/// Gets or sets the roles that are checked to determine whether the user is allowed to manage Live TV.
/// </summary>
public string[] LiveTvManagementRoles { get; set; }
/// <summary>
/// Gets or sets which folders map to what roles in RBAC.
/// </summary>
@@ -178,6 +203,31 @@ public class OidConfig
/// </summary>
public bool EnableFolderRoles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether RBAC is used to manage Live TV access.
/// </summary>
public bool EnableLiveTvRoles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Live TV is enabled by default.
/// </summary>
public bool EnableLiveTv { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Live TV is allowed to be managed by default.
/// </summary>
public bool EnableLiveTvManagement { get; set; }
/// <summary>
/// Gets or sets the roles that are checked to determine whether the user is allowed to view Live TV.
/// </summary>
public string[] LiveTvRoles { get; set; }
/// <summary>
/// Gets or sets the roles that are checked to determine whether the user is allowed to manage Live TV.
/// </summary>
public string[] LiveTvManagementRoles { get; set; }
/// <summary>
/// Gets or sets which folders map to what roles in RBAC.
/// </summary>
@@ -226,7 +276,17 @@ public class OidConfig
/// <summary>
/// Gets or sets a value indicating whether HTTPS in the discovery endpoint is required.
/// </summary>
public bool RequireHttps { get; set; }
public bool DisableHttps { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OpenID endpoints are validated.
/// </summary>
public bool DoNotValidateEndpoints { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OpenID issuer name is validated.
/// </summary>
public bool DoNotValidateIssuerName { get; set; }
}
/// <summary>
+17 -17
View File
@@ -4,7 +4,7 @@ const ssoConfigurationPage = {
ApiClient.getPluginConfiguration(ssoConfigurationPage.pluginUniqueId).then(
(config) => {
ssoConfigurationPage.populateProviders(page, config.OidConfigs);
}
},
);
const folder_container = page.querySelector("#EnabledFolders");
@@ -43,7 +43,7 @@ const ssoConfigurationPage = {
return ApiClient.getJSON(
ApiClient.getUrl("Library/MediaFolders", {
IsHidden: false,
})
}),
).then((folders) => {
ssoConfigurationPage._populateFolders(container, folders);
});
@@ -119,8 +119,8 @@ const ssoConfigurationPage = {
.then(() =>
ssoConfigurationPage.populateEnabledFolders(
enabled_folders,
checklist
)
checklist,
),
);
elem.querySelector(".sso-role-mapping-name").value = mapping["Role"];
@@ -128,7 +128,7 @@ const ssoConfigurationPage = {
.querySelector(".sso-remove-role-mapping")
.addEventListener(
"click",
ssoConfigurationPage.handleRoleMappingRemove
ssoConfigurationPage.handleRoleMappingRemove,
);
return elem;
@@ -168,11 +168,11 @@ const ssoConfigurationPage = {
const oidc_form = page.querySelector("#sso-new-oidc-provider");
const text_fields = [...oidc_form.querySelectorAll(text_class)].map(
(e) => e.id
(e) => e.id,
);
const json_fields = [...oidc_form.querySelectorAll(json_class)].map(
(e) => e.id
(e) => e.id,
);
const text_list_fields = [
@@ -180,7 +180,7 @@ const ssoConfigurationPage = {
].map((e) => e.id);
const check_fields = [...oidc_form.querySelectorAll(toggle_class)].map(
(e) => e.id
(e) => e.id,
);
const output = {
@@ -230,7 +230,7 @@ const ssoConfigurationPage = {
if (provider[id])
ssoConfigurationPage.fillTextList(
provider[id],
page.querySelector("#" + id)
page.querySelector("#" + id),
);
});
@@ -238,7 +238,7 @@ const ssoConfigurationPage = {
if (provider[id]) {
ssoConfigurationPage.populateEnabledFolders(
provider[id],
page.querySelector(`#${id}`)
page.querySelector(`#${id}`),
);
}
});
@@ -252,20 +252,20 @@ const ssoConfigurationPage = {
if (provider[id])
ssoConfigurationPage.populateRoleMappings(provider[id], elem);
});
}
},
);
},
deleteProvider: (page, provider_name) => {
if (
!window.confirm(
`Are you sure you want to delete the provider ${provider_name}?`
`Are you sure you want to delete the provider ${provider_name}?`,
)
) {
return;
}
return new Promise((resolve) => {
ApiClient.getPluginConfiguration(
ssoConfigurationPage.pluginUniqueId
ssoConfigurationPage.pluginUniqueId,
).then((config) => {
if (!config.OidConfigs.hasOwnProperty(provider_name)) {
resolve();
@@ -275,7 +275,7 @@ const ssoConfigurationPage = {
delete config.OidConfigs[provider_name];
ApiClient.updatePluginConfiguration(
ssoConfigurationPage.pluginUniqueId,
config
config,
).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
ssoConfigurationPage.loadConfiguration(page);
@@ -292,7 +292,7 @@ const ssoConfigurationPage = {
const form_elements = ssoConfigurationPage.listArgumentsByType(page);
ApiClient.getPluginConfiguration(
ssoConfigurationPage.pluginUniqueId
ssoConfigurationPage.pluginUniqueId,
).then((config) => {
var current_config = {};
if (config.OidConfigs.hasOwnProperty(provider_name)) {
@@ -315,7 +315,7 @@ const ssoConfigurationPage = {
form_elements.text_list_fields.forEach((id) => {
current_config[id] = ssoConfigurationPage.parseTextList(
page.querySelector("#" + id)
page.querySelector("#" + id),
);
});
@@ -334,7 +334,7 @@ const ssoConfigurationPage = {
ApiClient.updatePluginConfiguration(
ssoConfigurationPage.pluginUniqueId,
config
config,
).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
ssoConfigurationPage.loadConfiguration(page);
+148 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<title>SSO</title>
@@ -358,6 +358,109 @@
</div>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
id="EnableLiveTvRoles"
name="EnableLiveTvRoles"
type="checkbox"
class="sso-toggle"
/>
<span>Enable Live TV RBAC</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Determines whether the roles will be used to grant Live TV
privileges.
</div>
</div>
<div class="inputContainer">
<label
class="inputLabel inputLabelUnfocused"
for="LiveTvRoles"
>Live TV Roles:</label
>
<textarea
is="emby-textarea"
id="LiveTvRoles"
type="text"
class="sso-line-list emby-textarea"
></textarea>
<div class="fieldDescription">
A list of roles, one role per-line to look for in the OpenID
response.
<br />
Like <strong>"Roles"</strong>, but having any of the roles
confers Live TV privileges.
</div>
</div>
<div class="inputContainer">
<label
class="inputLabel inputLabelUnfocused"
for="LiveTvManagementRoles"
>Live TV Management Roles:</label
>
<textarea
is="emby-textarea"
id="LiveTvManagementRoles"
type="text"
class="sso-line-list emby-textarea"
></textarea>
<div class="fieldDescription">
A list of roles, one role per-line to look for in the OpenID
response.
<br />
Like <strong>"Roles"</strong>, but having any of the roles
confers Live TV administration privileges.
</div>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
id="EnableLiveTv"
name="EnableLiveTv"
type="checkbox"
class="sso-toggle"
/>
<span>Enable Live TV Access By Default</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Determines whether the user can view Live TV by default.
<br />
This value is still used if <strong>Live TV RBAC</strong> is
enabled!
</div>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
id="EnableLiveTvManagement"
name="EnableLiveTvManagement"
type="checkbox"
class="sso-toggle"
/>
<span>Enable Live TV Management By Default</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Determines whether the user can manage Live TV by default.
<br />
This value is still used if <strong>Live TV RBAC</strong> is
enabled!
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
>Role Claim:</label
@@ -456,6 +559,50 @@
</div>
</div>
<div class="checkboxContainer">
<label>
<input
is="emby-checkbox"
id="DisableHttps"
name="DisableHttps"
type="checkbox"
class="sso-toggle"
/>
<span>Disable OpenID HTTPS Discovery (Insecure)</span>
</label>
<div class="fieldDescription checkboxFieldDescription"></div>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
id="DoNotValidateEndpoints"
name="DoNotValidateEndpoints"
type="checkbox"
class="sso-toggle"
/>
<span>Do Not Validate OpenID Endpoints (Insecure)</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
May be required for Google OpenID
</div>
</div>
<div class="checkboxContainer">
<label>
<input
is="emby-checkbox"
id="DoNotValidateIssuerName"
name="DoNotValidateIssuerName"
type="checkbox"
class="sso-toggle"
/>
<span>Do Not Validate OpenID Issuer Name (Insecure)</span>
</label>
</div>
<button
id="SaveProvider"
is="emby-button"
+2 -2
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<!-- Polyfill styles that are missing when serving without dashboard -->
@@ -10,7 +10,7 @@
const view = document.querySelector("#sso-config-page");
document.querySelector("#theme-style").href = ApiClient.getUrl(
"/web/themes/dark/theme.css"
"/web/themes/dark/theme.css",
);
const homeButton = document.querySelector("a.emby-button.home");
+10 -10
View File
@@ -15,7 +15,7 @@ const ssoConfigLinking = {
ssoConfigLinking.loadProviderList(
provider_list_oid,
config_names,
"oid"
"oid",
);
});
});
@@ -24,7 +24,7 @@ const ssoConfigLinking = {
ssoConfigLinking.loadProviderList(
provider_list_saml,
config_names,
"saml"
"saml",
);
});
});
@@ -51,7 +51,7 @@ const ssoConfigLinking = {
></div>
`;
var add_provider = provider_config.querySelector(
".sso-provider-add-link"
".sso-provider-add-link",
);
//const provider_name_css = ssoConfigLinking.safeCSSId(provider_name);
@@ -60,7 +60,7 @@ const ssoConfigLinking = {
add_provider.classList.add("sso-provider");
add_provider.href = ApiClient.getUrl(
`/SSO/${provider_mode}/p/${provider_name}?isLinking=true`
`/SSO/${provider_mode}/p/${provider_name}?isLinking=true`,
);
container.appendChild(provider_config);
@@ -74,20 +74,20 @@ const ssoConfigLinking = {
type: "GET",
url: ApiClient.getUrl(`sso/${provider_mode}/links/${currentUserId}`),
},
true
true,
).then((resp) => {
resp.json().then((provider_map) => {
console.log({ provider_map, currentUserId });
Object.keys(provider_map).forEach((provider_name) => {
const provider_container = container.querySelector(
`.sso-provider-existing-links-container[data-provider="${provider_name}"]`
`.sso-provider-existing-links-container[data-provider="${provider_name}"]`,
);
ssoConfigLinking.populateExistingLinks(
provider_container,
provider_mode,
provider_name,
provider_map[provider_name]
provider_map[provider_name],
);
});
});
@@ -99,7 +99,7 @@ const ssoConfigLinking = {
container,
provider_mode,
provider_name,
canonical_names
canonical_names,
) => {
container
.querySelectorAll(".sso-provider-link-checkbox-wrapper")
@@ -158,7 +158,7 @@ const ssoConfigLinking = {
return ApiClient.fetch({
type: "DELETE",
url: ApiClient.getUrl(
`sso/${provider_mode}/link/${provider_name}/${currentUserId}/${canonical_name}`
`sso/${provider_mode}/link/${provider_name}/${currentUserId}/${canonical_name}`,
),
});
});
@@ -181,6 +181,6 @@ export default function (view) {
view
.querySelector("#btn-delete-selected-links")
.addEventListener("click", (e) =>
ssoConfigLinking.handleDeleteButtonPressed(e, view)
ssoConfigLinking.handleDeleteButtonPressed(e, view),
);
}
+4 -4
View File
@@ -66,7 +66,7 @@ export async function serverAddress({ basePath = "/web" }) {
url: obj.url,
config: obj.response.json(),
};
})
}),
);
})
.then((configs) => {
@@ -130,11 +130,11 @@ var localApiClient = new jellyfinApiclient.ApiClient(
appName,
appVersion,
getDeviceName(),
deviceId
deviceId,
);
localApiClient.setAuthenticationInfo(
current_server.AccessToken,
current_server.UserId
current_server.UserId,
);
var connections = new jellyfinApiclient.ConnectionManager(
@@ -143,7 +143,7 @@ var connections = new jellyfinApiclient.ConnectionManager(
appVersion,
getDeviceName(),
deviceId,
capabilities
capabilities,
);
connections.addApiClient(localApiClient);
+3 -1
View File
@@ -229,7 +229,9 @@ Emby Button
justify-content: center;
font-size: 82%;
border-radius: 100em;
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12),
box-shadow:
0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
background: #03a9f4;
font-weight: bold;
+1 -1
View File
@@ -493,7 +493,7 @@ async function main() {
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
localStorage.setItem('enableAutoLogin', 'true');
window.location.replace('" + baseUrl + @"');
window.location.replace('" + baseUrl + @"/web/index.html');
}
document.addEventListener('DOMContentLoaded', function () {
+3 -1
View File
@@ -1,7 +1,7 @@
name: "SSO Authentication"
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
version: "3.4.0.0"
version: "3.5.1.0"
targetAbi: "10.8.0.0"
framework: "net6.0"
owner: "9p4"
@@ -15,6 +15,8 @@ artifacts:
- "IdentityModel.OidcClient.dll"
- "IdentityModel.dll"
changelog: |
3.5.1.0: Improved paths! No more obscure "p" versus "r" URLs! Improve final redirect for automatic authentication. Add more configuration options for OpenID discovery.
3.5.0.0: Add support for Live TV authentication. Fix various null pointer bugs.
3.4.0.0: Add user self-service for linking existing accounts + managing existing links. Allow IDP accounts to be linked to jellyfin accounts with a different display-name.
3.3.0.0: Add fallback authentication provider. Add OpenID admin page.
3.2.0.0: Switch to hashmaps (BREAKING) for performance. Dump expected permissions in logs on error.
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1691218994,
"narHash": "sha256-46GJ5vLf9H+Oh7Jii2gJI9GATJHGbx2iQpon5nUSFPI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0d2fb29f5071a12d7983319c2c2576be6a130582",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+10
View File
@@ -0,0 +1,10 @@
{
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; };
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShell.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.prettier pkgs.dotnet-sdk ]; };
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+6 -4
View File
@@ -17,6 +17,7 @@ This section is broken into providers that support Role-Based Access Control (RB
- ✅ Google OIDC
- ❗ Usernames are numeric
- ❗ Requires disabling validating OpenID endpoints
## General Options, when RBAC is supported
@@ -52,7 +53,7 @@ identity_providers:
secret: <redacted>
authorization_policy: one_factor
redirect_uris:
- https://jellyfin.example.com/sso/OID/r/authelia
- https://jellyfin.example.com/sso/OID/redirect/authelia
```
### Jellyfin's Config
@@ -113,6 +114,7 @@ Now we can add this property mapping to authentik's Jellyfin OAuth provider:
![image](img/authentik-config-04.jpg)
- Edit / Update your Jellyfin OAuth provider
- Verify your **"Redirect URIs/Origins (RegEx)"** follows the format: `https://domain.tld/sso/OID/redirect/Authentik`.
- Under **"Advanced Protocol Settings"**, add the **Group Membership** Scope
![image](img/authentik-config-05.jpg)
@@ -144,7 +146,7 @@ Ensure that the following configuration options are set:
- Access Type: Confidential
- Standard Flow Enabled
- Redirect URI: https://myjellyfin.example.com/sso/OID/r/PROVIDER_NAME
- Redirect URI: https://myjellyfin.example.com/sso/OID/redirect/PROVIDER_NAME
- 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.
@@ -178,9 +180,9 @@ Ensure that the following configuration options are set:
- Sign Documents on
- Sign Assertions off
- Client Signature Required off
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME)
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/p/PROVIDER_NAME)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
Press the "Save" button at the bottom of the page.