Skip to content

Commit 2ee5f4b

Browse files
committed
feat(mfa): Add MFA endpoints for TOTP setup
Because: * We will be wrapping TOTP creation flow with an MFA guard, this sets up the backend support This commit: * Adds MFA-variants of the TOTP setup endpoints * Adds and updates unit tests * Adds integration tests for the MFA routes * Adds docs for the new endpoints and a bit of extra docs for the original session-token-based endpoints * Adds a little mail helper to obtain the MFA code in tests Issue #FXA-12229
1 parent d1ed41d commit 2ee5f4b

6 files changed

Lines changed: 861 additions & 420 deletions

File tree

packages/fxa-auth-client/lib/client.ts

Lines changed: 152 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,24 @@ export default class AuthClient {
17631763
);
17641764
}
17651765

1766+
/**
1767+
* @deprecated Use createTotpTokenWithJwt instead
1768+
*
1769+
* Create a TOTP secret for the account (session-token variant).
1770+
*
1771+
* Requires a verified session token. Returns the QR code URL and shared
1772+
* secret used to enroll an authenticator app, and optionally a set of
1773+
* recovery codes.
1774+
*
1775+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1776+
* prefer the MFA JWT variant.
1777+
*
1778+
* @param sessionToken - Verified session token for the signed-in user
1779+
* @param options - Optional request options
1780+
* @param options.metricsContext - Optional metrics context for telemetry
1781+
* @param headers - Optional additional request headers
1782+
* @returns Promise resolving to `{ qrCodeUrl, secret }`
1783+
*/
17661784
async createTotpToken(
17671785
sessionToken: hexstring,
17681786
options: {
@@ -1776,6 +1794,49 @@ export default class AuthClient {
17761794
return this.sessionPost('/totp/create', sessionToken, options, headers);
17771795
}
17781796

1797+
/**
1798+
* Create a new TOTP secret (JWT variant).
1799+
*
1800+
* Requires an MFA JWT with scope `mfa:2fa`. Returns the QR code URL,
1801+
* the shared secret, and optionally recovery codes when requested.
1802+
*
1803+
* @param jwt MFA access token including `mfa:2fa` scope
1804+
* @param options Optional request options
1805+
* @param options.metricsContext Optional metrics context for telemetry
1806+
* @param headers Optional extra headers
1807+
* @returns Promise resolving to `{ qrCodeUrl, secret }`
1808+
*/
1809+
async createTotpTokenWithJwt(
1810+
jwt: string,
1811+
options: {
1812+
metricsContext?: MetricsContext;
1813+
} = {},
1814+
headers?: Headers
1815+
): Promise<{
1816+
qrCodeUrl: string;
1817+
secret: string;
1818+
}> {
1819+
return this.jwtPost('/mfa/totp/create', jwt, options, headers);
1820+
}
1821+
1822+
/**
1823+
* @deprecated Use verifyTotpSetupCodeWithJwt instead
1824+
* Verify the authenticator code for the in-progress TOTP setup.
1825+
*
1826+
* Validates a 6-digit code generated by the app using the secret returned by
1827+
* `createTotpToken`. This does not finalize setup; call `completeTotpSetup`
1828+
* to activate TOTP.
1829+
*
1830+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1831+
* prefer the MFA JWT variant.
1832+
*
1833+
* @param sessionToken - Verified session token for the signed-in user
1834+
* @param code - 6-digit code from the authenticator app
1835+
* @param options - Optional request options
1836+
* @param options.metricsContext - Optional metrics context for telemetry
1837+
* @param headers - Optional additional request headers
1838+
* @returns Promise resolving to `{ success: boolean }`
1839+
*/
17791840
async verifyTotpSetupCode(
17801841
sessionToken: hexstring,
17811842
code: string,
@@ -1790,6 +1851,49 @@ export default class AuthClient {
17901851
);
17911852
}
17921853

1854+
/**
1855+
* Verify a TOTP setup code against the in-progress secret (JWT variant).
1856+
*
1857+
* Requires an MFA JWT with scope `mfa:2fa`. On success, marks the setup
1858+
* as verified in Redis and refreshes TTLs.
1859+
*
1860+
* @param jwt MFA access token including `mfa:2fa` scope
1861+
* @param code The 6-digit authenticator app code
1862+
* @param options Optional request options
1863+
* @param options.metricsContext Optional metrics context for telemetry
1864+
* @param headers Optional extra headers
1865+
* @returns Promise resolving to `{ success: boolean }`
1866+
*/
1867+
async verifyTotpSetupCodeWithJwt(
1868+
jwt: string,
1869+
code: string,
1870+
options: { metricsContext?: MetricsContext } = {},
1871+
headers?: Headers
1872+
): Promise<{ success: boolean }> {
1873+
return this.jwtPost(
1874+
'/mfa/totp/setup/verify',
1875+
jwt,
1876+
{ code, ...options },
1877+
headers
1878+
);
1879+
}
1880+
1881+
/**
1882+
* @deprecated Use completeTotpSetupWithJwt instead
1883+
* Finalize TOTP setup for the account.
1884+
*
1885+
* Marks the verified in-progress secret as the active second factor.
1886+
*
1887+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1888+
* prefer the MFA JWT variant.
1889+
*
1890+
* @param sessionToken - Verified session token for the signed-in user
1891+
* @param options - Optional request options
1892+
* @param options.service - Optional service name
1893+
* @param options.metricsContext - Optional metrics context for telemetry
1894+
* @param headers - Optional additional request headers
1895+
* @returns Promise resolving to `{ success: boolean }`
1896+
*/
17931897
async completeTotpSetup(
17941898
sessionToken: hexstring,
17951899
options: { service?: string; metricsContext?: MetricsContext } = {},
@@ -1803,6 +1907,31 @@ export default class AuthClient {
18031907
);
18041908
}
18051909

1910+
/**
1911+
* Complete TOTP setup after successful code verification (JWT variant).
1912+
*
1913+
* Requires an MFA JWT with scope `mfa:2fa`. Validates the verification
1914+
* flag for the current secret and persists the enabled, verified token.
1915+
*
1916+
* @param jwt MFA access token including `mfa:2fa` scope
1917+
* @param options Optional request options
1918+
* @param options.metricsContext Optional metrics context for telemetry
1919+
* @param headers Optional extra headers
1920+
* @returns Promise resolving to `{ success: boolean }`
1921+
*/
1922+
async completeTotpSetupWithJwt(
1923+
jwt: string,
1924+
options: { metricsContext?: MetricsContext } = {},
1925+
headers?: Headers
1926+
): Promise<{ success: boolean }> {
1927+
return this.jwtPost(
1928+
'/mfa/totp/setup/complete',
1929+
jwt,
1930+
{ ...options },
1931+
headers
1932+
);
1933+
}
1934+
18061935
/**
18071936
* @deprecated Use startReplaceTotpTokenWithJwt instead
18081937
* Initiates the TOTP replacement flow using a session token
@@ -1831,29 +1960,6 @@ export default class AuthClient {
18311960
);
18321961
}
18331962

1834-
/**
1835-
* @deprecated Use confirmReplaceTotpTokenWithJwt instead
1836-
* Confirms TOTP replacement by verifying the code for the in-progress secret.
1837-
* Requires a verified session token.
1838-
*
1839-
* @param sessionToken - required, must be a verified session token
1840-
* @param code - 6-digit authenticator app code to validate the new secret
1841-
* @param headers - Optional additional headers for the request
1842-
* @returns A promise that resolves when the replacement has been accepted
1843-
*/
1844-
async confirmReplaceTotpToken(
1845-
sessionToken: hexstring,
1846-
code: string,
1847-
headers?: Headers
1848-
): Promise<void> {
1849-
return this.sessionPost(
1850-
'/totp/replace/confirm',
1851-
sessionToken,
1852-
{ code },
1853-
headers
1854-
);
1855-
}
1856-
18571963
/**
18581964
* Initiates the TOTP replacement flow using an MFA JWT.
18591965
* Requires a JWT with scope `mfa:2fa` and returns data to
@@ -1877,6 +1983,29 @@ export default class AuthClient {
18771983
return this.jwtPost('/mfa/totp/replace/start', jwt, options, headers);
18781984
}
18791985

1986+
/**
1987+
* @deprecated Use confirmReplaceTotpTokenWithJwt instead
1988+
* Confirms TOTP replacement by verifying the code for the in-progress secret.
1989+
* Requires a verified session token.
1990+
*
1991+
* @param sessionToken - required, must be a verified session token
1992+
* @param code - 6-digit authenticator app code to validate the new secret
1993+
* @param headers - Optional additional headers for the request
1994+
* @returns A promise that resolves when the replacement has been accepted
1995+
*/
1996+
async confirmReplaceTotpToken(
1997+
sessionToken: hexstring,
1998+
code: string,
1999+
headers?: Headers
2000+
): Promise<void> {
2001+
return this.sessionPost(
2002+
'/totp/replace/confirm',
2003+
sessionToken,
2004+
{ code },
2005+
headers
2006+
);
2007+
}
2008+
18802009
/**
18812010
* Confirms TOTP replacement by verifying the code for the in-progress secret.
18822011
* Requires a valid MFA JWT scoped to the action (e.g. `mfa:2fa`).

packages/fxa-auth-server/docs/swagger/totp-api.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ const TOTP_CREATE_POST = {
2121
],
2222
};
2323

24+
const MFA_TOTP_CREATE_POST = {
25+
...TAGS_TOTP,
26+
description: '/mfa/totp/create',
27+
notes: [
28+
dedent`
29+
🔒 Authenticated with MFA JWT (scope: mfa:2fa)
30+
31+
Create a new randomly generated TOTP token for a user if they do not currently have one. This variant requires an MFA JWT and is intended for flows that have already passed MFA requirements.
32+
`,
33+
],
34+
};
35+
2436
const TOTP_DESTROY_POST = {
2537
...TAGS_TOTP,
2638
description: '/totp/destroy',
@@ -153,6 +165,18 @@ const TOTP_SETUP_VERIFY_POST = {
153165
],
154166
};
155167

168+
const MFA_TOTP_SETUP_VERIFY_POST = {
169+
...TAGS_TOTP,
170+
description: '/mfa/totp/setup/verify',
171+
notes: [
172+
dedent`
173+
🔒 Authenticated with MFA JWT (scope: mfa:2fa)
174+
175+
Verifies an authenticator app code against the in-progress TOTP secret stored in Redis during setup, using an MFA JWT. On success, marks the setup as verified in Redis and aligns TTLs.
176+
`,
177+
],
178+
};
179+
156180
const TOTP_SETUP_COMPLETE_POST = {
157181
...TAGS_TOTP,
158182
description: '/totp/setup/complete',
@@ -165,9 +189,22 @@ const TOTP_SETUP_COMPLETE_POST = {
165189
],
166190
};
167191

192+
const MFA_TOTP_SETUP_COMPLETE_POST = {
193+
...TAGS_TOTP,
194+
description: '/mfa/totp/setup/complete',
195+
notes: [
196+
dedent`
197+
🔒 Authenticated with MFA JWT (scope: mfa:2fa)
198+
199+
Completes TOTP setup (JWT variant) by validating the Redis verification flag for the current secret, then persisting the secret to the database as enabled and verified. Cleans up temporary Redis entries.
200+
`,
201+
],
202+
};
203+
168204
const API_DOCS = {
169205
SESSION_VERIFY_TOTP_POST,
170206
TOTP_CREATE_POST,
207+
MFA_TOTP_CREATE_POST,
171208
TOTP_DESTROY_POST,
172209
MFA_TOTP_DESTROY_POST,
173210
TOTP_EXISTS_GET,
@@ -178,7 +215,9 @@ const API_DOCS = {
178215
MFA_TOTP_REPLACE_START_POST,
179216
MFA_TOTP_REPLACE_CONFIRM_POST,
180217
TOTP_SETUP_VERIFY_POST,
218+
MFA_TOTP_SETUP_VERIFY_POST,
181219
TOTP_SETUP_COMPLETE_POST,
220+
MFA_TOTP_SETUP_COMPLETE_POST,
182221
};
183222

184223
export default API_DOCS;

0 commit comments

Comments
 (0)