Skip to content

Commit a5766df

Browse files
authored
Merge pull request #20028 from mozilla/fxa-13014-add-passwordless
feat(auth): add passwordless email otp routes
2 parents c212e03 + 89e7fd3 commit a5766df

13 files changed

Lines changed: 2035 additions & 65 deletions

File tree

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,7 @@ export default class AuthClient {
12111211

12121212
async accountStatusByEmail(
12131213
email: string,
1214-
options: { thirdPartyAuthStatus?: boolean } = {},
1214+
options: { thirdPartyAuthStatus?: boolean; clientId?: string } = {},
12151215
headers?: Headers
12161216
) {
12171217
return this.request(
@@ -1222,6 +1222,61 @@ export default class AuthClient {
12221222
);
12231223
}
12241224

1225+
/**
1226+
* Send a passwordless OTP code to the user's email
1227+
*/
1228+
async passwordlessSendCode(
1229+
email: string,
1230+
options: { service?: string; metricsContext?: MetricsContext } = {},
1231+
headers?: Headers
1232+
): Promise<{}> {
1233+
return this.request(
1234+
'POST',
1235+
'/account/passwordless/send_code',
1236+
{ email, ...options },
1237+
headers
1238+
);
1239+
}
1240+
1241+
/**
1242+
* Confirm a passwordless OTP code and get a session token
1243+
*/
1244+
async passwordlessConfirmCode(
1245+
email: string,
1246+
code: string,
1247+
options: { service?: string; metricsContext?: MetricsContext } = {},
1248+
headers?: Headers
1249+
): Promise<{
1250+
uid: string;
1251+
sessionToken: string;
1252+
verified: boolean;
1253+
authAt: number;
1254+
isNewAccount: boolean;
1255+
}> {
1256+
return this.request(
1257+
'POST',
1258+
'/account/passwordless/confirm_code',
1259+
{ email, code, ...options },
1260+
headers
1261+
);
1262+
}
1263+
1264+
/**
1265+
* Resend a passwordless OTP code
1266+
*/
1267+
async passwordlessResendCode(
1268+
email: string,
1269+
options: { service?: string; metricsContext?: MetricsContext } = {},
1270+
headers?: Headers
1271+
): Promise<{}> {
1272+
return this.request(
1273+
'POST',
1274+
'/account/passwordless/resend_code',
1275+
{ email, ...options },
1276+
headers
1277+
);
1278+
}
1279+
12251280
async emailBounceStatus(
12261281
email: string,
12271282
headers?: Headers
@@ -1907,11 +1962,17 @@ export default class AuthClient {
19071962
return this.sessionGet('/account/sessions', sessionToken, headers);
19081963
}
19091964

1910-
async securityEvents(sessionToken: hexstring, headers?: Headers): Promise<SecurityEvent[]> {
1965+
async securityEvents(
1966+
sessionToken: hexstring,
1967+
headers?: Headers
1968+
): Promise<SecurityEvent[]> {
19111969
return this.sessionGet('/securityEvents', sessionToken, headers);
19121970
}
19131971

1914-
async attachedClients(sessionToken: hexstring, headers?: Headers): Promise<AttachedClient[]> {
1972+
async attachedClients(
1973+
sessionToken: hexstring,
1974+
headers?: Headers
1975+
): Promise<AttachedClient[]> {
19151976
return this.sessionGet('/account/attached_clients', sessionToken, headers);
19161977
}
19171978

@@ -3377,4 +3438,4 @@ export default class AuthClient {
33773438
throw error;
33783439
}
33793440
}
3380-
}
3441+
}

packages/fxa-auth-server/config/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,38 @@ const convictConf = convict({
21662166
env: 'OTP_SIGNUP_DIGIT',
21672167
},
21682168
},
2169+
passwordlessOtp: {
2170+
enabled: {
2171+
doc: 'Enable passwordless authentication feature',
2172+
default: false,
2173+
format: Boolean,
2174+
env: 'PASSWORDLESS_ENABLED',
2175+
},
2176+
forcedEmailAddresses: {
2177+
doc: 'Force passwordless flow for email addresses matching this regex (for testing)',
2178+
format: RegExp,
2179+
default: /^passwordless.*@restmail\.net$/,
2180+
env: 'PASSWORDLESS_FORCED_EMAIL_REGEX',
2181+
},
2182+
allowedClientIds: {
2183+
doc: 'Array of clients ids allowed to use passwordless authentication. Empty array means all services allowed.',
2184+
format: Array,
2185+
default: [],
2186+
env: 'PASSWORDLESS_ALLOWED_SERVICES',
2187+
},
2188+
digits: {
2189+
doc: 'Number of digits in passwordless OTP code',
2190+
default: 8,
2191+
format: 'nat',
2192+
env: 'OTP_PASSWORDLESS_DIGITS',
2193+
},
2194+
ttl: {
2195+
doc: 'Duration in seconds when the passwordless OTP is valid',
2196+
default: 10 * 60,
2197+
format: 'nat',
2198+
env: 'OTP_PASSWORDLESS_TTL',
2199+
},
2200+
},
21692201
accountDestroy: {
21702202
requireVerifiedAccount: {
21712203
doc: 'Whether or not the account must be verified in order to destroy it.',

packages/fxa-auth-server/config/rate-limit-rules.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,19 @@ passkeyLogin : ip_uid : 15 : 15 mi
180180
passkeysList : ip_uid : 100 : 15 minutes : 15 minutes : block
181181
passkeysRename : ip_uid : 100 : 15 minutes : 15 minutes : block
182182
passkeyDelete : ip_uid : 100 : 15 minutes : 15 minutes : block
183+
184+
#
185+
# Passwordless Authentication OTP Limits
186+
# Controls the rate at which passwordless OTP codes can be sent and verified
187+
#
188+
passwordlessSendOtp : email : 2 : 15 minutes : 15 minutes : block
189+
passwordlessSendOtp : email : 5 : 24 hours : 12 hours : block
190+
passwordlessSendOtp : ip : 50 : 24 hours : 12 hours : block
191+
passwordlessSendOtp : ip : 20 : 15 minutes : 30 minutes : block
192+
passwordlessSendOtp : ip : 100 : 24 hours : 15 minutes : ban
193+
194+
# Passwordless OTP Verification Limits
195+
passwordlessVerifyOtp : ip_email : 5 : 10 minutes : 15 minutes : block
196+
passwordlessVerifyOtp : ip : 100 : 24 hours : 15 minutes : ban
197+
passwordlessVerifyOtpPerDay : ip_email : 10 : 24 hours : 24 hours : block
198+
passwordlessVerifyOtpPerDay : ip : 100 : 24 hours : 15 minutes : ban
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import dedent from 'dedent';
6+
import TAGS from './swagger-tags';
7+
8+
const TAGS_PASSWORDLESS = {
9+
tags: TAGS.PASSWORDLESS,
10+
};
11+
12+
const PASSWORDLESS_SEND_CODE_POST = {
13+
...TAGS_PASSWORDLESS,
14+
description: '/account/passwordless/send_code',
15+
notes: [
16+
dedent`
17+
Send a one-time password (OTP) code to the user's email for passwordless authentication.
18+
19+
This endpoint can be used for both:
20+
- New user registration (account doesn't exist)
21+
- Login for existing passwordless accounts (accounts without a password)
22+
23+
Accounts with passwords set cannot use this endpoint.
24+
`,
25+
],
26+
plugins: {
27+
'hapi-swagger': {
28+
responses: {
29+
400: {
30+
description: dedent`
31+
Failing requests may be caused by the following errors:
32+
- \`errno: 148\` - Account has a password set, use standard login flow
33+
`,
34+
},
35+
429: {
36+
description: 'Rate limit exceeded',
37+
},
38+
},
39+
},
40+
},
41+
};
42+
43+
const PASSWORDLESS_CONFIRM_CODE_POST = {
44+
...TAGS_PASSWORDLESS,
45+
description: '/account/passwordless/confirm_code',
46+
notes: [
47+
dedent`
48+
Confirm the OTP code sent via \`/account/passwordless/send_code\`.
49+
50+
On success:
51+
- For new users: Creates a new account and returns a session token
52+
- For existing users: Returns a session token for the existing account
53+
54+
The \`isNewAccount\` field in the response indicates whether a new account was created.
55+
`,
56+
],
57+
plugins: {
58+
'hapi-swagger': {
59+
responses: {
60+
400: {
61+
description: dedent`
62+
Failing requests may be caused by the following errors:
63+
- \`errno: 183\` - Invalid OTP code
64+
- \`errno: 148\` - Account has a password set
65+
`,
66+
},
67+
429: {
68+
description: 'Rate limit exceeded',
69+
},
70+
},
71+
},
72+
},
73+
};
74+
75+
const PASSWORDLESS_RESEND_CODE_POST = {
76+
...TAGS_PASSWORDLESS,
77+
description: '/account/passwordless/resend_code',
78+
notes: [
79+
dedent`
80+
Resend the OTP code for passwordless authentication.
81+
82+
This invalidates any previously sent code and sends a new one.
83+
Subject to the same rate limits as \`/account/passwordless/send_code\`.
84+
`,
85+
],
86+
plugins: {
87+
'hapi-swagger': {
88+
responses: {
89+
400: {
90+
description: dedent`
91+
Failing requests may be caused by the following errors:
92+
- \`errno: 148\` - Account has a password set
93+
`,
94+
},
95+
429: {
96+
description: 'Rate limit exceeded',
97+
},
98+
},
99+
},
100+
},
101+
};
102+
103+
export default {
104+
PASSWORDLESS_SEND_CODE_POST,
105+
PASSWORDLESS_CONFIRM_CODE_POST,
106+
PASSWORDLESS_RESEND_CODE_POST,
107+
};

packages/fxa-auth-server/docs/swagger/swagger-tags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const TAGS = {
1111
OAUTH: ['api', 'Oauth'],
1212
OAUTH_SERVER: ['api', 'OAuth Server API Overview'],
1313
PASSWORD: ['api', 'Password'],
14+
PASSWORDLESS: ['api', 'Passwordless'],
1415
RECOVERY_PHONE: ['api', 'Recovery phone'],
1516
RECOVERY_CODES: ['api', 'Backup authentication codes'],
1617
RECOVERY_KEY: ['api', 'Account recovery key'],

0 commit comments

Comments
 (0)