@@ -66,6 +66,19 @@ const secure = z
6666. default ( ! ( dev || config . ALLOW_INSECURE_COOKIES === "true" ) )
6767. parse ( config . COOKIE_SECURE === "" ?undefined :config . COOKIE_SECURE === "true" ) ;
6868
69+ function sanitizeReturnPath ( path :string | undefined | null ) :string | undefined {
70+ if ( ! path ) {
71+ return undefined ;
72+ }
73+ if ( path . startsWith ( "//" ) ) {
74+ return undefined ;
75+ }
76+ if ( ! path . startsWith ( "/" ) ) {
77+ return undefined ;
78+ }
79+ return path ;
80+ }
81+
6982export function refreshSessionCookie ( cookies :Cookies , sessionId :string ) {
7083cookies . set ( config . COOKIE_NAME , sessionId , {
7184path :"/" ,
@@ -197,10 +210,20 @@ export function tokenSetToSessionOauth(tokenSet: TokenSet): Session["oauth"] {
197210/**
198211 * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough.
199212 */
200- export async function generateCsrfToken ( sessionId :string , redirectUrl :string ) :Promise < string > {
213+ export async function generateCsrfToken (
214+ sessionId :string ,
215+ redirectUrl :string ,
216+ next ?:string
217+ ) :Promise < string > {
218+ const sanitizedNext = sanitizeReturnPath ( next ) ;
201219const data = {
202220expiration :addHours ( new Date ( ) , 1 ) . getTime ( ) ,
203221redirectUrl,
222+ ...( sanitizedNext ?{ next :sanitizedNext } :{ } ) ,
223+ } as {
224+ expiration :number ;
225+ redirectUrl :string ;
226+ next ?:string ;
204227} ;
205228
206229return Buffer . from (
@@ -249,10 +272,14 @@ async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
249272
250273export async function getOIDCAuthorizationUrl (
251274settings :OIDCSettings ,
252- params :{ sessionId :string }
275+ params :{ sessionId :string ; next ?: string }
253276) :Promise < string > {
254277const client = await getOIDCClient ( settings ) ;
255- const csrfToken = await generateCsrfToken ( params . sessionId , settings . redirectURI ) ;
278+ const csrfToken = await generateCsrfToken (
279+ params . sessionId ,
280+ settings . redirectURI ,
281+ sanitizeReturnPath ( params . next )
282+ ) ;
256283
257284return client . authorizationUrl ( {
258285scope :OIDConfig . SCOPES ,
@@ -291,13 +318,16 @@ export async function validateAndParseCsrfToken(
291318) :Promise < {
292319/** This is the redirect url that was passed to the OIDC provider */
293320redirectUrl :string ;
321+ /** Relative path (within this app) to return to after login */
322+ next ?:string ;
294323} | null > {
295324try {
296325const { data, signature} = z
297326. object ( {
298327data :z . object ( {
299328expiration :z . number ( ) . int ( ) ,
300329redirectUrl :z . string ( ) . url ( ) ,
330+ next :z . string ( ) . optional ( ) ,
301331} ) ,
302332signature :z . string ( ) . length ( 64 ) ,
303333} )
@@ -306,7 +336,7 @@ export async function validateAndParseCsrfToken(
306336const reconstructSign = await sha256 ( JSON . stringify ( data ) + "##" + sessionId ) ;
307337
308338if ( data . expiration > Date . now ( ) && signature === reconstructSign ) {
309- return { redirectUrl :data . redirectUrl } ;
339+ return { redirectUrl :data . redirectUrl , next : sanitizeReturnPath ( data . next ) } ;
310340}
311341} catch ( e ) {
312342logger . error ( e ) ;
@@ -493,9 +523,23 @@ export async function triggerOauthFlow({
493523}
494524}
495525
526+ // Preserve a safe in-app return path after login.
527+ // Priority: explicit ?next=... (must be an absolute path), else the current path (when auto-login kicks in).
528+ let next :string | undefined = undefined ;
529+ const nextParam = sanitizeReturnPath ( url . searchParams . get ( "next" ) ) ;
530+ if ( nextParam ) {
531+ // Only accept absolute in-app paths to prevent open redirects
532+ next = nextParam ;
533+ } else if ( ! url . pathname . startsWith ( `${ base } /login` ) ) {
534+ // For automatic login on protected pages, return to the page the user was on
535+ next = sanitizeReturnPath ( `${ url . pathname } ${ url . search } ` ) ?? `${ base } /` ;
536+ } else {
537+ next = sanitizeReturnPath ( `${ base } /` ) ?? "/" ;
538+ }
539+
496540const authorizationUrl = await getOIDCAuthorizationUrl (
497541{ redirectURI} ,
498- { sessionId :locals . sessionId }
542+ { sessionId :locals . sessionId , next }
499543) ;
500544
501545throw redirect ( 302 , authorizationUrl ) ;