@@ -15,12 +15,16 @@ async function runMiddleware(options: RunMiddlewareOptions, request: NextRequest
1515 return [ NextResponse . rewrite ( newURL ) ] ;
1616 }
1717
18- // TODO safari doesn't like "secure: true" if insecure, even when localhost, need to check emulators
19- // replace tenant id, we should probably use different cookies for emulated and not
20- // need to test in firefox
21- const ID_TOKEN_COOKIE_NAME = `firebase:authUser:${ options . apiKey } :[DEFAULT]` ;
22- const REFRESH_TOKEN_COOKIE_NAME = `${ ID_TOKEN_COOKIE_NAME } :refreshToken` ;
23- const ID_TOKEN_COOKIE = { path : "/" , secure : true , sameSite : "strict" , partitioned : true , name : ID_TOKEN_COOKIE_NAME , maxAge : 34560000 } as const ;
18+ const isDevMode = process . env . NODE_ENV === 'development' ;
19+ const userAgent = request . headers . get ( 'User-Agent' ) ;
20+ const isSafari = userAgent ?. includes ( 'Safari' ) && ! userAgent ?. includes ( "Chrome" ) ;
21+ const secureCookies = isSafari ? ! isDevMode : true ; // Safari can't do secure cookies on localhost
22+ // Need two different cookie names as Chrome doesn't allow __HOST- on localhost
23+ const ID_TOKEN_COOKIE_NAME = isDevMode ? `__dev_FIREBASE_[DEFAULT]` : `__HOST-FIREBASE_[DEFAULT]` ;
24+ const REFRESH_TOKEN_COOKIE_NAME = isDevMode ? '__dev_FIREBASEID_[DEFAULT]' : `__HOST-FIREBASEID_[DEFAULT]` ;
25+ console . log ( { isDevMode, isSafari, secureCookies, ID_TOKEN_COOKIE_NAME , REFRESH_TOKEN_COOKIE_NAME } ) ;
26+ // TODO max-age should be ttl
27+ const ID_TOKEN_COOKIE = { path : "/" , secure : secureCookies , sameSite : "strict" , partitioned : true , name : ID_TOKEN_COOKIE_NAME , maxAge : 34560000 , priority : 'high' } as const ;
2428 const REFRESH_TOKEN_COOKIE = { ...ID_TOKEN_COOKIE , httpOnly : true , name : REFRESH_TOKEN_COOKIE_NAME } as const ;
2529
2630 if ( request . nextUrl . pathname === '/__cookies__' ) {
@@ -50,6 +54,7 @@ async function runMiddleware(options: RunMiddlewareOptions, request: NextRequest
5054
5155 let body : ReadableStream < any > | string | null = request . body ;
5256
57+ // TODO error if emulated and not hitting emulators
5358 const isTokenRequest = ! ! url . pathname . match ( / ^ ( \/ s e c u r e t o k e n \. g o o g l e a p i s \. c o m ) ? \/ v 1 \/ t o k e n / ) ;
5459 const isSignInRequest = ! ! url . pathname . match ( / ^ ( \/ i d e n t i t y t o o l k i t \. g o o g l e a p i s \. c o m ) ? \/ v 1 \/ a c c o u n t s : s i g n I n W i t h / ) ;
5560
@@ -78,72 +83,110 @@ async function runMiddleware(options: RunMiddlewareOptions, request: NextRequest
7883 }
7984 let refreshToken ;
8085 let idToken ;
86+ let maxAge ;
8187 if ( isSignInRequest ) {
8288 refreshToken = json . refreshToken ;
8389 idToken = json . idToken ;
90+ maxAge = json . expiresIn ;
8491 json . refreshToken = "REDACTED" ;
8592 } else {
8693 refreshToken = json . refresh_token ;
8794 idToken = json . id_token ;
95+ maxAge = json . expires_in ;
8896 json . refresh_token = "REDACTED" ;
8997 }
9098
9199 const currentIdToken = request . cookies . get ( { ...ID_TOKEN_COOKIE , value : "" } ) ?. value ;
92100 const nextResponse = NextResponse . json ( json , { status, statusText } ) ;
93- if ( idToken && currentIdToken !== idToken ) nextResponse . cookies . set ( { ...ID_TOKEN_COOKIE , value : idToken } ) ;
101+ if ( idToken && currentIdToken !== idToken ) nextResponse . cookies . set ( { ...ID_TOKEN_COOKIE , maxAge , value : idToken } ) ;
94102 if ( refreshToken ) nextResponse . cookies . set ( { ...REFRESH_TOKEN_COOKIE , value : refreshToken } ) ;
95103 return [ nextResponse ] ;
96104 }
97105
98106 const logout = ( ) : RunMiddlewareResponse => {
99107 const decorateNextResponse = ( response : NextResponse ) => {
100- if ( request . cookies . get ( { ... ID_TOKEN_COOKIE , value : "" } ) ) response . cookies . delete ( { ...ID_TOKEN_COOKIE , maxAge : 0 } ) ;
101- if ( request . cookies . get ( { ... REFRESH_TOKEN_COOKIE , value : "" } ) ) response . cookies . delete ( { ...REFRESH_TOKEN_COOKIE , maxAge : 0 } ) ;
108+ if ( request . cookies . get ( ID_TOKEN_COOKIE_NAME ) ) response . cookies . delete ( { ...ID_TOKEN_COOKIE , maxAge : 0 } ) ;
109+ if ( request . cookies . get ( REFRESH_TOKEN_COOKIE_NAME ) ) response . cookies . delete ( { ...REFRESH_TOKEN_COOKIE , maxAge : 0 } ) ;
102110 return response ;
103111 }
104112 return [ undefined , decorateNextResponse , undefined ] ;
105113 }
106114
107- let authIdToken = request . cookies . get ( { ...ID_TOKEN_COOKIE , value : "" } ) ?. value ;
108- if ( ! authIdToken ) return logout ( ) ;
115+ const authIdToken = request . cookies . get ( { ...ID_TOKEN_COOKIE , value : "" } ) ?. value ;
116+ const refresh_token = request . cookies . get ( { ...REFRESH_TOKEN_COOKIE , value : "" } ) ?. value ;
117+
118+ if ( authIdToken === undefined && ! refresh_token ) {
119+ console . error ( "no authIdToken && no refresh token" ) ;
120+ return [ undefined , it => it , undefined ] ;
121+ }
109122
110- let isEmulatedCredential = false ;
111- let [ jwtHeader , jwtPayload ] = authIdToken . split ( "." ) . slice ( 0 , 2 ) . map ( it => JSON . parse ( atob ( it ) ) ) as [ JWTHeaderParameters , FirebaseJWTPayload ] ;
112- // TODO check the tenantId
113- if ( jwtHeader ?. typ !== 'JWT' || jwtPayload ?. iss !== `https://securetoken.google.com/${ options . projectId } ` || jwtPayload . aud !== options . projectId || jwtPayload . firebase ?. tenant !== options . tenantId ) {
114- console . error ( "I hates the claims." , jwtHeader , jwtPayload ) ;
123+ if ( authIdToken === "" ) {
124+ console . error ( "logout sentinel detected" ) ;
115125 return logout ( ) ;
116126 }
117- if ( jwtHeader ?. alg === 'none' ) {
118- isEmulatedCredential = true ;
119- } else if ( jwtHeader ?. alg !== 'RS256' ) {
120- console . error ( "I hates the alg." , jwtHeader ?. alg ) ;
127+
128+ if ( ! refresh_token ) {
129+ console . log ( "missing refresh token." ) ;
121130 return logout ( ) ;
122131 }
123132
124- if ( isEmulatedCredential && ! options . emulatorHost ) throw new Error ( "could not detirmine emulator hostname." ) ;
125-
126- const muchEpochWow = Math . floor ( + new Date ( ) / 1000 ) ;
127- if ( jwtPayload . exp && jwtPayload . exp > muchEpochWow ) {
128- if ( ! isEmulatedCredential ) {
129- try {
130- const jwks = createRemoteJWKSet ( new URL ( 'https://www.googleapis.com/robot/v1/metadata/jwk/[email protected] ' ) ) ; 131- await jwtVerify ( authIdToken , jwks ) ;
132- } catch ( e ) {
133- console . error ( "Jose hates the JWT" , e ) ;
134- return logout ( ) ;
133+ let isEmulatedCredential ;
134+ let jwtPayload ;
135+
136+ if ( authIdToken ) {
137+
138+ let jwtHeader ;
139+ try {
140+ [ jwtHeader , jwtPayload ] = authIdToken . split ( "." ) . slice ( 0 , 2 ) . map ( it => JSON . parse ( atob ( it ) ) ) as [ JWTHeaderParameters , FirebaseJWTPayload ] ;
141+ } catch ( e ) {
142+ console . error ( "Unable to parse JWT." ) ;
143+ }
144+
145+ if ( jwtHeader ?. typ !== 'JWT' || jwtPayload ?. iss !== `https://securetoken.google.com/${ options . projectId } ` || jwtPayload . aud !== options . projectId || jwtPayload . firebase ?. tenant !== options . tenantId ) {
146+ console . error ( "I hates the claims." ) ;
147+ jwtPayload = undefined ;
148+ }
149+ if ( jwtHeader ?. alg === 'none' ) {
150+ isEmulatedCredential = true ;
151+ } else if ( jwtHeader ?. alg !== 'RS256' ) {
152+ console . error ( "I hates the alg." ) ;
153+ jwtPayload = undefined ;
154+ }
155+
156+ if ( isEmulatedCredential && ! options . emulatorHost ) throw new Error ( "could not determine emulator hostname." ) ;
157+
158+ if ( jwtPayload ) {
159+ const muchEpochWow = Math . floor ( + new Date ( ) / 1000 ) ;
160+ if ( jwtPayload . exp && jwtPayload . exp > muchEpochWow ) {
161+ if ( isEmulatedCredential ) {
162+ console . log ( "We good, its emulated." ) ;
163+ return [ undefined , it => it , jwtPayload ] ;
164+ } else {
165+ try {
166+ const jwks = createRemoteJWKSet ( new URL ( 'https://www.googleapis.com/robot/v1/metadata/jwk/[email protected] ' ) ) ; 167+ await jwtVerify ( authIdToken , jwks ) ;
168+ console . log ( "JOSE is happy." ) ;
169+ return [ undefined , it => it , jwtPayload ] ;
170+ } catch ( e ) {
171+ console . error ( "JOSE is a hater." ) ;
172+ }
173+ }
174+ } else {
175+ console . error ( "So exp, wow." ) ;
135176 }
136177 }
137- const decorateNextResponse = ( response : NextResponse ) => response ;
138- return [ undefined , decorateNextResponse , jwtPayload ] ;
139178 }
140179
141- const refresh_token = request . cookies . get ( { ... REFRESH_TOKEN_COOKIE , value : "" } ) ?. value ;
142- if ( ! refresh_token ) {
143- console . error ( "Where's the refresh token bro?" ) ;
144- return logout ( ) ;
180+ try {
181+ isEmulatedCredential ??= ! ! JSON . parse ( Buffer . from ( decodeURIComponent ( refresh_token ) , 'base64' ) . toString ( ) ) [ "_AuthEmulatorRefreshToken" ] ;
182+ } catch ( e ) {
183+
145184 }
146185
186+ if ( isEmulatedCredential && ! options . emulatorHost ) throw new Error ( "could not determine emulator hostname." ) ;
187+
188+ console . log ( "attempting a refresh with" , { isEmulatedCredential } ) ;
189+
147190 const refreshUrl = new URL ( isEmulatedCredential ? `http://${ options . emulatorHost } ` : `https://securetoken.googleapis.com` ) ;
148191 refreshUrl . pathname = [ isEmulatedCredential && 'securetoken.googleapis.com' , 'v1/token' ] . filter ( Boolean ) . join ( '/' ) ;
149192 refreshUrl . searchParams . set ( "key" , options . apiKey ) ;
@@ -162,6 +205,8 @@ async function runMiddleware(options: RunMiddlewareOptions, request: NextRequest
162205 const json = await refreshResponse . json ( ) as any ;
163206 const newRefreshToken = json . refresh_token ;
164207 const newIdToken = json . id_token ;
208+ jwtPayload = JSON . parse ( atob ( newIdToken . split ( "." ) [ 1 ] ) ) ;
209+ console . log ( jwtPayload ) ;
165210 const decorateNextResponse = ( response : NextResponse ) => {
166211 if ( newIdToken ) response . cookies . set ( { ...ID_TOKEN_COOKIE , value : newIdToken } ) ;
167212 if ( newRefreshToken ) response . cookies . set ( { ...REFRESH_TOKEN_COOKIE , value : newRefreshToken } ) ;
0 commit comments