Skip to content

Commit 1346057

Browse files
committed
pivot: encode CSRF token in a state JWT for auth sync, and decode it to compare it with the POST sent CSRF token, to get the final JWT session
1 parent af2460b commit 1346057

File tree

2 files changed

+30
-14
lines changed

2 files changed

+30
-14
lines changed

middleware.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse, URLPattern } from 'next/server'
22
import { getDomainMapping } from '@/lib/domains'
33
import { SESSION_COOKIE, cookieOptions } from '@/lib/auth'
4+
import { encode } from 'next-auth/jwt'
45

56
const referrerPattern = new URLPattern({ pathname: ':pathname(*)/r/:referrer([\\w_]+)' })
67
const itemPattern = new URLPattern({ pathname: '/items/:id(\\d+){/:other(\\w+)}?' })
@@ -85,8 +86,13 @@ async function redirectToAuthSync (request, searchParams, domain, csrfToken, sig
8586

8687
const syncUrl = new URL('/api/auth/sync', SN_MAIN_DOMAIN)
8788
syncUrl.searchParams.set('domain', domain)
88-
// store the csrfToken in the search params
89-
syncUrl.searchParams.set('state', csrfToken)
89+
// encode the csrfToken as state JWT using NEXTAUTH_SECRET
90+
const randomState = await encode({
91+
token: { csrf: csrfToken },
92+
secret: process.env.NEXTAUTH_SECRET
93+
})
94+
// set the state in the search params
95+
syncUrl.searchParams.set('state', randomState)
9096

9197
// if we're signing up, we need to set the signup flag
9298
if (signup) {

pages/api/auth/sync.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Auth Sync API
22
import models from '@/api/models'
33
import { randomBytes } from 'node:crypto'
4-
import { encode as encodeJWT, getToken } from 'next-auth/jwt'
4+
import { encode as encodeJWT, decode, getToken } from 'next-auth/jwt'
55
import { validateSchema, customDomainSchema } from '@/lib/validate'
66

77
const SN_MAIN_DOMAIN = new URL(process.env.NEXT_PUBLIC_URL)
@@ -117,14 +117,14 @@ function handleNoSession (res, domainName, state, redirectUri, signup = false) {
117117
res.redirect(302, loginRedirectUrl.href)
118118
}
119119

120-
async function createVerificationToken (token, csrfToken) {
120+
async function createVerificationToken (token, state) {
121121
try {
122122
// a 5 minutes verification token using the session token's user id
123123
const verificationToken = await models.verificationToken.create({
124124
data: {
125125
identifier: token.id.toString(),
126-
// store csrf token with the verification token, to prevent CSRF attacks
127-
token: `${randomBytes(32).toString('hex')}|${csrfToken}`,
126+
// store encoded state JWT with the verification token to prevent CSRF attacks
127+
token: `${randomBytes(32).toString('hex')}|${state}`,
128128
expires: new Date(Date.now() + 1000 * 60 * 5) // 5 minutes
129129
}
130130
})
@@ -152,14 +152,15 @@ async function redirectToDomain (res, domainName, verificationToken, redirectUri
152152
}
153153

154154
async function consumeVerificationToken (verificationToken, csrfToken) {
155-
// sync tokens are stored as token|csrfToken
156-
const tokenWithState = `${verificationToken}|${csrfToken}`
155+
// sync tokens are stored as token
157156
try {
158157
// find and delete the verification token
159158
const identifier = await models.$transaction(async tx => {
160159
const token = await tx.verificationToken.findFirst({
161160
where: {
162-
token: tokenWithState,
161+
token: {
162+
startsWith: verificationToken
163+
},
163164
expires: { gt: new Date() }
164165
}
165166
})
@@ -168,12 +169,20 @@ async function consumeVerificationToken (verificationToken, csrfToken) {
168169
return null
169170
}
170171

171-
// delete the verification token, we don't need it anymore
172-
await tx.verificationToken.delete({
173-
where: {
174-
token: tokenWithState
175-
}
172+
// since we touched this token, we can delete it
173+
// so that if state is compromised, it's not used again
174+
await tx.verificationToken.delete({ where: { id: token.id } })
175+
176+
// check if the state is valid
177+
const encodedState = token.token.split('|')[1]
178+
const decodedState = await decode({
179+
token: encodedState,
180+
secret: process.env.NEXTAUTH_SECRET
176181
})
182+
// if the state is invalid, we can't use this token
183+
if (decodedState.csrf !== csrfToken) {
184+
return null
185+
}
177186

178187
return token.identifier
179188
})
@@ -186,6 +195,7 @@ async function consumeVerificationToken (verificationToken, csrfToken) {
186195
// return the user id
187196
return { status: 'OK', userId: Number(identifier) }
188197
} catch (error) {
198+
console.error('cannot validate verification token', error)
189199
return { status: 'ERROR', reason: 'cannot validate verification token' }
190200
}
191201
}

0 commit comments

Comments
 (0)