RegistrationTests.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { expect } from 'chai'
  2. import metrics from './helpers/metrics.mjs'
  3. import User from './helpers/User.mjs'
  4. import redis from './helpers/redis.mjs'
  5. import Features from '../../../app/src/infrastructure/Features.mjs'
  6. const UserPromises = User.promises
  7. // Expectations
  8. async function expectProjectAccess(user, projectId) {
  9. // should have access to project
  10. await user.openProject(projectId)
  11. }
  12. async function expectNoProjectAccess(user, projectId) {
  13. // should not have access to project page
  14. let error = null
  15. try {
  16. await user.openProject(projectId)
  17. } catch (err) {
  18. error = err
  19. }
  20. expect(error).to.be.instanceof(Error)
  21. }
  22. // Actions
  23. async function tryLoginThroughRegistrationForm(user, email, password) {
  24. await user.getCsrfToken()
  25. return await user.doRequest('POST', {
  26. url: '/register',
  27. json: { email, password },
  28. })
  29. }
  30. describe('Registration', function () {
  31. describe('LoginRateLimit', function () {
  32. let userA
  33. beforeEach(function () {
  34. userA = new UserPromises()
  35. })
  36. function loginRateLimited(line) {
  37. return line.includes('rate_limit_hit') && line.includes('login')
  38. }
  39. async function getLoginRateLimitHitMetricValue() {
  40. return await metrics.promises.getMetric(loginRateLimited)
  41. }
  42. let beforeCount
  43. beforeEach('get baseline metric value', async function () {
  44. beforeCount = await getLoginRateLimitHitMetricValue()
  45. })
  46. beforeEach('setup csrf token', async function () {
  47. await userA.getCsrfToken()
  48. })
  49. describe('pushing an account just below the rate limit', function () {
  50. async function doLoginAttempts(user, n, pushInto) {
  51. while (n--) {
  52. const { body } = await user.doRequest('POST', {
  53. url: '/login',
  54. json: {
  55. email: user.email,
  56. password: 'invalid-password',
  57. 'g-recaptcha-response': 'valid',
  58. },
  59. })
  60. const message = body && body.message && body.message.key
  61. pushInto.push(message)
  62. }
  63. }
  64. let results = []
  65. beforeEach('do 9 login attempts', async function () {
  66. results = []
  67. await doLoginAttempts(userA, 9, results)
  68. })
  69. it('should not record any rate limited requests', async function () {
  70. const afterCount = await getLoginRateLimitHitMetricValue()
  71. expect(afterCount).to.equal(beforeCount)
  72. })
  73. it('should produce the correct responses so far', function () {
  74. expect(results.length).to.equal(9)
  75. expect(results).to.deep.equal(
  76. Array(9).fill('invalid-password-retry-or-reset')
  77. )
  78. })
  79. describe('pushing the account past the limit', function () {
  80. beforeEach('do 6 login attempts', async function () {
  81. await doLoginAttempts(userA, 6, results)
  82. })
  83. it('should record 5 rate limited requests', async function () {
  84. const afterCount = await getLoginRateLimitHitMetricValue()
  85. expect(afterCount).to.equal(beforeCount + 5)
  86. })
  87. it('should produce the correct responses', function () {
  88. expect(results.length).to.equal(15)
  89. expect(results).to.deep.equal(
  90. Array(10)
  91. .fill('invalid-password-retry-or-reset')
  92. .concat(Array(5).fill('too-many-login-requests-2-mins'))
  93. )
  94. })
  95. describe('logging in with another user', function () {
  96. let userB
  97. beforeEach(function () {
  98. userB = new UserPromises()
  99. })
  100. beforeEach('update baseline metric value', async function () {
  101. beforeCount = await getLoginRateLimitHitMetricValue()
  102. })
  103. beforeEach('setup csrf token', async function () {
  104. await userB.getCsrfToken()
  105. })
  106. let messages = []
  107. beforeEach('do bad login', async function () {
  108. messages = []
  109. await doLoginAttempts(userB, 1, messages)
  110. })
  111. it('should not rate limit their request', function () {
  112. expect(messages).to.deep.equal(['invalid-password-retry-or-reset'])
  113. })
  114. it('should not record any further rate limited requests', async function () {
  115. const afterCount = await getLoginRateLimitHitMetricValue()
  116. expect(afterCount).to.equal(beforeCount)
  117. })
  118. })
  119. })
  120. describe('performing a valid login for clearing the limit', function () {
  121. beforeEach('do login', async function () {
  122. await userA.login()
  123. })
  124. it('should log the user in', async function () {
  125. const { response } = await userA.doRequest('GET', '/project')
  126. expect(response.statusCode).to.equal(200)
  127. })
  128. it('should not record any rate limited requests', async function () {
  129. const afterCount = await getLoginRateLimitHitMetricValue()
  130. expect(afterCount).to.equal(beforeCount)
  131. })
  132. describe('logging out and performing more invalid login requests', function () {
  133. beforeEach('logout', async function () {
  134. await userA.logout()
  135. })
  136. beforeEach('fetch new csrf token', async function () {
  137. await userA.getCsrfToken()
  138. })
  139. let results = []
  140. beforeEach('do 9 login attempts', async function () {
  141. results = []
  142. await doLoginAttempts(userA, 9, results)
  143. })
  144. it('should not record any rate limited requests yet', async function () {
  145. const afterCount = await getLoginRateLimitHitMetricValue()
  146. expect(afterCount).to.equal(beforeCount)
  147. })
  148. it('should not emit any rate limited responses yet', function () {
  149. expect(results.length).to.equal(9)
  150. expect(results).to.deep.equal(
  151. Array(9).fill('invalid-password-retry-or-reset')
  152. )
  153. })
  154. })
  155. })
  156. })
  157. })
  158. describe('CSRF protection', function () {
  159. before(function () {
  160. if (!Features.hasFeature('registration')) {
  161. this.skip()
  162. }
  163. })
  164. beforeEach(function () {
  165. this.user = new UserPromises()
  166. this.email = `test+${Math.random()}@example.com`
  167. this.password = 'password11'
  168. })
  169. afterEach(async function () {
  170. await this.user.fullDeleteUser(this.email)
  171. })
  172. it('should register with the csrf token', async function () {
  173. await this.user.request.get('/login')
  174. await this.user.getCsrfToken()
  175. const { response } = await this.user.doRequest('POST', {
  176. url: '/register',
  177. json: {
  178. email: this.email,
  179. password: this.password,
  180. },
  181. headers: {
  182. 'x-csrf-token': this.user.csrfToken,
  183. },
  184. })
  185. expect(response.statusCode).to.equal(200)
  186. })
  187. it('should fail with no csrf token', async function () {
  188. await this.user.request.get('/login')
  189. await this.user.getCsrfToken()
  190. const { response } = await this.user.doRequest('POST', {
  191. url: '/register',
  192. json: {
  193. email: this.email,
  194. password: this.password,
  195. },
  196. headers: {
  197. 'x-csrf-token': '',
  198. },
  199. })
  200. expect(response.statusCode).to.equal(403)
  201. })
  202. it('should fail with a stale csrf token', async function () {
  203. await this.user.request.get('/login')
  204. await this.user.getCsrfToken()
  205. const oldCsrfToken = this.user.csrfToken
  206. await this.user.logout()
  207. const { response } = await this.user.doRequest('POST', {
  208. url: '/register',
  209. json: {
  210. email: this.email,
  211. password: this.password,
  212. },
  213. headers: {
  214. 'x-csrf-token': oldCsrfToken,
  215. },
  216. })
  217. expect(response.statusCode).to.equal(403)
  218. })
  219. })
  220. describe('Register', function () {
  221. before(function () {
  222. if (!Features.hasFeature('registration')) {
  223. this.skip()
  224. }
  225. })
  226. beforeEach(function () {
  227. this.user = new UserPromises()
  228. })
  229. it('Set emails attribute', async function () {
  230. const user = await this.user.register()
  231. expect(user.email).to.equal(this.user.email)
  232. expect(user.emails).to.exist
  233. expect(user.emails).to.be.a('array')
  234. expect(user.emails.length).to.equal(1)
  235. expect(user.emails[0].email).to.equal(this.user.email)
  236. })
  237. })
  238. describe('LoginViaRegistration', function () {
  239. beforeEach(async function () {
  240. this.timeout(60000)
  241. this.user1 = new UserPromises()
  242. this.user2 = new UserPromises()
  243. await this.user1.login()
  244. await this.user1.logout()
  245. await redis.clearUserSessions(this.user1)
  246. await this.user2.login()
  247. await this.user2.logout()
  248. await redis.clearUserSessions(this.user2)
  249. this.project_id = null
  250. })
  251. describe('[Security] Trying to register/login as another user', function () {
  252. before(function () {
  253. if (!Features.hasFeature('registration')) {
  254. this.skip()
  255. }
  256. })
  257. it('should not allow sign in with secondary email', async function () {
  258. const secondaryEmail = 'acceptance-test-secondary@example.com'
  259. await this.user1.addEmail(secondaryEmail)
  260. let error = null
  261. try {
  262. await this.user1.loginWith(secondaryEmail)
  263. } catch (err) {
  264. error = err
  265. }
  266. expect(error).to.be.instanceOf(Error)
  267. expect(error.message).to.match(/login failed: status=401/)
  268. expect(error.info.body).to.deep.equal({
  269. message: {
  270. type: 'error',
  271. key: 'invalid-password-retry-or-reset',
  272. },
  273. })
  274. const isLoggedIn = await this.user1.isLoggedIn()
  275. expect(isLoggedIn).to.equal(false)
  276. })
  277. it('should have user1 login and create a project, which user2 cannot access', async function () {
  278. // user1 logs in and creates a project which only they can access
  279. await this.user1.login()
  280. const projectId = await this.user1.createProject('Private Project')
  281. await expectProjectAccess(this.user1, projectId)
  282. await expectNoProjectAccess(this.user2, projectId)
  283. // should prevent user2 from login/register with user1 email address
  284. const { body } = await tryLoginThroughRegistrationForm(
  285. this.user2,
  286. this.user1.email,
  287. 'totally_not_the_right_password'
  288. )
  289. expect(body.redir != null).to.equal(false)
  290. expect(body.message != null).to.equal(true)
  291. expect(body.message).to.have.all.keys('type', 'text')
  292. expect(body.message.type).to.equal('error')
  293. // check user still can't access the project
  294. await expectNoProjectAccess(this.user2, projectId)
  295. })
  296. })
  297. })
  298. })