Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
364 changes: 364 additions & 0 deletions EXAMPLES.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a recovery code is consumed, Auth0 can return a replacement recovery_code in the token response. Neither PR's examples mention this. A one-liner checking tokens.recovery_code and prompting the user to save it would go a long way, losing track of the new code locks the user out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, added a check for tokens.recovery_code in the recovery code example

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
- [Standalone Components and a more functional approach](#standalone-components-and-a-more-functional-approach)
- [Connect Accounts for using Token Vault](#connect-accounts-for-using-token-vault)
- [Native to Web SSO](#native-to-web-sso)
- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
- [Step-Up Authentication](#step-up-authentication)

## Add login to your application

Expand Down Expand Up @@ -1008,3 +1010,365 @@ this.auth.loginWithRedirect({
},
});
```

## Multi-Factor Authentication (MFA)

Access MFA operations through the `mfa` property on `AuthService`. All operations require an `mfa_token` from the `MfaRequiredError` thrown by `getAccessTokenSilently`.

> [!NOTE]
> Multi Factor Authentication support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative.

- [Setup](#setup)
- [Handling MFA Required Error](#handling-mfa-required-error)
- [Enrolling Authenticators](#enrolling-authenticators)
- [Challenging Authenticators](#challenging-authenticators)
- [Verifying Challenges](#verifying-challenges)
- [Error Handling](#mfa-error-handling)

### Setup
Comment thread
arpit-jn marked this conversation as resolved.
Outdated

Before using the MFA API, configure MFA in your [Auth0 Dashboard](https://manage.auth0.com) under **Security** > **Multi-factor Auth**. For detailed configuration, see the [Auth0 MFA documentation](https://auth0.com/docs/secure/multi-factor-authentication/customize-mfa/customize-mfa-enrollments-universal-login).

#### Understanding the MFA Response

When MFA is required, the error payload contains an `mfa_requirements` object that indicates either a **challenge** flow (user has enrolled authenticators) or an **enroll** flow (user needs to set up MFA).

**Challenge Flow Response** (user has existing authenticators):

```json
{
"error": "mfa_required",
"error_description": "Multifactor authentication required",
"mfa_token": "Fe26.2*...",
"mfa_requirements": {
"challenge": [{ "type": "otp" }, { "type": "email" }]
}
}
```

**Enroll Flow Response** (user needs to enroll an authenticator):

```json
{
"error": "mfa_required",
"error_description": "Multifactor authentication required",
"mfa_token": "Fe26.2*...",
"mfa_requirements": {
"enroll": [{ "type": "otp" }, { "type": "phone" }, { "type": "push-notification" }]
}
}
```

These two keys are mutually exclusive — a single response will contain either `challenge` or `enroll`, never both:

- **`mfa_requirements.challenge`**: User has enrolled authenticators → proceed with **List Authenticators → Challenge → Verify** flow
- **`mfa_requirements.enroll`**: User needs to set up MFA → proceed with **Enroll → Verify** flow

### Handling MFA Required Error

Catch the `MfaRequiredError` from `getAccessTokenSilently` and use `mfa_requirements` to determine which flow to follow:

```ts
import { Component } from '@angular/core';
import { AuthService, MfaRequiredError } from '@auth0/auth0-angular';
import { catchError, EMPTY, switchMap } from 'rxjs';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This imports switchMap but it's never used in this example.

Current:
import { catchError, EMPTY, switchMap } from 'rxjs';

Replace with:

import { catchError, EMPTY } from 'rxjs';
import { tap } from 'rxjs/operators';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed switchMap and added tap alongside catchError and EMPTY in a single import from rxjs


@Component({ selector: 'app-mfa', template: '' })
export class MfaComponent {
constructor(private auth: AuthService) {}

requestToken() {
this.auth
.getAccessTokenSilently()
.pipe(
catchError((error) => {
if (error instanceof MfaRequiredError) {
const mfaToken = error.mfa_token;

if (error.mfa_requirements?.enroll?.length) {
// New user — needs to enroll a factor first
this.auth.mfa.getEnrollmentFactors(mfaToken).subscribe((factors) => {
Comment thread
arpit-jn marked this conversation as resolved.
Outdated
// Show enrollment UI with available factors
});
} else {
// Existing user — list enrolled authenticators and challenge
this.auth.mfa.getAuthenticators(mfaToken).subscribe((authenticators) => {
// Show challenge UI
});
}
}
return EMPTY;
})
)
.subscribe();
}
}
```

### Enrolling Authenticators

```ts
import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-enroll', template: '' })
export class EnrollComponent {
constructor(private auth: AuthService) {}

// Enroll TOTP — returns a QR code to display to the user
enrollOtp(mfaToken: string) {
this.auth.mfa.enroll({ mfaToken, factorType: 'otp' }).subscribe((enrollment) => {
console.log('Scan QR:', enrollment.barcodeUri);
console.log('Recovery codes:', enrollment.recoveryCodes);
});
}

// Enroll SMS — include phone number in E.164 format
enrollSms(mfaToken: string) {
this.auth.mfa
.enroll({
mfaToken,
factorType: 'sms',
phoneNumber: '+12025551234',
})
.subscribe();
}

// Enroll Voice — include phone number in E.164 format
enrollVoice(mfaToken: string) {
this.auth.mfa
.enroll({
mfaToken,
factorType: 'voice',
phoneNumber: '+12025551234',
})
.subscribe();
}

// Enroll Email
enrollEmail(mfaToken: string) {
this.auth.mfa
.enroll({
mfaToken,
factorType: 'email',
email: 'user@example.com',
})
.subscribe();
}

// Enroll Push — returns authenticator ID for use with the Guardian app
enrollPush(mfaToken: string) {
this.auth.mfa.enroll({ mfaToken, factorType: 'push' }).subscribe((enrollment) => {
console.log('Authenticator ID:', enrollment.id);
});
}
}
```

### Challenging Authenticators

```ts
import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
import { switchMap } from 'rxjs';

@Component({ selector: 'app-challenge', template: '' })
export class ChallengeComponent {
constructor(private auth: AuthService) {}

// For OTP: challenge is optional — user can go straight to verify()
// with the 6-digit code from their authenticator app
challengeOtp(mfaToken: string, authenticatorId: string) {
this.auth.mfa
.challenge({
mfaToken,
challengeType: 'otp',
authenticatorId,
})
.subscribe();
}

// For SMS / Voice / Email / Push: challenge is required to send the code
challengeOob(mfaToken: string, authenticatorId: string) {
this.auth.mfa
.challenge({
mfaToken,
challengeType: 'oob',
authenticatorId,
})
.subscribe((response) => {
console.log('OOB Code:', response.oobCode); // use this in verify()
});
}

// Typical flow: list authenticators then challenge
listAndChallenge(mfaToken: string) {
this.auth.mfa
.getAuthenticators(mfaToken)
.pipe(
switchMap((authenticators) =>
this.auth.mfa.challenge({
mfaToken,
challengeType: 'oob',
authenticatorId: authenticators[0].id,
})
)
)
.subscribe((response) => {
// Code has been sent — show input to user
});
}
}
```

### Verifying Challenges
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every verify() example shows the subscribe but doesn't follow up with getAccessTokenSilently() to refresh Angular state. The JSDoc on ObservableMfaApiClient.verify() correctly notes this is needed, but the examples don't demonstrate it.

Vue PR handles this well — every verify call is followed by checkSession(). Worth keeping parity here so developers don't end up with stale isAuthenticated$ / user$ after completing MFA.

Something like:

this.auth.mfa.verify({ mfaToken, otp }).pipe(
  switchMap(() => this.auth.getAccessTokenSilently())
).subscribe();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, updated all verify examples to chain getAccessTokenSilently() after verify


> [!IMPORTANT] > `verify()` does not update Angular auth state (`isAuthenticated$`, `user$`). Call `getAccessTokenSilently()` after a successful verification to reflect the new session in the UI.

```ts
import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-verify', template: '' })
export class VerifyComponent {
constructor(private auth: AuthService) {}

// Verify with OTP code (TOTP authenticator app)
verifyOtp(mfaToken: string, otp: string) {
this.auth.mfa.verify({ mfaToken, otp }).subscribe((tokens) => {
console.log('Access token:', tokens.access_token);
});
}

// Verify with OOB code (SMS / Voice / Email / Push)
verifyOob(mfaToken: string, oobCode: string, bindingCode?: string) {
this.auth.mfa.verify({ mfaToken, oobCode, bindingCode }).subscribe((tokens) => {
console.log('Access token:', tokens.access_token);
});
}

// Verify with recovery code (fallback for any authenticator)
verifyRecoveryCode(mfaToken: string, recoveryCode: string) {
this.auth.mfa.verify({ mfaToken, recoveryCode }).subscribe((tokens) => {
console.log('Access token:', tokens.access_token);
});
}
}
```

### MFA Error Handling

Each MFA operation throws a specific error class you can import from `@auth0/auth0-angular`:

```ts
import { MfaVerifyError, MfaChallengeError, MfaEnrollmentError, MfaListAuthenticatorsError, MfaEnrollmentFactorsError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

this.auth.mfa
.verify({ mfaToken, otp })
.pipe(
catchError((error) => {
if (error instanceof MfaVerifyError) {
console.error('Invalid code:', error.error_description);
} else if (error instanceof MfaChallengeError) {
console.error('Challenge failed:', error.error_description);
} else if (error instanceof MfaEnrollmentError) {
console.error('Enrollment failed:', error.error_description);
}
return EMPTY;
})
)
.subscribe();
```

## Step-Up Authentication

When a protected API requires MFA, `getAccessTokenSilently` receives an `mfa_required` error from Auth0. By configuring `interactiveErrorHandler`, the SDK automatically handles this by opening a Universal Login popup for the user to complete MFA, then returns the token transparently. No custom MFA UI is required.

If you need full control over the MFA experience (custom UI for enrollment, challenge, and verification), see the [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa) section instead.

> [!WARNING]
> This feature only works with the refresh token flow (`useRefreshTokens: true`) and only handles `mfa_required` errors.

### Setup

Configure `provideAuth0` (or `AuthModule.forRoot`) with `interactiveErrorHandler` set to `"popup"` and refresh tokens enabled:

```ts
// app.config.ts — standalone / functional approach
import { provideAuth0 } from '@auth0/auth0-angular';

export const appConfig = {
providers: [
provideAuth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
authorizationParams: {
redirect_uri: window.location.origin,
audience: 'https://api.example.com/',
},
useRefreshTokens: true,
interactiveErrorHandler: 'popup',
}),
],
};
```

```ts
// app.module.ts — NgModule approach
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
imports: [
AuthModule.forRoot({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
authorizationParams: {
redirect_uri: window.location.origin,
audience: 'https://api.example.com/',
},
useRefreshTokens: true,
interactiveErrorHandler: 'popup',
}),
],
})
export class AppModule {}
```

### Usage

With this configuration, `getAccessTokenSilently` automatically opens a popup when the token request triggers an `mfa_required` error. Once the user completes MFA in the popup, the token is returned as if the call succeeded normally:

```ts
import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-protected', template: '' })
export class ProtectedComponent {
constructor(private auth: AuthService) {}

fetchSensitiveData() {
this.auth
.getAccessTokenSilently({
authorizationParams: {
audience: 'https://api.example.com/',
scope: 'read:sensitive',
},
})
.subscribe({
next: (token) => {
// If MFA was required, the popup opened and closed automatically.
// token is ready to use.
fetch('https://api.example.com/sensitive', {
headers: { Authorization: `Bearer ${token}` },
});
},
error: (e) => console.error(e),
});
}
}
```

### Error Handling

If the popup is blocked, cancelled, or times out, `getAccessTokenSilently` throws `PopupOpenError`, `PopupCancelledError`, or `PopupTimeoutError` respectively. These can be imported from `@auth0/auth0-angular`.
Comment thread
arpit-jn marked this conversation as resolved.
Outdated
Loading
Loading