CaptchaTests.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import { db } from '../../../app/src/infrastructure/mongodb.mjs'
  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('trustedUsersRegex', function () {
  90. let resetTrustedUsersRegex
  91. beforeEach(function () {
  92. resetTrustedUsersRegex = Settings.recaptcha.trustedUsersRegex
  93. Settings.recaptcha.trustedUsersRegex = /\+trusted@example\.com$/
  94. })
  95. afterEach(function () {
  96. Settings.recaptcha.trustedUsersRegex = resetTrustedUsersRegex
  97. })
  98. describe('when user is trusted, can login without captcha', function () {
  99. let trustedUser
  100. beforeEach(async function () {
  101. trustedUser = new User({
  102. email: 'acceptance-test+trusted@example.com',
  103. })
  104. await trustedUser.ensureUserExists()
  105. })
  106. it('should be able to skip captcha', async function () {
  107. expect(await canSkipCaptcha(trustedUser.email)).to.equal(true)
  108. })
  109. it('should note that the user is trusted', async function () {
  110. const { response, body } = await login(
  111. trustedUser.email,
  112. trustedUser.password,
  113. ''
  114. )
  115. expectSuccessfulLogin(response, body)
  116. const auditLog = await trustedUser.getAuditLog()
  117. expect(auditLog[0].info).to.deep.equal({
  118. captcha: 'trusted',
  119. method: 'Password login',
  120. })
  121. })
  122. })
  123. describe('when user is not trusted', function () {
  124. it('should not be able to skip captcha', async function () {
  125. expect(await canSkipCaptcha(user.email)).to.equal(false)
  126. })
  127. it('should not add an audit log entry for trusted user', async function () {
  128. const { response, body } = await login(user.email, user.password, '')
  129. expectBadCaptchaResponse(response, body)
  130. const auditLog = await user.getAuditLog()
  131. expect(auditLog).to.have.lengthOf(0)
  132. })
  133. })
  134. })
  135. describe('deviceHistory', function () {
  136. beforeEach('login', async function () {
  137. const { response, body } = await loginWithCaptcha('valid')
  138. expectSuccessfulLogin(response, body)
  139. })
  140. it('should be able to skip captcha with the same email', async function () {
  141. expect(await canSkipCaptcha(user.email)).to.equal(true)
  142. })
  143. it('should be able to omit captcha with the same email', async function () {
  144. const { response, body } = await loginWithCaptcha('')
  145. expectSuccessfulLogin(response, body)
  146. })
  147. it('should note the skipped captcha in audit log', async function () {
  148. const { response, body } = await loginWithCaptcha('')
  149. expectSuccessfulLogin(response, body)
  150. const auditLog = await user.getAuditLog()
  151. expect(auditLog[1].info).to.deep.equal({
  152. captcha: 'skipped',
  153. method: 'Password login',
  154. fromKnownDevice: true,
  155. })
  156. })
  157. it('should request a captcha for another email', async function () {
  158. expect(await canSkipCaptcha('a@bc.de')).to.equal(false)
  159. })
  160. it('should flag missing captcha for another email', async function () {
  161. const { response, body } = await loginWithEmailAndCaptcha('a@bc.de', '')
  162. expectBadCaptchaResponse(response, body)
  163. })
  164. describe('login failure', function () {
  165. beforeEach(async function () {
  166. const { response, body } = await login(
  167. user.email,
  168. 'bad password',
  169. 'valid'
  170. )
  171. expectBadLogin(response, body)
  172. })
  173. it('should be able to skip captcha per device history', async function () {
  174. expect(await canSkipCaptcha(user.email)).to.equal(true)
  175. })
  176. it('should request a captcha despite device history entry', async function () {
  177. const { response, body } = await loginWithCaptcha('')
  178. expectBadCaptchaResponse(response, body)
  179. })
  180. it('should accept the login with captcha', async function () {
  181. const { response, body } = await loginWithCaptcha('valid')
  182. expectSuccessfulLogin(response, body)
  183. })
  184. describe('when the login failure happened a long time ago', function () {
  185. beforeEach(async function () {
  186. db.users.updateOne(
  187. { email: user.email },
  188. {
  189. $set: {
  190. lastFailedLogin: new Date(
  191. Date.now() - 90 * 24 * 60 * 60 * 1000
  192. ),
  193. },
  194. }
  195. )
  196. })
  197. it('should be able to skip captcha per device history', async function () {
  198. expect(await canSkipCaptcha(user.email)).to.equal(true)
  199. })
  200. it('should accept the login without captcha', async function () {
  201. const { response, body } = await loginWithCaptcha('')
  202. expectSuccessfulLogin(response, body)
  203. })
  204. it('should accept the login with captcha', async function () {
  205. const { response, body } = await loginWithCaptcha('valid')
  206. expectSuccessfulLogin(response, body)
  207. })
  208. })
  209. })
  210. describe('cycle history', function () {
  211. beforeEach('create and login with 10 other users', async function () {
  212. for (let i = 0; i < 10; i++) {
  213. const otherUser = new User()
  214. otherUser.password = user.password
  215. await otherUser.ensureUserExists()
  216. const { response, body } = await loginWithEmailAndCaptcha(
  217. otherUser.email,
  218. 'valid'
  219. )
  220. expectSuccessfulLogin(response, body)
  221. }
  222. })
  223. it('should have rolled out the initial users email', async function () {
  224. const { response, body } = await loginWithCaptcha('')
  225. expectBadCaptchaResponse(response, body)
  226. })
  227. })
  228. describe('HIBP', function () {
  229. before(function () {
  230. Settings.apis.haveIBeenPwned.enabled = true
  231. })
  232. after(function () {
  233. Settings.apis.haveIBeenPwned.enabled = false
  234. })
  235. beforeEach(async function () {
  236. user = new User()
  237. user.password = 'aLeakedPassword42'
  238. await user.ensureUserExists()
  239. })
  240. beforeEach('login to populate deviceHistory', async function () {
  241. const { response, body } = await loginWithCaptcha('valid')
  242. expectSuccessfulLogin(response, body)
  243. })
  244. beforeEach(function () {
  245. // echo -n aLeakedPassword42 | sha1sum
  246. MockHaveIBeenPwnedApi.addPasswordByHash(
  247. 'D1ABBDEEE70CBE8BBCE5D9D039C53C0CE91C0C16'
  248. )
  249. })
  250. it('should be able to skip HIBP check with deviceHistory and valid captcha', async function () {
  251. const { response, body } = await loginWithCaptcha('valid')
  252. expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
  253. response,
  254. body
  255. )
  256. })
  257. it('should be able to skip HIBP check with deviceHistory and skipped captcha', async function () {
  258. const { response, body } = await loginWithCaptcha('')
  259. expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
  260. response,
  261. body
  262. )
  263. })
  264. it('should not be able to skip HIBP check without deviceHistory', async function () {
  265. user.resetCookies()
  266. const { response, body } = await loginWithCaptcha('valid')
  267. expect(response.statusCode).to.equal(400)
  268. expect(body.message.key).to.equal('password-compromised')
  269. })
  270. })
  271. })
  272. })