From ab0353aa3e67fe62e8267797a6a173a2a9fdc2c9 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Thu, 3 Sep 2026 13:50:42 +0530 Subject: [PATCH 1/4] feat: add Enterprise Connect documentation and tests for isFederatedDomain --- EXAMPLES.md | 179 +++++++++++++++--- projects/auth0-angular/src/public-api.spec.ts | 9 + projects/auth0-angular/src/public-api.ts | 2 + 3 files changed, 161 insertions(+), 29 deletions(-) create mode 100644 projects/auth0-angular/src/public-api.spec.ts diff --git a/EXAMPLES.md b/EXAMPLES.md index 698831d8..0843ee53 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -19,6 +19,7 @@ - [Step-Up Authentication](#step-up-authentication) - [Passkeys](#passkeys) - [MyAccount API](#myaccount-api) +- [Enterprise Connect](#enterprise-connect) ## Add login to your application @@ -1627,12 +1628,10 @@ export class SignupComponent { private auth = inject(AuthService); signup() { - this.auth.passkey - .signup({ email: 'user@example.com', name: 'Jane Doe' }) - .subscribe({ - next: (tokens) => console.log('Signed up, access token:', tokens.access_token), - error: (err) => console.error('Signup failed', err), - }); + this.auth.passkey.signup({ email: 'user@example.com', name: 'Jane Doe' }).subscribe({ + next: (tokens) => console.log('Signed up, access token:', tokens.access_token), + error: (err) => console.error('Signup failed', err), + }); } } ``` @@ -1657,7 +1656,7 @@ this.auth.passkey // At least one identifier is typically required email: 'user@example.com', phoneNumber: '+1234567890', // optional, E.164 format - username: 'janedoe', // optional + username: 'janedoe', // optional // Profile fields (all optional) name: 'Jane Doe', @@ -1734,17 +1733,15 @@ export class PasskeyAuthComponent { auth = inject(AuthService); signup() { - this.auth.passkey - .signup({ email: 'user@example.com' }) - .subscribe({ - error: (err) => { - if (err instanceof PasskeyRegisterError) { - console.error('Registration failed:', err.message); - } else if (err instanceof PasskeyError) { - console.error('Passkey error:', err.message); - } - }, - }); + this.auth.passkey.signup({ email: 'user@example.com' }).subscribe({ + error: (err) => { + if (err instanceof PasskeyRegisterError) { + console.error('Registration failed:', err.message); + } else if (err instanceof PasskeyError) { + console.error('Passkey error:', err.message); + } + }, + }); } login() { @@ -1947,14 +1944,10 @@ Rename a `totp` or `push-notification` method, or change the preferred delivery ```ts // Rename a totp or push-notification method -this.auth.myAccount - .updateAuthenticationMethod('am_abc123', { name: 'My Work Laptop' }) - .subscribe({ next: (updated) => console.log(updated) }); +this.auth.myAccount.updateAuthenticationMethod('am_abc123', { name: 'My Work Laptop' }).subscribe({ next: (updated) => console.log(updated) }); // Switch a phone method between SMS and voice -this.auth.myAccount - .updateAuthenticationMethod('am_abc123', { preferred_authentication_method: 'voice' }) - .subscribe({ next: (updated) => console.log(updated) }); +this.auth.myAccount.updateAuthenticationMethod('am_abc123', { preferred_authentication_method: 'voice' }).subscribe({ next: (updated) => console.log(updated) }); ``` ### Enrollment @@ -1993,8 +1986,7 @@ this.auth.myAccount .subscribe({ next: (method) => console.log('Passkey enrolled:', method) }); ``` -> [!NOTE] -> `base64urlToBuffer` and `serializeCredential` are platform-specific helpers you provide. The SDK does not handle the WebAuthn browser API directly — it handles the Auth0 challenge and token exchange on both sides. +> [!NOTE] > `base64urlToBuffer` and `serializeCredential` are platform-specific helpers you provide. The SDK does not handle the WebAuthn browser API directly — it handles the Auth0 challenge and token exchange on both sides. #### Enroll TOTP @@ -2153,9 +2145,7 @@ this.auth.myAccount console.error(err.status, err.title, err.detail); if (err.validation_errors) { - err.validation_errors.forEach((e) => - console.error(`${e.field}: ${e.detail}`) - ); + err.validation_errors.forEach((e) => console.error(`${e.field}: ${e.detail}`)); } } return EMPTY; @@ -2171,3 +2161,134 @@ this.auth.myAccount // delete:me:authentication_methods — deleteAuthenticationMethod // read:me:factors — getFactors ``` + +## Enterprise Connect + +Enterprise Connect lets a B2B SaaS layer enterprise SSO (SAML, OIDC federation) on top of its own auth server without replacing it. Auth0 acts as a relay: it authenticates the enterprise user against their IdP and returns an enriched ID token, which the SDK caches like any other login. + +> [!IMPORTANT] +> Enterprise Connect is an Early Access feature. The tenant setup (entitlements, connection type, and the claims a token carries) depends on your Auth0 configuration and may change. Confirm the tenant-side requirements with your Auth0 contact. The SDK surface described here is stable. + +### How the flow works + +1. The user enters their email. Your app calls `isFederatedDomain` with the email domain to run [WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) discovery. +2. If the domain is managed by Auth0 for enterprise SSO, call `loginWithRedirect` with the email as `login_hint` so Auth0 can resolve the connection and organization. If it is not managed, fall back to your own login. +3. The user authenticates at their identity provider and is redirected back to your callback. +4. Your app handles the redirect exactly as in a normal login. The ID token is verified and cached; read the claims from `idTokenClaims$` / `user$`. + +> [!IMPORTANT] > `isFederatedDomain` is a routing hint, not a security control. It returns `false` on any failure (a 429, a network error, or a genuinely unmanaged domain all look the same), so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in: the callback must still complete, and you must still validate the resulting claims (see [Validate the organization](#validate-the-organization)). + +### Configure the SDK + +```ts +AuthModule.forRoot({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + authorizationParams: { + redirect_uri: window.location.origin, + scope: 'openid profile email', // no offline_access -- EC issues no refresh token + // Do not set organization -- HRD resolves it from login_hint + }, +}), +``` + +### Log in + +`isFederatedDomain` is a standalone function re-exported from `@auth0/auth0-angular` (it is not a method on `AuthService`). Pass your Auth0 domain and the email domain. If the domain is managed, start the redirect with the email as `login_hint`: + +```ts +import { Component } from '@angular/core'; +import { AuthService, isFederatedDomain } from '@auth0/auth0-angular'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', +}) +export class LoginComponent { + constructor(public auth: AuthService) {} + + async login(email: string): Promise { + const emailDomain = email.split('@')[1]; + + // 1. Discover whether the domain is managed for enterprise SSO. + const federated = await isFederatedDomain('YOUR_AUTH0_DOMAIN', emailDomain); + + if (!federated) { + // Domain is not managed by Auth0; fall back to your own login. + this.showPasswordForm(email); + return; + } + + // 2. Redirect to Auth0 with the email as login_hint. Home Realm Discovery + // resolves the connection and organization from the domain -- do not + // pass organization yourself, or you break multi-customer setups. + this.auth + .loginWithRedirect({ + authorizationParams: { login_hint: email }, + }) + .subscribe(); + } +} +``` + +`isFederatedDomain` accepts an optional third argument (`IsFederatedDomainOptions`) with `customFetch` and `telemetry` fields, mirroring auth0-spa-js. + +### Handle the callback + +No changes to your existing callback handling. The SDK processes the redirect automatically; read the claims once authenticated: + +```ts +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; + +@Component({ + selector: 'app-callback', + template: '', +}) +export class CallbackComponent { + constructor(public auth: AuthService) {} + + // claims.org_id is the resolved organization. + claims$ = this.auth.idTokenClaims$; +} +``` + +### Validate the organization + +> [!WARNING] +> Check `org_id` after every callback. WebFinger discovery and `login_hint` only help route the user to the right login. They don't prove the user belongs to one of your customers. So read `org_id` from the ID token claims and make sure it's in your list of known organizations before you treat the user as signed in. Skip this, and anyone who logs in through a managed connection could end up with a session you never meant to give them. + +```ts +import { switchMap, throwError } from 'rxjs'; + +// Replace with your real organization IDs; these are dummy placeholders. +const allowedOrgs = ['org_123', 'org_456']; + +this.auth.idTokenClaims$ + .pipe( + switchMap((claims) => { + if (!claims || !allowedOrgs.includes(claims.org_id)) { + return this.auth.logout().pipe(switchMap(() => throwError(() => new Error('User does not belong to this organization')))); + } + return [claims]; + }) + ) + .subscribe(); +``` + +Remember, this check runs in the browser, so anyone can bypass it. It's only there to keep the UI tidy. The real check has to happen on your server, on every request that uses the token. Still, keep the check here even if you only have one org today. It's what stops other tenants' users from getting in once you add a second customer. + +### Log out + +EC logout must use `federated: true` to terminate the enterprise IdP session (SAML SLO). Without it the IdP session stays alive and the next login silently reuses the previous user: + +```ts +this.auth + .logout({ + logoutParams: { + federated: true, + returnTo: window.location.origin, + }, + }) + .subscribe(); +``` diff --git a/projects/auth0-angular/src/public-api.spec.ts b/projects/auth0-angular/src/public-api.spec.ts new file mode 100644 index 00000000..65419f2d --- /dev/null +++ b/projects/auth0-angular/src/public-api.spec.ts @@ -0,0 +1,9 @@ +import { isFederatedDomain } from './public-api'; + +describe('public-api', () => { + describe('Enterprise Connect', () => { + it('re-exports isFederatedDomain from auth0-spa-js', () => { + expect(typeof isFederatedDomain).toBe('function'); + }); + }); +}); diff --git a/projects/auth0-angular/src/public-api.ts b/projects/auth0-angular/src/public-api.ts index 051954f8..a0304732 100644 --- a/projects/auth0-angular/src/public-api.ts +++ b/projects/auth0-angular/src/public-api.ts @@ -22,6 +22,7 @@ export { GetTokenSilentlyOptions, RedirectConnectAccountOptions, ConnectAccountRedirectResult, + isFederatedDomain, ICache, Cacheable, LocalStorageCache, @@ -64,6 +65,7 @@ export { export type { InteractiveErrorHandler, + IsFederatedDomainOptions, Authenticator, AuthenticatorType, OobChannel, From 41943f95e3480ecb4bb4c081282c26d2074a168e Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 16 Sep 2026 19:48:29 +0530 Subject: [PATCH 2/4] test: add tests for enterprise connect login_hint and federated logout functionality --- EXAMPLES.md | 31 ++++++++--- .../src/lib/auth.service.spec.ts | 22 ++++++++ projects/auth0-angular/src/public-api.spec.ts | 52 ++++++++++++++++++- 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 0843ee53..00f15229 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1986,7 +1986,8 @@ this.auth.myAccount .subscribe({ next: (method) => console.log('Passkey enrolled:', method) }); ``` -> [!NOTE] > `base64urlToBuffer` and `serializeCredential` are platform-specific helpers you provide. The SDK does not handle the WebAuthn browser API directly — it handles the Auth0 challenge and token exchange on both sides. +> [!NOTE] +> `base64urlToBuffer` and `serializeCredential` are platform-specific helpers you provide. The SDK does not handle the WebAuthn browser API directly — it handles the Auth0 challenge and token exchange on both sides. #### Enroll TOTP @@ -2176,7 +2177,8 @@ Enterprise Connect lets a B2B SaaS layer enterprise SSO (SAML, OIDC federation) 3. The user authenticates at their identity provider and is redirected back to your callback. 4. Your app handles the redirect exactly as in a normal login. The ID token is verified and cached; read the claims from `idTokenClaims$` / `user$`. -> [!IMPORTANT] > `isFederatedDomain` is a routing hint, not a security control. It returns `false` on any failure (a 429, a network error, or a genuinely unmanaged domain all look the same), so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in: the callback must still complete, and you must still validate the resulting claims (see [Validate the organization](#validate-the-organization)). +> [!IMPORTANT] +> `isFederatedDomain` is a routing hint, not a security control. It returns `false` on any failure (a 429, a network error, or a genuinely unmanaged domain all look the same), so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in: the callback must still complete, and you must still validate the resulting claims (see [Validate the organization](#validate-the-organization)). ### Configure the SDK @@ -2184,6 +2186,7 @@ Enterprise Connect lets a B2B SaaS layer enterprise SSO (SAML, OIDC federation) AuthModule.forRoot({ domain: 'YOUR_AUTH0_DOMAIN', clientId: 'YOUR_AUTH0_CLIENT_ID', + enterpriseConnect: true, // lets the SDK warn at init if the config contradicts EC's constraints authorizationParams: { redirect_uri: window.location.origin, scope: 'openid profile email', // no offline_access -- EC issues no refresh token @@ -2192,6 +2195,11 @@ AuthModule.forRoot({ }), ``` +Set `enterpriseConnect: true` to enable Enterprise Connect mode. The SDK then warns you at startup if your config contradicts EC's constraints. + +> [!IMPORTANT] +> Enterprise Connect issues no refresh token, so the access token expires (24 hours by default) with no way to renew it silently. Treat EC as identity only: read the claims from the ID token (`idTokenClaims$` / `user$`) and mint your own application session or API tokens from them. Do not forward the Auth0 access token to your own APIs for long-lived authorization, and check `exp` / `expires_at` if you cache it. + ### Log in `isFederatedDomain` is a standalone function re-exported from `@auth0/auth0-angular` (it is not a method on `AuthService`). Pass your Auth0 domain and the email domain. If the domain is managed, start the redirect with the email as `login_hint`: @@ -2228,6 +2236,10 @@ export class LoginComponent { }) .subscribe(); } + + // Your own login UI for domains that are not federated (e.g. show a + // password field). Replace with your implementation. + private showPasswordForm(email: string): void {} } ``` @@ -2255,8 +2267,7 @@ export class CallbackComponent { ### Validate the organization -> [!WARNING] -> Check `org_id` after every callback. WebFinger discovery and `login_hint` only help route the user to the right login. They don't prove the user belongs to one of your customers. So read `org_id` from the ID token claims and make sure it's in your list of known organizations before you treat the user as signed in. Skip this, and anyone who logs in through a managed connection could end up with a session you never meant to give them. +Validating `org_id` is an application-level authorization decision, not something the SDK enforces. WebFinger discovery and `login_hint` only route the user to the right login; they don't prove the user belongs to one of your customers. If your app serves specific organizations, we recommend reading `org_id` from the ID token claims and checking it against your own list before treating the user as signed in for that customer. ```ts import { switchMap, throwError } from 'rxjs'; @@ -2268,7 +2279,13 @@ this.auth.idTokenClaims$ .pipe( switchMap((claims) => { if (!claims || !allowedOrgs.includes(claims.org_id)) { - return this.auth.logout().pipe(switchMap(() => throwError(() => new Error('User does not belong to this organization')))); + // The user authenticated via the enterprise IdP, so use a federated + // logout here too, otherwise the IdP session survives the rejection. + return this.auth + .logout({ + logoutParams: { federated: true, returnTo: window.location.origin }, + }) + .pipe(switchMap(() => throwError(() => new Error('User does not belong to this organization')))); } return [claims]; }) @@ -2276,7 +2293,7 @@ this.auth.idTokenClaims$ .subscribe(); ``` -Remember, this check runs in the browser, so anyone can bypass it. It's only there to keep the UI tidy. The real check has to happen on your server, on every request that uses the token. Still, keep the check here even if you only have one org today. It's what stops other tenants' users from getting in once you add a second customer. +This check runs in the browser, so a user can bypass it. Use it only to decide what the UI shows. Your backend must re-check `org_id` on every API request before trusting the token. Even if you serve a single organization today, keeping the check stops other tenants' users from getting in the day you add a second customer. ### Log out @@ -2292,3 +2309,5 @@ this.auth }) .subscribe(); ``` + +Ensure the `returnTo` URL is listed in your application's **Allowed Logout URLs** in the Auth0 Dashboard, otherwise Auth0 rejects the post-logout redirect. diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index 2732b86a..772ffae0 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -757,6 +757,28 @@ describe('AuthService', () => { expect(auth0Client.logout).toHaveBeenCalledWith(options); }); + // Enterprise Connect: login_hint drives Home Realm Discovery so Auth0 can + // resolve the connection and organization from the user's email domain. + it('should forward `login_hint` to `loginWithRedirect` for the enterprise flow', async () => { + const options = { + authorizationParams: { login_hint: 'user@acme.com' }, + }; + const service = createService(); + await service.loginWithRedirect(options).toPromise(); + expect(auth0Client.loginWithRedirect).toHaveBeenCalledWith(options); + }); + + // Enterprise Connect: federated logout terminates the enterprise IdP session + // (SAML SLO); without it the next login silently reuses the previous user. + it('should forward `federated: true` to `logout` for the enterprise flow', () => { + const options = { + logoutParams: { federated: true, returnTo: 'http://localhost' }, + }; + const service = createService(); + service.logout(options); + expect(auth0Client.logout).toHaveBeenCalledWith(options); + }); + it('should reset the authentication state when passing `localOnly` to logout', async () => { const options = { openUrl: async () => { diff --git a/projects/auth0-angular/src/public-api.spec.ts b/projects/auth0-angular/src/public-api.spec.ts index 65419f2d..889ec6cf 100644 --- a/projects/auth0-angular/src/public-api.spec.ts +++ b/projects/auth0-angular/src/public-api.spec.ts @@ -1,9 +1,59 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +// Mock spa-js so the re-export chain resolves without a real network call. +// isFederatedDomain gets a controllable implementation so we can drive the +// federated / non-federated / discovery-failure branches. +const isFederatedDomainMock = vi.fn(); + +vi.mock('@auth0/auth0-spa-js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isFederatedDomain: (...args: unknown[]) => isFederatedDomainMock(...args), + }; +}); + import { isFederatedDomain } from './public-api'; describe('public-api', () => { describe('Enterprise Connect', () => { - it('re-exports isFederatedDomain from auth0-spa-js', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('re-exports isFederatedDomain from the package root', () => { expect(typeof isFederatedDomain).toBe('function'); }); + + it('forwards the domain and email domain and returns true for a managed domain', async () => { + isFederatedDomainMock.mockResolvedValue(true); + + const federated = await isFederatedDomain('tenant.auth0.com', 'acme.com'); + + expect(federated).toBe(true); + expect(isFederatedDomainMock).toHaveBeenCalledWith( + 'tenant.auth0.com', + 'acme.com' + ); + }); + + it('propagates false for an unmanaged domain', async () => { + isFederatedDomainMock.mockResolvedValue(false); + + await expect( + isFederatedDomain('tenant.auth0.com', 'gmail.com') + ).resolves.toBe(false); + }); + + // spa-js fails closed: a network error, 429, or any non-ok status resolves + // to false rather than throwing, so discovery failures route to the fallback + // login instead of surfacing an error the caller must catch. + it('resolves false rather than rejecting when discovery fails', async () => { + isFederatedDomainMock.mockResolvedValue(false); + + await expect( + isFederatedDomain('tenant.auth0.com', 'acme.com') + ).resolves.toBe(false); + }); }); }); From e4adc144f06c1df34213f4f66938a2b520fa0dd8 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 16 Sep 2026 21:18:05 +0530 Subject: [PATCH 3/4] refactor: improve code readability by formatting subscription calls and removing redundant test case --- EXAMPLES.md | 44 ++++++++++++------- projects/auth0-angular/src/public-api.spec.ts | 10 ----- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 00f15229..17d42ba7 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1628,10 +1628,12 @@ export class SignupComponent { private auth = inject(AuthService); signup() { - this.auth.passkey.signup({ email: 'user@example.com', name: 'Jane Doe' }).subscribe({ - next: (tokens) => console.log('Signed up, access token:', tokens.access_token), - error: (err) => console.error('Signup failed', err), - }); + this.auth.passkey + .signup({ email: 'user@example.com', name: 'Jane Doe' }) + .subscribe({ + next: (tokens) => console.log('Signed up, access token:', tokens.access_token), + error: (err) => console.error('Signup failed', err), + }); } } ``` @@ -1656,7 +1658,7 @@ this.auth.passkey // At least one identifier is typically required email: 'user@example.com', phoneNumber: '+1234567890', // optional, E.164 format - username: 'janedoe', // optional + username: 'janedoe', // optional // Profile fields (all optional) name: 'Jane Doe', @@ -1733,15 +1735,17 @@ export class PasskeyAuthComponent { auth = inject(AuthService); signup() { - this.auth.passkey.signup({ email: 'user@example.com' }).subscribe({ - error: (err) => { - if (err instanceof PasskeyRegisterError) { - console.error('Registration failed:', err.message); - } else if (err instanceof PasskeyError) { - console.error('Passkey error:', err.message); - } - }, - }); + this.auth.passkey + .signup({ email: 'user@example.com' }) + .subscribe({ + error: (err) => { + if (err instanceof PasskeyRegisterError) { + console.error('Registration failed:', err.message); + } else if (err instanceof PasskeyError) { + console.error('Passkey error:', err.message); + } + }, + }); } login() { @@ -1944,10 +1948,14 @@ Rename a `totp` or `push-notification` method, or change the preferred delivery ```ts // Rename a totp or push-notification method -this.auth.myAccount.updateAuthenticationMethod('am_abc123', { name: 'My Work Laptop' }).subscribe({ next: (updated) => console.log(updated) }); +this.auth.myAccount + .updateAuthenticationMethod('am_abc123', { name: 'My Work Laptop' }) + .subscribe({ next: (updated) => console.log(updated) }); // Switch a phone method between SMS and voice -this.auth.myAccount.updateAuthenticationMethod('am_abc123', { preferred_authentication_method: 'voice' }).subscribe({ next: (updated) => console.log(updated) }); +this.auth.myAccount + .updateAuthenticationMethod('am_abc123', { preferred_authentication_method: 'voice' }) + .subscribe({ next: (updated) => console.log(updated) }); ``` ### Enrollment @@ -2146,7 +2154,9 @@ this.auth.myAccount console.error(err.status, err.title, err.detail); if (err.validation_errors) { - err.validation_errors.forEach((e) => console.error(`${e.field}: ${e.detail}`)); + err.validation_errors.forEach((e) => + console.error(`${e.field}: ${e.detail}`) + ); } } return EMPTY; diff --git a/projects/auth0-angular/src/public-api.spec.ts b/projects/auth0-angular/src/public-api.spec.ts index 889ec6cf..da7303f5 100644 --- a/projects/auth0-angular/src/public-api.spec.ts +++ b/projects/auth0-angular/src/public-api.spec.ts @@ -45,15 +45,5 @@ describe('public-api', () => { ).resolves.toBe(false); }); - // spa-js fails closed: a network error, 429, or any non-ok status resolves - // to false rather than throwing, so discovery failures route to the fallback - // login instead of surfacing an error the caller must catch. - it('resolves false rather than rejecting when discovery fails', async () => { - isFederatedDomainMock.mockResolvedValue(false); - - await expect( - isFederatedDomain('tenant.auth0.com', 'acme.com') - ).resolves.toBe(false); - }); }); }); From b4c6d19e2c05d8b4657b67ad8747d654df0cff48 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 16 Sep 2026 21:29:06 +0530 Subject: [PATCH 4/4] fix: validate org_id claims in enterprise logout process --- EXAMPLES.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 17d42ba7..2bd3296c 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -2280,15 +2280,19 @@ export class CallbackComponent { Validating `org_id` is an application-level authorization decision, not something the SDK enforces. WebFinger discovery and `login_hint` only route the user to the right login; they don't prove the user belongs to one of your customers. If your app serves specific organizations, we recommend reading `org_id` from the ID token claims and checking it against your own list before treating the user as signed in for that customer. ```ts -import { switchMap, throwError } from 'rxjs'; +import { filter, switchMap, take, throwError } from 'rxjs'; -// Replace with your real organization IDs; these are dummy placeholders. +// `allowedOrgs` is a placeholder for illustration -- replace it with your +// own list of org_id values that this app is allowed to serve. const allowedOrgs = ['org_123', 'org_456']; this.auth.idTokenClaims$ .pipe( + // Ignore the null from an unauthenticated state; only validate a real claim set. + filter((claims) => !!claims), + take(1), switchMap((claims) => { - if (!claims || !allowedOrgs.includes(claims.org_id)) { + if (!allowedOrgs.includes(claims.org_id)) { // The user authenticated via the enterprise IdP, so use a federated // logout here too, otherwise the IdP session survives the rejection. return this.auth