@@ -46,6 +46,59 @@ const OAUTH_STATE_COOKIE = 'oauth_state';
4646const DEFAULT_OAUTH_STATE_MAX_AGE = 10 * 60 * 1000 ; // 10 minutes
4747const DEFAULT_ERROR_REDIRECT_PATH = '/auth/error' ;
4848
49+ interface PgInterval {
50+ years ?: number ;
51+ months ?: number ;
52+ days ?: number ;
53+ hours ?: number ;
54+ minutes ?: number ;
55+ seconds ?: number ;
56+ milliseconds ?: number ;
57+ }
58+
59+ /**
60+ * Parse PostgreSQL interval to milliseconds.
61+ * Handles: pg library object {minutes: 10}, string '10 minutes', '00:10:00'
62+ */
63+ function parseIntervalToMs (
64+ interval : string | PgInterval | null | undefined ,
65+ ) : number {
66+ if ( ! interval ) return DEFAULT_OAUTH_STATE_MAX_AGE ;
67+
68+ // Handle pg library interval object (e.g., {minutes: 10})
69+ if ( typeof interval === 'object' ) {
70+ const ms =
71+ ( interval . days || 0 ) * 24 * 60 * 60 * 1000 +
72+ ( interval . hours || 0 ) * 60 * 60 * 1000 +
73+ ( interval . minutes || 0 ) * 60 * 1000 +
74+ ( interval . seconds || 0 ) * 1000 +
75+ ( interval . milliseconds || 0 ) ;
76+ return ms || DEFAULT_OAUTH_STATE_MAX_AGE ;
77+ }
78+
79+ // Handle HH:MM:SS format (PostgreSQL default interval output)
80+ const hhmmss = interval . match ( / ^ ( \d + ) : ( \d + ) : ( \d + ) $ / ) ;
81+ if ( hhmmss ) {
82+ const hours = parseInt ( hhmmss [ 1 ] , 10 ) ;
83+ const minutes = parseInt ( hhmmss [ 2 ] , 10 ) ;
84+ const seconds = parseInt ( hhmmss [ 3 ] , 10 ) ;
85+ return ( hours * 3600 + minutes * 60 + seconds ) * 1000 ;
86+ }
87+
88+ // Handle "N unit" format (e.g., "10 minutes")
89+ const match = interval . match ( / ^ ( \d + ) \s * ( s e c o n d | m i n u t e | h o u r | d a y ) s ? $ / i) ;
90+ if ( ! match ) return DEFAULT_OAUTH_STATE_MAX_AGE ;
91+ const value = parseInt ( match [ 1 ] , 10 ) ;
92+ const unit = match [ 2 ] . toLowerCase ( ) ;
93+ const multipliers : Record < string , number > = {
94+ second : 1000 ,
95+ minute : 60 * 1000 ,
96+ hour : 60 * 60 * 1000 ,
97+ day : 24 * 60 * 60 * 1000 ,
98+ } ;
99+ return value * ( multipliers [ unit ] || 60 * 1000 ) ;
100+ }
101+
49102// =============================================================================
50103// Signed State Utilities
51104// =============================================================================
@@ -376,28 +429,32 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
376429 }
377430
378431 const providerConfig = await getIdentityProvider ( ctx , modules , provider ) ;
432+ const { authSettings } = modules ;
433+ const errorRedirectPath =
434+ authSettings ?. oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH ;
435+
379436 if ( ! providerConfig ) {
380437 log . warn ( `[oauth] Provider ${ provider } not found or not configured` ) ;
381438 return redirectToError (
382439 res ,
383440 baseUrl ,
384- DEFAULT_ERROR_REDIRECT_PATH ,
441+ errorRedirectPath ,
385442 'PROVIDER_NOT_CONFIGURED' ,
386443 provider ,
387444 ) ;
388445 }
389446
390- const stateMaxAge = DEFAULT_OAUTH_STATE_MAX_AGE ;
447+ const stateMaxAge = parseIntervalToMs ( authSettings ?. oauthStateMaxAge ) ;
391448 const state = createSignedState (
392449 { redirect_uri : redirectUri , provider } ,
393450 stateMaxAge ,
394451 ) ;
395452
396453 res . cookie ( OAUTH_STATE_COOKIE , state , {
397- httpOnly : true ,
398- secure : isProduction ,
454+ httpOnly : authSettings ?. cookieHttponly ?? true ,
455+ secure : authSettings ?. cookieSecure ?? isProduction ,
399456 maxAge : stateMaxAge ,
400- sameSite : 'lax' ,
457+ sameSite : ( authSettings ?. cookieSamesite as 'lax' | 'strict' | 'none' ) ?? 'lax' ,
401458 } ) ;
402459
403460 const client = createOAuthClientForProvider ( providerConfig , baseUrl ) ;
@@ -485,8 +542,9 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
485542 ) ;
486543 }
487544
545+ let modules : OAuthModules | null = null ;
488546 try {
489- const modules = await resolveOAuthModules ( ctx ) ;
547+ modules = await resolveOAuthModules ( ctx ) ;
490548 if ( ! modules ) {
491549 log . error (
492550 `[oauth] Required modules not provisioned for ${ provider } ` ,
@@ -500,6 +558,12 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
500558 ) ;
501559 }
502560
561+ const { authSettings } = modules ;
562+ const errorRedirectPath =
563+ authSettings ?. oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH ;
564+ const requireVerifiedEmail =
565+ authSettings ?. oauthRequireVerifiedEmail ?? true ;
566+
503567 const providerConfig = await getIdentityProvider (
504568 ctx ,
505569 modules ,
@@ -510,7 +574,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
510574 return redirectToError (
511575 res ,
512576 baseUrl ,
513- DEFAULT_ERROR_REDIRECT_PATH ,
577+ errorRedirectPath ,
514578 'PROVIDER_NOT_CONFIGURED' ,
515579 provider ,
516580 ) ;
@@ -591,7 +655,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
591655 `[oauth] Account not found for ${ profile . email } , attempting signup` ,
592656 ) ;
593657
594- if ( ! emailVerified ) {
658+ if ( requireVerifiedEmail && ! emailVerified ) {
595659 log . warn (
596660 `[oauth] Rejecting unverified email for signup: ${ profile . email } ` ,
597661 ) ;
@@ -623,7 +687,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
623687 return redirectToError (
624688 res ,
625689 baseUrl ,
626- DEFAULT_ERROR_REDIRECT_PATH ,
690+ errorRedirectPath ,
627691 'EMAIL_NOT_VERIFIED' ,
628692 provider ,
629693 ) ;
@@ -677,13 +741,10 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
677741 }
678742 } catch ( error : any ) {
679743 log . error ( `[oauth] Callback failed for ${ provider } :` , error ) ;
680- redirectToError (
681- res ,
682- baseUrl ,
683- DEFAULT_ERROR_REDIRECT_PATH ,
684- 'CALLBACK_FAILED' ,
685- provider ,
686- ) ;
744+ const fallbackPath =
745+ modules ?. authSettings ?. oauthErrorRedirectPath ||
746+ DEFAULT_ERROR_REDIRECT_PATH ;
747+ redirectToError ( res , baseUrl , fallbackPath , 'CALLBACK_FAILED' , provider ) ;
687748 }
688749 } ,
689750 ) ;
0 commit comments