Repository: auth0/nextjs-auth0
Pull request #1291 (2023-07-14 12:50:47)

Support Organization Name
<!--
❗ For general support or usage questions, use the Auth0 Community forums or raise a support ticket.

By submitting a pull request to this repository, you agree to the terms within the Auth0 Code of Conduct: https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md.
-->

- [x] All new/changed/fixed functionality is covered by tests (or N/A)
- [x] I have added documentation for all new/changed functionality (or N/A)

<!--
❗ All the above items are required. Pull requests with an incomplete or missing checklist will be closed.
-->

### 📋 Changes

Adds support for the organization name by extending the ID Token validation as follows:

- If organization is set and has the `org_` prefix, we expect an org_id claim
- If organization is set and does not have the `org_` prefix, we expect an org_name claim


vercel[bot]: [vc]: #gWP/Avc9VcUMGqVoZVkVkI+Y2iFRCLlh63WawtQ+mx8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJuZXh0anMtYXV0aDAiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYXV0aDAvbmV4dGpzLWF1dGgwL0E0bUxNNUpyWE5XWjdIWm5tVTc5eU5SN2VaZU0iLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Ignored Deployment</summary>

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **nextjs-auth0** | ⬜️ Ignored ([Inspect](https://vercel.com/auth0/nextjs-auth0/A4mLM5JrXNWZ7HZnmU79yNR7eZeM)) |  | Jul 14, 2023 0:50am |
</details>



Patch:
diff --git a/src/config.ts b/src/config.ts
index fa339eb1..45e1d24f 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -347,7 +347,7 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> {
    * 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 ac7bc7f7..6efbf18d 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 243e0094..223747eb 100644
--- a/src/handlers/login.ts
+++ b/src/handlers/login.ts
@@ -115,7 +115,7 @@ export interface AuthorizationParams extends Partial<AuthorizationParameters> {
    * ```
    *
    * 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;
 


Test patch:
diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts
index 0ef24f11..c3d9437d 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 () => {