-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathoauth-exchange-state.ts
More file actions
88 lines (78 loc) · 2.43 KB
/
oauth-exchange-state.ts
File metadata and controls
88 lines (78 loc) · 2.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { and, eq, inArray, isNull } from 'drizzle-orm'
import type { FastifyReply, FastifyRequest } from 'fastify'
import type { Verification } from '../db/schema/index.js'
import { verification } from '../db/schema/index.js'
type Db = Awaited<ReturnType<typeof import('../db/index.js').getDb>>
export type ValidateOAuthStateResult =
| { ok: true; isLinkMode: boolean; linkUserId?: string; stateRecord: Verification }
| { ok: false }
export async function validateAndConsumeOAuthState({
db,
stateHash,
request,
reply,
preConsumeCheck,
}: {
db: Db
stateHash: string
request: FastifyRequest
reply: FastifyReply
preConsumeCheck?: (stateRecord: Verification) => { code: string; message: string } | null
}): Promise<ValidateOAuthStateResult> {
const [stateRecord] = await db
.select()
.from(verification)
.where(
and(
eq(verification.value, stateHash),
inArray(verification.type, ['oauth_state', 'oauth_link_state']),
isNull(verification.consumedAt),
),
)
if (!stateRecord) {
reply.code(401).send({ code: 'INVALID_STATE', message: 'Invalid or expired state' })
return { ok: false }
}
if (stateRecord.expiresAt < new Date()) {
await db.delete(verification).where(eq(verification.id, stateRecord.id))
reply.code(401).send({ code: 'EXPIRED_STATE', message: 'State has expired' })
return { ok: false }
}
const isLinkMode = stateRecord.type === 'oauth_link_state'
const linkUserId = stateRecord.meta?.userId
if (preConsumeCheck) {
const err = preConsumeCheck(stateRecord)
if (err) {
reply.code(401).send(err)
return { ok: false }
}
}
if (isLinkMode) {
if (!linkUserId) {
reply.code(401).send({ code: 'INVALID_STATE', message: 'Invalid link state' })
return { ok: false }
}
if (!request.session || request.session.user.id !== linkUserId) {
reply.code(401).send({
code: 'INVALID_STATE',
message: 'Session required for account linking',
})
return { ok: false }
}
}
const consumed = await db
.update(verification)
.set({ consumedAt: new Date() })
.where(and(eq(verification.id, stateRecord.id), isNull(verification.consumedAt)))
.returning()
if (consumed.length === 0) {
reply.code(401).send({ code: 'INVALID_STATE', message: 'Invalid or expired state' })
return { ok: false }
}
return {
ok: true,
isLinkMode,
linkUserId,
stateRecord,
}
}