From ab0353aa3e67fe62e8267797a6a173a2a9fdc2c9 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Thu, 3 Sep 2026 13:50:42 +0530 Subject: [PATCH] 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,