11import { usersCrudHandlers } from '@/app/api/latest/users/crud' ;
22import { globalPrismaClient } from '@/prisma-client' ;
3- import { Prisma } from '@prisma/client' ;
43import { KnownErrors } from '@stackframe/stack-shared' ;
54import { yupBoolean , yupNumber , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
5+ import { AccessTokenPayload } from '@stackframe/stack-shared/dist/sessions' ;
66import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto' ;
77import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env' ;
8- import { StackAssertionError , throwErr } from '@stackframe/stack-shared/dist/utils/errors' ;
8+ import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors' ;
99import { getPrivateJwks , getPublicJwkSet , signJWT , verifyJWT } from '@stackframe/stack-shared/dist/utils/jwt' ;
1010import { Result } from '@stackframe/stack-shared/dist/utils/results' ;
1111import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry' ;
1212import * as jose from 'jose' ;
1313import { JOSEError , JWTExpired } from 'jose/errors' ;
1414import { SystemEventTypes , logEvent } from './events' ;
1515import { Tenancy } from './tenancies' ;
16- import { AccessTokenPayload } from '@stackframe/stack-shared/dist/sessions' ;
1716
1817export const authorizationHeaderSchema = yupString ( ) . matches ( / ^ S t a c k S e s s i o n [ ^ ] + $ / ) ;
1918
@@ -116,25 +115,45 @@ export async function decodeAccessToken(accessToken: string, { allowAnonymous }:
116115 } ) ;
117116}
118117
119- export async function generateAccessToken ( options : {
118+ export async function isRefreshTokenValid ( options : {
120119 tenancy : Tenancy ,
121- userId : string ,
122- refreshTokenId : string ,
120+ refreshTokenObj : null | {
121+ projectUserId : string ,
122+ id : string ,
123+ expiresAt : Date | null ,
124+ } ,
123125} ) {
126+ return ! ! await generateAccessTokenFromRefreshTokenIfValid ( options ) ;
127+ }
128+
129+ export async function generateAccessTokenFromRefreshTokenIfValid ( options : {
130+ tenancy : Tenancy ,
131+ refreshTokenObj : null | {
132+ projectUserId : string ,
133+ id : string ,
134+ expiresAt : Date | null ,
135+ } ,
136+ } ) {
137+ if ( ! options . refreshTokenObj ) {
138+ return null ;
139+ }
140+
141+ if ( options . refreshTokenObj . expiresAt && options . refreshTokenObj . expiresAt < new Date ( ) ) {
142+ return null ;
143+ }
144+
124145 let user ;
125146 try {
126147 user = await usersCrudHandlers . adminRead ( {
127148 tenancy : options . tenancy ,
128- user_id : options . userId ,
149+ user_id : options . refreshTokenObj . projectUserId ,
129150 allowedErrorTypes : [ KnownErrors . UserNotFound ] ,
130151 } ) ;
131152 } catch ( error ) {
132153 if ( error instanceof KnownErrors . UserNotFound ) {
133- throw new StackAssertionError ( `User not found in generateAccessToken. Was the user's account deleted?` , {
134- userId : options . userId ,
135- refreshTokenId : options . refreshTokenId ,
136- tenancy : options . tenancy ,
137- } ) ;
154+ // The user was deleted — their refresh token still exists because we don't cascade deletes across source-of-truth/global tables.
155+ // => refresh token is invalid
156+ return null ;
138157 }
139158 throw error ;
140159 }
@@ -144,17 +163,17 @@ export async function generateAccessToken(options: {
144163 {
145164 projectId : options . tenancy . project . id ,
146165 branchId : options . tenancy . branchId ,
147- userId : options . userId ,
148- sessionId : options . refreshTokenId ,
166+ userId : options . refreshTokenObj . projectUserId ,
167+ sessionId : options . refreshTokenObj . id ,
149168 isAnonymous : user . is_anonymous ,
150169 }
151170 ) ;
152171
153172 const payload : Omit < AccessTokenPayload , "iss" | "aud" > = {
154- sub : options . userId ,
173+ sub : options . refreshTokenObj . projectUserId ,
155174 project_id : options . tenancy . project . id ,
156175 branch_id : options . tenancy . branchId ,
157- refresh_token_id : options . refreshTokenId ,
176+ refresh_token_id : options . refreshTokenObj . id ,
158177 role : 'authenticated' ,
159178 name : user . display_name ,
160179 email : user . primary_email ,
@@ -171,44 +190,39 @@ export async function generateAccessToken(options: {
171190 } ) ;
172191}
173192
174- export async function createAuthTokens ( options : {
193+ type CreateRefreshTokenOptions = {
175194 tenancy : Tenancy ,
176195 projectUserId : string ,
177196 expiresAt ?: Date ,
178197 isImpersonation ?: boolean ,
179- } ) {
198+ }
199+
200+ export async function createRefreshTokenObj ( options : CreateRefreshTokenOptions ) {
180201 options . expiresAt ??= new Date ( Date . now ( ) + 1000 * 60 * 60 * 24 * 365 ) ;
181202 options . isImpersonation ??= false ;
182203
183204 const refreshToken = generateSecureRandomString ( ) ;
184205
185- try {
186- const refreshTokenObj = await globalPrismaClient . projectUserRefreshToken . create ( {
187- data : {
188- tenancyId : options . tenancy . id ,
189- projectUserId : options . projectUserId ,
190- refreshToken : refreshToken ,
191- expiresAt : options . expiresAt ,
192- isImpersonation : options . isImpersonation ,
193- } ,
194- } ) ;
206+ const refreshTokenObj = await globalPrismaClient . projectUserRefreshToken . create ( {
207+ data : {
208+ tenancyId : options . tenancy . id ,
209+ projectUserId : options . projectUserId ,
210+ refreshToken : refreshToken ,
211+ expiresAt : options . expiresAt ,
212+ isImpersonation : options . isImpersonation ,
213+ } ,
214+ } ) ;
195215
196- const accessToken = await generateAccessToken ( {
197- tenancy : options . tenancy ,
198- userId : options . projectUserId ,
199- refreshTokenId : refreshTokenObj . id ,
200- } ) ;
216+ return refreshTokenObj ;
217+ }
201218
219+ export async function createAuthTokens ( options : CreateRefreshTokenOptions ) {
220+ const refreshTokenObj = await createRefreshTokenObj ( options ) ;
202221
203- return { refreshToken, accessToken } ;
222+ const accessToken = await generateAccessTokenFromRefreshTokenIfValid ( {
223+ tenancy : options . tenancy ,
224+ refreshTokenObj : refreshTokenObj ,
225+ } ) ;
204226
205- } catch ( error ) {
206- if ( error instanceof Prisma . PrismaClientKnownRequestError && error . code === 'P2003' ) {
207- throwErr ( new Error (
208- `Auth token creation failed for tenancyId ${ options . tenancy . id } and projectUserId ${ options . projectUserId } : ${ error . message } ` ,
209- { cause : error }
210- ) ) ;
211- }
212- throw error ;
213- }
227+ return { refreshToken : refreshTokenObj . refreshToken , accessToken } ;
214228}
0 commit comments