Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 150 additions & 29 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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),
});
}
}
```
Expand All @@ -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',
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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;
Expand All @@ -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<void> {
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();
```
9 changes: 9 additions & 0 deletions projects/auth0-angular/src/public-api.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
2 changes: 2 additions & 0 deletions projects/auth0-angular/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
GetTokenSilentlyOptions,
RedirectConnectAccountOptions,
ConnectAccountRedirectResult,
isFederatedDomain,
ICache,
Cacheable,
LocalStorageCache,
Expand Down Expand Up @@ -64,6 +65,7 @@ export {

export type {
InteractiveErrorHandler,
IsFederatedDomainOptions,
Authenticator,
AuthenticatorType,
OobChannel,
Expand Down
Loading