|
153 | 153 | - [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers) |
154 | 154 | - [Run code after callback](#run-code-after-callback) |
155 | 155 | - [Next.js 16 Compatibility](#nextjs-16-compatibility) |
| 156 | +- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa-1) |
| 157 | + - [Setup & Configuration](#setup--configuration) |
| 158 | + - [Handling MfaRequiredError](#handling-mfarequirederror-1) |
| 159 | + - [Accessing the MFA API](#accessing-the-mfa-api) |
| 160 | + - [Getting Authenticators](#getting-authenticators) |
| 161 | + - [Enrollment](#enrollment) |
| 162 | + - [Challenge](#challenge) |
| 163 | + - [Verify](#verify) |
| 164 | + - [MFA Tenant Configuration](#mfa-tenant-configuration-1) |
| 165 | + - [MFA Error Handling](#mfa-error-handling) |
156 | 166 | - [Multiple Custom Domains (MCD)](#multiple-custom-domains-mcd) |
157 | 167 | - [Overview](#overview-1) |
158 | 168 | - [Static Mode (Default)](#static-mode-default) |
@@ -3758,6 +3768,259 @@ For more details, see the official Next.js documentation: |
3758 | 3768 | ➡️ [Upgrading to Next 16 Middleware](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#upgrading-to-nextjs-16) |
3759 | 3769 | ➡️ [Proxy.ts Conventions](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) |
3760 | 3770 |
|
| 3771 | +## Multi-Factor Authentication (MFA) |
| 3772 | +
|
| 3773 | +> [!NOTE] |
| 3774 | +> Multi Factor Authentication support via SDKs is currently in Early Access. |
| 3775 | +
|
| 3776 | +The SDK provides comprehensive MFA client APIs to manage multi-factor authentication for your users. The MFA client is accessible via the `mfa` property on both server and client Auth0 instances. |
| 3777 | +
|
| 3778 | +### Setup & Configuration |
| 3779 | +
|
| 3780 | +Before using MFA APIs, configure your Auth0 tenant: |
| 3781 | +
|
| 3782 | +1. **Enable MFA** in [Auth0 Dashboard > Security > Multi-factor Auth](https://manage.auth0.com/#/security/multi-factor-authentication) |
| 3783 | +2. **Configure Factors**: Enable OTP, SMS, Email, or Push Notification |
| 3784 | +3. **Set Tenant Policy** to "Adaptive" or "Never" (see [MFA Tenant Configuration](#mfa-tenant-configuration)) |
| 3785 | +4. **Configure MFA Actions** to conditionally enforce MFA for specific resources |
| 3786 | +
|
| 3787 | +### Configuration |
| 3788 | +
|
| 3789 | +Configure MFA token TTL via options or environment variable: |
| 3790 | +
|
| 3791 | +```typescript |
| 3792 | +// lib/auth0.ts |
| 3793 | +import { Auth0Client } from "@auth0/nextjs-auth0/server"; |
| 3794 | +
|
| 3795 | +export const auth0 = new Auth0Client({ |
| 3796 | + mfaContextTtl: 600 // 10 minutes in seconds |
| 3797 | +}); |
| 3798 | +``` |
| 3799 | +
|
| 3800 | +```bash |
| 3801 | +# .env.local |
| 3802 | +AUTH0_MFA_CONTEXT_TTL=600 |
| 3803 | +``` |
| 3804 | +
|
| 3805 | +Default TTL is 300 seconds (5 minutes), matching Auth0's mfa_token expiration. |
| 3806 | +
|
| 3807 | +### Handling MfaRequiredError |
| 3808 | +
|
| 3809 | +When you request an Access Token for a resource that requires MFA, Auth0 will return a `403 Forbidden`. The SDK automatically catches this and throws an `MfaRequiredError` containing the `mfaToken` needed to resolve the challenge. |
| 3810 | +
|
| 3811 | +**`mfa_required` Response:** |
| 3812 | +```json |
| 3813 | +{ |
| 3814 | + "error": "mfa_required", |
| 3815 | + "error_description": "Multifactor authentication required", |
| 3816 | + "mfa_token": "Fe26...encoded_token" |
| 3817 | +} |
| 3818 | +``` |
| 3819 | +
|
| 3820 | +Add a catch handler for `MfaRequiredError` around `getAccessToken` call: |
| 3821 | +```js |
| 3822 | +try { |
| 3823 | + const { token } = await getAccessToken({ audience: "https://api.example.com" }); |
| 3824 | +} catch (error) { |
| 3825 | + if (error instanceof MfaRequiredError) { |
| 3826 | + // MFA logic here |
| 3827 | + // You can pass the `error.mfa_token` to SDK MFA methods |
| 3828 | + // Example, redirect to MFA challenge page that contains MFA handling logic |
| 3829 | + redirect(`/mfa?token=${error.mfa_token}`); |
| 3830 | + } |
| 3831 | + throw error; |
| 3832 | +} |
| 3833 | +``` |
| 3834 | +
|
| 3835 | +### Accessing the MFA API |
| 3836 | +
|
| 3837 | +The MFA API is accessible on both the server and the client to manage authenticators and perform verification. |
| 3838 | +
|
| 3839 | +**On the Server:** |
| 3840 | +
|
| 3841 | +The MFA API is available via the `mfa` property of your `Auth0Client` instance. |
| 3842 | +
|
| 3843 | +```ts |
| 3844 | +// lib/auth0.ts |
| 3845 | +import { Auth0Client } from "@auth0/nextjs-auth0/server"; |
| 3846 | +
|
| 3847 | +export const auth0 = new Auth0Client(); |
| 3848 | +
|
| 3849 | +// Usage in Route Handler or Server Action |
| 3850 | +const authenticators = await auth0.mfa.getAuthenticators({ mfaToken }); |
| 3851 | +``` |
| 3852 | +
|
| 3853 | +**On the Client:** |
| 3854 | +
|
| 3855 | +The MFA API is available as a named export `mfa` from the client entry point. |
| 3856 | +
|
| 3857 | +```ts |
| 3858 | +// components/mfa-form.tsx |
| 3859 | +import { mfa } from "@auth0/nextjs-auth0/client"; |
| 3860 | +
|
| 3861 | +// Usage in client component |
| 3862 | +await mfa.verify({ mfaToken, otp }); |
| 3863 | +``` |
| 3864 | +
|
| 3865 | +### Getting Authenticators |
| 3866 | +
|
| 3867 | +List all enrolled authenticators for the current user: |
| 3868 | +
|
| 3869 | +```ts |
| 3870 | +const authenticators = await auth0.mfa.getAuthenticators({ mfaToken }); |
| 3871 | +``` |
| 3872 | +
|
| 3873 | +### Enrollment |
| 3874 | +
|
| 3875 | +Enroll new authenticators for MFA. Support includes OTP (TOTP apps), SMS, Email, and Push Notification. |
| 3876 | +
|
| 3877 | +**OTP (Authenticator App)** |
| 3878 | +
|
| 3879 | +```ts |
| 3880 | +// Returns secret, barcodeUri for QR code |
| 3881 | +const enrollment = await auth0.mfa.enroll({ |
| 3882 | + mfaToken, |
| 3883 | + authenticatorTypes: ["otp"] |
| 3884 | +}); |
| 3885 | +``` |
| 3886 | +
|
| 3887 | +**SMS** |
| 3888 | +
|
| 3889 | +```ts |
| 3890 | +const enrollment = await auth0.mfa.enroll({ |
| 3891 | + mfaToken, |
| 3892 | + authenticatorTypes: ["oob"], |
| 3893 | + oobChannels: ["sms"], |
| 3894 | + phoneNumber: "+15555555555" |
| 3895 | +}); |
| 3896 | +``` |
| 3897 | +
|
| 3898 | +**Email** |
| 3899 | +
|
| 3900 | +```ts |
| 3901 | +const enrollment = await auth0.mfa.enroll({ |
| 3902 | + mfaToken, |
| 3903 | + authenticatorTypes: ["oob"], |
| 3904 | + oobChannels: ["email"], |
| 3905 | + |
| 3906 | +}); |
| 3907 | +``` |
| 3908 | +
|
| 3909 | +**Push Notification** |
| 3910 | +
|
| 3911 | +```ts |
| 3912 | +const enrollment = await auth0.mfa.enroll({ |
| 3913 | + mfaToken, |
| 3914 | + authenticatorTypes: ["oob"], |
| 3915 | + oobChannels: ["auth0"] |
| 3916 | +}); |
| 3917 | +``` |
| 3918 | +
|
| 3919 | +### Challenge |
| 3920 | +
|
| 3921 | +Initiate an MFA challenge for OOB authenticators (SMS/Email/Push). OTP authenticators do not require explicit challenge. |
| 3922 | +
|
| 3923 | +```ts |
| 3924 | +// Returns oobCode and bindingMethod |
| 3925 | +const challenge = await auth0.mfa.challenge({ |
| 3926 | + mfaToken, |
| 3927 | + challengeType: "oob", |
| 3928 | + authenticatorId: "sms|..." |
| 3929 | +}); |
| 3930 | +``` |
| 3931 | +
|
| 3932 | +### Verify |
| 3933 | +
|
| 3934 | +Verify MFA with OTP code, OOB code, or recovery code. |
| 3935 | +
|
| 3936 | +**OTP Verification** |
| 3937 | +
|
| 3938 | +```ts |
| 3939 | +await auth0.mfa.verify({ |
| 3940 | + mfaToken, |
| 3941 | + otp: "123456" |
| 3942 | +}); |
| 3943 | +``` |
| 3944 | +
|
| 3945 | +**OOB Verification (SMS/Email/Push)** |
| 3946 | +
|
| 3947 | +```ts |
| 3948 | +await auth0.mfa.verify({ |
| 3949 | + mfaToken, |
| 3950 | + oobCode: challenge.oobCode, |
| 3951 | + bindingCode: "123456" // User input |
| 3952 | +}); |
| 3953 | +``` |
| 3954 | +
|
| 3955 | +**Recovery Code Verification** |
| 3956 | +
|
| 3957 | +```ts |
| 3958 | +await auth0.mfa.verify({ |
| 3959 | + mfaToken, |
| 3960 | + recoveryCode: "ABCD-EFGH-IJKL-MNOP" |
| 3961 | +}); |
| 3962 | +``` |
| 3963 | +
|
| 3964 | +### Complete Flow Examples |
| 3965 | +
|
| 3966 | +For complete implementation guides and best practices, refer to the official Auth0 documentation: |
| 3967 | +
|
| 3968 | +- [Explore multi-factor authentication](https://auth0.com/docs/secure/multi-factor-authentication) |
| 3969 | +- [Customize Multi-Factor Authentication Pages](https://auth0.com/docs/brand-and-customize/universal-login-pages/customize-mfa-pages) |
| 3970 | +
|
| 3971 | +### MFA Tenant Configuration |
| 3972 | +
|
| 3973 | +The SDK relies on background token refreshes to maintain user sessions. For these non-interactive requests to succeed, configure your MFA policies to allow `refresh_token` exchanges without immediate user challenge. |
| 3974 | +
|
| 3975 | +> [!NOTE] |
| 3976 | +> Enforcing **"Always"** or **"All Applications"** in your global Tenant MFA Policy will block background token refreshes, as they cannot satisfy an interactive MFA challenge. |
| 3977 | +
|
| 3978 | +**Recommended Configuration:** |
| 3979 | +Set Tenant MFA Policy to **"Adaptive"** or **"Never"**. |
| 3980 | +
|
| 3981 | +**Example Action Code:** |
| 3982 | +```javascript |
| 3983 | +exports.onExecutePostLogin = async (event, api) => { |
| 3984 | + // Only trigger on refresh_token grant (step-up) |
| 3985 | + if (event.request?.body?.grant_type == "refresh_token") { |
| 3986 | + |
| 3987 | + if (event.user.enrolledFactors.length) { |
| 3988 | + // User has factors enrolled - challenge |
| 3989 | + api.authentication.challengeWithAny([ |
| 3990 | + { type: 'otp' }, |
| 3991 | + { type: 'phone' }, |
| 3992 | + { type: 'push-notification' }, |
| 3993 | + { type: 'email' }, |
| 3994 | + { type: 'recovery-code' } |
| 3995 | + ]); |
| 3996 | + } else { |
| 3997 | + // No factors enrolled - prompt enrollment |
| 3998 | + api.authentication.enrollWithAny([ |
| 3999 | + { type: 'otp'}, |
| 4000 | + { type: 'phone'}, |
| 4001 | + { type: 'push-notification' } |
| 4002 | + ]); |
| 4003 | + } |
| 4004 | + } |
| 4005 | +}; |
| 4006 | +``` |
| 4007 | +
|
| 4008 | +### MFA Error Handling |
| 4009 | +
|
| 4010 | +The SDK provides typed error classes for all MFA operations: |
| 4011 | +
|
| 4012 | +| Error Class | Code | When Thrown | Example | |
| 4013 | +|-------------|------|-------------|---------| |
| 4014 | +| `MfaRequiredError` | `mfa_required` | Token refresh requires MFA step-up | Accessing protected API | |
| 4015 | +| `MfaGetAuthenticatorsError` | Various | Failed to list authenticators | Invalid/expired token | |
| 4016 | +| `MfaEnrollmentError` | Various | Enrollment failed | Unsupported factor type | |
| 4017 | +| `MfaDeleteAuthenticatorError` | Various | Delete failed | Authenticator not found | |
| 4018 | +| `MfaChallengeError` | Various | Challenge failed | Invalid authenticator ID | |
| 4019 | +| `MfaVerifyError` | `invalid_grant` | Verification failed | Invalid OTP code | |
| 4020 | +| `MfaTokenNotFoundError` | `mfa_token_not_found` | No MFA context for token | Token not in session | |
| 4021 | +| `MfaTokenExpiredError` | `mfa_token_expired` | Token TTL exceeded | Context expired | |
| 4022 | +| `MfaTokenInvalidError` | `mfa_token_invalid` | Token tampered or wrong secret | Decryption failed | |
| 4023 | +
|
3761 | 4024 | ## Reactive MFA Step-Up (Popup) |
3762 | 4025 |
|
3763 | 4026 | ### Overview |
|
0 commit comments