@@ -4,14 +4,32 @@ import {
44 Injectable ,
55 UnauthorizedException ,
66} from '@nestjs/common' ;
7+ import { ConfigService } from '@nestjs/config' ;
78import { db } from '@trycompai/db' ;
89import { createRemoteJWKSet , jwtVerify } from 'jose' ;
910import { ApiKeyService } from './api-key.service' ;
11+ import type { BetterAuthConfig } from '../config/better-auth.config' ;
1012import { AuthenticatedRequest } from './types' ;
1113
1214@Injectable ( )
1315export class HybridAuthGuard implements CanActivate {
14- constructor ( private readonly apiKeyService : ApiKeyService ) { }
16+ private readonly betterAuthUrl : string ;
17+
18+ constructor (
19+ private readonly apiKeyService : ApiKeyService ,
20+ private readonly configService : ConfigService ,
21+ ) {
22+ const betterAuthConfig =
23+ this . configService . get < BetterAuthConfig > ( 'betterAuth' ) ;
24+ this . betterAuthUrl =
25+ betterAuthConfig ?. url || process . env . BETTER_AUTH_URL || '' ;
26+
27+ if ( ! this . betterAuthUrl ) {
28+ console . warn (
29+ '[HybridAuthGuard] BETTER_AUTH_URL not configured. JWT authentication will fail.' ,
30+ ) ;
31+ }
32+ }
1533
1634 async canActivate ( context : ExecutionContext ) : Promise < boolean > {
1735 const request = context . switchToHttp ( ) . getRequest < AuthenticatedRequest > ( ) ;
@@ -61,20 +79,71 @@ export class HybridAuthGuard implements CanActivate {
6179 authHeader : string ,
6280 ) : Promise < boolean > {
6381 try {
82+ // Validate BETTER_AUTH_URL is configured
83+ if ( ! this . betterAuthUrl ) {
84+ console . error (
85+ '[HybridAuthGuard] BETTER_AUTH_URL environment variable is not set' ,
86+ ) ;
87+ throw new UnauthorizedException (
88+ 'Authentication configuration error: BETTER_AUTH_URL not configured' ,
89+ ) ;
90+ }
91+
6492 // Extract token from "Bearer <token>"
6593 const token = authHeader . substring ( 7 ) ;
6694
67- // Create JWKS for token verification using Better Auth endpoint
68- const JWKS = createRemoteJWKSet (
69- new URL ( `${ process . env . BETTER_AUTH_URL } /api/auth/jwks` ) ,
70- ) ;
95+ const jwksUrl = `${ this . betterAuthUrl } /api/auth/jwks` ;
7196
72- // Verify JWT token
73- const { payload } = await jwtVerify ( token , JWKS , {
74- issuer : process . env . BETTER_AUTH_URL ,
75- audience : process . env . BETTER_AUTH_URL ,
97+ // Create JWKS for token verification using Better Auth endpoint
98+ // Use shorter cache time to handle key rotation better
99+ const JWKS = createRemoteJWKSet ( new URL ( jwksUrl ) , {
100+ cacheMaxAge : 60000 , // 1 minute cache (default is 5 minutes)
101+ cooldownDuration : 10000 , // 10 seconds cooldown before refetching
76102 } ) ;
77103
104+ // Verify JWT token with automatic retry on key mismatch
105+ let payload ;
106+ try {
107+ payload = (
108+ await jwtVerify ( token , JWKS , {
109+ issuer : this . betterAuthUrl ,
110+ audience : this . betterAuthUrl ,
111+ } )
112+ ) . payload ;
113+ } catch ( verifyError : any ) {
114+ // If we get a key mismatch error, retry with a fresh JWKS fetch
115+ if (
116+ verifyError . code === 'ERR_JWKS_NO_MATCHING_KEY' ||
117+ verifyError . message ?. includes ( 'no applicable key found' ) ||
118+ verifyError . message ?. includes ( 'JWKSNoMatchingKey' )
119+ ) {
120+ console . log (
121+ '[HybridAuthGuard] Key mismatch detected, fetching fresh JWKS and retrying...' ,
122+ ) ;
123+
124+ // Create a fresh JWKS instance with no cache to force immediate fetch
125+ const freshJWKS = createRemoteJWKSet ( new URL ( jwksUrl ) , {
126+ cacheMaxAge : 0 , // No cache - force fresh fetch
127+ cooldownDuration : 0 , // No cooldown - allow immediate retry
128+ } ) ;
129+
130+ // Retry verification with fresh keys
131+ payload = (
132+ await jwtVerify ( token , freshJWKS , {
133+ issuer : this . betterAuthUrl ,
134+ audience : this . betterAuthUrl ,
135+ } )
136+ ) . payload ;
137+
138+ console . log (
139+ '[HybridAuthGuard] Successfully verified token with fresh JWKS' ,
140+ ) ;
141+ } else {
142+ // Re-throw if it's not a key mismatch error
143+ throw verifyError ;
144+ }
145+ }
146+
78147 // Extract user information from JWT payload (user data is directly in payload for Better Auth JWT)
79148 const userId = payload . id as string ;
80149 const userEmail = payload . email as string ;
@@ -112,6 +181,41 @@ export class HybridAuthGuard implements CanActivate {
112181 return true ;
113182 } catch ( error ) {
114183 console . error ( 'JWT verification failed:' , error ) ;
184+
185+ // Provide more helpful error messages
186+ if ( error instanceof Error ) {
187+ // Connection errors
188+ if (
189+ error . message . includes ( 'ECONNREFUSED' ) ||
190+ error . message . includes ( 'fetch failed' )
191+ ) {
192+ console . error (
193+ `[HybridAuthGuard] Cannot connect to Better Auth JWKS endpoint at ${ this . betterAuthUrl } /api/auth/jwks` ,
194+ ) ;
195+ console . error (
196+ '[HybridAuthGuard] Make sure BETTER_AUTH_URL is set correctly and the Better Auth server is running' ,
197+ ) ;
198+ throw new UnauthorizedException (
199+ `Cannot connect to authentication service. Please check BETTER_AUTH_URL configuration.` ,
200+ ) ;
201+ }
202+
203+ // Key mismatch errors should have been handled by retry logic above
204+ // If we still get one here, it means the retry also failed (token truly invalid)
205+ if (
206+ ( error as any ) . code === 'ERR_JWKS_NO_MATCHING_KEY' ||
207+ error . message . includes ( 'no applicable key found' ) ||
208+ error . message . includes ( 'JWKSNoMatchingKey' )
209+ ) {
210+ console . error (
211+ '[HybridAuthGuard] Token key not found even after fetching fresh JWKS. Token may be from a different environment or truly invalid.' ,
212+ ) ;
213+ throw new UnauthorizedException (
214+ 'Authentication token is invalid. Please log out and log back in to refresh your session.' ,
215+ ) ;
216+ }
217+ }
218+
115219 throw new UnauthorizedException ( 'Invalid or expired JWT token' ) ;
116220 }
117221 }
0 commit comments