/packages/fxa-auth-server/test/remote/recovery_code_tests.js

https://github.com/mozilla/fxa · JavaScript · 251 lines · 224 code · 20 blank · 7 comment · 2 complexity · 232ed57723ab3fe209fd5559fbdda79b MD5 · raw file

  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. 'use strict';
  5. const { assert } = require('chai');
  6. const config = require('../../config').getProperties();
  7. const TestServer = require('../test_server');
  8. const Client = require('../client')();
  9. const otplib = require('otplib');
  10. const BASE_36 = require('../../lib/routes/validators').BASE_36;
  11. describe('remote recovery codes', function () {
  12. let server, client, email, recoveryCodes;
  13. const recoveryCodeCount = 9;
  14. const password = 'pssssst';
  15. const metricsContext = {
  16. flowBeginTime: Date.now(),
  17. flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
  18. };
  19. this.timeout(10000);
  20. otplib.authenticator.options = {
  21. encoding: 'hex',
  22. window: 10,
  23. };
  24. before(() => {
  25. config.totp.recoveryCodes.count = recoveryCodeCount;
  26. config.totp.recoveryCodes.notifyLowCount = recoveryCodeCount - 2;
  27. return TestServer.start(config).then((s) => {
  28. server = s;
  29. });
  30. });
  31. beforeEach(() => {
  32. email = server.uniqueEmail();
  33. return Client.createAndVerify(
  34. config.publicUrl,
  35. email,
  36. password,
  37. server.mailbox
  38. ).then((x) => {
  39. client = x;
  40. assert.ok(client.authAt, 'authAt was set');
  41. return client.createTotpToken({ metricsContext }).then((result) => {
  42. otplib.authenticator.options = {
  43. secret: result.secret,
  44. };
  45. recoveryCodes = result.recoveryCodes;
  46. assert.equal(
  47. result.recoveryCodes.length,
  48. recoveryCodeCount,
  49. 'recovery codes returned'
  50. );
  51. // Verify TOTP token so that initial recovery codes are generated
  52. const code = otplib.authenticator.generate();
  53. return client
  54. .verifyTotpCode(code, { metricsContext })
  55. .then((response) => {
  56. assert.equal(response.success, true, 'totp codes match');
  57. return server.mailbox.waitForEmail(email);
  58. })
  59. .then((emailData) => {
  60. assert.equal(
  61. emailData.headers['x-template-name'],
  62. 'postAddTwoStepAuthentication'
  63. );
  64. });
  65. });
  66. });
  67. });
  68. it('should create recovery codes', () => {
  69. assert.ok(recoveryCodes);
  70. assert.equal(
  71. recoveryCodes.length,
  72. recoveryCodeCount,
  73. 'recovery codes returned'
  74. );
  75. recoveryCodes.forEach((code) => {
  76. assert.equal(code.length > 1, true, 'correct length');
  77. assert.equal(BASE_36.test(code), true, 'code is hex');
  78. });
  79. });
  80. it('should replace recovery codes', () => {
  81. return client
  82. .replaceRecoveryCodes()
  83. .then((result) => {
  84. assert.ok(
  85. result.recoveryCodes.length,
  86. recoveryCodeCount,
  87. 'recovery codes returned'
  88. );
  89. assert.notDeepEqual(
  90. result,
  91. recoveryCodes,
  92. 'recovery codes should not match'
  93. );
  94. return server.mailbox.waitForEmail(email);
  95. })
  96. .then((emailData) => {
  97. assert.equal(
  98. emailData.headers['x-template-name'],
  99. 'postNewRecoveryCodes'
  100. );
  101. });
  102. });
  103. describe('recovery code verification', () => {
  104. beforeEach(() => {
  105. // Create a new unverified session to test recovery codes
  106. return Client.login(config.publicUrl, email, password)
  107. .then((response) => {
  108. client = response;
  109. return client.emailStatus();
  110. })
  111. .then((res) =>
  112. assert.equal(res.sessionVerified, false, 'session not verified')
  113. );
  114. });
  115. it('should fail to consume unknown recovery code', () => {
  116. return client
  117. .consumeRecoveryCode('1234abcd', { metricsContext })
  118. .then(assert.fail, (err) => {
  119. assert.equal(err.code, 400, 'correct error code');
  120. assert.equal(err.errno, 156, 'correct error errno');
  121. });
  122. });
  123. it('should consume recovery code and verify session', () => {
  124. return client
  125. .consumeRecoveryCode(recoveryCodes[0], { metricsContext })
  126. .then((res) => {
  127. assert.equal(
  128. res.remaining,
  129. recoveryCodeCount - 1,
  130. 'correct remaining codes'
  131. );
  132. return client.emailStatus();
  133. })
  134. .then((res) => {
  135. assert.equal(res.sessionVerified, true, 'session verified');
  136. return server.mailbox.waitForEmail(email);
  137. })
  138. .then((emailData) => {
  139. assert.equal(
  140. emailData.headers['x-template-name'],
  141. 'postConsumeRecoveryCode'
  142. );
  143. });
  144. });
  145. it('should consume recovery code and can remove TOTP token', () => {
  146. return client
  147. .consumeRecoveryCode(recoveryCodes[0], { metricsContext })
  148. .then((res) => {
  149. assert.equal(
  150. res.remaining,
  151. recoveryCodeCount - 1,
  152. 'correct remaining codes'
  153. );
  154. return server.mailbox.waitForEmail(email);
  155. })
  156. .then((emailData) => {
  157. assert.equal(
  158. emailData.headers['x-template-name'],
  159. 'postConsumeRecoveryCode'
  160. );
  161. return client.deleteTotpToken();
  162. })
  163. .then((result) => {
  164. assert.ok(result, 'delete totp token successfully');
  165. return server.mailbox.waitForEmail(email);
  166. })
  167. .then((emailData) => {
  168. assert.equal(
  169. emailData.headers['x-template-name'],
  170. 'postRemoveTwoStepAuthentication'
  171. );
  172. });
  173. });
  174. });
  175. describe('should notify user when recovery codes are low', () => {
  176. beforeEach(() => {
  177. // Create a new unverified session to test recovery codes
  178. return Client.login(config.publicUrl, email, password)
  179. .then((response) => {
  180. client = response;
  181. return client.emailStatus();
  182. })
  183. .then((res) =>
  184. assert.equal(res.sessionVerified, false, 'session not verified')
  185. );
  186. });
  187. it('should consume recovery code and verify session', () => {
  188. return client
  189. .consumeRecoveryCode(recoveryCodes[0], { metricsContext })
  190. .then((res) => {
  191. assert.equal(
  192. res.remaining,
  193. recoveryCodeCount - 1,
  194. 'correct remaining codes'
  195. );
  196. return server.mailbox.waitForEmail(email);
  197. })
  198. .then((emailData) => {
  199. assert.equal(
  200. emailData.headers['x-template-name'],
  201. 'postConsumeRecoveryCode'
  202. );
  203. return client.consumeRecoveryCode(recoveryCodes[1], {
  204. metricsContext,
  205. });
  206. })
  207. .then((res) => {
  208. assert.equal(
  209. res.remaining,
  210. recoveryCodeCount - 2,
  211. 'correct remaining codes'
  212. );
  213. return server.mailbox.waitForEmail(email);
  214. })
  215. .then((emails) => {
  216. // The order in which the emails are sent is not guaranteed, test for both possible templates
  217. const email1 = emails[0].headers['x-template-name'];
  218. const email2 = emails[1].headers['x-template-name'];
  219. if (email1 === 'postConsumeRecoveryCode') {
  220. assert.equal(email2, 'lowRecoveryCodes');
  221. }
  222. if (email1 === 'lowRecoveryCodes') {
  223. assert.equal(email2, 'postConsumeRecoveryCode');
  224. }
  225. });
  226. });
  227. });
  228. after(() => {
  229. return TestServer.stop(server);
  230. });
  231. });