Skip to content

Commit cec090a

Browse files
authored
Merge pull request #20273 from mozilla/nshirley/email-skip-verify-regex
feat(auth): Add email verify regex bypass
2 parents 58c1e19 + 104f397 commit cec090a

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,12 @@ const convictConf = convict({
16931693
default: [],
16941694
env: 'SIGNIN_CONFIRMATION_SKIP_FOR_EMAIL_ADDRESS',
16951695
},
1696+
skipForEmailRegex: {
1697+
doc: 'Regex pattern for email addresses that will always skip any non-TOTP sign-in confirmation. Checked in addition to skipForEmailAddresses.',
1698+
format: RegExp,
1699+
default: /^$/,
1700+
env: 'SIGNIN_CONFIRMATION_SKIP_FOR_EMAIL_REGEX',
1701+
},
16961702
skipForNewAccounts: {
16971703
enabled: {
16981704
doc: 'Skip all sign-in email confirmations for newly-created accounts',

packages/fxa-auth-server/lib/routes/account.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ describe('deleteAccountIfUnverified', () => {
675675
mockConfig.oauth = {};
676676
mockConfig.signinConfirmation = {};
677677
mockConfig.signinConfirmation.skipForEmailAddresses = [];
678+
mockConfig.signinConfirmation.skipForEmailRegex = /^$/;
678679
const emailRecord: any = {
679680
isPrimary: true,
680681
isVerified: false,
@@ -3171,6 +3172,80 @@ describe('/account/login', () => {
31713172
);
31723173
});
31733174

3175+
describe('skip for email regex', () => {
3176+
function setupSkipForEmailRegex(email: string, regex: RegExp) {
3177+
config.securityHistory.ipProfiling.allowedRecency = 0;
3178+
config.signinConfirmation.skipForNewAccounts = { enabled: false };
3179+
config.signinConfirmation.skipForEmailAddresses = [];
3180+
config.signinConfirmation.skipForEmailRegex = regex;
3181+
3182+
mockDB.verifiedLoginSecurityEvents = sinon.spy(() =>
3183+
Promise.resolve([])
3184+
);
3185+
3186+
mockRequest.payload.email = email;
3187+
3188+
mockDB.accountRecord = () => {
3189+
return Promise.resolve({
3190+
authSalt: hexString(32),
3191+
data: hexString(32),
3192+
email,
3193+
emailVerified: true,
3194+
primaryEmail: {
3195+
normalizedEmail: normalizeEmail(email),
3196+
email,
3197+
isVerified: true,
3198+
isPrimary: true,
3199+
},
3200+
kA: hexString(32),
3201+
lastAuthAt: () => Date.now(),
3202+
uid,
3203+
wrapWrapKb: hexString(32),
3204+
});
3205+
};
3206+
3207+
const innerAccountRoutes = makeRoutes({
3208+
checkPassword: () => Promise.resolve(true),
3209+
config,
3210+
customs: mockCustoms,
3211+
db: mockDB,
3212+
log: mockLog,
3213+
mailer: mockMailer,
3214+
push: mockPush,
3215+
});
3216+
3217+
route = getRoute(innerAccountRoutes, '/account/login');
3218+
}
3219+
3220+
afterEach(() => {
3221+
config.securityHistory.ipProfiling.allowedRecency =
3222+
defaultConfig.securityHistory.ipProfiling.allowedRecency;
3223+
config.signinConfirmation.skipForEmailRegex = /^$/;
3224+
});
3225+
3226+
it('should skip sign-in confirmation for email matching regex', () => {
3227+
setupSkipForEmailRegex('[email protected]', /.+@example\.com$/);
3228+
3229+
return runTest(route, mockRequest, (response: any) => {
3230+
expect(mockDB.createSessionToken.callCount).toBe(1);
3231+
const tokenData = mockDB.createSessionToken.getCall(0).args[0];
3232+
expect(tokenData.tokenVerificationId).toBeFalsy();
3233+
expect(response.emailVerified).toBeTruthy();
3234+
});
3235+
});
3236+
3237+
it('should not skip sign-in confirmation for email not matching regex', () => {
3238+
setupSkipForEmailRegex('[email protected]', /.+@example\.com$/);
3239+
3240+
return runTest(route, mockRequest, (response: any) => {
3241+
expect(mockDB.createSessionToken.callCount).toBe(1);
3242+
const tokenData = mockDB.createSessionToken.getCall(0).args[0];
3243+
expect(tokenData.tokenVerificationId).toBeTruthy();
3244+
expect(response.verified).toBeFalsy();
3245+
});
3246+
});
3247+
});
3248+
31743249
describe('skip for known device', () => {
31753250
let mockAccountEventsManager: any;
31763251
let savedIpProfiling: any;

packages/fxa-auth-server/lib/routes/account.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export class AccountHandler {
8686
private otpUtils: OtpUtils;
8787
private otpOptions: ConfigType['otp'];
8888
private skipConfirmationForEmailAddresses: string[];
89+
private skipConfirmationForEmailRegex: RegExp;
8990
private capabilityService: CapabilityService;
9091
private accountEventsManager: AccountEventsManager;
9192
private accountDeleteManager: AccountDeleteManager;
@@ -116,6 +117,8 @@ export class AccountHandler {
116117
this.otpUtils = require('./utils/otp').default(db, statsd);
117118
this.skipConfirmationForEmailAddresses = config.signinConfirmation
118119
.skipForEmailAddresses as string[];
120+
this.skipConfirmationForEmailRegex =
121+
config.signinConfirmation.skipForEmailRegex;
119122

120123
this.OAUTH_DISABLE_NEW_CONNECTIONS_FOR_CLIENTS = new Set(
121124
(config.oauth.disableNewConnectionsForClients as string[]) || []
@@ -1259,7 +1262,10 @@ export class AccountHandler {
12591262
// to guarantee the login experience.
12601263
const lowerCaseEmail = account.primaryEmail.normalizedEmail.toLowerCase();
12611264
const alwaysSkip =
1262-
this.skipConfirmationForEmailAddresses?.includes(lowerCaseEmail);
1265+
this.skipConfirmationForEmailAddresses?.includes(lowerCaseEmail) ||
1266+
// use both as a backward compatibility and eventually remove
1267+
// the array of emails in favor of just a regex which is more flexible
1268+
this.skipConfirmationForEmailRegex?.test(lowerCaseEmail);
12631269
if (alwaysSkip) {
12641270
this.log.info('account.signin.confirm.bypass.emailAlways', {
12651271
uid: account.uid,

0 commit comments

Comments
 (0)