diff --git a/packages/fxa-auth-server/lib/routes/auth-schemes/hawk-fxa-token.js b/packages/fxa-auth-server/lib/routes/auth-schemes/hawk-fxa-token.js index a7f83255c67..714ae01d53c 100644 --- a/packages/fxa-auth-server/lib/routes/auth-schemes/hawk-fxa-token.js +++ b/packages/fxa-auth-server/lib/routes/auth-schemes/hawk-fxa-token.js @@ -4,7 +4,6 @@ const AppError = require('../../error'); const Boom = require('@hapi/boom'); -const error = require('../../error'); // The following regexes and Hawk header parsing are taken from the Hawk library. // See https://github.com/mozilla/hawk/blob/01f3d35479fe76654bb50f2886b37310555d088e/lib/utils.js#L126 @@ -109,15 +108,8 @@ function strategy( try { token = await getCredentialsFunc(parsedHeader.id); - } catch (err) { - // An error in the getCredentialsFunc means that the token was not found - // or it does not have a high enough assurance level to be used for this request. - // (e.g. a session token that is not 2FA verified) - if (err.errno === error.ERRNO.SESSION_UNVERIFIED) { - throw err; - } - - // handle the empty token case below + } catch (_) { + // we'll handle the empty token case below } // If a token isn't found, this means it doesn't exist or expired and diff --git a/packages/fxa-auth-server/lib/routes/emails.js b/packages/fxa-auth-server/lib/routes/emails.js index 79f92753f02..a4d43f14a3e 100644 --- a/packages/fxa-auth-server/lib/routes/emails.js +++ b/packages/fxa-auth-server/lib/routes/emails.js @@ -104,7 +104,7 @@ module.exports = ( options: { ...EMAILS_DOCS.RECOVERY_EMAIL_STATUS_GET, auth: { - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', }, validate: { query: { diff --git a/packages/fxa-auth-server/lib/routes/oauth/token.js b/packages/fxa-auth-server/lib/routes/oauth/token.js index 7fce4d5d394..4c41fe427ea 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/token.js +++ b/packages/fxa-auth-server/lib/routes/oauth/token.js @@ -446,7 +446,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { // XXX TODO: To be able to fully replace the /token route from oauth-server, // this route must also be able to accept 'client_secret' as Basic Auth in header. mode: 'optional', - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', }, validate: { // Note: the use of 'alternatives' here means that `grant_type` will default to diff --git a/packages/fxa-auth-server/lib/routes/recovery-codes.js b/packages/fxa-auth-server/lib/routes/recovery-codes.js index 4d93997ded8..0b7b93f5e28 100644 --- a/packages/fxa-auth-server/lib/routes/recovery-codes.js +++ b/packages/fxa-auth-server/lib/routes/recovery-codes.js @@ -128,7 +128,7 @@ module.exports = (log, db, config, customs, mailer, glean) => { options: { ...RECOVERY_CODES_DOCS.SESSION_VERIFY_RECOVERYCODE_POST, auth: { - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', payload: 'required', }, validate: { diff --git a/packages/fxa-auth-server/lib/routes/session.js b/packages/fxa-auth-server/lib/routes/session.js index 240de940447..6835868f4ee 100644 --- a/packages/fxa-auth-server/lib/routes/session.js +++ b/packages/fxa-auth-server/lib/routes/session.js @@ -103,7 +103,7 @@ module.exports = function ( options: { ...SESSION_DOCS.SESSION_REAUTH_POST, auth: { - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', payload: 'required', }, validate: { diff --git a/packages/fxa-auth-server/lib/routes/totp.js b/packages/fxa-auth-server/lib/routes/totp.js index dd2bbe69ff9..289619df730 100644 --- a/packages/fxa-auth-server/lib/routes/totp.js +++ b/packages/fxa-auth-server/lib/routes/totp.js @@ -128,7 +128,7 @@ module.exports = (log, db, mailer, customs, config, glean, profileClient) => { options: { ...TOTP_DOCS.TOTP_DESTROY_POST, auth: { - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', }, response: {}, }, @@ -397,7 +397,7 @@ module.exports = (log, db, mailer, customs, config, glean, profileClient) => { options: { ...TOTP_DOCS.SESSION_VERIFY_TOTP_POST, auth: { - strategy: 'sessionTokenNoAssurance', + strategy: 'sessionToken', payload: 'required', }, validate: { diff --git a/packages/fxa-auth-server/lib/server.js b/packages/fxa-auth-server/lib/server.js index 0dc457efac3..cd78181c696 100644 --- a/packages/fxa-auth-server/lib/server.js +++ b/packages/fxa-auth-server/lib/server.js @@ -86,7 +86,6 @@ async function create(log, error, config, routes, db, statsd, glean) { const metricsContext = require('./metrics/context')(log, config); const metricsEvents = require('./metrics/events')(log, config, glean); const { sharedSecret: SUBSCRIPTIONS_SECRET } = config.subscriptions; - const otpUtils = require('./routes/utils/otp')(log, config, db); function makeCredentialFn(dbGetFn) { return function (id) { @@ -377,22 +376,6 @@ async function create(log, error, config, routes, db, statsd, glean) { // Register auth strategies for all token types. These strategies support Hawk (without validation) and FxA token types. server.auth.scheme( 'fxa-hawk-session-token', - hawkFxAToken.strategy( - makeCredentialFn(async function (id) { - const sessionToken = await db.sessionToken(id); - - const hasTotpToken = await otpUtils.hasTotpToken(sessionToken); - - if (hasTotpToken && sessionToken.authenticatorAssuranceLevel <= 1) { - throw error.unverifiedSession(); - } - - return sessionToken; - }) - ) - ); - server.auth.scheme( - 'fxa-hawk-session-token-no-assurance', hawkFxAToken.strategy(makeCredentialFn(db.sessionToken.bind(db))) ); server.auth.scheme( @@ -437,10 +420,6 @@ async function create(log, error, config, routes, db, statsd, glean) { ); server.auth.strategy('sessionToken', 'fxa-hawk-session-token'); - server.auth.strategy( - 'sessionTokenNoAssurance', - 'fxa-hawk-session-token-no-assurance' - ); server.auth.strategy('keyFetchToken', 'fxa-hawk-keyFetch-token'); server.auth.strategy( // This strategy fetches the keyFetchToken with its diff --git a/packages/fxa-auth-server/test/remote/recovery_code_tests.js b/packages/fxa-auth-server/test/remote/recovery_code_tests.js index d93f9580487..118d83cc866 100644 --- a/packages/fxa-auth-server/test/remote/recovery_code_tests.js +++ b/packages/fxa-auth-server/test/remote/recovery_code_tests.js @@ -13,264 +13,246 @@ const BASE_36 = require('../../lib/routes/validators').BASE_36; [{version:""},{version:"V2"}].forEach((testOptions) => { - describe(`#integration${testOptions.version} - remote backup authentication codes`, function () { - this.timeout(60000); +describe(`#integration${testOptions.version} - remote backup authentication codes`, function () { + this.timeout(60000); - let server, client, email, recoveryCodes; - const recoveryCodeCount = 9; - const password = 'pssssst'; - const metricsContext = { - flowBeginTime: Date.now(), - flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - }; + let server, client, email, recoveryCodes; + const recoveryCodeCount = 9; + const password = 'pssssst'; + const metricsContext = { + flowBeginTime: Date.now(), + flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + }; - otplib.authenticator.options = { - encoding: 'hex', - window: 10, - }; + otplib.authenticator.options = { + encoding: 'hex', + window: 10, + }; - before(async () => { - config.totp.recoveryCodes.count = recoveryCodeCount; - config.totp.recoveryCodes.notifyLowCount = recoveryCodeCount - 2; - server = await TestServer.start(config); - }); + before(async () => { + config.totp.recoveryCodes.count = recoveryCodeCount; + config.totp.recoveryCodes.notifyLowCount = recoveryCodeCount - 2; + server = await TestServer.start(config); + }); - after(async () => { - await TestServer.stop(server); - }); + after(async () => { + await TestServer.stop(server); + }); - beforeEach(() => { - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ).then((x) => { - client = x; - assert.ok(client.authAt, 'authAt was set'); - return client.createTotpToken({ metricsContext }).then((result) => { - otplib.authenticator.options = { - secret: result.secret, - }; - recoveryCodes = result.recoveryCodes; - assert.equal( - result.recoveryCodes.length, - recoveryCodeCount, - 'backup authentication codes returned' - ); + beforeEach(() => { + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ).then((x) => { + client = x; + assert.ok(client.authAt, 'authAt was set'); + return client.createTotpToken({ metricsContext }).then((result) => { + otplib.authenticator.options = { + secret: result.secret, + }; + recoveryCodes = result.recoveryCodes; + assert.equal( + result.recoveryCodes.length, + recoveryCodeCount, + 'backup authentication codes returned' + ); - // Verify TOTP token so that initial backup authentication codes are generated - const code = otplib.authenticator.generate(); - return client - .verifyTotpCode(code, { metricsContext }) - .then((response) => { - assert.equal(response.success, true, 'totp codes match'); + // Verify TOTP token so that initial backup authentication codes are generated + const code = otplib.authenticator.generate(); + return client + .verifyTotpCode(code, { metricsContext }) + .then((response) => { + assert.equal(response.success, true, 'totp codes match'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postAddTwoStepAuthentication' - ); - }); - }); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postAddTwoStepAuthentication' + ); + }); }); }); + }); + + it('should create backup authentication codes', () => { + assert.ok(recoveryCodes); + assert.equal( + recoveryCodes.length, + recoveryCodeCount, + 'backup authentication codes returned' + ); + recoveryCodes.forEach((code) => { + assert.equal(code.length > 1, true, 'correct length'); + assert.equal(BASE_36.test(code), true, 'code is hex'); + }); + }); - it('should create backup authentication codes', () => { - assert.ok(recoveryCodes); - assert.equal( - recoveryCodes.length, - recoveryCodeCount, - 'backup authentication codes returned' - ); - recoveryCodes.forEach((code) => { - assert.equal(code.length > 1, true, 'correct length'); - assert.equal(BASE_36.test(code), true, 'code is hex'); + it('should replace backup authentication codes', () => { + return client + .replaceRecoveryCodes() + .then((result) => { + assert.ok( + result.recoveryCodes.length, + recoveryCodeCount, + 'backup authentication codes returned' + ); + assert.notDeepEqual( + result, + recoveryCodes, + 'backup authentication codes should not match' + ); + + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postNewRecoveryCodes' + ); }); + }); + + describe('backup authentication code verification', () => { + beforeEach(() => { + // Create a new unverified session to test backup authentication codes + return Client.login(config.publicUrl, email, password, testOptions) + .then((response) => { + client = response; + return client.emailStatus(); + }) + .then((res) => + assert.equal(res.sessionVerified, false, 'session not verified') + ); }); - it('should replace backup authentication codes', () => { + it('should fail to consume unknown backup authentication code', () => { return client - .replaceRecoveryCodes() - .then((result) => { - assert.ok( - result.recoveryCodes.length, - recoveryCodeCount, - 'backup authentication codes returned' + .consumeRecoveryCode('1234abcd', { metricsContext }) + .then(assert.fail, (err) => { + assert.equal(err.code, 400, 'correct error code'); + assert.equal(err.errno, 156, 'correct error errno'); + }); + }); + + it('should consume backup authentication code and verify session', () => { + return client + .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) + .then((res) => { + assert.equal( + res.remaining, + recoveryCodeCount - 1, + 'correct remaining codes' ); - assert.notDeepEqual( - result, - recoveryCodes, - 'backup authentication codes should not match' + return client.emailStatus(); + }) + .then((res) => { + assert.equal(res.sessionVerified, true, 'session verified'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postConsumeRecoveryCode' ); + }); + }); + it('should consume backup authentication code and can remove TOTP token', () => { + return client + .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) + .then((res) => { + assert.equal( + res.remaining, + recoveryCodeCount - 1, + 'correct remaining codes' + ); return server.mailbox.waitForEmail(email); }) .then((emailData) => { assert.equal( emailData.headers['x-template-name'], - 'postNewRecoveryCodes' + 'postConsumeRecoveryCode' + ); + return client.deleteTotpToken(); + }) + .then((result) => { + assert.ok(result, 'delete totp token successfully'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postRemoveTwoStepAuthentication' ); }); }); + }); - it('should fail to replace backup authentication codes in non 2FA verified session', async () => { - // Create a new unverified session - const clientUnverified = await Client.login( - config.publicUrl, - email, - password, - testOptions - ); - - try { - await clientUnverified.replaceRecoveryCodes(); - assert.fail('should have thrown'); - } catch (err) { - assert.equal(err.code, 400, 'correct error code'); - assert.equal(err.errno, 138, 'correct error errno'); - } + describe('should notify user when backup authentication codes are low', () => { + beforeEach(() => { + // Create a new unverified session to test backup authentication codes + return Client.login(config.publicUrl, email, password, testOptions) + .then((response) => { + client = response; + return client.emailStatus(); + }) + .then((res) => + assert.equal(res.sessionVerified, false, 'session not verified') + ); }); - describe('backup authentication code verification', () => { - beforeEach(() => { - // Create a new unverified session to test backup authentication codes - return Client.login(config.publicUrl, email, password, testOptions) - .then((response) => { - client = response; - return client.emailStatus(); - }) - .then((res) => - assert.equal(res.sessionVerified, false, 'session not verified') + it('should consume backup authentication code and verify session', () => { + return client + .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) + .then((res) => { + assert.equal( + res.remaining, + recoveryCodeCount - 1, + 'correct remaining codes' ); - }); - - it('should fail to consume unknown backup authentication code', () => { - return client - .consumeRecoveryCode('1234abcd', { metricsContext }) - .then(assert.fail, (err) => { - assert.equal(err.code, 400, 'correct error code'); - assert.equal(err.errno, 156, 'correct error errno'); - }); - }); - - it('should consume backup authentication code and verify session', () => { - return client - .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) - .then((res) => { - assert.equal( - res.remaining, - recoveryCodeCount - 1, - 'correct remaining codes' - ); - return client.emailStatus(); - }) - .then((res) => { - assert.equal(res.sessionVerified, true, 'session verified'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postConsumeRecoveryCode' - ); - }); - }); - - it('should consume backup authentication code and can remove TOTP token', () => { - return client - .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) - .then((res) => { - assert.equal( - res.remaining, - recoveryCodeCount - 1, - 'correct remaining codes' - ); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postConsumeRecoveryCode' - ); - return client.deleteTotpToken(); - }) - .then((result) => { - assert.ok(result, 'delete totp token successfully'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postRemoveTwoStepAuthentication' - ); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postConsumeRecoveryCode' + ); + return client.consumeRecoveryCode(recoveryCodes[1], { + metricsContext, }); - }); - }); - - describe('should notify user when backup authentication codes are low', () => { - beforeEach(() => { - // Create a new unverified session to test backup authentication codes - return Client.login(config.publicUrl, email, password, testOptions) - .then((response) => { - client = response; - return client.emailStatus(); - }) - .then((res) => - assert.equal(res.sessionVerified, false, 'session not verified') + }) + .then((res) => { + assert.equal( + res.remaining, + recoveryCodeCount - 2, + 'correct remaining codes' ); - }); - - it('should consume backup authentication code and verify session', () => { - return client - .consumeRecoveryCode(recoveryCodes[0], { metricsContext }) - .then((res) => { - assert.equal( - res.remaining, - recoveryCodeCount - 1, - 'correct remaining codes' - ); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postConsumeRecoveryCode' - ); - return client.consumeRecoveryCode(recoveryCodes[1], { - metricsContext, - }); - }) - .then((res) => { - assert.equal( - res.remaining, - recoveryCodeCount - 2, - 'correct remaining codes' - ); - return server.mailbox.waitForEmail(email); - }) - .then((emails) => { - // The order in which the emails are sent is not guaranteed, test for both possible templates - const email1 = emails[0].headers['x-template-name']; - const email2 = emails[1].headers['x-template-name']; - if (email1 === 'postConsumeRecoveryCode') { - assert.equal(email2, 'lowRecoveryCodes'); - } + return server.mailbox.waitForEmail(email); + }) + .then((emails) => { + // The order in which the emails are sent is not guaranteed, test for both possible templates + const email1 = emails[0].headers['x-template-name']; + const email2 = emails[1].headers['x-template-name']; + if (email1 === 'postConsumeRecoveryCode') { + assert.equal(email2, 'lowRecoveryCodes'); + } - if (email1 === 'lowRecoveryCodes') { - assert.equal(email2, 'postConsumeRecoveryCode'); - } - }); - }); + if (email1 === 'lowRecoveryCodes') { + assert.equal(email2, 'postConsumeRecoveryCode'); + } + }); }); + }); - }); +}); }); diff --git a/packages/fxa-auth-server/test/remote/totp_tests.js b/packages/fxa-auth-server/test/remote/totp_tests.js index d7c03518b91..8e5ef2a0d1a 100644 --- a/packages/fxa-auth-server/test/remote/totp_tests.js +++ b/packages/fxa-auth-server/test/remote/totp_tests.js @@ -20,332 +20,365 @@ const { [{version:""},{version:"V2"}].forEach((testOptions) => { - describe(`#integration${testOptions.version} - remote totp`, function () { - this.timeout(60000); - - let server, client, email, totpToken, authenticator; - const password = 'pssssst'; - const metricsContext = { - flowBeginTime: Date.now(), - flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - }; - - otplib.authenticator.options = { - crypto: crypto, - encoding: 'hex', - window: 10, - }; - - before(async () => { - config.securityHistory.ipProfiling = {}; - config.signinConfirmation.skipForNewAccounts.enabled = false; - - Container.set(PlaySubscriptions, {}); - Container.set(AppStoreSubscriptions, {}); - - server = await TestServer.start(config); - }); - - after(async () => { - await TestServer.stop(server); - }); +describe(`#integration${testOptions.version} - remote totp`, function () { + this.timeout(60000); + + let server, client, email, totpToken, authenticator; + const password = 'pssssst'; + const metricsContext = { + flowBeginTime: Date.now(), + flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + }; + + otplib.authenticator.options = { + crypto: crypto, + encoding: 'hex', + window: 10, + }; + + before(async () => { + config.securityHistory.ipProfiling = {}; + config.signinConfirmation.skipForNewAccounts.enabled = false; + + Container.set(PlaySubscriptions, {}); + Container.set(AppStoreSubscriptions, {}); + + server = await TestServer.start(config); + }); - function verifyTOTP(client) { - return client - .createTotpToken({ metricsContext }) - .then((result) => { - authenticator = new otplib.authenticator.Authenticator(); - authenticator.options = Object.assign( - {}, - otplib.authenticator.options, - { secret: result.secret } - ); - totpToken = result; - assert.equal( - result.recoveryCodes.length > 1, - true, - 'backup authentication codes returned' - ); + after(async () => { + await TestServer.stop(server); + }); - // Verify TOTP token - const code = authenticator.generate(); - return client.verifyTotpCode(code, { metricsContext, service: 'sync' }); - }) - .then((response) => { - assert.equal(response.success, true, 'totp codes match'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postAddTwoStepAuthentication' - ); - }); - } + function verifyTOTP(client) { + return client + .createTotpToken({ metricsContext }) + .then((result) => { + authenticator = new otplib.authenticator.Authenticator(); + authenticator.options = Object.assign( + {}, + otplib.authenticator.options, + { secret: result.secret } + ); + totpToken = result; + assert.equal( + result.recoveryCodes.length > 1, + true, + 'backup authentication codes returned' + ); - beforeEach(() => { - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ).then((x) => { - client = x; - assert.ok(client.authAt, 'authAt was set'); - return verifyTOTP(client); + // Verify TOTP token + const code = authenticator.generate(); + return client.verifyTotpCode(code, { metricsContext, service: 'sync' }); + }) + .then((response) => { + assert.equal(response.success, true, 'totp codes match'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postAddTwoStepAuthentication' + ); }); + } + + beforeEach(() => { + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ).then((x) => { + client = x; + assert.ok(client.authAt, 'authAt was set'); + return verifyTOTP(client); }); + }); - it('should create totp token', () => { - assert.ok(totpToken); - assert.ok(totpToken.qrCodeUrl); - }); - - it('should check if totp token exists for user', () => { - return client.checkTotpTokenExists().then((response) => { - assert.equal(response.exists, true, 'token exists'); - }); - }); + it('should create totp token', () => { + assert.ok(totpToken); + assert.ok(totpToken.qrCodeUrl); + }); - it('should fail to create second totp token for same user', () => { - return client.createTotpToken().then(assert.fail, (err) => { - assert.equal(err.code, 400, 'correct error code'); - assert.equal(err.errno, 154, 'correct error errno'); - }); + it('should check if totp token exists for user', () => { + return client.checkTotpTokenExists().then((response) => { + assert.equal(response.exists, true, 'token exists'); }); + }); - it('should not fail to delete unknown totp token', () => { - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ).then((x) => { - client = x; - assert.ok(client.authAt, 'authAt was set'); - return client - .deleteTotpToken() - .then((result) => assert.ok(result, 'delete totp token successfully')); - }); + it('should fail to create second totp token for same user', () => { + return client.createTotpToken().then(assert.fail, (err) => { + assert.equal(err.code, 400, 'correct error code'); + assert.equal(err.errno, 154, 'correct error errno'); }); + }); - it('should delete totp token', () => { + it('should not fail to delete unknown totp token', () => { + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ).then((x) => { + client = x; + assert.ok(client.authAt, 'authAt was set'); return client .deleteTotpToken() - .then((result) => { - assert.ok(result, 'delete totp token successfully'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postRemoveTwoStepAuthentication' - ); + .then((result) => assert.ok(result, 'delete totp token successfully')); + }); + }); - // Can create a new token - return client.checkTotpTokenExists().then((result) => { - assert.equal(result.exists, false, 'token does not exist'); - }); + it('should delete totp token', () => { + return client + .deleteTotpToken() + .then((result) => { + assert.ok(result, 'delete totp token successfully'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postRemoveTwoStepAuthentication' + ); + + // Can create a new token + return client.checkTotpTokenExists().then((result) => { + assert.equal(result.exists, false, 'token does not exist'); }); - }); + }); + }); - it('should allow verified sessions before totp enabled to delete totp token', () => { - let client2, code; - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ) - .then((x) => { - client = x; - return client.login({ keys: true }); - }) - .then((response) => { - assert.equal( - response.verificationMethod, - 'email', - 'challenge method set to email' - ); - assert.equal( - response.verificationReason, - 'login', - 'challenge reason set to signin' - ); - assert.equal(response.verified, false, 'verified set to false'); + it('should allow verified sessions before totp enabled to delete totp token', () => { + let client2, code; + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ) + .then((x) => { + client = x; + return client.login({ keys: true }); + }) + .then((response) => { + assert.equal( + response.verificationMethod, + 'email', + 'challenge method set to email' + ); + assert.equal( + response.verificationReason, + 'login', + 'challenge reason set to signin' + ); + assert.equal(response.verified, false, 'verified set to false'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + code = emailData.headers['x-verify-code']; + return client.verifyEmail(code); + }) + .then(() => { + // Login with a new client and enabled TOTP + return Client.loginAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + { + ...testOptions, + keys: true, + } + ); + }) + .then((x) => { + client2 = x; + return verifyTOTP(client2); + }) + .then((res) => { + // Delete totp from original client that only was email verified + return client.deleteTotpToken().then((result) => { + assert.ok(result, 'delete totp token successfully'); return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - code = emailData.headers['x-verify-code']; - return client.verifyEmail(code); - }) - .then(() => { - // Login with a new client and enabled TOTP - return Client.loginAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - { - ...testOptions, - keys: true, - } - ); - }) - .then((x) => { - client2 = x; - return verifyTOTP(client2); - }) - .then((res) => { - // Delete totp from original client that only was email verified - return client.deleteTotpToken().then((result) => { - assert.ok(result, 'delete totp token successfully'); - return server.mailbox.waitForEmail(email); - }); - }) - .then((emailData) => { - assert.equal( - emailData.headers['x-template-name'], - 'postRemoveTwoStepAuthentication' - ); + }); + }) + .then((emailData) => { + assert.equal( + emailData.headers['x-template-name'], + 'postRemoveTwoStepAuthentication' + ); - // Can create a new token - return client.checkTotpTokenExists().then((result) => { - assert.equal(result.exists, false, 'token does not exist'); - }); + // Can create a new token + return client.checkTotpTokenExists().then((result) => { + assert.equal(result.exists, false, 'token does not exist'); }); - }); + }); + }); - it('should not allow unverified sessions before totp enabled to delete totp token', () => { - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ) - .then((x) => { - client = x; - return client.login({ keys: true }); - }) - .then((response) => { - assert.equal( - response.verificationMethod, - 'email', - 'challenge method set to email' - ); - assert.equal( - response.verificationReason, - 'login', - 'challenge reason set to signin' - ); - assert.equal(response.verified, false, 'verified set to false'); + it('should not allow unverified sessions before totp enabled to delete totp token', () => { + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ) + .then((x) => { + client = x; + return client.login({ keys: true }); + }) + .then((response) => { + assert.equal( + response.verificationMethod, + 'email', + 'challenge method set to email' + ); + assert.equal( + response.verificationReason, + 'login', + 'challenge reason set to signin' + ); + assert.equal(response.verified, false, 'verified set to false'); - return server.mailbox.waitForEmail(email); - }) - .then(() => { - // Login with a new client and enabled TOTP - return Client.loginAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - { - ...testOptions, - keys: true, - } - ); - }) - .then((client2) => verifyTOTP(client2)) - .then((res) => { - // Attempt to delete totp from original unverified session - return client.deleteTotpToken().then(assert.fail, (err) => { - assert.equal(err.errno, 138, 'correct unverified session errno'); - }); + return server.mailbox.waitForEmail(email); + }) + .then(() => { + // Login with a new client and enabled TOTP + return Client.loginAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + { + ...testOptions, + keys: true, + } + ); + }) + .then((client2) => verifyTOTP(client2)) + .then((res) => { + // Attempt to delete totp from original unverified session + return client.deleteTotpToken().then(assert.fail, (err) => { + assert.equal(err.errno, 138, 'correct unverified session errno'); }); + }); + }); + + it('should request `totp-2fa` on login if user has verified totp token', () => { + return Client.login(config.publicUrl, email, password, { + ...testOptions, + keys: true, + }).then((response) => { + assert.equal( + response.verificationMethod, + 'totp-2fa', + 'verification method set' + ); + assert.equal( + response.verificationReason, + 'login', + 'verification reason set' + ); }); + }); - it('should request `totp-2fa` on login if user has verified totp token', () => { - return Client.login(config.publicUrl, email, password, { - ...testOptions, - keys: true, - }).then((response) => { - assert.equal( + it('should not have `totp-2fa` verification if user has unverified totp token', () => { + return client + .deleteTotpToken() + .then(() => client.createTotpToken()) + .then(() => + Client.login(config.publicUrl, email, password, { + ...testOptions, + keys: true, + }) + ) + .then((response) => { + assert.notEqual( response.verificationMethod, 'totp-2fa', - 'verification method set' + 'verification method not set to `totp-2fa`' ); assert.equal( response.verificationReason, 'login', - 'verification reason set' + 'verification reason set to `login`' ); }); - }); + }); - it('should not have `totp-2fa` verification if user has unverified totp token', () => { - return client - .deleteTotpToken() - .then(() => client.createTotpToken()) - .then(() => - Client.login(config.publicUrl, email, password, { - ...testOptions, - keys: true, - }) - ) - .then((response) => { - assert.notEqual( - response.verificationMethod, - 'totp-2fa', - 'verification method not set to `totp-2fa`' - ); - assert.equal( - response.verificationReason, - 'login', - 'verification reason set to `login`' - ); - }); + it('should not bypass `totp-2fa` by resending sign-in confirmation code', () => { + return Client.login(config.publicUrl, email, password, { + ...testOptions, + keys: true, + }).then((response) => { + client = response; + assert.equal( + response.verificationMethod, + 'totp-2fa', + 'verification method set' + ); + assert.equal( + response.verificationReason, + 'login', + 'verification reason set' + ); + + return client.requestVerifyEmail().then((res) => { + assert.deepEqual(res, {}, 'returns empty response'); + }); }); + }); - it('should not bypass `totp-2fa` by resending sign-in confirmation code', () => { - return Client.login(config.publicUrl, email, password, { - ...testOptions, - keys: true, - }) - .then((response) => { - client = response; - assert.equal( - response.verificationMethod, - 'totp-2fa', - 'verification method set' - ); + it('should not bypass `totp-2fa` by signing a cert with an unverified session', () => { + return Client.login(config.publicUrl, email, password, { + ...testOptions, + keys: false, + }).then((response) => { + client = response; + assert.equal( + response.verificationMethod, + 'totp-2fa', + 'verification method set' + ); + assert.equal( + response.verificationReason, + 'login', + 'verification reason set' + ); + + const publicKey = { + algorithm: 'RS', + n: + '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862' + + '993582789079872007974809511698859885077002492642203267408776123', + e: '65537', + }; + return client.sign(publicKey, 600).then( + () => { + assert.fail('should not have succeeded'); + }, + (err) => { assert.equal( - response.verificationReason, - 'login', - 'verification reason set' + err.errno, + 138, + 'should have failed due to unverified session' ); - - return client.requestVerifyEmail().then((res) => { - assert.fail('should have failed'); - }); - }) - .catch((err) => { - assert.equal(err.errno, 138); - }); + } + ); }); + }); - it('should not bypass `totp-2fa` by signing a cert with an unverified session', () => { - return Client.login(config.publicUrl, email, password, { - ...testOptions, - keys: false, - }).then((response) => { + it('should not bypass `totp-2fa` by when using session reauth', () => { + return Client.login(config.publicUrl, email, password, testOptions).then( + (response) => { client = response; assert.equal( response.verificationMethod, @@ -358,32 +391,8 @@ const { 'verification reason set' ); - const publicKey = { - algorithm: 'RS', - n: - '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862' + - '993582789079872007974809511698859885077002492642203267408776123', - e: '65537', - }; - return client.sign(publicKey, 600).then( - () => { - assert.fail('should not have succeeded'); - }, - (err) => { - assert.equal( - err.errno, - 138, - 'should have failed due to unverified session' - ); - } - ); - }); - }); - - it('should not bypass `totp-2fa` by when using session reauth', () => { - return Client.login(config.publicUrl, email, password, testOptions).then( - (response) => { - client = response; + // Lets attempt to sign-in reusing session reauth + return client.reauth().then((response) => { assert.equal( response.verificationMethod, 'totp-2fa', @@ -394,142 +403,129 @@ const { 'login', 'verification reason set' ); + }); + } + ); + }); - // Lets attempt to sign-in reusing session reauth - return client.reauth().then((response) => { - assert.equal( - response.verificationMethod, - 'totp-2fa', - 'verification method set' - ); - assert.equal( - response.verificationReason, - 'login', - 'verification reason set' - ); - }); - } - ); - }); + it('should not create verified session after account reset with totp', async () => { + const newPassword = 'anotherPassword'; - it('should not create verified session after account reset with totp', async () => { - const newPassword = 'anotherPassword'; + const client = await Client.login(config.publicUrl, email, password, { + ...testOptions, + keys: true, + }); + assert.equal( + client.verificationMethod, + 'totp-2fa', + 'verification method set' + ); + assert.equal(client.verificationReason, 'login', 'verification reason set'); + await client.forgotPassword(); + const code = await server.mailbox.waitForCode(email); + await client.verifyPasswordResetCode(code); + const res = await client.resetPassword(newPassword, {}, { keys: true }); + + assert.equal(res.verificationMethod, 'totp-2fa', 'verificationMethod set'); + assert.equal(res.verificationReason, 'login', 'verificationMethod set'); + assert.equal(res.verified, false); + assert.ok(res.keyFetchToken); + assert.ok(res.sessionToken); + assert.ok(res.authAt); + }); - const client = await Client.login(config.publicUrl, email, password, { - ...testOptions, - keys: true, - }); - assert.equal( - client.verificationMethod, - 'totp-2fa', - 'verification method set' + describe('totp code verification', () => { + beforeEach(() => { + // Create a new unverified session to test totp codes + return Client.login(config.publicUrl, email, password, testOptions).then( + (response) => (client = response) ); - assert.equal(client.verificationReason, 'login', 'verification reason set'); - await client.forgotPassword(); - const code = await server.mailbox.waitForCode(email); - await client.verifyPasswordResetCode(code); - const res = await client.resetPassword(newPassword, {}, { keys: true }); - - assert.equal(res.verificationMethod, 'totp-2fa', 'verificationMethod set'); - assert.equal(res.verificationReason, 'login', 'verificationMethod set'); - assert.equal(res.verified, false); - assert.ok(res.keyFetchToken); - assert.ok(res.sessionToken); - assert.ok(res.authAt); }); - describe('totp code verification', () => { - beforeEach(() => { - // Create a new unverified session to test totp codes - return Client.login(config.publicUrl, email, password, testOptions).then( - (response) => (client = response) - ); - }); + it('should fail to verify totp code', () => { + const code = authenticator.generate(); + const incorrectCode = code === '123456' ? '123455' : '123456'; + return client + .verifyTotpCode(incorrectCode, { metricsContext, service: 'sync' }) + .then((result) => { + assert.equal(result.success, false, 'failed'); + }); + }); - it('should fail to verify totp code', () => { - const code = authenticator.generate(); - const incorrectCode = code === '123456' ? '123455' : '123456'; - return client - .verifyTotpCode(incorrectCode, { metricsContext, service: 'sync' }) - .then((result) => { - assert.equal(result.success, false, 'failed'); - }); - }); + it('should reject non-numeric codes', () => { + return client + .verifyTotpCode('wrong', { metricsContext, service: 'sync' }) + .then(assert.fail, (err) => { + assert.equal(err.code, 400, 'correct error code'); + assert.equal(err.errno, 107, 'correct error errno'); + }); + }); - it('should reject non-numeric codes', () => { + it('should fail to verify totp code that does not have totp token', () => { + email = server.uniqueEmail(); + return Client.createAndVerify( + config.publicUrl, + email, + password, + server.mailbox, + testOptions + ).then((x) => { + client = x; + assert.ok(client.authAt, 'authAt was set'); return client - .verifyTotpCode('wrong', { metricsContext, service: 'sync' }) + .verifyTotpCode('123456', { metricsContext, service: 'sync' }) .then(assert.fail, (err) => { assert.equal(err.code, 400, 'correct error code'); - assert.equal(err.errno, 107, 'correct error errno'); + assert.equal(err.errno, 155, 'correct error errno'); }); }); + }); - it('should fail to verify totp code that does not have totp token', () => { - email = server.uniqueEmail(); - return Client.createAndVerify( - config.publicUrl, - email, - password, - server.mailbox, - testOptions - ).then((x) => { - client = x; - assert.ok(client.authAt, 'authAt was set'); - return client - .verifyTotpCode('123456', { metricsContext, service: 'sync' }) - .then(assert.fail, (err) => { - assert.equal(err.code, 400, 'correct error code'); - assert.equal(err.errno, 155, 'correct error errno'); - }); + it('should verify totp code', () => { + const code = authenticator.generate(); + return client + .verifyTotpCode(code, { metricsContext, service: 'sync' }) + .then((response) => { + assert.equal(response.success, true, 'totp codes match'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal(emailData.headers['x-template-name'], 'newDeviceLogin'); }); - }); + }); - it('should verify totp code', () => { - const code = authenticator.generate(); - return client - .verifyTotpCode(code, { metricsContext, service: 'sync' }) - .then((response) => { - assert.equal(response.success, true, 'totp codes match'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal(emailData.headers['x-template-name'], 'newDeviceLogin'); - }); + it('should verify totp code from previous code window', () => { + const futureAuthenticator = new otplib.authenticator.Authenticator(); + futureAuthenticator.options = Object.assign({}, authenticator.options, { + epoch: Date.now() / 1000 - 30, }); - - it('should verify totp code from previous code window', () => { - const futureAuthenticator = new otplib.authenticator.Authenticator(); - futureAuthenticator.options = Object.assign({}, authenticator.options, { - epoch: Date.now() / 1000 - 30, + const code = futureAuthenticator.generate(); + return client + .verifyTotpCode(code, { metricsContext, service: 'sync' }) + .then((response) => { + assert.equal(response.success, true, 'totp codes match'); + return server.mailbox.waitForEmail(email); + }) + .then((emailData) => { + assert.equal(emailData.headers['x-template-name'], 'newDeviceLogin'); }); - const code = futureAuthenticator.generate(); - return client - .verifyTotpCode(code, { metricsContext, service: 'sync' }) - .then((response) => { - assert.equal(response.success, true, 'totp codes match'); - return server.mailbox.waitForEmail(email); - }) - .then((emailData) => { - assert.equal(emailData.headers['x-template-name'], 'newDeviceLogin'); - }); - }); + }); - it('should not verify totp code from future code window', () => { - const futureAuthenticator = new otplib.authenticator.Authenticator(); - futureAuthenticator.options = Object.assign({}, authenticator.options, { - epoch: Date.now() / 1000 + 3000, - }); - const code = futureAuthenticator.generate(); - return client - .verifyTotpCode(code, { metricsContext, service: 'sync' }) - .then((response) => { - assert.equal(response.success, false, 'totp codes do not match'); - }); + it('should not verify totp code from future code window', () => { + const futureAuthenticator = new otplib.authenticator.Authenticator(); + futureAuthenticator.options = Object.assign({}, authenticator.options, { + epoch: Date.now() / 1000 + 3000, }); + const code = futureAuthenticator.generate(); + return client + .verifyTotpCode(code, { metricsContext, service: 'sync' }) + .then((response) => { + assert.equal(response.success, false, 'totp codes do not match'); + }); }); + }); - }); +}); });