🐦 fix: Prioritize OIDC Username Claims to Prevent First Name Usernames (#8695)

Now prioritizes preferred_username claim, then the nonstandard
username claim, then email.

Removed given_name as a possible username choice to avoid exposing users’ first names as
usernames.

Updated openidStrategy.spec.js to reflect the new claim order.

Fixed mock OpenID server behavior where preferred_username was always
hardcoded, causing test failures.

Adjusted OpenID setup test to align with new username parameter
behavior.
This commit is contained in:
Josh Mullin 2025-07-30 14:43:42 -04:00 committed by GitHub
parent 1050346915
commit 19a8f5c545
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 17 deletions

View file

@ -353,7 +353,7 @@ async function setupOpenId() {
username = userinfo[process.env.OPENID_USERNAME_CLAIM]; username = userinfo[process.env.OPENID_USERNAME_CLAIM];
} else { } else {
username = convertToUsername( username = convertToUsername(
userinfo.username || userinfo.given_name || userinfo.email, userinfo.preferred_username || userinfo.username || userinfo.email,
); );
} }

View file

@ -52,9 +52,7 @@ jest.mock('openid-client', () => {
}), }),
fetchUserInfo: jest.fn().mockImplementation((config, accessToken, sub) => { fetchUserInfo: jest.fn().mockImplementation((config, accessToken, sub) => {
// Only return additional properties, but don't override any claims // Only return additional properties, but don't override any claims
return Promise.resolve({ return Promise.resolve({});
preferred_username: 'preferred_username',
});
}), }),
customFetch: Symbol('customFetch'), customFetch: Symbol('customFetch'),
}; };
@ -104,6 +102,7 @@ describe('setupOpenId', () => {
given_name: 'First', given_name: 'First',
family_name: 'Last', family_name: 'Last',
name: 'My Full', name: 'My Full',
preferred_username: 'testusername',
username: 'flast', username: 'flast',
picture: 'https://example.com/avatar.png', picture: 'https://example.com/avatar.png',
}), }),
@ -156,20 +155,20 @@ describe('setupOpenId', () => {
verifyCallback = require('openid-client/passport').__getVerifyCallback(); verifyCallback = require('openid-client/passport').__getVerifyCallback();
}); });
it('should create a new user with correct username when username claim exists', async () => { it('should create a new user with correct username when preferred_username claim exists', async () => {
// Arrange our userinfo already has username 'flast' // Arrange our userinfo already has preferred_username 'testusername'
const userinfo = tokenset.claims(); const userinfo = tokenset.claims();
// Act // Act
const { user } = await validate(tokenset); const { user } = await validate(tokenset);
// Assert // Assert
expect(user.username).toBe(userinfo.username); expect(user.username).toBe(userinfo.preferred_username);
expect(createUser).toHaveBeenCalledWith( expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
provider: 'openid', provider: 'openid',
openidId: userinfo.sub, openidId: userinfo.sub,
username: userinfo.username, username: userinfo.preferred_username,
email: userinfo.email, email: userinfo.email,
name: `${userinfo.given_name} ${userinfo.family_name}`, name: `${userinfo.given_name} ${userinfo.family_name}`,
}), }),
@ -179,12 +178,12 @@ describe('setupOpenId', () => {
); );
}); });
it('should use given_name as username when username claim is missing', async () => { it('should use username as username when preferred_username claim is missing', async () => {
// Arrange remove username from userinfo // Arrange remove preferred_username from userinfo
const userinfo = { ...tokenset.claims() }; const userinfo = { ...tokenset.claims() };
delete userinfo.username; delete userinfo.preferred_username;
// Expect the username to be the given name (unchanged case) // Expect the username to be the "username"
const expectUsername = userinfo.given_name; const expectUsername = userinfo.username;
// Act // Act
const { user } = await validate({ ...tokenset, claims: () => userinfo }); const { user } = await validate({ ...tokenset, claims: () => userinfo });
@ -199,11 +198,11 @@ describe('setupOpenId', () => {
); );
}); });
it('should use email as username when username and given_name are missing', async () => { it('should use email as username when username and preferred_username are missing', async () => {
// Arrange remove username and given_name // Arrange remove username and preferred_username
const userinfo = { ...tokenset.claims() }; const userinfo = { ...tokenset.claims() };
delete userinfo.username; delete userinfo.username;
delete userinfo.given_name; delete userinfo.preferred_username;
const expectUsername = userinfo.email; const expectUsername = userinfo.email;
// Act // Act
@ -289,7 +288,7 @@ describe('setupOpenId', () => {
expect.objectContaining({ expect.objectContaining({
provider: 'openid', provider: 'openid',
openidId: userinfo.sub, openidId: userinfo.sub,
username: userinfo.username, username: userinfo.preferred_username,
name: `${userinfo.given_name} ${userinfo.family_name}`, name: `${userinfo.given_name} ${userinfo.family_name}`,
}), }),
); );