eletrotupi / tcc/ commit / 1c52a0b

api/auth: enforce the activation code expiry

Verification now reads only unexpired codes, so a code emailed weeks ago
no longer activates an account. An expired code is indistinguishable
from one that never existed, and a code stored without an expiry is
refused too
Pedro Lucas Porcellis porcellis@eletrotupi.com · 25 days ago 1c52a0ba618ba144fac9b1a112e5c5cc1b248571 Browse files
Parents 49e58e0
Changed files
2
Additions
+56
Deletions
-1
api/src/services/user.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { User } from '@prisma/client';
3 3
import bcrypt from 'bcryptjs';
4 4

5 5
import {
35 35
};
36 36

37 37
export const findUserByActivationCode = async(code: string): Promise<User | null> => {
38 38
  return await prisma.user.findUnique({
39 39
    where: {
40 -
      activationCode: code
40 +
      activationCode: code,
41 +
      activationCodeExpiresAt: { gt: new Date() }
41 42
    }
42 43
  });
43 44
}
44 45

45 46
export const updateUser = async (input: UpdateUserInput): Promise<User> => {
65 66
        not: null
66 67
      }
67 68
    }
68 69
  });
69 70
}
api/tests/auth/activate.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import {
5 5
  buildPrisma,
42 42
    expect(res.body.user.activationCode).toBeNull()
43 43

44 44
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
45 45
    expect(dbUser!.active).toBe(true)
46 46
    expect(dbUser!.activationCode).toBeNull()
47 +
    expect(dbUser!.activationCodeExpiresAt).toBeNull()
47 48
  })
48 49

49 50
  it('activates an account whose code begins with zero', async () => {
50 51
    const user = await createTestUser(db, { email: 'leading-zero@example.com' })
51 52
    await seedActivationCode(user.id, '012345')
58 59
    expect(res.body.user.active).toBe(true)
59 60

60 61
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
61 62
    expect(dbUser!.active).toBe(true)
62 63
    expect(dbUser!.activationCode).toBeNull()
64 +
  })
65 +

66 +
  it('rejects a correctly issued code past its expiry', async () => {
67 +
    const user = await createTestUser(db, { email: 'expired@example.com' })
68 +
    await seedActivationCode(user.id, '123456', -1000)
69 +

70 +
    const res = await request(app)
71 +
      .post('/auth/activate')
72 +
      .send({ code: '123456' })
73 +

74 +
    expect(res.status).toBe(422)
75 +
    expect(res.body.error).toBeTruthy()
76 +
  })
77 +

78 +
  it('rejects a code recorded without an expiry', async () => {
79 +
    const user = await createTestUser(db, { email: 'no-expiry@example.com' })
80 +
    await db.user.update({
81 +
      where: { id: user.id },
82 +
      data: { activationCode: '123456', activationCodeExpiresAt: null },
83 +
    })
84 +

85 +
    const res = await request(app)
86 +
      .post('/auth/activate')
87 +
      .send({ code: '123456' })
88 +

89 +
    expect(res.status).toBe(422)
63 90
  })
64 91

65 92
  it('returns 422 for a well-formed but unknown code', async () => {
66 93
    await createTestUser(db, { email: 'activate@example.com' })
67 94

69 96
      .post('/auth/activate')
70 97
      .send({ code: '999999' })
71 98

72 99
    expect(res.status).toBe(422)
73 100
    expect(res.body.error).toBeTruthy()
101 +
  })
102 +

103 +
  it('cannot tell an expired code from an unknown one', async () => {
104 +
    const user = await createTestUser(db, { email: 'indistinct@example.com' })
105 +
    await seedActivationCode(user.id, '123456', -1000)
106 +

107 +
    const expired = await request(app)
108 +
      .post('/auth/activate')
109 +
      .send({ code: '123456' })
110 +

111 +
    const unknown = await request(app)
112 +
      .post('/auth/activate')
113 +
      .send({ code: '654321' })
114 +

115 +
    expect(expired.status).toBe(unknown.status)
116 +
    expect(expired.body).toEqual(unknown.body)
117 +
  })
118 +

119 +
  it('leaves the stored code in place when an expired code is rejected', async () => {
120 +
    const user = await createTestUser(db, { email: 'no-reissue@example.com' })
121 +
    await seedActivationCode(user.id, '123456', -1000)
122 +

123 +
    await request(app).post('/auth/activate').send({ code: '123456' })
124 +

125 +
    const dbUser = await db.user.findUnique({ where: { id: user.id } })
126 +
    expect(dbUser!.active).toBe(false)
127 +
    expect(dbUser!.activationCode).toBe('123456')
74 128
  })
75 129

76 130
  it.each([
77 131
    ['too short', '12345'],
78 132
    ['too long', '1234567'],
109 163
  it('returns 401 without a token', async () => {
110 164
    const res = await request(app).post('/auth/resend_code')
111 165
    expect(res.status).toBe(401)
112 166
  })
113 167
})