mirror of
https://github.com/9p4/jellyfin-plugin-sso.git
synced 2026-09-19 13:12:19 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54586640fb | ||
|
|
ee26556ddf | ||
|
|
f19cd74c19 | ||
|
|
8700686e2b | ||
|
|
9c8f6c64c7 | ||
|
|
075b4fb69f | ||
|
|
09bbe3aa47 | ||
|
|
aa135cdc65 | ||
|
|
dae0043192 | ||
|
|
32e009969b | ||
|
|
9cb3057c90 | ||
|
|
d5315a5263 | ||
|
|
e9273ca82c |
@@ -1,6 +1,6 @@
|
||||
# Jellyfin SSO Plugin
|
||||
|
||||
This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
|
||||
This plugin allows users to sign in through an SSO provider (such as Google, Microsoft, or your own provider). This enables one-click signin.
|
||||
|
||||
https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-a909-e5414878a864.mp4
|
||||
|
||||
@@ -75,7 +75,7 @@ Make sure that `clientid` is replaced with the actual client ID!
|
||||
|
||||
Example for adding an OpenID configuration with the API using [curl](https://curl.se/)
|
||||
|
||||
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAuthorization": true, "enableAllFolders": false, "enabledFolders": [], "adminRoles": ["jellyfin-admin"], "roles": ["allowed-to-use-jellyfin"], "enableFolderRoles": true, "folderRoleMapping": [{"role": "allowed-to-watch-movies", "folders": ["cc7df17e2f3509a4b5fc1d1ff0a6c4d0", "f137a2dd21bbc1b99aa5c0f6bf02a805"]}]' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
|
||||
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAuthorization": true, "enableAllFolders": false, "enabledFolders": [], "adminRoles": ["jellyfin-admin"], "roles": ["allowed-to-use-jellyfin"], "enableFolderRoles": true, "folderRoleMapping": [{"role": "allowed-to-watch-movies", "folders": ["cc7df17e2f3509a4b5fc1d1ff0a6c4d0", "f137a2dd21bbc1b99aa5c0f6bf02a805"]}], "roleClaim": "realm_access"}' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
|
||||
|
||||
The OpenID provider must have the following configuration (again, I am using Keycloak)
|
||||
|
||||
@@ -154,7 +154,7 @@ 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.
|
||||
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_roles` by default.
|
||||
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_access` by default.
|
||||
- GET `OID/Del/clientId`: This removes a configuration for OpenID for a given client ID.
|
||||
- GET `OID/Get`: Lists the configurations currently available.
|
||||
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
||||
@@ -173,7 +173,7 @@ There is also no logout callback. Logging out of Jellyfin will log you out of Je
|
||||
|
||||
~~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 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.
|
||||
**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).
|
||||
|
||||
## Credits and Thanks
|
||||
|
||||
@@ -184,3 +184,7 @@ I use the [AspNet SAML](https://github.com/jitbit/AspNetSaml/) library for the S
|
||||
I use the [IdentityModel OIDC Client](https://github.com/IdentityModel/IdentityModel.OidcClient/) library for the OpenID side of things.
|
||||
|
||||
Thanks to these projects, without which I would have been pulling my hair out implementing these protocols from scratch.
|
||||
|
||||
## Something funny about the origins of this plugin
|
||||
|
||||
It totally slipped my mind, but I had [requested this functionality a few years back](https://github.com/jellyfin/jellyfin/issues/2012). What goes around comes around, I guess.
|
||||
|
||||
@@ -145,7 +145,7 @@ public class SSOController : ControllerBase
|
||||
}
|
||||
if (StateManager[Request.Query["state"]].Valid)
|
||||
{
|
||||
return Content(WebResponse.OIDGenerator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
return Content(WebResponse.Generator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase(), mode: "OID"), MediaTypeNames.Text.Html);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -267,7 +267,7 @@ public class SSOController : ControllerBase
|
||||
// If no roles are configured, don't use RBAC
|
||||
if (config.Roles.Length == 0)
|
||||
{
|
||||
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
return Content(WebResponse.Generator(data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase(), mode: "SAML"), MediaTypeNames.Text.Html);
|
||||
}
|
||||
// Check if user is allowed to log in based on roles
|
||||
foreach (string role in samlResponse.GetCustomAttributes("Role"))
|
||||
@@ -276,7 +276,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (allowedRole.Equals(role))
|
||||
{
|
||||
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
return Content(WebResponse.Generator(data: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase(), mode: "SAML"), MediaTypeNames.Text.Html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class OIDConfig
|
||||
public bool EnableFolderRoles { get; set; }
|
||||
|
||||
[XmlArray("FolderRoleMappings")]
|
||||
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
|
||||
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMap")]
|
||||
public List<FolderRoleMap> FolderRoleMapping { get; set; }
|
||||
|
||||
public string RoleClaim { get; set; }
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
|
||||
<AssemblyVersion>3.0.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0.0</FileVersion>
|
||||
<AssemblyVersion>3.1.0.1</AssemblyVersion>
|
||||
<FileVersion>3.1.0.1</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
+21
-53
@@ -3,7 +3,11 @@ namespace Jellyfin.Plugin.SSO_Auth;
|
||||
public static class WebResponse
|
||||
{
|
||||
public static readonly string Base = @"<!DOCTYPE html>
|
||||
<html><head></head><body><script>
|
||||
<html><head></head><body>
|
||||
<p>Logging in...</p>
|
||||
<noscript>Please enable Javascript to complete the login</noscript>
|
||||
<script>
|
||||
|
||||
function isTv() {
|
||||
// This is going to be really difficult to get right
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
@@ -389,22 +393,32 @@ function getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
const sleep = (milliseconds) => {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
";
|
||||
|
||||
public static string OIDGenerator(string data, string provider, string baseUrl)
|
||||
public static string Generator(string data, string provider, string baseUrl, string mode)
|
||||
{
|
||||
return Base + @"
|
||||
async function main() {
|
||||
var data = '" + data + @"';
|
||||
while (localStorage.getItem(""_deviceId2"") == null ||
|
||||
localStorage.getItem(""jellyfin_credentials"") == null ||
|
||||
JSON.parse(localStorage.getItem(""jellyfin_credentials""))['Servers'][0]['Id'] == null) {
|
||||
// If localStorage isn't initialized yet, try again.
|
||||
await sleep(100);
|
||||
}
|
||||
var deviceId = localStorage.getItem(""_deviceId2"");
|
||||
var appName = ""Jellyfin Web"";
|
||||
var appVersion = ""10.8.0"";
|
||||
var deviceName = getDeviceName();
|
||||
var provider = '" + provider + @"';
|
||||
|
||||
var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: data, provider: '" + provider + @"'};
|
||||
var request = {deviceId, appName, appVersion, deviceName, data, provider: '" + provider + @"'};
|
||||
|
||||
var url = '" + baseUrl + @"/sso/OID/Auth';
|
||||
var url = '" + baseUrl + "/sso/" + mode + @"/Auth';
|
||||
|
||||
let response = await new Promise(resolve => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
@@ -428,60 +442,14 @@ async function main() {
|
||||
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
||||
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
||||
localStorage.setItem('enableAutoLogin', 'true');
|
||||
window.location.replace('/');
|
||||
window.location.replace('" + baseUrl + @"');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
main();
|
||||
});
|
||||
|
||||
</script></body></html>";
|
||||
}
|
||||
|
||||
public static string SamlGenerator(string xml, string provider, string baseUrl)
|
||||
{
|
||||
return Base + @"
|
||||
async function main() {
|
||||
var xml = '" + xml + @"';
|
||||
var deviceId = localStorage.getItem(""_deviceId2"");
|
||||
var appName = ""Jellyfin Web"";
|
||||
var appVersion = ""10.8.0"";
|
||||
var deviceName = getDeviceName();
|
||||
var provider = '" + provider + @"';
|
||||
|
||||
var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: xml, provider: '" + provider + @"'};
|
||||
|
||||
var url = '" + baseUrl + @"/sso/SAML/Auth';
|
||||
|
||||
let response = await new Promise(resolve => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = function(e) {
|
||||
resolve(xhr.response);
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
resolve(undefined);
|
||||
};
|
||||
xhr.send(JSON.stringify(request));
|
||||
})
|
||||
var responseJson = JSON.parse(response);
|
||||
var userId = 'user-' + responseJson['User']['Id'] + '-' + responseJson['User']['ServerId'];
|
||||
responseJson['User']['EnableAutoLogin'] = true;
|
||||
localStorage.setItem(userId, JSON.stringify(responseJson['User']));
|
||||
var jfCreds = JSON.parse(localStorage.getItem('jellyfin_credentials'));
|
||||
jfCreds['Servers'][0]['AccessToken'] = responseJson['AccessToken'];
|
||||
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
||||
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
||||
localStorage.setItem('enableAutoLogin', 'true');
|
||||
window.location.replace('/');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
main();
|
||||
});
|
||||
|
||||
</script></body></html>";
|
||||
// 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>";
|
||||
}
|
||||
}
|
||||
|
||||
+3
-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.0.0.0"
|
||||
version: "3.1.0.1"
|
||||
targetAbi: "10.8.0.0"
|
||||
framework: "net6.0"
|
||||
owner: "9p4"
|
||||
@@ -15,6 +15,8 @@ artifacts:
|
||||
- "IdentityModel.OidcClient.dll"
|
||||
- "IdentityModel.dll"
|
||||
changelog: |
|
||||
3.1.0.1: Fix redirect bug in WebResponse (#7)
|
||||
3.1.0.0: Simplify auth flow so loading the web UI is not required
|
||||
3.0.0.0: Add more RBAC features and option to unregister user from SSO
|
||||
2.0.1.0: Fix improper artifact loading
|
||||
2.0.0.0: Add RBAC and Google support
|
||||
|
||||
Reference in New Issue
Block a user