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
@@ -145,6 +145,7 @@ beforeEach(() => {
resetTestDb(testDb);
delete process.env.OIDC_ADMIN_VALUE;
delete process.env.OIDC_ADMIN_CLAIM;
delete process.env.OIDC_USERNAME_CLAIM;
delete process.env.NODE_ENV;
});
@@ -529,6 +530,48 @@ describe('findOrCreateUser', () => {
expect((result as { user: any }).user.role).toBe('admin');
});
it('OIDC-SVC-022b: without OIDC_USERNAME_CLAIM the username comes from name, as before (#1677)', () => {
createUser(testDb, { email: 'someone@example.com' });
const result = svc.findOrCreateUser(
{ sub: 'sub-jane-1', email: 'jane@example.com', name: 'Jane Doe', preferred_username: 'jane' },
MOCK_CONFIG
);
expect((result as { user: { username: string } }).user.username).toBe('JaneDoe');
});
it('OIDC-SVC-022c: OIDC_USERNAME_CLAIM picks the claim the username is built from (#1677)', () => {
createUser(testDb, { email: 'someone@example.com' });
process.env.OIDC_USERNAME_CLAIM = 'preferred_username';
const result = svc.findOrCreateUser(
{ sub: 'sub-jane-2', email: 'jane2@example.com', name: 'Jane Doe', preferred_username: 'jane.d' },
MOCK_CONFIG
);
expect((result as { user: { username: string } }).user.username).toBe('jane.d');
});
it('OIDC-SVC-022d: a named claim the provider leaves empty falls back to name, then the email (#1677)', () => {
createUser(testDb, { email: 'someone@example.com' });
process.env.OIDC_USERNAME_CLAIM = 'nickname';
const withName = svc.findOrCreateUser(
{ sub: 'sub-jane-3', email: 'jane3@example.com', name: 'Jane Three', nickname: ' ' },
MOCK_CONFIG
);
expect((withName as { user: { username: string } }).user.username).toBe('JaneThree');
const bare = svc.findOrCreateUser({ sub: 'sub-jane-4', email: 'j.four@example.com' }, MOCK_CONFIG);
expect((bare as { user: { username: string } }).user.username).toBe('j.four');
});
it('OIDC-SVC-022e: a later login never renames the account to the claim', () => {
const { user } = createUser(testDb, { email: 'kept@example.com', username: 'chosen' });
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ? WHERE id = ?').run('sub-kept', MOCK_CONFIG.issuer, user.id);
process.env.OIDC_USERNAME_CLAIM = 'preferred_username';
const result = svc.findOrCreateUser(
{ sub: 'sub-kept', email: 'kept@example.com', name: 'Kept', preferred_username: 'idp-name' },
MOCK_CONFIG
);
expect((result as { user: { username: string } }).user.username).toBe('chosen');
});
it('OIDC-SVC-024: returns registration_disabled error when registration is off', () => {
createUser(testDb, { email: 'existing@example.com' });
testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'false')").run();