Skip to content

Commit 802375b

Browse files
feat: add MyAccount API client support (#1121)
The `myAccount` client introduced in [auth0-spa-js#1615](auth0/auth0-spa-js#1615) was not yet surfaced through the React SDK. This PR exposes it so it is accessible via `useAuth0`. ## Changes ### `useAuth0().myAccount` (new) **Factors** - `getFactors()`: list all MFA factors enabled on the tenant with their `type` and `usage` **Authentication methods** - `getAuthenticationMethods(type?)`: list enrolled methods, optionally filtered by type - `getAuthenticationMethod(id)`: get a single method by ID - `updateAuthenticationMethod(id, data)`: update `name` or `preferred_authentication_method` - `deleteAuthenticationMethod(id)`: remove an enrolled method **Enrollment (two-step)** - `enrollmentChallenge(options)`: initiate enrollment for any factor type; returns challenge data (WebAuthn creation options, barcode URI, recovery code, password policy, etc.) - `enrollmentVerify(options)`: confirm enrollment; returns the created `AuthenticationMethod` Supports: `passkey`, `webauthn-platform`, `webauthn-roaming`, `phone`, `email`, `totp`, `push-notification`, `recovery-code`, `password`. ### Implementation pattern `myAccount` follows the same pattern as `mfa` — a `useMemo(() => client.myAccount, [client])` passthrough. No `useCallback` wrappers or `GET_ACCESS_TOKEN_COMPLETE` dispatches are needed since none of these operations affect `isAuthenticated` or `user`. ### Exports `MyAccountApiClient` (type) and `MyAccountApiError` are now exported from the package root. ### Dependency Bumps `@auth0/auth0-spa-js` to `^2.21.1`. ## Test plan **Manual: authentication methods CRUD** - [x] `getAuthenticationMethods()` / `getAuthenticationMethods("passkey")`: correct fields, filtering works - [x] `getAuthenticationMethod(id)`: correct shape - [x] `updateAuthenticationMethod(id, { name })`: confirmed working for `totp` - [x] `deleteAuthenticationMethod(id)`: secondary methods deleted; primary passkey deletion correctly rejected - [x] `getFactors()`: all enabled factors returned with correct `type` and `usage` **Manual: end-to-end passkey enrollment** - [x] User logs in with username/password (PKCE) - [x] User enrolls a passkey via `enrollmentChallenge` + `enrollmentVerify` - [x] User logs out, then logs back in via `auth0.passkey.login()` with no password required **Regression** - [x] `connectAccountWithRedirect` + `handleRedirectCallback`: connected accounts flow unaffected - [x] `auth0.passkey.signup()` / `auth0.passkey.login()`: passkey flows unaffected
1 parent 3c69716 commit 802375b

7 files changed

Lines changed: 462 additions & 3 deletions

File tree

EXAMPLES.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Step-Up Authentication](#step-up-authentication)
1818
- [Native to Web SSO](#native-to-web-sso)
1919
- [Passkeys](#passkeys)
20+
- [MyAccount API](#myaccount-api)
2021

2122
## Use with a Class Component
2223

@@ -1380,3 +1381,238 @@ try {
13801381
13811382
> [!TIP]
13821383
> Both `signup()` and `login()` throw an error if the user cancels the biometric prompt. Wrap calls in `try/catch` to handle cancellation, network failures, or misconfigured connections.
1384+
1385+
## MyAccount API
1386+
1387+
The MyAccount API lets you manage the current user's authentication methods, factors, and connected accounts directly from the SPA.
1388+
1389+
> [!NOTE]
1390+
> The MyAccount API requires refresh tokens and MRRT if your app is configured with a custom API audience. DPoP is supported but optional.
1391+
1392+
### Factors
1393+
1394+
Get the list of MFA factors and their enabled status for the current user.
1395+
1396+
```jsx
1397+
const { myAccount } = useAuth0();
1398+
1399+
const factors = await myAccount.getFactors();
1400+
// [{ type: 'totp', usage: ['secondary'] }, { type: 'phone', usage: ['secondary'] }]
1401+
```
1402+
1403+
### Authentication Methods
1404+
1405+
#### List All
1406+
1407+
```jsx
1408+
const { myAccount } = useAuth0();
1409+
1410+
const methods = await myAccount.getAuthenticationMethods();
1411+
```
1412+
1413+
#### Filter by Type
1414+
1415+
```jsx
1416+
const passkeys = await myAccount.getAuthenticationMethods('passkey');
1417+
```
1418+
1419+
#### Get by ID
1420+
1421+
```jsx
1422+
const method = await myAccount.getAuthenticationMethod('am_abc123');
1423+
```
1424+
1425+
#### Delete
1426+
1427+
```jsx
1428+
await myAccount.deleteAuthenticationMethod('am_abc123');
1429+
```
1430+
1431+
#### Update
1432+
1433+
```jsx
1434+
// Rename any method
1435+
const updated = await myAccount.updateAuthenticationMethod('am_abc123', {
1436+
name: 'My Work Laptop'
1437+
});
1438+
```
1439+
1440+
```jsx
1441+
// Change preferred delivery method for phone
1442+
const updated = await myAccount.updateAuthenticationMethod('am_abc123', {
1443+
preferred_authentication_method: 'voice'
1444+
});
1445+
```
1446+
1447+
### Enrollment
1448+
1449+
Enrollment is a two-step flow: get a challenge, then verify the credential.
1450+
1451+
#### Passkey
1452+
1453+
```jsx
1454+
const { myAccount } = useAuth0();
1455+
1456+
// Step 1: get the WebAuthn creation challenge
1457+
const challenge = await myAccount.enrollmentChallenge({ type: 'passkey' });
1458+
1459+
// Step 2: trigger the browser ceremony
1460+
const credential = await navigator.credentials.create({
1461+
publicKey: {
1462+
...challenge.authn_params_public_key,
1463+
challenge: base64urlToBuffer(challenge.authn_params_public_key.challenge),
1464+
user: {
1465+
...challenge.authn_params_public_key.user,
1466+
id: base64urlToBuffer(challenge.authn_params_public_key.user.id)
1467+
}
1468+
}
1469+
});
1470+
1471+
// Step 3: verify and complete enrollment
1472+
const method = await myAccount.enrollmentVerify({
1473+
type: 'passkey',
1474+
location: challenge.location,
1475+
auth_session: challenge.auth_session,
1476+
authn_response: serializeCredential(credential)
1477+
});
1478+
```
1479+
1480+
#### Phone
1481+
1482+
```jsx
1483+
// Step 1: request OTP to the phone number
1484+
const challenge = await myAccount.enrollmentChallenge({
1485+
type: 'phone',
1486+
phone_number: '+15551234567',
1487+
preferred_authentication_method: 'sms'
1488+
});
1489+
1490+
// Step 2: verify with the OTP the user received
1491+
await myAccount.enrollmentVerify({
1492+
type: 'phone',
1493+
location: challenge.location,
1494+
auth_session: challenge.auth_session,
1495+
otp_code: '123456'
1496+
});
1497+
```
1498+
1499+
#### Email
1500+
1501+
```jsx
1502+
const challenge = await myAccount.enrollmentChallenge({
1503+
type: 'email',
1504+
email: 'user@example.com'
1505+
});
1506+
1507+
await myAccount.enrollmentVerify({
1508+
type: 'email',
1509+
location: challenge.location,
1510+
auth_session: challenge.auth_session,
1511+
otp_code: '123456'
1512+
});
1513+
```
1514+
1515+
#### TOTP
1516+
1517+
```jsx
1518+
const challenge = await myAccount.enrollmentChallenge({ type: 'totp' });
1519+
// challenge.barcode_uri — show this as a QR code for the user to scan
1520+
// challenge.manual_input_code — fallback manual entry code
1521+
1522+
await myAccount.enrollmentVerify({
1523+
type: 'totp',
1524+
location: challenge.location,
1525+
auth_session: challenge.auth_session,
1526+
otp_code: '123456'
1527+
});
1528+
```
1529+
1530+
#### WebAuthn Platform / Roaming
1531+
1532+
Same flow as [Passkey](#passkey) above — just change the `type`:
1533+
1534+
```jsx
1535+
// Platform authenticator (e.g. Touch ID, Windows Hello)
1536+
const challenge = await myAccount.enrollmentChallenge({ type: 'webauthn-platform' });
1537+
1538+
// Roaming authenticator (e.g. a hardware security key)
1539+
const challenge = await myAccount.enrollmentChallenge({ type: 'webauthn-roaming' });
1540+
1541+
// The credential creation ceremony and verify call are identical to passkey
1542+
await myAccount.enrollmentVerify({
1543+
type: 'webauthn-platform', // or 'webauthn-roaming'
1544+
location: challenge.location,
1545+
auth_session: challenge.auth_session,
1546+
authn_response: serializeCredential(credential)
1547+
});
1548+
```
1549+
1550+
#### Push Notification
1551+
1552+
```jsx
1553+
const challenge = await myAccount.enrollmentChallenge({ type: 'push-notification' });
1554+
// challenge.barcode_uri — show this as a QR code to link the authenticator app
1555+
1556+
// No OTP needed — user approves on their device
1557+
await myAccount.enrollmentVerify({
1558+
type: 'push-notification',
1559+
location: challenge.location,
1560+
auth_session: challenge.auth_session
1561+
});
1562+
```
1563+
1564+
#### Recovery Code
1565+
1566+
```jsx
1567+
const challenge = await myAccount.enrollmentChallenge({ type: 'recovery-code' });
1568+
// challenge.recovery_code — display this to the user to save securely
1569+
1570+
// Verify just confirms the user has saved the code
1571+
await myAccount.enrollmentVerify({
1572+
type: 'recovery-code',
1573+
location: challenge.location,
1574+
auth_session: challenge.auth_session
1575+
});
1576+
```
1577+
1578+
#### Password
1579+
1580+
```jsx
1581+
const challenge = await myAccount.enrollmentChallenge({ type: 'password' });
1582+
1583+
await myAccount.enrollmentVerify({
1584+
type: 'password',
1585+
location: challenge.location,
1586+
auth_session: challenge.auth_session,
1587+
new_password: 'newSecurePassword123!'
1588+
});
1589+
```
1590+
1591+
### Error Handling
1592+
1593+
All MyAccount API errors throw `MyAccountApiError` with RFC 7807 fields.
1594+
1595+
```jsx
1596+
import { MyAccountApiError } from '@auth0/auth0-react';
1597+
1598+
const { myAccount } = useAuth0();
1599+
1600+
try {
1601+
await myAccount.enrollmentChallenge({ type: 'passkey' });
1602+
} catch (err) {
1603+
if (err instanceof MyAccountApiError) {
1604+
console.error(err.status, err.title, err.detail);
1605+
if (err.validation_errors) {
1606+
err.validation_errors.forEach(e => console.error(e.field, e.detail));
1607+
}
1608+
}
1609+
}
1610+
1611+
try {
1612+
await myAccount.deleteAuthenticationMethod('am_abc123');
1613+
} catch (err) {
1614+
if (err instanceof MyAccountApiError) {
1615+
console.error(err.status, err.title, err.detail);
1616+
}
1617+
}
1618+
```

__mocks__/@auth0/auth0-spa-js.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ const mfaGetEnrollmentFactors = jest.fn(() => Promise.resolve([]));
3030
const passkeySignup = jest.fn(() => Promise.resolve({ access_token: 'passkey-token', id_token: 'passkey-id-token' }));
3131
const passkeyLogin = jest.fn(() => Promise.resolve({ access_token: 'passkey-token', id_token: 'passkey-id-token' }));
3232

33+
const myAccountGetFactors = jest.fn(() => Promise.resolve([]));
34+
const myAccountGetAuthenticationMethods = jest.fn(() => Promise.resolve([]));
35+
const myAccountGetAuthenticationMethod = jest.fn(() => Promise.resolve({ id: 'test-method-id' }));
36+
const myAccountUpdateAuthenticationMethod = jest.fn(() => Promise.resolve({ id: 'test-method-id' }));
37+
const myAccountDeleteAuthenticationMethod = jest.fn(() => Promise.resolve());
38+
const myAccountEnrollmentChallenge = jest.fn(() => Promise.resolve({ id: 'test-challenge-id', location: 'https://example.auth0.com/enroll', auth_session: 'test-auth-session', type: 'totp', barcode_uri: 'otpauth://totp/...' }));
39+
const myAccountEnrollmentVerify = jest.fn(() => Promise.resolve({ id: 'test-method-id' }));
40+
3341
export const Auth0Client = jest.fn(() => {
3442
return {
3543
buildAuthorizeUrl,
@@ -64,6 +72,15 @@ export const Auth0Client = jest.fn(() => {
6472
signup: passkeySignup,
6573
login: passkeyLogin,
6674
},
75+
myAccount: {
76+
getFactors: myAccountGetFactors,
77+
getAuthenticationMethods: myAccountGetAuthenticationMethods,
78+
getAuthenticationMethod: myAccountGetAuthenticationMethod,
79+
updateAuthenticationMethod: myAccountUpdateAuthenticationMethod,
80+
deleteAuthenticationMethod: myAccountDeleteAuthenticationMethod,
81+
enrollmentChallenge: myAccountEnrollmentChallenge,
82+
enrollmentVerify: myAccountEnrollmentVerify,
83+
},
6784
};
6885
});
6986

@@ -79,4 +96,5 @@ export const MfaEnrollmentFactorsError = actual.MfaEnrollmentFactorsError;
7996
export const PasskeyError = actual.PasskeyError;
8097
export const PasskeyRegisterError = actual.PasskeyRegisterError;
8198
export const PasskeyChallengeError = actual.PasskeyChallengeError;
82-
export const PasskeyGetTokenError = actual.PasskeyGetTokenError;
99+
export const PasskeyGetTokenError = actual.PasskeyGetTokenError;
100+
export const MyAccountApiError = actual.MyAccountApiError;

0 commit comments

Comments
 (0)