diff --git a/EXAMPLES.md b/EXAMPLES.md index 3ea04b45..a002e848 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -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 @@ -1008,3 +1010,410 @@ 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](#mfa-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) + +### MFA Setup + +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, tap } 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 + return this.auth.mfa.getEnrollmentFactors(mfaToken).pipe( + tap((factors) => { + // Show enrollment UI with available factors + }) + ); + } else { + // Existing user — list enrolled authenticators and challenge + return this.auth.mfa.getAuthenticators(mfaToken).pipe( + tap((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 + +> [!IMPORTANT] +> The `verify()` method does not update Angular auth state (`isAuthenticated$`, `user$`). Always chain `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'; +import { switchMap, tap } from 'rxjs'; + +@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 }) + .pipe( + switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$ + ) + .subscribe(); + } + + // Verify with OOB code (SMS / Voice / Email / Push) + verifyOob(mfaToken: string, oobCode: string, bindingCode?: string) { + this.auth.mfa + .verify({ mfaToken, oobCode, bindingCode }) + .pipe( + switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$ + ) + .subscribe(); + } + + // Verify with recovery code (fallback for any authenticator) + // When a recovery code is consumed, Auth0 may return a replacement recovery_code + // in the response. Prompt the user to save it — losing the new code locks them out. + verifyRecoveryCode(mfaToken: string, recoveryCode: string) { + this.auth.mfa + .verify({ mfaToken, recoveryCode }) + .pipe( + tap((tokens) => { + if (tokens.recovery_code) { + console.warn('Save your new recovery code:', tokens.recovery_code); + } + }), + switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$ + ) + .subscribe(); + } +} +``` + +### 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. + +### Step-Up 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`: + +```ts +import { PopupOpenError, PopupCancelledError, PopupTimeoutError } from '@auth0/auth0-angular'; +import { catchError, EMPTY } from 'rxjs'; + +this.auth + .getAccessTokenSilently({ + authorizationParams: { audience: 'https://api.example.com/' }, + }) + .pipe( + catchError((error) => { + if (error instanceof PopupOpenError) { + console.error('Popup was blocked by the browser'); + } else if (error instanceof PopupCancelledError) { + console.error('User closed the popup'); + } else if (error instanceof PopupTimeoutError) { + console.error('Popup timed out'); + } + return EMPTY; + }) + ) + .subscribe(); +``` diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index 5f8cf165..b4d60e56 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -101,6 +101,29 @@ describe('AuthService', () => { fetch: jest.fn(), } as any); + jest + .spyOn(auth0Client.mfa, 'getAuthenticators') + .mockResolvedValue([ + { id: 'auth-1', authenticatorType: 'otp', active: true }, + ]); + jest.spyOn(auth0Client.mfa, 'enroll').mockResolvedValue({ + authenticatorType: 'otp', + secret: '__totp_secret__', + barcodeUri: '__barcode_uri__', + }); + jest.spyOn(auth0Client.mfa, 'challenge').mockResolvedValue({ + challengeType: 'otp', + }); + jest + .spyOn(auth0Client.mfa, 'getEnrollmentFactors') + .mockResolvedValue([{ type: 'otp' }]); + jest.spyOn(auth0Client.mfa, 'verify').mockResolvedValue({ + access_token: '__mfa_access_token__', + id_token: '__mfa_id_token__', + token_type: 'Bearer', + expires_in: 86400, + }); + window.history.replaceState(null, '', ''); moduleSetup = { @@ -1328,4 +1351,245 @@ describe('AuthService', () => { expect(auth0Client.createFetcher).toHaveBeenCalledWith(config); }); }); + + describe('mfa', () => { + describe('getAuthenticators', () => { + it('should call the underlying SDK', (done) => { + const service = createService(); + const mfaToken = '__mfa_token__'; + + service.mfa.getAuthenticators(mfaToken).subscribe(() => { + expect(auth0Client.mfa.getAuthenticators).toHaveBeenCalledWith( + mfaToken + ); + done(); + }); + }); + + it('should return the list of authenticators', (done) => { + const service = createService(); + + service.mfa.getAuthenticators('__mfa_token__').subscribe((result) => { + expect(result).toEqual([ + { id: 'auth-1', authenticatorType: 'otp', active: true }, + ]); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('getAuthenticators failed'); + ( + auth0Client.mfa.getAuthenticators as unknown as jest.SpyInstance + ).mockRejectedValue(errorObj); + const service = createService(); + + service.mfa.getAuthenticators('__mfa_token__').subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); + }); + + describe('enroll', () => { + it('should call the underlying SDK', (done) => { + const service = createService(); + const params = { + mfaToken: '__mfa_token__', + factorType: 'otp' as const, + }; + + service.mfa.enroll(params).subscribe(() => { + expect(auth0Client.mfa.enroll).toHaveBeenCalledWith(params); + done(); + }); + }); + + it('should return the enrollment response', (done) => { + const service = createService(); + + service.mfa + .enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) + .subscribe((result) => { + expect(result).toEqual({ + authenticatorType: 'otp', + secret: '__totp_secret__', + barcodeUri: '__barcode_uri__', + }); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('enroll failed'); + ( + auth0Client.mfa.enroll as unknown as jest.SpyInstance + ).mockRejectedValue(errorObj); + const service = createService(); + + service.mfa + .enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) + .subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); + }); + + describe('challenge', () => { + it('should call the underlying SDK', (done) => { + const service = createService(); + const params = { + mfaToken: '__mfa_token__', + challengeType: 'otp' as const, + }; + + service.mfa.challenge(params).subscribe(() => { + expect(auth0Client.mfa.challenge).toHaveBeenCalledWith(params); + done(); + }); + }); + + it('should return the challenge response', (done) => { + const service = createService(); + + service.mfa + .challenge({ mfaToken: '__mfa_token__', challengeType: 'otp' }) + .subscribe((result) => { + expect(result).toEqual({ challengeType: 'otp' }); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('challenge failed'); + ( + auth0Client.mfa.challenge as unknown as jest.SpyInstance + ).mockRejectedValue(errorObj); + const service = createService(); + + service.mfa + .challenge({ mfaToken: '__mfa_token__', challengeType: 'otp' }) + .subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); + }); + + describe('getEnrollmentFactors', () => { + it('should call the underlying SDK', (done) => { + const service = createService(); + const mfaToken = '__mfa_token__'; + + service.mfa.getEnrollmentFactors(mfaToken).subscribe(() => { + expect(auth0Client.mfa.getEnrollmentFactors).toHaveBeenCalledWith( + mfaToken + ); + done(); + }); + }); + + it('should return the enrollment factors', (done) => { + const service = createService(); + + service.mfa + .getEnrollmentFactors('__mfa_token__') + .subscribe((result) => { + expect(result).toEqual([{ type: 'otp' }]); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('getEnrollmentFactors failed'); + ( + auth0Client.mfa.getEnrollmentFactors as unknown as jest.SpyInstance + ).mockRejectedValue(errorObj); + const service = createService(); + + service.mfa.getEnrollmentFactors('__mfa_token__').subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); + }); + + describe('verify', () => { + it('should call the underlying SDK', (done) => { + const service = createService(); + const params = { mfaToken: '__mfa_token__', otp: '123456' }; + + service.mfa.verify(params).subscribe(() => { + expect(auth0Client.mfa.verify).toHaveBeenCalledWith(params); + done(); + }); + }); + + it('should return the token response', (done) => { + const service = createService(); + + service.mfa + .verify({ mfaToken: '__mfa_token__', otp: '123456' }) + .subscribe((result) => { + expect(result).toEqual({ + access_token: '__mfa_access_token__', + id_token: '__mfa_id_token__', + token_type: 'Bearer', + expires_in: 86400, + }); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('verify failed'); + ( + auth0Client.mfa.verify as unknown as jest.SpyInstance + ).mockRejectedValue(errorObj); + const service = createService(); + + service.mfa + .verify({ mfaToken: '__mfa_token__', otp: '123456' }) + .subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); + + it('should not update isAuthenticated$ or user$ after a successful verify', (done) => { + // verify() intentionally does not update Angular auth state — callers must + // follow up with getAccessTokenSilently() to reflect the new MFA session. + const service = createService(); + let isAuthEmissions = 0; + let userEmissions = 0; + + service.isAuthenticated$.subscribe(() => isAuthEmissions++); + service.user$.subscribe(() => userEmissions++); + + loaded(service) + .pipe( + mergeMap(() => + service.mfa.verify({ mfaToken: '__mfa_token__', otp: '123456' }) + ), + delay(0) + ) + .subscribe(() => { + expect(isAuthEmissions).toBe(1); + expect(userEmissions).toBe(1); + done(); + }); + }); + }); + }); }); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index 69507866..eb801ff5 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -17,6 +17,11 @@ import { TokenEndpointResponse, ResponseType, } from '@auth0/auth0-spa-js'; +import type { + EnrollParams, + ChallengeAuthenticatorParams, + VerifyParams, +} from '@auth0/auth0-spa-js'; import { of, @@ -43,7 +48,11 @@ import { Auth0ClientService } from './auth.client'; import { AbstractNavigator } from './abstract-navigator'; import { AuthClientConfig, AppState, ConnectedAccount } from './auth.config'; import { AuthState } from './auth.state'; -import { LogoutOptions, RedirectLoginOptions } from './interfaces'; +import { + LogoutOptions, + ObservableMfaApiClient, + RedirectLoginOptions, +} from './interfaces'; @Injectable({ providedIn: 'root', @@ -484,6 +493,23 @@ export class AuthService return this.auth0Client.createFetcher(config); } + /** + * Provides MFA (Multi-Factor Authentication) operations as Observables. + * + * These methods are available after `getAccessTokenSilently` throws an `MfaRequiredError`. + * Use the `mfa_token` from the error to call these methods. + */ + readonly mfa: ObservableMfaApiClient = { + getAuthenticators: (mfaToken: string) => + from(this.auth0Client.mfa.getAuthenticators(mfaToken)), + enroll: (params: EnrollParams) => from(this.auth0Client.mfa.enroll(params)), + challenge: (params: ChallengeAuthenticatorParams) => + from(this.auth0Client.mfa.challenge(params)), + getEnrollmentFactors: (mfaToken: string) => + from(this.auth0Client.mfa.getEnrollmentFactors(mfaToken)), + verify: (params: VerifyParams) => from(this.auth0Client.mfa.verify(params)), + }; + private shouldHandleCallback(): Observable { return of(location.search).pipe( map((search) => { diff --git a/projects/auth0-angular/src/lib/interfaces.ts b/projects/auth0-angular/src/lib/interfaces.ts index a0f088db..6a2e78ea 100644 --- a/projects/auth0-angular/src/lib/interfaces.ts +++ b/projects/auth0-angular/src/lib/interfaces.ts @@ -2,8 +2,66 @@ import { RedirectLoginOptions as SPARedirectLoginOptions, LogoutOptions as SPALogoutOptions, } from '@auth0/auth0-spa-js'; +import type { + Authenticator, + EnrollParams, + EnrollmentResponse, + ChallengeAuthenticatorParams, + ChallengeResponse, + VerifyParams, + EnrollmentFactor, + TokenEndpointResponse, +} from '@auth0/auth0-spa-js'; +import { Observable } from 'rxjs'; export interface RedirectLoginOptions extends Omit, 'onRedirect'> {} export interface LogoutOptions extends Omit {} + +/** + * Observable-based MFA API client exposed by `AuthService.mfa`. + * + * This is the Angular counterpart of `MfaApiClient` from `@auth0/auth0-spa-js`. + * All methods return `Observable` instead of `Promise`. + */ +export interface ObservableMfaApiClient { + /** + * Returns the list of enrolled MFA authenticators for the current user, + * filtered by the challenge types stored in the MFA context. + * @throws {MfaListAuthenticatorsError} + */ + getAuthenticators(mfaToken: string): Observable; + + /** + * Enrolls a new MFA authenticator (OTP, SMS, Voice, Email, or Push). + * @throws {MfaEnrollmentError} + */ + enroll(params: EnrollParams): Observable; + + /** + * Initiates an MFA challenge, sending an OOB code via SMS, email, or push, + * or preparing for OTP entry. Required for all factor types except OTP, + * where it is optional. + * @throws {MfaChallengeError} + */ + challenge( + params: ChallengeAuthenticatorParams + ): Observable; + + /** + * Returns the available enrollment factors from the stored MFA context. + * A non-empty result means the user must enroll before they can authenticate. + * @throws {MfaEnrollmentFactorsError} + */ + getEnrollmentFactors(mfaToken: string): Observable; + + /** + * Verifies an MFA challenge and returns raw tokens. The grant type is inferred + * automatically from the provided field: `otp`, `oobCode`, or `recoveryCode`. + * Angular auth state (`isAuthenticated$`, `user$`) is not updated automatically — + * call `getAccessTokenSilently()` afterwards to reflect the new session. + * @throws {MfaVerifyError} + */ + verify(params: VerifyParams): Observable; +} diff --git a/projects/auth0-angular/src/public-api.ts b/projects/auth0-angular/src/public-api.ts index 77d815b1..a125b93f 100644 --- a/projects/auth0-angular/src/public-api.ts +++ b/projects/auth0-angular/src/public-api.ts @@ -32,6 +32,7 @@ export { TimeoutError, MfaRequiredError, PopupTimeoutError, + PopupOpenError, AuthenticationError, PopupCancelledError, MissingRefreshTokenError, @@ -43,4 +44,32 @@ export { CustomTokenExchangeOptions, TokenEndpointResponse, ResponseType, + MfaError, + MfaListAuthenticatorsError, + MfaEnrollmentError, + MfaChallengeError, + MfaVerifyError, + MfaEnrollmentFactorsError, +} from '@auth0/auth0-spa-js'; + +export type { + InteractiveErrorHandler, + Authenticator, + AuthenticatorType, + OobChannel, + MfaFactorType, + EnrollParams, + EnrollOtpParams, + EnrollSmsParams, + EnrollVoiceParams, + EnrollEmailParams, + EnrollPushParams, + EnrollmentResponse, + OtpEnrollmentResponse, + OobEnrollmentResponse, + ChallengeAuthenticatorParams, + ChallengeResponse, + VerifyParams, + MfaGrantType, + EnrollmentFactor, } from '@auth0/auth0-spa-js';