@@ -12,14 +12,14 @@ export default async function handler (req, res) {
12
12
// POST /api/auth/sync
13
13
// exchange a verification token for an ephemeral session token
14
14
if ( req . method === 'POST' ) {
15
- // a verification token is received from the middleware
16
- const { verificationToken } = req . body
17
- if ( ! verificationToken ) {
18
- return res . status ( 400 ) . json ( { status : 'ERROR' , reason : 'verification token is required' } )
15
+ // verification token and csrf token are received from the middleware
16
+ const { verificationToken, csrfToken } = req . body
17
+ if ( ! verificationToken || ! csrfToken ) {
18
+ return res . status ( 400 ) . json ( { status : 'ERROR' , reason : 'verification token and csrf token are required' } )
19
19
}
20
20
21
21
// validate and consume the verification token
22
- const validationResult = await consumeVerificationToken ( verificationToken )
22
+ const validationResult = await consumeVerificationToken ( verificationToken , csrfToken )
23
23
if ( validationResult . status === 'ERROR' ) {
24
24
return res . status ( 400 ) . json ( validationResult )
25
25
}
@@ -40,10 +40,10 @@ export default async function handler (req, res) {
40
40
// if there's a session, create a verification token and redirect to the domain
41
41
if ( req . method === 'GET' ) {
42
42
// STEP 1: check if the domain is correct
43
- const { domain, redirectUri , signup } = req . query
43
+ const { domain, state , signup, redirectUri } = req . query
44
44
// domain and a path redirectUri are required
45
- if ( ! domain || ! redirectUri ?. startsWith ( '/' ) ) {
46
- return res . status ( 400 ) . json ( { status : 'ERROR' , reason : 'domain and a correct redirectUri are required' } )
45
+ if ( ! domain || ! state || ! redirectUri ?. startsWith ( '/' ) ) {
46
+ return res . status ( 400 ) . json ( { status : 'ERROR' , reason : 'domain, unique state and a correct redirectUri are required' } )
47
47
}
48
48
49
49
// STEP 2: check if domain is valid and ACTIVE
@@ -54,18 +54,18 @@ export default async function handler (req, res) {
54
54
55
55
// if we're signing up, redirect to the SN signup page and come back here
56
56
if ( signup ) {
57
- return handleNoSession ( res , domain , redirectUri , signup )
57
+ return handleNoSession ( res , domain , state , redirectUri , signup )
58
58
}
59
59
60
60
// STEP 3: check if we have a session, if not, redirect to the SN login page
61
61
const sessionToken = await getToken ( { req } ) // from cookie
62
62
if ( ! sessionToken ) {
63
63
// we don't have a session, redirect to the login page and come back here
64
- return handleNoSession ( res , domain , redirectUri )
64
+ return handleNoSession ( res , domain , state , redirectUri )
65
65
}
66
66
67
67
// STEP 4: create a verification token
68
- const verificationToken = await createVerificationToken ( sessionToken )
68
+ const verificationToken = await createVerificationToken ( sessionToken , state )
69
69
if ( verificationToken . status === 'ERROR' ) {
70
70
return res . status ( 500 ) . json ( verificationToken )
71
71
}
@@ -74,6 +74,7 @@ export default async function handler (req, res) {
74
74
return redirectToDomain ( res , domain , verificationToken . token , redirectUri )
75
75
}
76
76
} catch ( error ) {
77
+ console . error ( 'auth sync broke its legs' , error )
77
78
return res . status ( 500 ) . json ( { status : 'ERROR' , reason : 'auth sync broke its legs' } )
78
79
}
79
80
}
@@ -99,10 +100,12 @@ async function isDomainAllowed (domainName) {
99
100
}
100
101
}
101
102
102
- function handleNoSession ( res , domainName , redirectUri , signup = false ) {
103
+ function handleNoSession ( res , domainName , state , redirectUri , signup = false ) {
103
104
// create the sync callback URL that we'll return to after login
104
105
const syncUrl = new URL ( '/api/auth/sync' , SN_MAIN_DOMAIN )
105
106
syncUrl . searchParams . set ( 'domain' , domainName )
107
+ // preserve the state from the original request
108
+ syncUrl . searchParams . set ( 'state' , state )
106
109
syncUrl . searchParams . set ( 'redirectUri' , redirectUri )
107
110
108
111
// create SN login URL and add our sync callback URL
@@ -114,13 +117,14 @@ function handleNoSession (res, domainName, redirectUri, signup = false) {
114
117
res . redirect ( 302 , loginRedirectUrl . href )
115
118
}
116
119
117
- async function createVerificationToken ( token ) {
120
+ async function createVerificationToken ( token , csrfToken ) {
118
121
try {
119
122
// a 5 minutes verification token using the session token's user id
120
123
const verificationToken = await models . verificationToken . create ( {
121
124
data : {
122
125
identifier : token . id . toString ( ) ,
123
- token : randomBytes ( 32 ) . toString ( 'hex' ) ,
126
+ // store csrf token with the verification token, to prevent CSRF attacks
127
+ token : `${ randomBytes ( 32 ) . toString ( 'hex' ) } |${ csrfToken } ` ,
124
128
expires : new Date ( Date . now ( ) + 1000 * 60 * 5 ) // 5 minutes
125
129
}
126
130
} )
@@ -136,8 +140,9 @@ async function redirectToDomain (res, domainName, verificationToken, redirectUri
136
140
const protocol = process . env . NODE_ENV === 'development' ? 'http' : 'https'
137
141
const target = new URL ( `${ protocol } ://${ domainName } ` )
138
142
139
- // add the verification token and the redirectUri to the URL
140
- target . searchParams . set ( 'token' , verificationToken )
143
+ // add the verification sync token and the redirectUri to the URL
144
+ target . searchParams . set ( 'synctoken' , verificationToken . split ( '|' ) [ 0 ] )
145
+ target . searchParams . set ( 'state' , verificationToken . split ( '|' ) [ 1 ] )
141
146
target . searchParams . set ( 'redirectUri' , redirectUri )
142
147
143
148
// redirect to the custom domain
@@ -147,27 +152,38 @@ async function redirectToDomain (res, domainName, verificationToken, redirectUri
147
152
}
148
153
}
149
154
150
- async function consumeVerificationToken ( verificationToken ) {
155
+ async function consumeVerificationToken ( verificationToken , csrfToken ) {
156
+ // sync tokens are stored as token|csrfToken
157
+ const tokenWithState = `${ verificationToken } |${ csrfToken } `
151
158
try {
152
- // find the verification token
153
- const { identifier } = await models . verificationToken . findFirst ( {
154
- where : {
155
- token : verificationToken ,
156
- expires : { gt : new Date ( ) }
159
+ // find and delete the verification token
160
+ const identifier = await models . $transaction ( async tx => {
161
+ const token = await tx . verificationToken . findFirst ( {
162
+ where : {
163
+ token : tokenWithState ,
164
+ expires : { gt : new Date ( ) }
165
+ }
166
+ } )
167
+
168
+ if ( ! token ?. identifier ) {
169
+ return null
157
170
}
171
+
172
+ // delete the verification token, we don't need it anymore
173
+ await tx . verificationToken . delete ( {
174
+ where : {
175
+ token : tokenWithState
176
+ }
177
+ } )
178
+
179
+ return token . identifier
158
180
} )
181
+
159
182
// if we can't find the verification token, it's invalid or expired
160
183
if ( ! identifier ) {
161
184
return { status : 'ERROR' , reason : 'invalid verification token' }
162
185
}
163
186
164
- // delete the verification token, we don't need it anymore
165
- await models . verificationToken . delete ( {
166
- where : {
167
- token : verificationToken
168
- }
169
- } )
170
-
171
187
// return the user id
172
188
return { status : 'OK' , userId : Number ( identifier ) }
173
189
} catch ( error ) {
0 commit comments