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 filesParents
49e58e0Changed 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
})