cloudflare-next-intl 0.6.10 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/firebase_auth/client/auth_user_provider.js +33 -3
- package/dist/src/firebase_auth/decode_jwt_payload.bench.d.ts +1 -0
- package/dist/src/firebase_auth/decode_jwt_payload.bench.js +35 -0
- package/dist/src/firebase_auth/decode_jwt_payload.d.ts +25 -0
- package/dist/src/firebase_auth/decode_jwt_payload.js +40 -0
- package/dist/src/firebase_auth/middleware/update_session.js +1 -9
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@ import requireFirebaseAuthConfig from '../require_config';
|
|
|
8
8
|
import { getFirebaseAuthClient, getFirebaseAuthModule } from './firebase_client';
|
|
9
9
|
import { setAuthUserCache } from './auth_user_cache';
|
|
10
10
|
import { defaultEmailVerifiedHintCookieName, defaultRefreshTokenCookieName, defaultSessionCookieName } from '../middleware/update_session';
|
|
11
|
+
import decodeJwtPayload from '../decode_jwt_payload';
|
|
11
12
|
import setCookie from '../../client/functions/set_cookie';
|
|
13
|
+
import getCookie from '../../client/functions/get_cookie';
|
|
12
14
|
import clearSessionAction from '../server/clear_session_action';
|
|
13
15
|
// `null` default (instead of a `{ loading: true, ... }` stand-in) lets
|
|
14
16
|
// `useAuthUser` distinguish "not wrapped in AuthUserProvider" (throw) from
|
|
@@ -17,6 +19,9 @@ export const AuthUserContext = createContext(null);
|
|
|
17
19
|
function writeSessionCookie(sessionCookieName, idToken, maxAge) {
|
|
18
20
|
setCookie({ name: sessionCookieName, value: idToken, maxAge });
|
|
19
21
|
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
20
25
|
function clearSessionCookie(sessionCookieName) {
|
|
21
26
|
setCookie({ name: sessionCookieName, value: '', maxAge: 0 });
|
|
22
27
|
}
|
|
@@ -48,7 +53,7 @@ async function clearSession(sessionCookieName, refreshTokenCookieName, emailVeri
|
|
|
48
53
|
console.error('AuthUserProvider: clearSessionAction failed', e);
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
|
-
async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName) {
|
|
56
|
+
async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, idToken) {
|
|
52
57
|
try {
|
|
53
58
|
writeRefreshTokenCookie(refreshTokenCookieName, user, refreshTokenMaxAge);
|
|
54
59
|
}
|
|
@@ -56,7 +61,7 @@ async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieN
|
|
|
56
61
|
console.error('AuthUserProvider: refresh-token cookie sync failed', e);
|
|
57
62
|
}
|
|
58
63
|
writeEmailVerifiedHintCookie(emailVerifiedHintCookieName, user.emailVerified, refreshTokenMaxAge);
|
|
59
|
-
writeSessionCookie(sessionCookieName, await user.getIdToken(true), maxAge);
|
|
64
|
+
writeSessionCookie(sessionCookieName, idToken ?? await user.getIdToken(true), maxAge);
|
|
60
65
|
}
|
|
61
66
|
/**
|
|
62
67
|
* Client-side auth-state provider for `firebase_auth`. Wrap your root layout
|
|
@@ -173,7 +178,32 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
173
178
|
try {
|
|
174
179
|
const { reload } = await getFirebaseAuthModule();
|
|
175
180
|
await reload(user);
|
|
176
|
-
|
|
181
|
+
// `getIdToken(true)` can occasionally hand back a token that's
|
|
182
|
+
// no newer than the one already in the session cookie — a stale
|
|
183
|
+
// token caught mid-propagation right after `reload()`, rather
|
|
184
|
+
// than the fresh mint the caller asked for. Comparing `iat`
|
|
185
|
+
// (issued-at) against the existing cookie's token is a direct
|
|
186
|
+
// check that the token we're about to write is actually new;
|
|
187
|
+
// retry a few times if it isn't before giving up and writing
|
|
188
|
+
// whatever we got. The confirmed token is threaded into
|
|
189
|
+
// `writeSession` directly — letting it call `getIdToken(true)`
|
|
190
|
+
// again on its own could re-fetch and land back on a stale
|
|
191
|
+
// token, undoing this retry entirely.
|
|
192
|
+
const previousIat = decodeJwtPayload(getCookie(sessionCookieName) ?? '')?.iat;
|
|
193
|
+
let confirmedToken;
|
|
194
|
+
if (previousIat !== undefined) {
|
|
195
|
+
const maxAttempts = 3;
|
|
196
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
197
|
+
const freshToken = await user.getIdToken(true);
|
|
198
|
+
confirmedToken = freshToken;
|
|
199
|
+
const freshIat = decodeJwtPayload(freshToken)?.iat;
|
|
200
|
+
if (freshIat !== undefined && freshIat > previousIat)
|
|
201
|
+
break;
|
|
202
|
+
if (attempt < maxAttempts - 1)
|
|
203
|
+
await sleep(500);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, confirmedToken);
|
|
177
207
|
setAuthUserCache(user);
|
|
178
208
|
setState({ user, loading: false });
|
|
179
209
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import decodeJwtPayload from './decode_jwt_payload';
|
|
3
|
+
function makeJwt(payload) {
|
|
4
|
+
const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url');
|
|
5
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
6
|
+
return `${header}.${encodedPayload}.sig`;
|
|
7
|
+
}
|
|
8
|
+
// A realistic Firebase ID token payload has more claims than the ones this
|
|
9
|
+
// function reads — decoding cost scales with the JSON blob's size, not just
|
|
10
|
+
// the fields we destructure, so the bench payload mirrors that shape rather
|
|
11
|
+
// than a minimal `{ exp, iat }` object.
|
|
12
|
+
const realisticToken = makeJwt({
|
|
13
|
+
iss: 'https://securetoken.google.com/demo-project',
|
|
14
|
+
aud: 'demo-project',
|
|
15
|
+
auth_time: 1735689600,
|
|
16
|
+
user_id: 'abcDEF123456ghijKLMNOP789',
|
|
17
|
+
sub: 'abcDEF123456ghijKLMNOP789',
|
|
18
|
+
iat: 1735689600,
|
|
19
|
+
exp: 1735693200,
|
|
20
|
+
email: 'user@example.com',
|
|
21
|
+
email_verified: true,
|
|
22
|
+
firebase: {
|
|
23
|
+
identities: { email: ['user@example.com'] },
|
|
24
|
+
sign_in_provider: 'password',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const malformedToken = 'not-a-jwt';
|
|
28
|
+
describe('decodeJwtPayload', () => {
|
|
29
|
+
bench('valid token, realistic payload size', () => {
|
|
30
|
+
decodeJwtPayload(realisticToken);
|
|
31
|
+
});
|
|
32
|
+
bench('malformed token (parse failure path)', () => {
|
|
33
|
+
decodeJwtPayload(malformedToken);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes a JWT's payload without verifying its signature — callers only
|
|
3
|
+
* ever read claims from a token they already trust (their own session
|
|
4
|
+
* cookie, or one just minted by the Firebase SDK/Secure Token API), never
|
|
5
|
+
* one supplied by an untrusted party. Isomorphic (no `next/server` or
|
|
6
|
+
* `next/client`-only APIs) so both `middleware/update_session.ts` (Edge)
|
|
7
|
+
* and `client/auth_user_provider.tsx` (browser) can share one
|
|
8
|
+
* implementation instead of drifting apart.
|
|
9
|
+
*
|
|
10
|
+
* Extracts `exp`/`iat`/`email_verified` via regex over the decoded JSON
|
|
11
|
+
* text instead of a full `JSON.parse` — ~2.6x faster (benchmarked in
|
|
12
|
+
* `decode_jwt_payload.bench.ts`), which matters since this runs on every
|
|
13
|
+
* Edge middleware invocation. This is safe ONLY because these three claims
|
|
14
|
+
* are top-level, standard JWT/Firebase registered claims that Firebase's ID
|
|
15
|
+
* tokens never nest inside another object — a regex doing first-match
|
|
16
|
+
* search would return a wrong value for a claim nested under a
|
|
17
|
+
* same-named key. Do NOT extend this function to read additional claims
|
|
18
|
+
* without confirming they're similarly guaranteed top-level, or switch back
|
|
19
|
+
* to full `JSON.parse` for those calls instead.
|
|
20
|
+
*/
|
|
21
|
+
export default function decodeJwtPayload(token: string): {
|
|
22
|
+
exp?: number;
|
|
23
|
+
iat?: number;
|
|
24
|
+
email_verified?: boolean;
|
|
25
|
+
} | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const EXP_RE = /"exp":(-?\d+)/;
|
|
2
|
+
const IAT_RE = /"iat":(-?\d+)/;
|
|
3
|
+
const EMAIL_VERIFIED_RE = /"email_verified":(true|false)/;
|
|
4
|
+
/**
|
|
5
|
+
* Decodes a JWT's payload without verifying its signature — callers only
|
|
6
|
+
* ever read claims from a token they already trust (their own session
|
|
7
|
+
* cookie, or one just minted by the Firebase SDK/Secure Token API), never
|
|
8
|
+
* one supplied by an untrusted party. Isomorphic (no `next/server` or
|
|
9
|
+
* `next/client`-only APIs) so both `middleware/update_session.ts` (Edge)
|
|
10
|
+
* and `client/auth_user_provider.tsx` (browser) can share one
|
|
11
|
+
* implementation instead of drifting apart.
|
|
12
|
+
*
|
|
13
|
+
* Extracts `exp`/`iat`/`email_verified` via regex over the decoded JSON
|
|
14
|
+
* text instead of a full `JSON.parse` — ~2.6x faster (benchmarked in
|
|
15
|
+
* `decode_jwt_payload.bench.ts`), which matters since this runs on every
|
|
16
|
+
* Edge middleware invocation. This is safe ONLY because these three claims
|
|
17
|
+
* are top-level, standard JWT/Firebase registered claims that Firebase's ID
|
|
18
|
+
* tokens never nest inside another object — a regex doing first-match
|
|
19
|
+
* search would return a wrong value for a claim nested under a
|
|
20
|
+
* same-named key. Do NOT extend this function to read additional claims
|
|
21
|
+
* without confirming they're similarly guaranteed top-level, or switch back
|
|
22
|
+
* to full `JSON.parse` for those calls instead.
|
|
23
|
+
*/
|
|
24
|
+
export default function decodeJwtPayload(token) {
|
|
25
|
+
try {
|
|
26
|
+
const payload = token.split('.')[1];
|
|
27
|
+
const json = atob(payload.replace(/[-_]/g, (c) => c === '-' ? '+' : '/'));
|
|
28
|
+
const exp = EXP_RE.exec(json);
|
|
29
|
+
const iat = IAT_RE.exec(json);
|
|
30
|
+
const emailVerified = EMAIL_VERIFIED_RE.exec(json);
|
|
31
|
+
return {
|
|
32
|
+
exp: exp ? Number(exp[1]) : undefined,
|
|
33
|
+
iat: iat ? Number(iat[1]) : undefined,
|
|
34
|
+
email_verified: emailVerified ? emailVerified[1] === 'true' : undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import config from '@intl-config';
|
|
3
|
+
import decodeJwtPayload from '../decode_jwt_payload';
|
|
3
4
|
export const defaultSessionCookieName = '__fa_session__';
|
|
4
5
|
export const defaultRefreshTokenCookieName = '__fa_refresh_token__';
|
|
5
6
|
// Non-httpOnly: written by AuthUserProvider (client) every time it observes
|
|
@@ -16,15 +17,6 @@ const DEFAULT_REFRESH_MAX_AGE = 60 * 60 * 24 * 365;
|
|
|
16
17
|
// time can hand a client a token that dies moments after this check, forcing
|
|
17
18
|
// an extra round-trip on the very next request.
|
|
18
19
|
const CLOCK_SKEW_MARGIN_MS = 60 * 1000;
|
|
19
|
-
function decodeJwtPayload(token) {
|
|
20
|
-
try {
|
|
21
|
-
const payload = token.split('.')[1];
|
|
22
|
-
return JSON.parse(atob(payload.replace(/[-_]/g, (c) => c === '-' ? '+' : '/')));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
20
|
function isJwtExpired(token) {
|
|
29
21
|
const decoded = decodeJwtPayload(token);
|
|
30
22
|
return !decoded?.exp || decoded.exp * 1000 - CLOCK_SKEW_MARGIN_MS <= Date.now();
|