diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 1b0728dac0..f33fdb82ad 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -510,7 +510,7 @@ async function applyOpenIdRoleSync({ decodeToken: jwtDecode, resolveGroupOverage, }); - if (!openIdRoleValues) { + if (openIdRoleValues === undefined) { logger.warn( `[openidStrategy] OpenID role sync skipped; claim '${options.claim}' was not found, invalid, or unresolved`, ); diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index 4c09b527ca..916fac5d07 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -1652,6 +1652,24 @@ describe('setupOpenId', () => { expect(user.role).toBe('USER'); }); + it('uses fallback when the role claim is present but empty', async () => { + // The required-role gate reads the same `roles` claim this test empties, so + // disable it to model an IdP that authenticates the user yet emits no roles. + delete process.env.OPENID_REQUIRED_ROLE; + jwtDecode.mockReturnValue({ + roles: '', + permissions: ['not-admin'], + }); + + const { user } = await validate(tokenset); + + expect(user.role).toBe('USER'); + expect(updateUser).toHaveBeenCalledWith( + 'newUserId', + expect.objectContaining({ role: 'USER' }), + ); + }); + it('rejects login when configured sync roles do not exist', async () => { findRolesByNames.mockImplementation(async (roleNames) => roleNames diff --git a/packages/api/src/middleware/remoteAgentAuth.spec.ts b/packages/api/src/middleware/remoteAgentAuth.spec.ts index 29e111234b..f7c7c96db0 100644 --- a/packages/api/src/middleware/remoteAgentAuth.spec.ts +++ b/packages/api/src/middleware/remoteAgentAuth.spec.ts @@ -1500,6 +1500,22 @@ describe('createRemoteAgentAuth', () => { expect(req.user).toMatchObject({ role: 'USER' }); }); + it('applies fallback when the role claim is present but empty', async () => { + enableApiRoleSync(); + setupOidcMocks({ + sub: 'sub123', + email: 'agent@test.com', + roles: '', + }); + + const deps = makeDeps(); + const req = makeReq({ authorization: `Bearer ${FAKE_TOKEN}` }); + await createRemoteAgentAuth(deps)(req as Request, makeRes().res, mockNext); + + expect(deps.updateUser).toHaveBeenCalledWith('uid123', { role: 'USER' }); + expect(req.user).toMatchObject({ role: 'USER' }); + }); + it('preserves an existing ADMIN role because generic role sync cannot manage admin', async () => { enableApiRoleSync(); setupOidcMocks({ diff --git a/packages/api/src/middleware/remoteAgentAuth.ts b/packages/api/src/middleware/remoteAgentAuth.ts index dc50e5428c..7e7dc483c4 100644 --- a/packages/api/src/middleware/remoteAgentAuth.ts +++ b/packages/api/src/middleware/remoteAgentAuth.ts @@ -480,7 +480,7 @@ async function selectOpenIdRoleForOpenIdSync( accessClaims: payload, decodeToken: () => payload, }); - if (!openIdRoleValues) { + if (openIdRoleValues === undefined) { logger.warn( `[remoteAgentAuth] OpenID role sync skipped; claim '${options.claim}' was not found or invalid`, );