Skip to content

Commit 9e4ad95

Browse files
authored
Merge pull request #19473 from mozilla/FXA-12229-backend
feat(mfa): Add MFA endpoints for TOTP setup
2 parents 602b7c1 + 2ee5f4b commit 9e4ad95

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
@@ -1784,6 +1784,24 @@ export default class AuthClient {
17841784
);
17851785
}
17861786

1787+
/**
1788+
* @deprecated Use createTotpTokenWithJwt instead
1789+
*
1790+
* Create a TOTP secret for the account (session-token variant).
1791+
*
1792+
* Requires a verified session token. Returns the QR code URL and shared
1793+
* secret used to enroll an authenticator app, and optionally a set of
1794+
* recovery codes.
1795+
*
1796+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1797+
* prefer the MFA JWT variant.
1798+
*
1799+
* @param sessionToken - Verified session token for the signed-in user
1800+
* @param options - Optional request options
1801+
* @param options.metricsContext - Optional metrics context for telemetry
1802+
* @param headers - Optional additional request headers
1803+
* @returns Promise resolving to `{ qrCodeUrl, secret }`
1804+
*/
17871805
async createTotpToken(
17881806
sessionToken: hexstring,
17891807
options: {
@@ -1797,6 +1815,49 @@ export default class AuthClient {
17971815
return this.sessionPost('/totp/create', sessionToken, options, headers);
17981816
}
17991817

1818+
/**
1819+
* Create a new TOTP secret (JWT variant).
1820+
*
1821+
* Requires an MFA JWT with scope `mfa:2fa`. Returns the QR code URL,
1822+
* the shared secret, and optionally recovery codes when requested.
1823+
*
1824+
* @param jwt MFA access token including `mfa:2fa` scope
1825+
* @param options Optional request options
1826+
* @param options.metricsContext Optional metrics context for telemetry
1827+
* @param headers Optional extra headers
1828+
* @returns Promise resolving to `{ qrCodeUrl, secret }`
1829+
*/
1830+
async createTotpTokenWithJwt(
1831+
jwt: string,
1832+
options: {
1833+
metricsContext?: MetricsContext;
1834+
} = {},
1835+
headers?: Headers
1836+
): Promise<{
1837+
qrCodeUrl: string;
1838+
secret: string;
1839+
}> {
1840+
return this.jwtPost('/mfa/totp/create', jwt, options, headers);
1841+
}
1842+
1843+
/**
1844+
* @deprecated Use verifyTotpSetupCodeWithJwt instead
1845+
* Verify the authenticator code for the in-progress TOTP setup.
1846+
*
1847+
* Validates a 6-digit code generated by the app using the secret returned by
1848+
* `createTotpToken`. This does not finalize setup; call `completeTotpSetup`
1849+
* to activate TOTP.
1850+
*
1851+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1852+
* prefer the MFA JWT variant.
1853+
*
1854+
* @param sessionToken - Verified session token for the signed-in user
1855+
* @param code - 6-digit code from the authenticator app
1856+
* @param options - Optional request options
1857+
* @param options.metricsContext - Optional metrics context for telemetry
1858+
* @param headers - Optional additional request headers
1859+
* @returns Promise resolving to `{ success: boolean }`
1860+
*/
18001861
async verifyTotpSetupCode(
18011862
sessionToken: hexstring,
18021863
code: string,
@@ -1811,6 +1872,49 @@ export default class AuthClient {
18111872
);
18121873
}
18131874

1875+
/**
1876+
* Verify a TOTP setup code against the in-progress secret (JWT variant).
1877+
*
1878+
* Requires an MFA JWT with scope `mfa:2fa`. On success, marks the setup
1879+
* as verified in Redis and refreshes TTLs.
1880+
*
1881+
* @param jwt MFA access token including `mfa:2fa` scope
1882+
* @param code The 6-digit authenticator app code
1883+
* @param options Optional request options
1884+
* @param options.metricsContext Optional metrics context for telemetry
1885+
* @param headers Optional extra headers
1886+
* @returns Promise resolving to `{ success: boolean }`
1887+
*/
1888+
async verifyTotpSetupCodeWithJwt(
1889+
jwt: string,
1890+
code: string,
1891+
options: { metricsContext?: MetricsContext } = {},
1892+
headers?: Headers
1893+
): Promise<{ success: boolean }> {
1894+
return this.jwtPost(
1895+
'/mfa/totp/setup/verify',
1896+
jwt,
1897+
{ code, ...options },
1898+
headers
1899+
);
1900+
}
1901+
1902+
/**
1903+
* @deprecated Use completeTotpSetupWithJwt instead
1904+
* Finalize TOTP setup for the account.
1905+
*
1906+
* Marks the verified in-progress secret as the active second factor.
1907+
*
1908+
* Note: Maintained for legacy/inline setup flows. For new integrations,
1909+
* prefer the MFA JWT variant.
1910+
*
1911+
* @param sessionToken - Verified session token for the signed-in user
1912+
* @param options - Optional request options
1913+
* @param options.service - Optional service name
1914+
* @param options.metricsContext - Optional metrics context for telemetry
1915+
* @param headers - Optional additional request headers
1916+
* @returns Promise resolving to `{ success: boolean }`
1917+
*/
18141918
async completeTotpSetup(
18151919
sessionToken: hexstring,
18161920
options: { service?: string; metricsContext?: MetricsContext } = {},
@@ -1824,6 +1928,31 @@ export default class AuthClient {
18241928
);
18251929
}
18261930

1931+
/**
1932+
* Complete TOTP setup after successful code verification (JWT variant).
1933+
*
1934+
* Requires an MFA JWT with scope `mfa:2fa`. Validates the verification
1935+
* flag for the current secret and persists the enabled, verified token.
1936+
*
1937+
* @param jwt MFA access token including `mfa:2fa` scope
1938+
* @param options Optional request options
1939+
* @param options.metricsContext Optional metrics context for telemetry
1940+
* @param headers Optional extra headers
1941+
* @returns Promise resolving to `{ success: boolean }`
1942+
*/
1943+
async completeTotpSetupWithJwt(
1944+
jwt: string,
1945+
options: { metricsContext?: MetricsContext } = {},
1946+
headers?: Headers
1947+
): Promise<{ success: boolean }> {
1948+
return this.jwtPost(
1949+
'/mfa/totp/setup/complete',
1950+
jwt,
1951+
{ ...options },
1952+
headers
1953+
);
1954+
}
1955+
18271956
/**
18281957
* @deprecated Use startReplaceTotpTokenWithJwt instead
18291958
* Initiates the TOTP replacement flow using a session token
@@ -1852,29 +1981,6 @@ export default class AuthClient {
18521981
);
18531982
}
18541983

1855-
/**
1856-
* @deprecated Use confirmReplaceTotpTokenWithJwt instead
1857-
* Confirms TOTP replacement by verifying the code for the in-progress secret.
1858-
* Requires a verified session token.
1859-
*
1860-
* @param sessionToken - required, must be a verified session token
1861-
* @param code - 6-digit authenticator app code to validate the new secret
1862-
* @param headers - Optional additional headers for the request
1863-
* @returns A promise that resolves when the replacement has been accepted
1864-
*/
1865-
async confirmReplaceTotpToken(
1866-
sessionToken: hexstring,
1867-
code: string,
1868-
headers?: Headers
1869-
): Promise<void> {
1870-
return this.sessionPost(
1871-
'/totp/replace/confirm',
1872-
sessionToken,
1873-
{ code },
1874-
headers
1875-
);
1876-
}
1877-
18781984
/**
18791985
* Initiates the TOTP replacement flow using an MFA JWT.
18801986
* Requires a JWT with scope `mfa:2fa` and returns data to
@@ -1898,6 +2004,29 @@ export default class AuthClient {
18982004
return this.jwtPost('/mfa/totp/replace/start', jwt, options, headers);
18992005
}
19002006

2007+
/**
2008+
* @deprecated Use confirmReplaceTotpTokenWithJwt instead
2009+
* Confirms TOTP replacement by verifying the code for the in-progress secret.
2010+
* Requires a verified session token.
2011+
*
2012+
* @param sessionToken - required, must be a verified session token
2013+
* @param code - 6-digit authenticator app code to validate the new secret
2014+
* @param headers - Optional additional headers for the request
2015+
* @returns A promise that resolves when the replacement has been accepted
2016+
*/
2017+
async confirmReplaceTotpToken(
2018+
sessionToken: hexstring,
2019+
code: string,
2020+
headers?: Headers
2021+
): Promise<void> {
2022+
return this.sessionPost(
2023+
'/totp/replace/confirm',
2024+
sessionToken,
2025+
{ code },
2026+
headers
2027+
);
2028+
}
2029+
19012030
/**
19022031
* Confirms TOTP replacement by verifying the code for the in-progress secret.
19032032
* 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)