CaptchaTests.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 expectBadLogin(response, body) {
  51. expect(response.statusCode).to.equal(401)
  52. expect(body).to.deep.equal({
  53. message: {
  54. type: 'error',
  55. key: 'invalid-password-retry-or-reset',
  56. },
  57. })
  58. }
  59. it('should reject a login without captcha response', async function () {
  60. const { response, body } = await loginWithCaptcha('')
  61. expectBadCaptchaResponse(response, body)
  62. })
  63. it('should reject a login with an invalid captcha response', async function () {
  64. const { response, body } = await loginWithCaptcha('invalid')
  65. expectBadCaptchaResponse(response, body)
  66. })
  67. it('should accept a login with a valid captcha response', async function () {
  68. const { response, body } = await loginWithCaptcha('valid')
  69. expectSuccessfulLogin(response, body)
  70. })
  71. it('should note the solved captcha in audit log', async function () {
  72. const { response, body } = await loginWithCaptcha('valid')
  73. expectSuccessfulLogin(response, body)
  74. const auditLog = await user.getAuditLog()
  75. expect(auditLog[0].info).to.deep.equal({
  76. captcha: 'solved',
  77. method: 'Password login',
  78. fromKnownDevice: false,
  79. })
  80. })
  81. describe('deviceHistory', function () {
  82. beforeEach('login', async function () {
  83. const { response, body } = await loginWithCaptcha('valid')
  84. expectSuccessfulLogin(response, body)
  85. })
  86. it('should be able to skip captcha with the same email', async function () {
  87. expect(await canSkipCaptcha(user.email)).to.equal(true)
  88. })
  89. it('should be able to omit captcha with the same email', async function () {
  90. const { response, body } = await loginWithCaptcha('')
  91. expectSuccessfulLogin(response, body)
  92. })
  93. it('should note the skipped captcha in audit log', async function () {
  94. const { response, body } = await loginWithCaptcha('')
  95. expectSuccessfulLogin(response, body)
  96. const auditLog = await user.getAuditLog()
  97. expect(auditLog[1].info).to.deep.equal({
  98. captcha: 'skipped',
  99. method: 'Password login',
  100. fromKnownDevice: true,
  101. })
  102. })
  103. it('should request a captcha for another email', async function () {
  104. expect(await canSkipCaptcha('a@bc.de')).to.equal(false)
  105. })
  106. it('should flag missing captcha for another email', async function () {
  107. const { response, body } = await loginWithEmailAndCaptcha('a@bc.de', '')
  108. expectBadCaptchaResponse(response, body)
  109. })
  110. describe('login failure', function () {
  111. beforeEach(async function () {
  112. const { response, body } = await login(
  113. user.email,
  114. 'bad password',
  115. 'valid'
  116. )
  117. expectBadLogin(response, body)
  118. })
  119. it('should be able to skip captcha per device history', async function () {
  120. expect(await canSkipCaptcha(user.email)).to.equal(true)
  121. })
  122. it('should request a captcha despite device history entry', async function () {
  123. const { response, body } = await loginWithCaptcha('')
  124. expectBadCaptchaResponse(response, body)
  125. })
  126. it('should accept the login with captcha', async function () {
  127. const { response, body } = await loginWithCaptcha('valid')
  128. expectSuccessfulLogin(response, body)
  129. })
  130. describe('when the login failure happened a long time ago', function () {
  131. beforeEach(async function () {
  132. db.users.updateOne(
  133. { email: user.email },
  134. {
  135. $set: {
  136. lastFailedLogin: new Date(
  137. Date.now() - 90 * 24 * 60 * 60 * 1000
  138. ),
  139. },
  140. }
  141. )
  142. })
  143. it('should be able to skip captcha per device history', async function () {
  144. expect(await canSkipCaptcha(user.email)).to.equal(true)
  145. })
  146. it('should accept the login without captcha', async function () {
  147. const { response, body } = await loginWithCaptcha('')
  148. expectSuccessfulLogin(response, body)
  149. })
  150. it('should accept the login with captcha', async function () {
  151. const { response, body } = await loginWithCaptcha('valid')
  152. expectSuccessfulLogin(response, body)
  153. })
  154. })
  155. })
  156. describe('cycle history', function () {
  157. beforeEach('create and login with 10 other users', async function () {
  158. for (let i = 0; i < 10; i++) {
  159. const otherUser = new User()
  160. otherUser.password = user.password
  161. await otherUser.ensureUserExists()
  162. const { response, body } = await loginWithEmailAndCaptcha(
  163. otherUser.email,
  164. 'valid'
  165. )
  166. expectSuccessfulLogin(response, body)
  167. }
  168. })
  169. it('should have rolled out the initial users email', async function () {
  170. const { response, body } = await loginWithCaptcha('')
  171. expectBadCaptchaResponse(response, body)
  172. })
  173. })
  174. describe('HIBP', function () {
  175. before(function () {
  176. Settings.apis.haveIBeenPwned.enabled = true
  177. })
  178. after(function () {
  179. Settings.apis.haveIBeenPwned.enabled = false
  180. })
  181. beforeEach(async function () {
  182. user = new User()
  183. user.password = 'aLeakedPassword42'
  184. await user.ensureUserExists()
  185. })
  186. beforeEach('login to populate deviceHistory', async function () {
  187. const { response, body } = await loginWithCaptcha('valid')
  188. expectSuccessfulLogin(response, body)
  189. })
  190. beforeEach(function () {
  191. // echo -n aLeakedPassword42 | sha1sum
  192. MockHaveIBeenPwnedApi.addPasswordByHash(
  193. 'D1ABBDEEE70CBE8BBCE5D9D039C53C0CE91C0C16'
  194. )
  195. })
  196. it('should be able to skip HIBP check with deviceHistory and valid captcha', async function () {
  197. const { response, body } = await loginWithCaptcha('valid')
  198. expectSuccessfulLogin(response, body)
  199. })
  200. it('should be able to skip HIBP check with deviceHistory and skipped captcha', async function () {
  201. const { response, body } = await loginWithCaptcha('')
  202. expectSuccessfulLogin(response, body)
  203. })
  204. it('should not be able to skip HIBP check without deviceHistory', async function () {
  205. user.resetCookies()
  206. const { response, body } = await loginWithCaptcha('valid')
  207. expect(response.statusCode).to.equal(400)
  208. expect(body.message.key).to.equal('password-compromised')
  209. })
  210. })
  211. })
  212. })