forked from mirrors/jellyfin-plugin-sso
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87edc5f90a | |||
| 304170e214 | |||
| 9321c25f20 | |||
| ebf7eef83e | |||
| 065ea8b5cd | |||
| 0476f817a6 | |||
| 3bb241b6d2 | |||
| cb26ba9852 | |||
| 1b6761bf73 | |||
| eeabc3cc46 | |||
| e65c5add95 | |||
| fce636bddd | |||
| 94d1c7a95a | |||
| a1a355591c | |||
| 71802d954c | |||
| f37e8dd3bb | |||
| 9b667b3175 | |||
| 2c3fc4d8e1 | |||
| f897704100 | |||
| d0b9b10a36 | |||
| 172f22e19f | |||
| 5777c8038e | |||
| 95776b427b | |||
| b82a3867e4 | |||
| 657f90ef13 | |||
| dd01e729b8 | |||
| f2452103bf | |||
| 076cb1b893 | |||
| defab9c4e8 | |||
| 2ead846ed8 | |||
| 8b755361b4 | |||
| 340eb2c6c3 | |||
| 08d24ba0db | |||
| 0d19a3376f | |||
| 1de72effca | |||
| 698fbce81b | |||
| c6cac45004 | |||
| dfd519ae9d | |||
| 40120dd127 | |||
| d78e8c88a3 |
@@ -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']
|
||||
@@ -1,11 +0,0 @@
|
||||
name: Lint Commit Messages
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: wagoid/commitlint-github-action@4caf21aed4a778f940d0b17eb109942ef167bb27
|
||||
@@ -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}'
|
||||
|
||||
@@ -32,9 +32,9 @@ 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!**
|
||||
**[This is for Jellyfin 10.8](https://github.com/9p4/jellyfin-plugin-sso/issues/3) and only on the Web UI and clients supporting [Quick Connect](https://jellyfin.org/docs/general/server/quick-connect)**
|
||||
|
||||
**This README reflects the branch it is currently on! Switch tags to view version-specific documentation!**
|
||||
|
||||
@@ -91,6 +91,37 @@ The nightly build may have new features unavailable in other builds, but **be wa
|
||||
|
||||
## Examples
|
||||
|
||||
**Note that you should add both "/r/" and "/redirect/" paths to your SSO provider's configuration!**
|
||||
|
||||
### Creating A Login Button On The Main Page
|
||||
|
||||
In the Jellyfin administration UI, under "General", there is a "Branding" section. In that section, add the following code in the "Login disclaimer" block (replacing `PROVIDER_NAME` and the domain):
|
||||
|
||||
```html
|
||||
<a
|
||||
href="https://jellyfin.example.com/sso/OID/start/PROVIDER_NAME"
|
||||
class="raised cancel block emby-button"
|
||||
>Sign in with SSO</a
|
||||
>
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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 +135,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/post/PROVIDER_NAME](https://myjellyfin.example.com/sso/SAML/start/PROVIDER_NAME)
|
||||
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
|
||||
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/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 +151,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 +164,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.
|
||||
@@ -151,14 +182,20 @@ These all require authorization. Append an API key to the end of the request: `c
|
||||
- `samlClientId`: string. The SAML client ID.
|
||||
- `samlCertificate`: string. The base64 encoded SAML certificate.
|
||||
- `enabled`: boolean. Determines if the provider is enabled or not.
|
||||
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
|
||||
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. If disabled, then the permissions of users will not be modified and the Jellyfin defaults will be used instead.
|
||||
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
|
||||
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
|
||||
- `roles`: array of strings. This validates the SAML response against the `Role` attribute. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking.
|
||||
- `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.
|
||||
- `schemeOverride`: string. Sets the scheme for URLs used. Can be useful if the plugin refuses to use HTTPS URLs.
|
||||
- GET `SAML/Del/PROVIDER_NAME`: This removes a configuration for SAML for a given provider name.
|
||||
- GET `SAML/Get`: Lists the configurations currently available.
|
||||
|
||||
@@ -166,8 +203,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.
|
||||
@@ -184,20 +221,28 @@ These all require authorization. Append an API key to the end of the request: `c
|
||||
- `oidClientId`: string. The OpenID client ID.
|
||||
- `oidSecret`: string. The OpenID secret.
|
||||
- `enabled`: boolean. Determines if the provider is enabled or not.
|
||||
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
|
||||
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. If disabled, then the permissions of users will not be modified and the Jellyfin defaults will be used instead.
|
||||
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
|
||||
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
|
||||
- `roles`: array of strings. This validates the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
|
||||
- `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.
|
||||
- `schemeOverride`: string. Sets the scheme for URLs used. Can be useful if the plugin refuses to use HTTPS URLs.
|
||||
- GET `OID/Del/PROVIDER_NAME`: This removes a configuration for OpenID for a given provider name.
|
||||
- GET `OID/Get`: Lists the configurations currently available.
|
||||
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
||||
@@ -208,32 +253,38 @@ 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).
|
||||
|
||||
# Contributing
|
||||
|
||||
## Dependencies
|
||||
|
||||
This project uses Nix flakes to manage development dependencies. Run `nix develop` to use the same toolchain versions.
|
||||
|
||||
## Building
|
||||
|
||||
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll`, the `IdentityModel.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
|
||||
|
||||
### 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 +301,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
|
||||
|
||||
+201
-78
@@ -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
|
||||
{
|
||||
@@ -81,25 +82,27 @@ public class SSOController : ControllerBase
|
||||
|
||||
if (config.Enabled)
|
||||
{
|
||||
var scopes = config.OidScopes == null ? new string[2] : config.OidScopes;
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = config.OidEndpoint,
|
||||
ClientId = config.OidClientId,
|
||||
ClientSecret = config.OidSecret,
|
||||
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
|
||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||
Authority = config.OidEndpoint?.Trim(),
|
||||
ClientId = config.OidClientId?.Trim(),
|
||||
ClientSecret = config.OidSecret?.Trim(),
|
||||
RedirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
|
||||
Scope = string.Join(" ", scopes.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.");
|
||||
}
|
||||
|
||||
if (!config.EnableFolderRoles)
|
||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||
{
|
||||
StateManager[state].Folders = new List<string>(config.EnabledFolders);
|
||||
}
|
||||
@@ -108,12 +111,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 +128,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +247,7 @@ public class SSOController : ControllerBase
|
||||
if (StateManager[state].Valid)
|
||||
{
|
||||
_logger.LogInformation($"Is request linking: {isLinking}");
|
||||
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
|
||||
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(config.SchemeOverride), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,6 +272,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();
|
||||
@@ -253,12 +288,21 @@ public class SSOController : ControllerBase
|
||||
|
||||
if (config.Enabled)
|
||||
{
|
||||
bool newPath = config.NewPath;
|
||||
if (!isLinking)
|
||||
{
|
||||
newPath = Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase);
|
||||
config.NewPath = newPath;
|
||||
}
|
||||
|
||||
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/OID/{(newPath ? "redirect" : "r")}/" + provider;
|
||||
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = config.OidEndpoint,
|
||||
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 = redirectUri,
|
||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||
};
|
||||
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
|
||||
@@ -372,7 +416,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 +436,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 +458,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 +473,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(config.SchemeOverride),
|
||||
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 +508,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;
|
||||
@@ -463,6 +523,14 @@ public class SSOController : ControllerBase
|
||||
|
||||
if (config.Enabled)
|
||||
{
|
||||
bool newPath = config.NewPath;
|
||||
if (!isLinking)
|
||||
{
|
||||
newPath = Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase);
|
||||
config.NewPath = newPath;
|
||||
}
|
||||
|
||||
string redirectUri = GetRequestBase(config.SchemeOverride) + $"/sso/SAML/{(newPath ? "post" : "p")}/" + provider;
|
||||
string relayState = null;
|
||||
if (isLinking)
|
||||
{
|
||||
@@ -470,10 +538,10 @@ public class SSOController : ControllerBase
|
||||
}
|
||||
|
||||
var request = new AuthRequest(
|
||||
config.SamlClientId,
|
||||
GetRequestBase() + "/sso/SAML/p/" + provider);
|
||||
config.SamlClientId.Trim(),
|
||||
redirectUri);
|
||||
|
||||
return Redirect(request.GetRedirectUrl(config.SamlEndpoint, relayState));
|
||||
return Redirect(request.GetRedirectUrl(config.SamlEndpoint.Trim(), relayState));
|
||||
}
|
||||
|
||||
throw new ArgumentException("Provider does not exist");
|
||||
@@ -545,9 +613,11 @@ 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)
|
||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||
{
|
||||
folders = new List<string>(config.EnabledFolders);
|
||||
}
|
||||
@@ -558,21 +628,52 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.EnableFolderRoles)
|
||||
{
|
||||
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
|
||||
if (config.FolderRoleMapping != null)
|
||||
{
|
||||
if (folderRoleMap.Role.Equals(role))
|
||||
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
|
||||
{
|
||||
folders.AddRange(folderRoleMap.Folders);
|
||||
if (folderRoleMap.Role.Equals(role))
|
||||
{
|
||||
folders.AddRange(folderRoleMap.Folders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,7 +681,7 @@ public class SSOController : ControllerBase
|
||||
|
||||
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("saml", provider, samlResponse.GetNameID());
|
||||
|
||||
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), 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 +1011,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 +1028,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();
|
||||
@@ -957,7 +1063,7 @@ public class SSOController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRequestBase()
|
||||
private string GetRequestBase(string schemeOverride = null)
|
||||
{
|
||||
int requestPort = Request.Host.Port ?? -1;
|
||||
if ((requestPort == 80 && string.Equals(Request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(Request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||
@@ -965,9 +1071,14 @@ public class SSOController : ControllerBase
|
||||
requestPort = -1;
|
||||
}
|
||||
|
||||
if (schemeOverride != "http" && schemeOverride != "https")
|
||||
{
|
||||
schemeOverride = null;
|
||||
}
|
||||
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = Request.Scheme,
|
||||
Scheme = schemeOverride ?? Request.Scheme,
|
||||
Host = Request.Host.Host,
|
||||
Port = requestPort,
|
||||
Path = Request.PathBase
|
||||
@@ -1032,6 +1143,8 @@ public class TimedAuthorizeState
|
||||
Valid = false;
|
||||
Admin = false;
|
||||
IsLinking = false;
|
||||
EnableLiveTv = false;
|
||||
EnableLiveTvManagement = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1069,4 +1182,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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -101,6 +126,16 @@ public class SamlConfig
|
||||
/// </summary>
|
||||
public string DefaultProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect scheme override.
|
||||
/// </summary>
|
||||
public string SchemeOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
||||
/// </summary>
|
||||
public bool NewPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a mapping of canonical names from the provider to jellyfin user ids.
|
||||
/// </summary>
|
||||
@@ -178,6 +213,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>
|
||||
@@ -200,6 +260,16 @@ public class OidConfig
|
||||
/// </summary>
|
||||
public string DefaultProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect scheme override.
|
||||
/// </summary>
|
||||
public string SchemeOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the new, more descriptive paths are to be used.
|
||||
/// </summary>
|
||||
public bool NewPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a mapping of canonical names from the provider to jellyfin user ids.
|
||||
/// </summary>
|
||||
@@ -226,7 +296,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
@@ -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);
|
||||
|
||||
@@ -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,66 @@
|
||||
</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>
|
||||
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
|
||||
>Scheme Override</label
|
||||
>
|
||||
<input
|
||||
is="emby-input"
|
||||
id="SchemeOverride"
|
||||
type="text"
|
||||
class="sso-text"
|
||||
/>
|
||||
<div class="fieldDescription">
|
||||
If the plugin is redirecting to an insecure URL, set this to
|
||||
"https"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="SaveProvider"
|
||||
is="emby-button"
|
||||
|
||||
@@ -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
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () {
|
||||
@@ -501,6 +501,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
|
||||
// https://stackoverflow.com/a/25435165
|
||||
</script><iframe class='docs-texteventtarget-iframe' sandbox='allow-same-origin allow-forms allow-scripts' src='" + baseUrl + "' style='position: absolute;width:0;height:0;border:0;'></iframe></body></html>";
|
||||
</script><iframe 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>";
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -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.2.2"
|
||||
targetAbi: "10.8.0.0"
|
||||
framework: "net6.0"
|
||||
owner: "9p4"
|
||||
@@ -15,6 +15,12 @@ artifacts:
|
||||
- "IdentityModel.OidcClient.dll"
|
||||
- "IdentityModel.dll"
|
||||
changelog: |
|
||||
3.5.2.2: Fix linking page when using new paths
|
||||
3.5.2.1: Hotfix for SAML null checks
|
||||
3.5.2.0: Allow overriding the scheme used for generating URLs.
|
||||
3.5.1.1: Change iframe URL to point to the web UI instead of the root
|
||||
3.5.1.0: Improved paths! No more obscure "p" versus "r" URLs! Improve final redirect for automatic authentication. Add more configuration options for OpenID discovery.
|
||||
3.5.0.0: Add support for Live TV authentication. Fix various null pointer bugs.
|
||||
3.4.0.0: Add user self-service for linking existing accounts + managing existing links. Allow IDP accounts to be linked to jellyfin accounts with a different display-name.
|
||||
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
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1704842529,
|
||||
"narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -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 |
+8
-4
@@ -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:
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
@@ -132,6 +134,8 @@ authentik:
|
||||
OidScopes: ["groups"]
|
||||
```
|
||||
|
||||
If you recieve the error `Error processing request.` from Jellyfin when attempting to login and the Jellyfin logs show `Error loading discovery document: Endpoint belongs to different authority` try setting `Do not validate endpoints` in the plugin settings.
|
||||
|
||||
## Keycloak OIDC
|
||||
|
||||
Keycloak in general is a little more complicated than other providers. Ensure that you have a realm created and have some usable users.
|
||||
@@ -144,7 +148,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 +182,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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user