1
1
// Auth Sync API
2
2
import models from '@/api/models'
3
3
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'
5
5
import { validateSchema , customDomainSchema } from '@/lib/validate'
6
6
7
7
const SN_MAIN_DOMAIN = new URL ( process . env . NEXT_PUBLIC_URL )
@@ -117,14 +117,14 @@ function handleNoSession (res, domainName, state, redirectUri, signup = false) {
117
117
res . redirect ( 302 , loginRedirectUrl . href )
118
118
}
119
119
120
- async function createVerificationToken ( token , csrfToken ) {
120
+ async function createVerificationToken ( token , state ) {
121
121
try {
122
122
// a 5 minutes verification token using the session token's user id
123
123
const verificationToken = await models . verificationToken . create ( {
124
124
data : {
125
125
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 } ` ,
128
128
expires : new Date ( Date . now ( ) + 1000 * 60 * 5 ) // 5 minutes
129
129
}
130
130
} )
@@ -152,14 +152,15 @@ async function redirectToDomain (res, domainName, verificationToken, redirectUri
152
152
}
153
153
154
154
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
157
156
try {
158
157
// find and delete the verification token
159
158
const identifier = await models . $transaction ( async tx => {
160
159
const token = await tx . verificationToken . findFirst ( {
161
160
where : {
162
- token : tokenWithState ,
161
+ token : {
162
+ startsWith : verificationToken
163
+ } ,
163
164
expires : { gt : new Date ( ) }
164
165
}
165
166
} )
@@ -168,12 +169,20 @@ async function consumeVerificationToken (verificationToken, csrfToken) {
168
169
return null
169
170
}
170
171
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
176
181
} )
182
+ // if the state is invalid, we can't use this token
183
+ if ( decodedState . csrf !== csrfToken ) {
184
+ return null
185
+ }
177
186
178
187
return token . identifier
179
188
} )
@@ -186,6 +195,7 @@ async function consumeVerificationToken (verificationToken, csrfToken) {
186
195
// return the user id
187
196
return { status : 'OK' , userId : Number ( identifier ) }
188
197
} catch ( error ) {
198
+ console . error ( 'cannot validate verification token' , error )
189
199
return { status : 'ERROR' , reason : 'cannot validate verification token' }
190
200
}
191
201
}
0 commit comments