CaptchaTests.mjs 8.3 KB

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