AuthenticationTests.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { expect } from 'chai'
  2. import mongodb from 'mongodb-legacy'
  3. import Settings from '@overleaf/settings'
  4. import UserHelper from './helpers/User.mjs'
  5. const ObjectId = mongodb.ObjectId
  6. const User = UserHelper.promises
  7. describe('Authentication', function () {
  8. let user
  9. beforeEach('init vars', function () {
  10. user = new User()
  11. })
  12. describe('CSRF regeneration on login', function () {
  13. it('should prevent use of csrf token from before login', function (done) {
  14. user.logout(err => {
  15. if (err) {
  16. return done(err)
  17. }
  18. user.getCsrfToken(err => {
  19. if (err) {
  20. return done(err)
  21. }
  22. const oldToken = user.csrfToken
  23. user.login(err => {
  24. if (err) {
  25. return done(err)
  26. }
  27. expect(oldToken === user.csrfToken).to.equal(false)
  28. user.request.post(
  29. {
  30. headers: {
  31. 'x-csrf-token': oldToken,
  32. },
  33. url: '/project/new',
  34. json: { projectName: 'test' },
  35. },
  36. (err, response, body) => {
  37. expect(err).to.not.exist
  38. expect(response.statusCode).to.equal(403)
  39. expect(body).to.equal('Forbidden')
  40. done()
  41. }
  42. )
  43. })
  44. })
  45. })
  46. })
  47. })
  48. describe('login', function () {
  49. beforeEach('doLogin', async function () {
  50. await user.login()
  51. })
  52. it('should log the user in', async function () {
  53. const {
  54. response: { statusCode },
  55. } = await user.doRequest('GET', '/project')
  56. expect(statusCode).to.equal(200)
  57. })
  58. it('should emit an user auditLog entry for the login', async function () {
  59. const auditLog = await user.getAuditLog()
  60. const auditLogEntry = auditLog[0]
  61. expect(auditLogEntry).to.exist
  62. expect(auditLogEntry.timestamp).to.exist
  63. expect(auditLogEntry.initiatorId).to.deep.equal(new ObjectId(user.id))
  64. expect(auditLogEntry.userId).to.deep.equal(new ObjectId(user.id))
  65. expect(auditLogEntry.operation).to.equal('login')
  66. expect(auditLogEntry.info).to.deep.equal({
  67. method: 'Password login',
  68. captcha: 'solved',
  69. fromKnownDevice: false,
  70. })
  71. expect(auditLogEntry.ipAddress).to.equal('127.0.0.1')
  72. })
  73. })
  74. describe('failed login', function () {
  75. beforeEach('fetchCsrfToken', async function () {
  76. await user.login()
  77. await user.logout()
  78. await user.getCsrfToken()
  79. })
  80. it('should return a 401, and add an entry to the audit log', async function () {
  81. const {
  82. response: { statusCode },
  83. } = await user.doRequest('POST', {
  84. url: Settings.enableLegacyLogin ? '/login/legacy' : '/login',
  85. json: {
  86. email: user.email,
  87. password: 'foo-bar-baz',
  88. 'g-recaptcha-response': 'valid',
  89. },
  90. })
  91. expect(statusCode).to.equal(401)
  92. const auditLog = await user.getAuditLog()
  93. const auditLogEntry = auditLog.pop()
  94. expect(auditLogEntry).to.exist
  95. expect(auditLogEntry.timestamp).to.exist
  96. expect(auditLogEntry.initiatorId).to.deep.equal(new ObjectId(user.id))
  97. expect(auditLogEntry.userId).to.deep.equal(new ObjectId(user.id))
  98. expect(auditLogEntry.operation).to.equal('failed-password-match')
  99. expect(auditLogEntry.info).to.deep.equal({
  100. method: 'Password login',
  101. fromKnownDevice: true,
  102. })
  103. expect(auditLogEntry.ipAddress).to.equal('127.0.0.1')
  104. })
  105. })
  106. describe('rate-limit', function () {
  107. const RATE_LIMIT = 10
  108. beforeEach('fetchCsrfToken', async function () {
  109. await user.login()
  110. await user.logout()
  111. await user.getCsrfToken()
  112. })
  113. async function tryLogin(i = 0, prefix = '') {
  114. const { statusCode } = await tryLoginWithEmail(
  115. `${prefix}${user.email}${' '.repeat(i)}`
  116. )
  117. return statusCode
  118. }
  119. async function tryLoginWithEmail(email) {
  120. const {
  121. response: { statusCode },
  122. body,
  123. } = await user.doRequest('POST', {
  124. url: Settings.enableLegacyLogin ? '/login/legacy' : '/login',
  125. json: {
  126. email,
  127. password: 'wrong-password',
  128. 'g-recaptcha-response': 'valid',
  129. },
  130. })
  131. return { statusCode, body }
  132. }
  133. it('should return 429 after 10 unsuccessful login attempts', async function () {
  134. for (let i = 0; i < RATE_LIMIT; i++) {
  135. const statusCode = await tryLogin()
  136. expect(statusCode).to.equal(401)
  137. }
  138. for (let i = 0; i < 10; i++) {
  139. const statusCode = await tryLogin()
  140. expect(statusCode).to.equal(429)
  141. }
  142. })
  143. it('ignore extra spaces in email address', async function () {
  144. for (let i = 0; i < RATE_LIMIT; i++) {
  145. const statusCode = await tryLogin(i)
  146. expect(statusCode).to.equal(401)
  147. }
  148. for (let i = 0; i < 10; i++) {
  149. const statusCode = await tryLogin(i)
  150. expect(statusCode).to.equal(429)
  151. }
  152. })
  153. it('normalizes the email address', async function () {
  154. for (let i = 0; i < RATE_LIMIT / 2; i++) {
  155. const statusCode = await tryLogin(i, 'x') // lower
  156. expect(statusCode).to.equal(401)
  157. }
  158. for (let i = 0; i < RATE_LIMIT / 2; i++) {
  159. const statusCode = await tryLogin(i, 'X') // upper
  160. expect(statusCode).to.equal(401)
  161. }
  162. // combined, they exceed the rate-limit
  163. for (let i = 0; i < 10; i++) {
  164. const statusCode = await tryLogin(i, 'x')
  165. expect(statusCode).to.equal(429)
  166. }
  167. })
  168. it('should return 400 with bad email (missing @)', async function () {
  169. const { statusCode, body } = await tryLoginWithEmail('foo')
  170. expect(statusCode).to.equal(400)
  171. expect(body).to.deep.equal({
  172. name: 'ZodValidationError',
  173. details: [
  174. { code: 'custom', path: [], message: 'Invalid email address' },
  175. ],
  176. statusCode: 400,
  177. })
  178. })
  179. it('should return 400 with bad email (number)', async function () {
  180. const { statusCode, body } = await tryLoginWithEmail(1)
  181. expect(statusCode).to.equal(400)
  182. expect(body).to.deep.equal({
  183. name: 'ZodValidationError',
  184. details: [
  185. {
  186. expected: 'string',
  187. code: 'invalid_type',
  188. path: [],
  189. message: 'Invalid input: expected string, received number',
  190. },
  191. ],
  192. statusCode: 400,
  193. })
  194. })
  195. })
  196. })