CaptchaTests.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. const { db } = require('../../../app/src/infrastructure/mongodb')
  2. const { expect } = require('chai')
  3. const Settings = require('@overleaf/settings')
  4. const User = require('./helpers/User').promises
  5. const MockHaveIBeenPwnedApiClass = require('./mocks/MockHaveIBeenPwnedApi')
  6. let MockHaveIBeenPwnedApi
  7. before(function () {
  8. MockHaveIBeenPwnedApi = MockHaveIBeenPwnedApiClass.instance()
  9. })
  10. describe('Captcha', function () {
  11. let user
  12. beforeEach('create user', async function () {
  13. user = new User()
  14. await user.ensureUserExists()
  15. })
  16. async function login(email, password, captchaResponse) {
  17. await user.getCsrfToken()
  18. return user.doRequest('POST', {
  19. url: '/login',
  20. json: {
  21. email,
  22. password,
  23. 'g-recaptcha-response': captchaResponse,
  24. },
  25. })
  26. }
  27. async function loginWithCaptcha(captchaResponse) {
  28. return login(user.email, user.password, captchaResponse)
  29. }
  30. async function loginWithEmailAndCaptcha(email, captchaResponse) {
  31. return login(email, user.password, captchaResponse)
  32. }
  33. async function canSkipCaptcha(email) {
  34. await user.getCsrfToken()
  35. const { response, body } = await user.doRequest('POST', {
  36. url: '/login/can-skip-captcha',
  37. json: { email },
  38. })
  39. expect(response.statusCode).to.equal(200)
  40. return body
  41. }
  42. function expectBadCaptchaResponse(response, body) {
  43. expect(response.statusCode).to.equal(400)
  44. expect(body.errorReason).to.equal('cannot_verify_user_not_robot')
  45. }
  46. function expectSuccessfulLogin(response, body) {
  47. expect(response.statusCode).to.equal(200)
  48. expect(body).to.deep.equal({ redir: '/project' })
  49. }
  50. function expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
  51. response,
  52. body
  53. ) {
  54. expect(response.statusCode).to.equal(200)
  55. expect(body).to.deep.equal({ redir: '/compromised-password' })
  56. }
  57. function expectBadLogin(response, body) {
  58. expect(response.statusCode).to.equal(401)
  59. expect(body).to.deep.equal({
  60. message: {
  61. type: 'error',
  62. key: 'invalid-password-retry-or-reset',
  63. },
  64. })
  65. }
  66. it('should reject a login without captcha response', async function () {
  67. const { response, body } = await loginWithCaptcha('')
  68. expectBadCaptchaResponse(response, body)
  69. })
  70. it('should reject a login with an invalid captcha response', async function () {
  71. const { response, body } = await loginWithCaptcha('invalid')
  72. expectBadCaptchaResponse(response, body)
  73. })
  74. it('should accept a login with a valid captcha response', async function () {
  75. const { response, body } = await loginWithCaptcha('valid')
  76. expectSuccessfulLogin(response, body)
  77. })
  78. it('should note the solved captcha in audit log', async function () {
  79. const { response, body } = await loginWithCaptcha('valid')
  80. expectSuccessfulLogin(response, body)
  81. const auditLog = await user.getAuditLog()
  82. expect(auditLog[0].info).to.deep.equal({
  83. captcha: 'solved',
  84. method: 'Password login',
  85. fromKnownDevice: false,
  86. })
  87. })
  88. describe('deviceHistory', function () {
  89. beforeEach('login', async function () {
  90. const { response, body } = await loginWithCaptcha('valid')
  91. expectSuccessfulLogin(response, body)
  92. })
  93. it('should be able to skip captcha with the same email', async function () {
  94. expect(await canSkipCaptcha(user.email)).to.equal(true)
  95. })
  96. it('should be able to omit captcha with the same email', async function () {
  97. const { response, body } = await loginWithCaptcha('')
  98. expectSuccessfulLogin(response, body)
  99. })
  100. it('should note the skipped captcha in audit log', async function () {
  101. const { response, body } = await loginWithCaptcha('')
  102. expectSuccessfulLogin(response, body)
  103. const auditLog = await user.getAuditLog()
  104. expect(auditLog[1].info).to.deep.equal({
  105. captcha: 'skipped',
  106. method: 'Password login',
  107. fromKnownDevice: true,
  108. })
  109. })
  110. it('should request a captcha for another email', async function () {
  111. expect(await canSkipCaptcha('a@bc.de')).to.equal(false)
  112. })
  113. it('should flag missing captcha for another email', async function () {
  114. const { response, body } = await loginWithEmailAndCaptcha('a@bc.de', '')
  115. expectBadCaptchaResponse(response, body)
  116. })
  117. describe('login failure', function () {
  118. beforeEach(async function () {
  119. const { response, body } = await login(
  120. user.email,
  121. 'bad password',
  122. 'valid'
  123. )
  124. expectBadLogin(response, body)
  125. })
  126. it('should be able to skip captcha per device history', async function () {
  127. expect(await canSkipCaptcha(user.email)).to.equal(true)
  128. })
  129. it('should request a captcha despite device history entry', async function () {
  130. const { response, body } = await loginWithCaptcha('')
  131. expectBadCaptchaResponse(response, body)
  132. })
  133. it('should accept the login with captcha', async function () {
  134. const { response, body } = await loginWithCaptcha('valid')
  135. expectSuccessfulLogin(response, body)
  136. })
  137. describe('when the login failure happened a long time ago', function () {
  138. beforeEach(async function () {
  139. db.users.updateOne(
  140. { email: user.email },
  141. {
  142. $set: {
  143. lastFailedLogin: new Date(
  144. Date.now() - 90 * 24 * 60 * 60 * 1000
  145. ),
  146. },
  147. }
  148. )
  149. })
  150. it('should be able to skip captcha per device history', async function () {
  151. expect(await canSkipCaptcha(user.email)).to.equal(true)
  152. })
  153. it('should accept the login without captcha', async function () {
  154. const { response, body } = await loginWithCaptcha('')
  155. expectSuccessfulLogin(response, body)
  156. })
  157. it('should accept the login with captcha', async function () {
  158. const { response, body } = await loginWithCaptcha('valid')
  159. expectSuccessfulLogin(response, body)
  160. })
  161. })
  162. })
  163. describe('cycle history', function () {
  164. beforeEach('create and login with 10 other users', async function () {
  165. for (let i = 0; i < 10; i++) {
  166. const otherUser = new User()
  167. otherUser.password = user.password
  168. await otherUser.ensureUserExists()
  169. const { response, body } = await loginWithEmailAndCaptcha(
  170. otherUser.email,
  171. 'valid'
  172. )
  173. expectSuccessfulLogin(response, body)
  174. }
  175. })
  176. it('should have rolled out the initial users email', async function () {
  177. const { response, body } = await loginWithCaptcha('')
  178. expectBadCaptchaResponse(response, body)
  179. })
  180. })
  181. describe('HIBP', function () {
  182. before(function () {
  183. Settings.apis.haveIBeenPwned.enabled = true
  184. })
  185. after(function () {
  186. Settings.apis.haveIBeenPwned.enabled = false
  187. })
  188. beforeEach(async function () {
  189. user = new User()
  190. user.password = 'aLeakedPassword42'
  191. await user.ensureUserExists()
  192. })
  193. beforeEach('login to populate deviceHistory', async function () {
  194. const { response, body } = await loginWithCaptcha('valid')
  195. expectSuccessfulLogin(response, body)
  196. })
  197. beforeEach(function () {
  198. // echo -n aLeakedPassword42 | sha1sum
  199. MockHaveIBeenPwnedApi.addPasswordByHash(
  200. 'D1ABBDEEE70CBE8BBCE5D9D039C53C0CE91C0C16'
  201. )
  202. })
  203. it('should be able to skip HIBP check with deviceHistory and valid captcha', async function () {
  204. const { response, body } = await loginWithCaptcha('valid')
  205. expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
  206. response,
  207. body
  208. )
  209. })
  210. it('should be able to skip HIBP check with deviceHistory and skipped captcha', async function () {
  211. const { response, body } = await loginWithCaptcha('')
  212. expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
  213. response,
  214. body
  215. )
  216. })
  217. it('should not be able to skip HIBP check without deviceHistory', async function () {
  218. user.resetCookies()
  219. const { response, body } = await loginWithCaptcha('valid')
  220. expect(response.statusCode).to.equal(400)
  221. expect(body.message.key).to.equal('password-compromised')
  222. })
  223. })
  224. })
  225. })