Files
Pascal Linxweiler fda59741b5
All checks were successful
Build Plugin / build (push) Successful in 1m1s
feat: sync the profile picture from the OIDC provider
Pocket ID exposes an avatar through the standard picture claim, pointing at
/api/users/{id}/profile-picture.png. On login the plugin now fetches that URL
and writes it to the Jellyfin user's profile image, so SSO users get their
avatar without setting one by hand.

The picture is fetched anonymously. The access token is deliberately not sent,
because the address ultimately originates from a token payload and must not be
treated as a trusted destination for credentials. Non-http(s) URLs, non-image
content types and anything over 5 MB are rejected, and an identical image is
not rewritten so the image cache is left alone. Any failure is logged and
swallowed: a broken avatar must never block a login.

Controlled by the new "Sync the profile picture" setting, on by default, with
the claim name configurable for providers that do not use "picture".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 15:41:51 +02:00

140 lines
7.8 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>OIDC Auth</title>
</head>
<body>
<div id="OidcAuthConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="OidcAuthConfigForm">
<div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">OIDC Auth</h2>
</div>
<div class="inputContainer">
<input is="emby-input" id="OidIssuer" type="url" label="Issuer URL" />
<div class="fieldDescription">Base URL of your OIDC provider, e.g. https://auth.example.com. The discovery document is loaded from &lt;issuer&gt;/.well-known/openid-configuration.</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="OidClientId" type="text" label="Client ID" />
</div>
<div class="inputContainer">
<input is="emby-input" id="OidClientSecret" type="password" label="Client Secret" autocomplete="new-password" />
</div>
<div class="inputContainer">
<input is="emby-input" id="OidScopes" type="text" label="Scopes" />
<div class="fieldDescription">Space separated. Default: openid profile email. Add e.g. "groups" if your provider needs it for the role claim.</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="UsernameClaim" type="text" label="Username claim" />
<div class="fieldDescription">Claim used as Jellyfin username. Default: preferred_username. Falls back to "sub".</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="RoleClaim" type="text" label="Role claim" />
<div class="fieldDescription">Claim containing groups/roles. Default: groups.</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="PictureClaim" type="text" label="Picture claim" />
<div class="fieldDescription">Claim holding a URL to the user's avatar. Default: picture.</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="AllowedRoles" type="text" label="Allowed roles" />
<div class="fieldDescription">Comma separated. Only users with one of these roles may log in. Empty = everyone.</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="AdminRoles" type="text" label="Admin roles" />
<div class="fieldDescription">Comma separated. Users with one of these roles become Jellyfin administrators. Empty = never touch admin flag.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" id="RevokeAdminWithoutRole" type="checkbox" />
<span>Also revoke admin when the admin role is missing (careful: can demote your existing admin account on SSO login)</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" id="SyncProfilePicture" type="checkbox" />
<span>Sync the profile picture from the provider on every login (overwrites a picture set in Jellyfin)</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" id="CreateUsersIfMissing" type="checkbox" />
<span>Create users on first login</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" id="SetRandomPasswordOnCreate" type="checkbox" />
<span>Set random password for auto-created users (recommended — without it, anyone can log in as them with a blank password)</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" id="DisableEndpointValidation" type="checkbox" />
<span>Disable strict endpoint validation (needed for Google and other providers whose endpoints are on a different host than the issuer)</span>
</label>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</div>
<div class="fieldDescription" style="margin-top: 1em;">
Login URL: <code>&lt;your jellyfin address&gt;/OidcAuth/login</code>
register <code>&lt;your jellyfin address&gt;/OidcAuth/callback</code> as redirect URI at your provider.
</div>
</div>
</form>
</div>
</div>
<script type="text/javascript">
(function () {
var pluginId = '96badb44-9940-4ff0-befc-5f987159152c';
var textFields = ['OidIssuer', 'OidClientId', 'OidClientSecret', 'OidScopes', 'UsernameClaim', 'RoleClaim', 'PictureClaim', 'AllowedRoles', 'AdminRoles'];
var boolFields = ['RevokeAdminWithoutRole', 'SyncProfilePicture', 'CreateUsersIfMissing', 'SetRandomPasswordOnCreate', 'DisableEndpointValidation'];
document.querySelector('#OidcAuthConfigPage').addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
textFields.forEach(function (f) { document.querySelector('#' + f).value = config[f] || ''; });
boolFields.forEach(function (f) { document.querySelector('#' + f).checked = !!config[f]; });
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#OidcAuthConfigForm').addEventListener('submit', function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
textFields.forEach(function (f) { config[f] = document.querySelector('#' + f).value; });
boolFields.forEach(function (f) { config[f] = document.querySelector('#' + f).checked; });
ApiClient.updatePluginConfiguration(pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
return false;
});
})();
</script>
</div>
</body>
</html>