Compare commits

...
Author SHA1 Message Date
Sambhav Saggi f19cd74c19 3.1.0.0 release 2022-02-18 13:43:32 -05:00
9p4 8700686e2b Remove Facebook since they don't support OIDC
Facebook only officially supports oauth2, not OpenID. Therefore, they are not officially compatible with this plugin.
2022-02-18 13:11:20 -05:00
9p4 9c8f6c64c7 Merge pull request #6 from Pfuenzle/patch-1
Fixed DeviceName and inconsistent SAML and OID behaviour
2022-02-18 13:07:13 -05:00
Sambhav Saggi 075b4fb69f Consolidate responses and improve localstorage checks 2022-02-18 13:02:04 -05:00
Sambhav Saggi 09bbe3aa47 Finalize client login flow via WebResponse 2022-02-18 12:52:01 -05:00
Sambhav Saggi aa135cdc65 Clean up and simplify JS in WebResponse 2022-02-18 12:38:55 -05:00
Pfuenzle dae0043192 Fixed DeviceName
Replaced the string "deviceName" with the correct variable for obvious reasons.
2022-02-17 15:09:55 +01:00
Sambhav Saggi 32e009969b Wait until iFrame is loaded properly (all browsers) 2022-02-16 22:40:40 -05:00
Sambhav Saggi 9cb3057c90 Update README to reflect proper Keycloak defaults 2022-02-16 21:49:55 -05:00
Sambhav Saggi d5315a5263 Clarify config values, add iframe for localstorage-less login 2022-02-16 19:16:53 -05:00
Sambhav Saggi e9273ca82c Add error message when nojs or localstorage isn't populated 2022-02-15 21:38:39 -05:00
6 changed files with 59 additions and 91 deletions
+8 -4
View File
@@ -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.
+3 -3
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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; }
+2 -2
View File
@@ -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.0</AssemblyVersion>
<FileVersion>3.1.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
+43 -80
View File
@@ -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();
@@ -391,97 +395,56 @@ function getDeviceName() {
";
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 + @"';
var deviceId = localStorage.getItem(""_deviceId2"");
var appName = ""Jellyfin Web"";
var appVersion = ""10.8.0"";
var deviceName = getDeviceName();
var provider = '" + provider + @"';
if (localStorage.getItem(""_deviceId2"") == null || localStorage.getItem(""jellyfin_credentials"") == null) {
// If localStorage isn't initialized yet, try again.
setTimeout(main, 100);
} else {
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();
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('/');
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('" + 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' src='" + baseUrl + "' style='position: absolute;width:0;height:0;border:0;'></iframe></body></html>";
}
}
+2 -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.0.0.0"
version: "3.1.0.0"
targetAbi: "10.8.0.0"
framework: "net6.0"
owner: "9p4"
@@ -15,6 +15,7 @@ artifacts:
- "IdentityModel.OidcClient.dll"
- "IdentityModel.dll"
changelog: |
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