@@ -46,6 +46,36 @@ 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+ /**
50+ * Parse PostgreSQL interval string to milliseconds.
51+ * Supports: '10 minutes', '1 hour', '00:10:00' (HH:MM:SS), etc.
52+ */
53+ function parseIntervalToMs ( interval : string | null | undefined ) : number {
54+ if ( ! interval ) return DEFAULT_OAUTH_STATE_MAX_AGE ;
55+
56+ // Handle HH:MM:SS format (PostgreSQL default interval output)
57+ const hhmmss = interval . match ( / ^ ( \d + ) : ( \d + ) : ( \d + ) $ / ) ;
58+ if ( hhmmss ) {
59+ const hours = parseInt ( hhmmss [ 1 ] , 10 ) ;
60+ const minutes = parseInt ( hhmmss [ 2 ] , 10 ) ;
61+ const seconds = parseInt ( hhmmss [ 3 ] , 10 ) ;
62+ return ( hours * 3600 + minutes * 60 + seconds ) * 1000 ;
63+ }
64+
65+ // Handle "N unit" format (e.g., "10 minutes")
66+ 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) ;
67+ if ( ! match ) return DEFAULT_OAUTH_STATE_MAX_AGE ;
68+ const value = parseInt ( match [ 1 ] , 10 ) ;
69+ const unit = match [ 2 ] . toLowerCase ( ) ;
70+ const multipliers : Record < string , number > = {
71+ second : 1000 ,
72+ minute : 60 * 1000 ,
73+ hour : 60 * 60 * 1000 ,
74+ day : 24 * 60 * 60 * 1000 ,
75+ } ;
76+ return value * ( multipliers [ unit ] || 60 * 1000 ) ;
77+ }
78+
4979// =============================================================================
5080// Signed State Utilities
5181// =============================================================================
@@ -376,28 +406,32 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
376406 }
377407
378408 const providerConfig = await getIdentityProvider ( ctx , modules , provider ) ;
409+ const { authSettings } = modules ;
410+ const errorRedirectPath =
411+ authSettings ?. oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH ;
412+
379413 if ( ! providerConfig ) {
380414 log . warn ( `[oauth] Provider ${ provider } not found or not configured` ) ;
381415 return redirectToError (
382416 res ,
383417 baseUrl ,
384- DEFAULT_ERROR_REDIRECT_PATH ,
418+ errorRedirectPath ,
385419 'PROVIDER_NOT_CONFIGURED' ,
386420 provider ,
387421 ) ;
388422 }
389423
390- const stateMaxAge = DEFAULT_OAUTH_STATE_MAX_AGE ;
424+ const stateMaxAge = parseIntervalToMs ( authSettings ?. oauthStateMaxAge ) ;
391425 const state = createSignedState (
392426 { redirect_uri : redirectUri , provider } ,
393427 stateMaxAge ,
394428 ) ;
395429
396430 res . cookie ( OAUTH_STATE_COOKIE , state , {
397- httpOnly : true ,
398- secure : isProduction ,
431+ httpOnly : authSettings ?. cookieHttponly ?? true ,
432+ secure : authSettings ?. cookieSecure ?? isProduction ,
399433 maxAge : stateMaxAge ,
400- sameSite : 'lax' ,
434+ sameSite : ( authSettings ?. cookieSamesite as 'lax' | 'strict' | 'none' ) ?? 'lax' ,
401435 } ) ;
402436
403437 const client = createOAuthClientForProvider ( providerConfig , baseUrl ) ;
@@ -485,8 +519,9 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
485519 ) ;
486520 }
487521
522+ let modules : OAuthModules | null = null ;
488523 try {
489- const modules = await resolveOAuthModules ( ctx ) ;
524+ modules = await resolveOAuthModules ( ctx ) ;
490525 if ( ! modules ) {
491526 log . error (
492527 `[oauth] Required modules not provisioned for ${ provider } ` ,
@@ -500,6 +535,12 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
500535 ) ;
501536 }
502537
538+ const { authSettings } = modules ;
539+ const errorRedirectPath =
540+ authSettings ?. oauthErrorRedirectPath || DEFAULT_ERROR_REDIRECT_PATH ;
541+ const requireVerifiedEmail =
542+ authSettings ?. oauthRequireVerifiedEmail ?? true ;
543+
503544 const providerConfig = await getIdentityProvider (
504545 ctx ,
505546 modules ,
@@ -510,7 +551,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
510551 return redirectToError (
511552 res ,
512553 baseUrl ,
513- DEFAULT_ERROR_REDIRECT_PATH ,
554+ errorRedirectPath ,
514555 'PROVIDER_NOT_CONFIGURED' ,
515556 provider ,
516557 ) ;
@@ -591,7 +632,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
591632 `[oauth] Account not found for ${ profile . email } , attempting signup` ,
592633 ) ;
593634
594- if ( ! emailVerified ) {
635+ if ( requireVerifiedEmail && ! emailVerified ) {
595636 log . warn (
596637 `[oauth] Rejecting unverified email for signup: ${ profile . email } ` ,
597638 ) ;
@@ -623,7 +664,7 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
623664 return redirectToError (
624665 res ,
625666 baseUrl ,
626- DEFAULT_ERROR_REDIRECT_PATH ,
667+ errorRedirectPath ,
627668 'EMAIL_NOT_VERIFIED' ,
628669 provider ,
629670 ) ;
@@ -677,13 +718,10 @@ export function createOAuthRoutes(_opts: ConstructiveOptions): Router {
677718 }
678719 } catch ( error : any ) {
679720 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- ) ;
721+ const fallbackPath =
722+ modules ?. authSettings ?. oauthErrorRedirectPath ||
723+ DEFAULT_ERROR_REDIRECT_PATH ;
724+ redirectToError ( res , baseUrl , fallbackPath , 'CALLBACK_FAILED' , provider ) ;
687725 }
688726 } ,
689727 ) ;
0 commit comments