@@ -2,6 +2,7 @@ import _ from 'lodash';
22import { sql , Transaction } from 'kysely' ;
33import log from 'loglevel' ;
44import jwt from 'jsonwebtoken' ;
5+ import { randomBytes } from 'crypto' ;
56
67import { reportError } from './errors.ts'
78import * as auth0 from './auth0.ts' ;
@@ -184,10 +185,51 @@ export async function loginWithPasswordlessCode(email: string, code: string, use
184185 return auth0LoginResult ;
185186}
186187
187- export async function refreshToken ( refreshToken : string , userIp : string ) {
188- const auth0RefreshResult = await auth0 . refreshToken ( refreshToken , userIp ) ;
189- await pullUserIntoDBFromRefresh ( refreshToken , auth0RefreshResult , userIp ) ;
190- return auth0RefreshResult ;
188+ export function refreshToken ( refreshToken : string , userIp : string ) {
189+ return db . transaction ( ) . execute ( async ( trx ) => {
190+ // If we're already in the DB, we skip Auth0 entirely:
191+ if ( await doesRefreshTokenExist ( trx , refreshToken ) ) {
192+ const newAccessToken = `at-${ randomBytes ( 32 ) . toString ( 'hex' ) } ` ;
193+ const expiresAt = Date . now ( ) + ( 1000 * 60 * 60 * 24 ) ;
194+
195+ await Promise . all ( [
196+ trx . insertInto ( 'access_tokens' )
197+ . values ( {
198+ value : newAccessToken ,
199+ refresh_token : refreshToken ,
200+ expires_at : new Date ( expiresAt )
201+ } )
202+ . execute ( ) ,
203+ trx . updateTable ( 'refresh_tokens' )
204+ . set ( {
205+ last_used : new Date ( )
206+ } )
207+ . where ( 'value' , '=' , refreshToken )
208+ . execute ( )
209+ ] ) ;
210+
211+ return {
212+ accessToken : newAccessToken ,
213+ expiresAt
214+ } ;
215+ } else {
216+ // If not, we go to Auth0 as before, and then cache the result:
217+ const auth0RefreshResult = await auth0 . refreshToken ( refreshToken , userIp ) ;
218+ await pullUserIntoDBFromRefresh ( trx , refreshToken , auth0RefreshResult , userIp ) ;
219+ return {
220+ accessToken : auth0RefreshResult . access_token ! ,
221+ expiresAt : Date . now ( ) + auth0RefreshResult . expires_in ! * 1000
222+ } ;
223+ }
224+ } ) ;
225+ }
226+
227+ function doesRefreshTokenExist ( trx : Transaction < Database > , refreshToken : string ) {
228+ return trx . selectFrom ( 'refresh_tokens' )
229+ . where ( 'value' , '=' , refreshToken )
230+ . selectAll ( )
231+ . executeTakeFirst ( )
232+ . then ( ( rt ) => ! ! rt ) ;
191233}
192234
193235// On initial login or token refresh, we pull the user data from Auth0 en route, and migrate it
@@ -241,7 +283,7 @@ async function pullUserIntoDBFromLogin(tokens: TokenSet, userIp: string) {
241283 }
242284}
243285
244- async function pullUserIntoDBFromRefresh ( refreshToken : string , refreshResult : TokenSet , userIp : string ) {
286+ async function pullUserIntoDBFromRefresh ( trx : Transaction < Database > , refreshToken : string , refreshResult : TokenSet , userIp : string ) {
245287 try {
246288 // Insert refresh token, access token & user, if each is not already present. Go from the user down?
247289 // We need to do a query to get the user data from auth0, if we don't have it already... Awkward.
@@ -250,58 +292,38 @@ async function pullUserIntoDBFromRefresh(refreshToken: string, refreshResult: To
250292 throw new Error ( 'No access token present after refresh' ) ;
251293 }
252294
253- await db . transaction ( ) . execute ( async ( trx ) => {
254- const refreshTokenExists = await trx . selectFrom ( 'refresh_tokens' )
255- . where ( 'value' , '=' , refreshToken )
256- . selectAll ( )
257- . executeTakeFirst ( )
258- . then ( ( rt ) => ! ! rt ) ;
259-
260- if ( refreshTokenExists ) {
261- return trx . insertInto ( 'access_tokens' )
262- . values ( {
263- value : refreshResult . access_token ,
264- refresh_token : refreshToken ,
265- expires_at : new Date ( Date . now ( ) + ( refreshResult . expires_in * 1000 ) )
266- } )
267- . execute ( ) ;
268- }
269-
270- // If the refresh token didn't exist, we need to fetch the user data given the access token,
271- // and then create the RT to reference it (and maybe create the user too, if required).
272- const auth0User = await auth0 . getUserInfoFromToken ( refreshResult . access_token ) ;
273- if ( ! auth0User ?. sub || ! auth0User ?. email ) {
274- console . warn ( `Returned user data:` , auth0User ) ;
275- throw new Error ( 'Could not get user info from access token during refresh' ) ;
276- }
277-
278- // Create user (or just get id) in our DB, in case it doesn't exist:
279- const user = await trx . insertInto ( 'users' )
280- . values ( {
281- email : auth0User . email ,
282- auth0_user_id : auth0User . sub ,
283- last_ip : userIp ,
284- logins_count : 1 ,
285- app_metadata : { } ,
295+ const auth0User = await auth0 . getUserInfoFromToken ( refreshResult . access_token ) ;
296+ if ( ! auth0User ?. sub || ! auth0User ?. email ) {
297+ console . warn ( `Returned user data:` , auth0User ) ;
298+ throw new Error ( 'Could not get user info from access token during refresh' ) ;
299+ }
300+
301+ // Create user (or just get id) in our DB, in case it doesn't exist:
302+ const user = await trx . insertInto ( 'users' )
303+ . values ( {
304+ email : auth0User . email ,
305+ auth0_user_id : auth0User . sub ,
306+ last_ip : userIp ,
307+ logins_count : 1 ,
308+ app_metadata : { } ,
309+ } )
310+ . onConflict ( ( oc ) => oc
311+ . column ( 'auth0_user_id' )
312+ . doUpdateSet ( {
313+ last_ip : ( eb ) => eb . ref ( 'excluded.last_ip' )
286314 } )
287- . onConflict ( ( oc ) => oc
288- . column ( 'auth0_user_id' )
289- . doUpdateSet ( {
290- last_ip : ( eb ) => eb . ref ( 'excluded.last_ip' )
291- } )
292- )
293- . returning ( 'id' )
294- . executeTakeFirstOrThrow ( ) ;
295-
296- // Store the refresh & access tokens again with the full info this time round:
297- await storeRefreshAndAccessTokens (
298- trx ,
299- user . id ,
300- refreshToken ,
301- refreshResult . access_token ! ,
302- refreshResult . expires_in !
303- ) ;
304- } ) ;
315+ )
316+ . returning ( 'id' )
317+ . executeTakeFirstOrThrow ( ) ;
318+
319+ // Store the refresh & access tokens again with the full info this time round:
320+ await storeRefreshAndAccessTokens (
321+ trx ,
322+ user . id ,
323+ refreshToken ,
324+ refreshResult . access_token ! ,
325+ refreshResult . expires_in !
326+ ) ;
305327 } catch ( err : any ) {
306328 log . error ( 'Error pulling user into DB during token refresh:' , err ) ;
307329 reportError ( err ) ;
0 commit comments