diff --git a/src/config.ts b/src/config.ts index fa339eb19..45e1d24f5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -347,7 +347,7 @@ export interface NextConfig extends Pick { * Log users in to a specific organization. * * This will specify an `organization` parameter in your user's login request and will add a step to validate - * the `org_id` claim in your user's ID token. + * the `org_id` or `org_name` claim in your user's ID token. * * If your app supports multiple organizations, you should take a look at {@link AuthorizationParams.organization}. */ diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index ac7bc7f73..6efbf18d4 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -203,13 +203,23 @@ const idTokenValidator = (afterCallback?: AfterCallback, organization?: string): AfterCallback => (req, res, session, state) => { if (organization) { - assert(session.user.org_id, 'Organization Id (org_id) claim must be a string present in the ID token'); - assert.equal( - session.user.org_id, - organization, - `Organization Id (org_id) claim value mismatch in the ID token; ` + - `expected "${organization}", found "${session.user.org_id}"` - ); + if (organization.startsWith('org_')) { + assert(session.user.org_id, 'Organization Id (org_id) claim must be a string present in the ID token'); + assert.equal( + session.user.org_id, + organization, + `Organization Id (org_id) claim value mismatch in the ID token; ` + + `expected "${organization}", found "${session.user.org_id}"` + ); + } else { + assert(session.user.org_name, 'Organization Name (org_name) claim must be a string present in the ID token'); + assert.equal( + session.user.org_name.toLowerCase(), + organization.toLowerCase(), + `Organization Name (org_name) claim value mismatch in the ID token; ` + + `expected "${organization}", found "${session.user.org_name}"` + ); + } } if (afterCallback) { return afterCallback(req, res, session, state); diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 243e00946..223747eb4 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -115,7 +115,7 @@ export interface AuthorizationParams extends Partial { * ``` * * Your invite url can then take the format: - * `https://example.com/api/invite?invitation=invitation_id&organization=org_id`. + * `https://example.com/api/invite?invitation=invitation_id&organization=org_id_or_name`. */ invitation?: string; diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts index 0ef24f11b..c3d9437d4 100644 --- a/tests/handlers/callback.test.ts +++ b/tests/handlers/callback.test.ts @@ -313,7 +313,7 @@ describe('callback handler', () => { }); test('throws for missing org_id claim', async () => { - const baseUrl = await setup({ ...withApi, organization: 'foo' }); + const baseUrl = await setup({ ...withApi, organization: 'org_foo' }); const state = encodeState({ returnTo: baseUrl }); const cookieJar = await toSignedCookieJar( { @@ -334,8 +334,77 @@ describe('callback handler', () => { ).rejects.toThrow('Organization Id (org_id) claim must be a string present in the ID token'); }); + test('throws for missing org_name claim', async () => { + const baseUrl = await setup({ ...withApi, organization: 'foo' }); + const state = encodeState({ returnTo: baseUrl }); + const cookieJar = await toSignedCookieJar( + { + state, + nonce: '__test_nonce__' + }, + baseUrl + ); + await expect( + callback( + baseUrl, + { + state, + code: 'code' + }, + cookieJar + ) + ).rejects.toThrow('Organization Name (org_name) claim must be a string present in the ID token'); + }); + test('throws for org_id claim mismatch', async () => { - const baseUrl = await setup({ ...withApi, organization: 'foo' }, { idTokenClaims: { org_id: 'bar' } }); + const baseUrl = await setup({ ...withApi, organization: 'org_foo' }, { idTokenClaims: { org_id: 'org_bar' } }); + const state = encodeState({ returnTo: baseUrl }); + const cookieJar = await toSignedCookieJar( + { + state, + nonce: '__test_nonce__' + }, + baseUrl + ); + await expect( + callback( + baseUrl, + { + state, + code: 'code' + }, + cookieJar + ) + ).rejects.toThrow('Organization Id (org_id) claim value mismatch in the ID token; expected "org_foo", found "org_bar"'); + }); + + test('throws for org_name claim mismatch', async () => { + const baseUrl = await setup({ ...withApi, organization: 'foo' }, { idTokenClaims: { org_name: 'bar' } }); + const state = encodeState({ returnTo: baseUrl }); + const cookieJar = await toSignedCookieJar( + { + state, + nonce: '__test_nonce__' + }, + baseUrl + ); + await expect( + callback( + baseUrl, + { + state, + code: 'code' + }, + cookieJar + ) + ).rejects.toThrow('Organization Name (org_name) claim value mismatch in the ID token; expected "foo", found "bar"'); + }); + + test('accepts a valid organization id', async () => { + const baseUrl = await setup(withApi, { + idTokenClaims: { org_id: 'org_foo' }, + callbackOptions: { organization: 'org_foo' } + }); const state = encodeState({ returnTo: baseUrl }); const cookieJar = await toSignedCookieJar( { @@ -353,12 +422,15 @@ describe('callback handler', () => { }, cookieJar ) - ).rejects.toThrow('Organization Id (org_id) claim value mismatch in the ID token; expected "foo", found "bar"'); + ).resolves.not.toThrow(); + const session = await get(baseUrl, '/api/session', { cookieJar }); + + expect(session.user.org_id).toEqual('org_foo'); }); - test('accepts a valid organization', async () => { + test('accepts a valid organization name', async () => { const baseUrl = await setup(withApi, { - idTokenClaims: { org_id: 'foo' }, + idTokenClaims: { org_name: 'foo' }, callbackOptions: { organization: 'foo' } }); const state = encodeState({ returnTo: baseUrl }); @@ -381,7 +453,7 @@ describe('callback handler', () => { ).resolves.not.toThrow(); const session = await get(baseUrl, '/api/session', { cookieJar }); - expect(session.user.org_id).toEqual('foo'); + expect(session.user.org_name).toEqual('foo'); }); test('should pass custom params to the token exchange', async () => {