Skip to content
Open
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
202 changes: 201 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,4 +778,204 @@ const ConfigInfo = () => {
export default ConfigInfo;
```

This is useful for debugging, logging, or building custom Auth0-related URLs without duplicating configuration values.
This is useful for debugging, logging, or building custom Auth0-related URLs without duplicating configuration values.

## Multi-Factor Authentication (MFA)

Access MFA operations through the `mfa` property from `useAuth0()`. All operations require an `mfa_token` from the MFA required error.

### 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" }
...
]
}
}
```

Based on the response:
- **`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

> [!NOTE]
> The SDK handles this logic automatically. When you call `getEnrollmentFactors()` or `getAuthenticators()`, the SDK uses the stored context to return the appropriate data.


### Handling MFA Required Error
When MFA is required, the SDK automatically stores the context. You can then call MFA methods with just the token:

```jsx
import { useAuth0, MfaRequiredError } from '@auth0/auth0-react';

try {
await getAccessTokenSilently();
} catch (error) {
if (error instanceof MfaRequiredError) {
const mfaToken = error.mfa_token;

// Check if user needs to enroll
const factors = await mfa.getEnrollmentFactors(mfaToken);
if (factors.length > 0) {
// Show enrollment UI
} else {
// User has enrolled authenticators - get the list of enrolled authenticator
const authenticators = await mfa.getAuthenticators(error.mfa_token);

// proceed with challenge
}
}
}
```

### Enrolling Authenticators

```jsx
const { mfa } = useAuth0();

// Enroll any factor type
const enrollment = await mfa.enroll({
mfaToken,
factorType: 'otp' // 'otp' | 'sms' | 'email' | 'voice' | 'push'
});

// For OTP: Display QR code
console.log('Scan:', enrollment.barcodeUri);
console.log('Recovery codes:', enrollment.recoveryCodes);

// For SMS: Include phone number
await mfa.enroll({
mfaToken,
factorType: 'sms',
phoneNumber: '+12025551234' // E.164 format
});

// For Voice: Include phone number
await mfa.enroll({
mfaToken,
factorType: 'voice',
phoneNumber: '+12025551234' // E.164 format
});

// For Email: Include email address
await mfa.enroll({
mfaToken,
factorType: 'email',
email: 'user@example.com'
});

// For Push: Returns authenticator for mobile app
const pushEnrollment = await mfa.enroll({
mfaToken,
factorType: 'push'
});
console.log('Authenticator ID:', pushEnrollment.id); // Use with Guardian app
```

### Challenging Authenticators

```jsx
const { mfa } = useAuth0();

// Get enrolled authenticators
const authenticators = await mfa.getAuthenticators(mfaToken);

// For OTP: Challenge is OPTIONAL - code already available in authenticator app
// Skip directly to verify() with the 6-digit code, or optionally challenge with:
const otpResponse = await mfa.challenge({
mfaToken,
challengeType: 'otp',
authenticatorId: authenticators[0].id
});

// For SMS/Voice/Email/Push: Challenge REQUIRED to send code (use 'oob' type)
const oobResponse = await mfa.challenge({
mfaToken,
challengeType: 'oob', // Use 'oob' for all out-of-band authenticators
authenticatorId: authenticators[0].id // ID of SMS/Voice/Email/Push authenticator
});
console.log('OOB Code:', oobResponse.oobCode); // Code sent via SMS/Voice/Email/Push
```

### Verifying Challenges

```jsx
const { mfa } = useAuth0();

// Verify with OTP code (for OTP authenticators)
const tokens = await mfa.verify({
mfaToken,
otp: '123456' // 6-digit code from authenticator app
});

// Verify with OOB code (for SMS/Voice/Email authenticators)
const tokens = await mfa.verify({
mfaToken,
oobCode: smsResponse.oobCode,
bindingCode: '123456' // Optional: code shown in challenge
});

// Verify with recovery code (works for any authenticator)
const tokens = await mfa.verify({
mfaToken,
recoveryCode: 'recovery-code-here'
});

// Tokens are now cached - user is authenticated
console.log('Access token:', tokens.access_token);
```

### Error Handling

```jsx
import {
MfaEnrollmentError,
MfaChallengeError,
MfaVerifyError
} from '@auth0/auth0-react';

try {
await mfa.verify({ mfaToken, otp });
} catch (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);
}
}
```
21 changes: 20 additions & 1 deletion __mocks__/@auth0/auth0-spa-js.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const setDpopNonce = jest.fn();
const generateDpopProof = jest.fn();
const createFetcher = jest.fn();
const getConfiguration = jest.fn();
const mfaGetAuthenticators = jest.fn(() => Promise.resolve([]));
const mfaEnroll = jest.fn(() => Promise.resolve({ id: 'test-id', barcodeUri: 'test-uri', recoveryCodes: [] }));
const mfaChallenge = jest.fn(() => Promise.resolve({ challengeType: 'otp', oobCode: null }));
const mfaVerify = jest.fn(() => Promise.resolve({ access_token: 'test-token', id_token: 'test-id-token' }));
const mfaGetEnrollmentFactors = jest.fn(() => Promise.resolve([]));

export const Auth0Client = jest.fn(() => {
return {
Expand All @@ -43,7 +48,21 @@ export const Auth0Client = jest.fn(() => {
generateDpopProof,
createFetcher,
getConfiguration,
mfa: {
getAuthenticators: mfaGetAuthenticators,
enroll: mfaEnroll,
challenge: mfaChallenge,
verify: mfaVerify,
getEnrollmentFactors: mfaGetEnrollmentFactors,
},
};
});

export const ResponseType = actual.ResponseType;
export const ResponseType = actual.ResponseType;

export const MfaError = actual.MfaError;
export const MfaListAuthenticatorsError = actual.MfaListAuthenticatorsError;
export const MfaEnrollmentError = actual.MfaEnrollmentError;
export const MfaChallengeError = actual.MfaChallengeError;
export const MfaVerifyError = actual.MfaVerifyError;
export const MfaEnrollmentFactorsError = actual.MfaEnrollmentFactorsError;
96 changes: 96 additions & 0 deletions __tests__/mfa.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { renderHook, waitFor } from '@testing-library/react';
import useAuth0 from '../src/use-auth0';
import { createWrapper } from './helpers';

describe('MFA API', () => {
describe('Basic Availability', () => {
it('should provide mfa client through useAuth0', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => {
expect(result.current.mfa).toBeDefined();
});
});

it('should provide all five MFA methods', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => {
expect(result.current.mfa.getAuthenticators).toBeDefined();
expect(result.current.mfa.enroll).toBeDefined();
expect(result.current.mfa.challenge).toBeDefined();
expect(result.current.mfa.verify).toBeDefined();
expect(result.current.mfa.getEnrollmentFactors).toBeDefined();
});
});
});

describe('Method Success Tests', () => {
it('should call mfa.getAuthenticators', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(async () => {
const authenticators = await result.current.mfa.getAuthenticators('test-mfa-token');
expect(authenticators).toBeDefined();
expect(Array.isArray(authenticators)).toBe(true);
});
});

it('should call mfa.enroll', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(async () => {
const enrollment = await result.current.mfa.enroll({
mfaToken: 'test-mfa-token',
factorType: 'otp',
});
expect(enrollment).toBeDefined();
expect(enrollment.id).toBe('test-id');
});
});

it('should call mfa.challenge', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(async () => {
const response = await result.current.mfa.challenge({
mfaToken: 'test-mfa-token',
challengeType: 'otp',
authenticatorId: 'test-auth-id',
});
expect(response).toBeDefined();
expect(response.challengeType).toBe('otp');
});
});

it('should call mfa.verify', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(async () => {
const tokens = await result.current.mfa.verify({
mfaToken: 'test-mfa-token',
otp: '123456',
});
expect(tokens).toBeDefined();
expect(tokens.access_token).toBe('test-token');
});
});

it('should call mfa.getEnrollmentFactors', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(async () => {
const factors = await result.current.mfa.getEnrollmentFactors('test-mfa-token');
expect(factors).toBeDefined();
expect(Array.isArray(factors)).toBe(true);
});
});
});
});
Loading