feat(oidc): OIDC_USERNAME_CLAIM picks the claim a new account's username comes from

Providers like Pocket ID send the full name in 'name', which became 'JaneDoe'. OIDC_USERNAME_CLAIM=preferred_username builds the username from that claim instead. Unset, or with the claim empty, the old order applies (name, preferred_username, the email's local part), and it is only read when an account is created. (#1677)
This commit is contained in:
Maurice
2026-10-01 09:25:23 +02:00
parent ba3dc65926
commit 1fee38e8f7
12 changed files with 79 additions and 2 deletions
+3
View File
@@ -206,6 +206,9 @@ export function deriveOidc(raw: RawEnv) {
only: parseBool(raw.OIDC_ONLY) === true,
adminClaim: raw.OIDC_ADMIN_CLAIM || 'groups',
adminValue: raw.OIDC_ADMIN_VALUE,
// Unset keeps the old order (name, then preferred_username), so an instance
// that never sets it names new accounts exactly as before (#1677).
usernameClaim: raw.OIDC_USERNAME_CLAIM?.trim() || undefined,
};
}
+1
View File
@@ -97,6 +97,7 @@ export const envSchema = z.object({
OIDC_ONLY: boolStr,
OIDC_ADMIN_CLAIM: anyString,
OIDC_ADMIN_VALUE: anyString,
OIDC_USERNAME_CLAIM: anyString,
// SMTP
SMTP_HOST: anyString,
+15 -1
View File
@@ -607,6 +607,20 @@ export class OidcService implements OnModuleDestroy {
// Find or create user by OIDC sub / email
// -------------------------------------------------------------------------
/**
* The claim a new account's username comes from (#1677). OIDC_USERNAME_CLAIM
* names one, typically `preferred_username` for providers whose `name` is the
* full "Jane Doe"; when it is unset, or the provider leaves that claim empty,
* the old order applies. Only read when an account is created: a username the
* user has changed since is never overwritten on a later login.
*/
private usernameSource(userInfo: OidcUserInfo): string | undefined {
const claimKey = readEnv().oidc.usernameClaim;
const claimed = claimKey ? userInfo[claimKey] : undefined;
if (typeof claimed === 'string' && claimed.trim()) return claimed.trim();
return userInfo.name || userInfo.preferred_username;
}
findOrCreateUser(
userInfo: OidcUserInfo,
config: OidcConfig,
@@ -617,7 +631,7 @@ export class OidcService implements OnModuleDestroy {
// `oidc_error=' + result.error` pass-through if reached here.
if (!userInfo.email) return { error: 'no_email' };
const email = userInfo.email.trim().toLowerCase();
const name = userInfo.name || userInfo.preferred_username || email.split('@')[0];
const name = this.usernameSource(userInfo) || email.split('@')[0];
const sub = userInfo.sub;
const picture = safeOidcPicture(userInfo.picture);