cloudflare-next-intl 0.6.24 → 0.6.26
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 +35 -10
- package/dist/src/firebase_auth/client/firebase_client.d.ts +8 -0
- package/dist/src/firebase_auth/client/firebase_client.js +17 -2
- package/dist/src/firebase_auth/middleware/update_session.d.ts +1 -0
- package/dist/src/firebase_auth/middleware/update_session.js +7 -0
- package/dist/src/firebase_auth/server/firebase_server.js +25 -3
- package/dist/src/firebase_auth/server/mint_server_app_check_token.d.ts +18 -0
- package/dist/src/firebase_auth/server/mint_server_app_check_token.js +55 -0
- package/dist/src/types/types.d.ts +52 -0
- package/package.json +3 -2
|
@@ -5,9 +5,9 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import usePathname from '../../client/hooks/use_path_name';
|
|
6
6
|
import config from '@intl-config';
|
|
7
7
|
import requireFirebaseAuthConfig from '../require_config';
|
|
8
|
-
import { getFirebaseAuthClient, getFirebaseAuthModule } from './firebase_client';
|
|
8
|
+
import { getAppCheckToken, getFirebaseAuthClient, getFirebaseAuthModule } from './firebase_client';
|
|
9
9
|
import { setAuthUserCache } from './auth_user_cache';
|
|
10
|
-
import { defaultEmailVerifiedHintCookieName, defaultRefreshTokenCookieName, defaultSessionCookieName } from '../middleware/update_session';
|
|
10
|
+
import { defaultAppCheckTokenCookieName, defaultEmailVerifiedHintCookieName, defaultRefreshTokenCookieName, defaultSessionCookieName } from '../middleware/update_session';
|
|
11
11
|
import decodeJwtPayload from '../decode_jwt_payload';
|
|
12
12
|
import isWhitelisted from '../is_whitelisted';
|
|
13
13
|
import setCookie from '../../client/functions/set_cookie';
|
|
@@ -39,9 +39,31 @@ function clearRefreshTokenCookie(refreshTokenCookieName) {
|
|
|
39
39
|
function writeEmailVerifiedHintCookie(emailVerifiedHintCookieName, emailVerified, maxAge) {
|
|
40
40
|
setCookie({ name: emailVerifiedHintCookieName, value: String(emailVerified), maxAge });
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
function clearAppCheckTokenCookie(appCheckTokenCookieName) {
|
|
43
|
+
setCookie({ name: appCheckTokenCookieName, value: '', maxAge: 0 });
|
|
44
|
+
}
|
|
45
|
+
// Mirrors the live App Check token into a client-readable cookie so
|
|
46
|
+
// `getAuthenticatedAppForUser` can forward it to `initializeServerApp` —
|
|
47
|
+
// required whenever App Check enforcement is on for Auth, or every
|
|
48
|
+
// server-side `getAuthUser()` call is rejected with
|
|
49
|
+
// `auth/firebase-app-check-token-is-invalid`. Best-effort: App Check may not
|
|
50
|
+
// be configured, or a token fetch can transiently fail (e.g. reCAPTCHA not
|
|
51
|
+
// yet ready) — either case just leaves the cookie unset rather than blocking
|
|
52
|
+
// session sync on it.
|
|
53
|
+
async function writeAppCheckTokenCookie(appCheckTokenCookieName, maxAge) {
|
|
54
|
+
try {
|
|
55
|
+
const token = await getAppCheckToken();
|
|
56
|
+
if (token)
|
|
57
|
+
setCookie({ name: appCheckTokenCookieName, value: token, maxAge });
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error('AuthUserProvider: App Check token cookie sync failed', e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function clearSession(sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, refreshTokenMaxAge) {
|
|
43
64
|
clearSessionCookie(sessionCookieName);
|
|
44
65
|
clearRefreshTokenCookie(refreshTokenCookieName);
|
|
66
|
+
clearAppCheckTokenCookie(appCheckTokenCookieName);
|
|
45
67
|
// Signed-out is not "unknown" — it's a confirmed non-verified state, so
|
|
46
68
|
// write 'false' explicitly rather than clearing (an absent hint means
|
|
47
69
|
// "no signal yet", which forces the middleware to refresh unnecessarily
|
|
@@ -54,7 +76,7 @@ async function clearSession(sessionCookieName, refreshTokenCookieName, emailVeri
|
|
|
54
76
|
console.error('AuthUserProvider: clearSessionAction failed', e);
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
|
-
async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, idToken) {
|
|
79
|
+
async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, idToken) {
|
|
58
80
|
try {
|
|
59
81
|
writeRefreshTokenCookie(refreshTokenCookieName, user, refreshTokenMaxAge);
|
|
60
82
|
}
|
|
@@ -62,6 +84,7 @@ async function writeSession(user, sessionCookieName, maxAge, refreshTokenCookieN
|
|
|
62
84
|
console.error('AuthUserProvider: refresh-token cookie sync failed', e);
|
|
63
85
|
}
|
|
64
86
|
writeEmailVerifiedHintCookie(emailVerifiedHintCookieName, user.emailVerified, refreshTokenMaxAge);
|
|
87
|
+
await writeAppCheckTokenCookie(appCheckTokenCookieName, appCheckTokenMaxAge);
|
|
65
88
|
writeSessionCookie(sessionCookieName, idToken ?? await user.getIdToken(true), maxAge);
|
|
66
89
|
}
|
|
67
90
|
/**
|
|
@@ -94,6 +117,8 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
94
117
|
const refreshTokenMaxAge = fa.refreshTokenCookieMaxAge ?? 60 * 60 * 24 * 365;
|
|
95
118
|
const refreshTokenCookieName = fa.refreshTokenCookieName ?? defaultRefreshTokenCookieName;
|
|
96
119
|
const emailVerifiedHintCookieName = fa.emailVerifiedHintCookieName ?? defaultEmailVerifiedHintCookieName;
|
|
120
|
+
const appCheckTokenCookieName = fa.appCheckTokenCookieName ?? defaultAppCheckTokenCookieName;
|
|
121
|
+
const appCheckTokenMaxAge = fa.appCheckTokenCookieMaxAge ?? 60 * 60;
|
|
97
122
|
const [state, setState] = useState({
|
|
98
123
|
user: initialUser,
|
|
99
124
|
loading: initialUser === null,
|
|
@@ -164,10 +189,10 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
164
189
|
const previous = syncedSignedIn.current;
|
|
165
190
|
try {
|
|
166
191
|
if (user) {
|
|
167
|
-
await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName);
|
|
192
|
+
await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge);
|
|
168
193
|
}
|
|
169
194
|
else {
|
|
170
|
-
await clearSession(sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, refreshTokenMaxAge);
|
|
195
|
+
await clearSession(sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, refreshTokenMaxAge);
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
198
|
catch (e) {
|
|
@@ -231,7 +256,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
231
256
|
unsubscribe?.();
|
|
232
257
|
};
|
|
233
258
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
234
|
-
}, [router, isAuthPage, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName]);
|
|
259
|
+
}, [router, isAuthPage, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge]);
|
|
235
260
|
const reloadUser = useCallback(async () => {
|
|
236
261
|
const { auth } = await getFirebaseAuthClient();
|
|
237
262
|
const user = auth.currentUser;
|
|
@@ -265,7 +290,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
265
290
|
await sleep(500);
|
|
266
291
|
}
|
|
267
292
|
}
|
|
268
|
-
await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, confirmedToken);
|
|
293
|
+
await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, confirmedToken);
|
|
269
294
|
if (!emailVerifiedRef.current && user.emailVerified) {
|
|
270
295
|
emailVerifiedRef.current = true;
|
|
271
296
|
try {
|
|
@@ -300,10 +325,10 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
300
325
|
await signOut(auth);
|
|
301
326
|
}
|
|
302
327
|
finally {
|
|
303
|
-
await clearSession(sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, refreshTokenMaxAge);
|
|
328
|
+
await clearSession(sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, refreshTokenMaxAge);
|
|
304
329
|
router.push(fa.redirectAuthPath);
|
|
305
330
|
}
|
|
306
331
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
307
|
-
}, [fa.redirectAuthPath, sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName]);
|
|
332
|
+
}, [fa.redirectAuthPath, sessionCookieName, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName]);
|
|
308
333
|
return _jsx(AuthUserContext.Provider, { value: { ...state, reloadUser, sendVerificationEmail, logout }, children: children });
|
|
309
334
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { FirebaseApp } from 'firebase/app';
|
|
2
2
|
import type { Auth } from 'firebase/auth';
|
|
3
|
+
/**
|
|
4
|
+
* Current App Check token, or `undefined` if `appCheck` isn't configured or
|
|
5
|
+
* hasn't initialized yet. Forces a refresh only when the cached token is
|
|
6
|
+
* expired/near-expiry — mirrors `getToken`'s own semantics, just exposed
|
|
7
|
+
* here so callers (e.g. `AuthUserProvider`'s session-cookie sync) don't need
|
|
8
|
+
* to import `firebase/app-check` themselves.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getAppCheckToken(): Promise<string | undefined>;
|
|
3
11
|
/**
|
|
4
12
|
* Lazily loads and initializes `firebase/app`/`firebase/auth` — a dynamic
|
|
5
13
|
* import, not a static one, so consumers who never call a firebase_auth
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import config from '@intl-config';
|
|
3
3
|
import requireFirebaseAuthConfig from '../require_config';
|
|
4
|
+
let cachedAppCheck;
|
|
4
5
|
async function initializeFirebaseAppCheck(app, appCheckConfig) {
|
|
5
6
|
const { initializeAppCheck, ReCaptchaV3Provider, ReCaptchaEnterpriseProvider } = await import('firebase/app-check');
|
|
6
7
|
if (appCheckConfig.debugToken) {
|
|
@@ -10,11 +11,25 @@ async function initializeFirebaseAppCheck(app, appCheckConfig) {
|
|
|
10
11
|
const provider = appCheckConfig.recaptchaEnterpriseSiteKey
|
|
11
12
|
? new ReCaptchaEnterpriseProvider(appCheckConfig.recaptchaEnterpriseSiteKey)
|
|
12
13
|
: new ReCaptchaV3Provider(appCheckConfig.recaptchaV3SiteKey);
|
|
13
|
-
initializeAppCheck(app, {
|
|
14
|
+
return initializeAppCheck(app, {
|
|
14
15
|
provider,
|
|
15
16
|
isTokenAutoRefreshEnabled: appCheckConfig.isTokenAutoRefreshEnabled ?? true,
|
|
16
17
|
});
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Current App Check token, or `undefined` if `appCheck` isn't configured or
|
|
21
|
+
* hasn't initialized yet. Forces a refresh only when the cached token is
|
|
22
|
+
* expired/near-expiry — mirrors `getToken`'s own semantics, just exposed
|
|
23
|
+
* here so callers (e.g. `AuthUserProvider`'s session-cookie sync) don't need
|
|
24
|
+
* to import `firebase/app-check` themselves.
|
|
25
|
+
*/
|
|
26
|
+
export async function getAppCheckToken() {
|
|
27
|
+
if (!cachedAppCheck)
|
|
28
|
+
return undefined;
|
|
29
|
+
const { getToken } = await import('firebase/app-check');
|
|
30
|
+
const result = await getToken(cachedAppCheck);
|
|
31
|
+
return result.token;
|
|
32
|
+
}
|
|
18
33
|
let cached;
|
|
19
34
|
let cachedPromise;
|
|
20
35
|
/**
|
|
@@ -42,7 +57,7 @@ export async function getFirebaseAuthClient() {
|
|
|
42
57
|
};
|
|
43
58
|
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
|
|
44
59
|
if (fa.appCheck) {
|
|
45
|
-
await initializeFirebaseAppCheck(app, fa.appCheck);
|
|
60
|
+
cachedAppCheck = await initializeFirebaseAppCheck(app, fa.appCheck);
|
|
46
61
|
}
|
|
47
62
|
const auth = getAuth(app);
|
|
48
63
|
cached = { app, auth };
|
|
@@ -2,6 +2,7 @@ import { NextResponse, type NextRequest } from 'next/server';
|
|
|
2
2
|
export declare const defaultSessionCookieName = "__fa_session__";
|
|
3
3
|
export declare const defaultRefreshTokenCookieName = "__fa_refresh_token__";
|
|
4
4
|
export declare const defaultEmailVerifiedHintCookieName = "__fa_email_verified_hint__";
|
|
5
|
+
export declare const defaultAppCheckTokenCookieName = "__fa_app_check_token__";
|
|
5
6
|
export declare const defaultResetPasswordPath = "/reset-password";
|
|
6
7
|
/**
|
|
7
8
|
* Layers Firebase session-cookie validation/refresh and auth redirects onto
|
|
@@ -11,6 +11,13 @@ export const defaultRefreshTokenCookieName = '__fa_refresh_token__';
|
|
|
11
11
|
// — force one refresh). Readable client-side is fine: it carries no secret,
|
|
12
12
|
// only a boolean mirror of a claim already inside the session JWT.
|
|
13
13
|
export const defaultEmailVerifiedHintCookieName = '__fa_email_verified_hint__';
|
|
14
|
+
// Non-httpOnly: written by AuthUserProvider (client) whenever it mints a
|
|
15
|
+
// fresh App Check token, so the server can forward it to
|
|
16
|
+
// `initializeServerApp` — required whenever App Check enforcement is on for
|
|
17
|
+
// Auth, or every server-side `getAuthUser()` call is rejected with
|
|
18
|
+
// `auth/firebase-app-check-token-is-invalid`. Carries no secret beyond what
|
|
19
|
+
// the client already attaches to every Firebase SDK request itself.
|
|
20
|
+
export const defaultAppCheckTokenCookieName = '__fa_app_check_token__';
|
|
14
21
|
export const defaultResetPasswordPath = '/reset-password';
|
|
15
22
|
/**
|
|
16
23
|
* Firebase's console exposes ONE project-wide action URL, so every email
|
|
@@ -2,8 +2,9 @@ import { cookies } from 'next/headers';
|
|
|
2
2
|
import { cache } from 'react';
|
|
3
3
|
import config from '@intl-config';
|
|
4
4
|
import requireFirebaseAuthConfig from '../require_config';
|
|
5
|
-
import { defaultSessionCookieName } from '../middleware/update_session';
|
|
5
|
+
import { defaultAppCheckTokenCookieName, defaultSessionCookieName } from '../middleware/update_session';
|
|
6
6
|
import reportError from '../../error_handling/report_error';
|
|
7
|
+
import mintServerAppCheckToken from './mint_server_app_check_token';
|
|
7
8
|
let baseApp;
|
|
8
9
|
let firebaseAppModule;
|
|
9
10
|
let firebaseAuthModule;
|
|
@@ -20,10 +21,31 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
|
|
|
20
21
|
const fa = config.firebaseAuth;
|
|
21
22
|
requireFirebaseAuthConfig(fa);
|
|
22
23
|
const sessionCookieName = fa.sessionCookieName ?? defaultSessionCookieName;
|
|
23
|
-
const
|
|
24
|
+
const appCheckTokenCookieName = fa.appCheckTokenCookieName ?? defaultAppCheckTokenCookieName;
|
|
25
|
+
const cookieStore = await cookies();
|
|
26
|
+
const authIdToken = cookieStore.get(sessionCookieName)?.value;
|
|
24
27
|
if (!authIdToken) {
|
|
25
28
|
return { firebaseServerApp: null, currentUser: null };
|
|
26
29
|
}
|
|
30
|
+
// Only meaningful when `appCheck` is configured — if App Check
|
|
31
|
+
// enforcement is on for Auth in the Firebase console, `initializeServerApp`
|
|
32
|
+
// rejects with `auth/firebase-app-check-token-is-invalid` unless this is
|
|
33
|
+
// supplied. Absent when App Check isn't configured, which is fine:
|
|
34
|
+
// `initializeServerApp` simply skips App Check validation in that case.
|
|
35
|
+
//
|
|
36
|
+
// The client-written cookie is the fast path (no round-trip) but can be
|
|
37
|
+
// missing on a cold navigation — a fresh tab/hard-refresh renders on the
|
|
38
|
+
// server BEFORE `AuthUserProvider` has had a chance to run and write it,
|
|
39
|
+
// even though `authIdToken` above proves this is a genuinely signed-in
|
|
40
|
+
// user. Falling straight through to `initializeServerApp` with no App
|
|
41
|
+
// Check token in that case would reject the whole lookup and render the
|
|
42
|
+
// page as signed-out. Minting one server-side (service-account-backed,
|
|
43
|
+
// see `mintServerAppCheckToken`) closes that gap; it's a no-op returning
|
|
44
|
+
// `undefined` if `appCheck.clientEmail`/`privateKey`/`appId` aren't set,
|
|
45
|
+
// so apps that never configure server-side minting keep today's exact
|
|
46
|
+
// behavior.
|
|
47
|
+
const appCheckToken = cookieStore.get(appCheckTokenCookieName)?.value
|
|
48
|
+
?? await mintServerAppCheckToken(fa.projectId, fa.appCheck);
|
|
27
49
|
try {
|
|
28
50
|
if (!firebaseAppModule)
|
|
29
51
|
firebaseAppModule = await import('firebase/app');
|
|
@@ -47,7 +69,7 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
|
|
|
47
69
|
// registering a new named app in Firebase's global app registry.
|
|
48
70
|
if (!baseApp)
|
|
49
71
|
baseApp = initializeApp(firebaseConfig, 'firebase-auth-server-base');
|
|
50
|
-
const firebaseServerApp = initializeServerApp(baseApp, { authIdToken });
|
|
72
|
+
const firebaseServerApp = initializeServerApp(baseApp, { authIdToken, appCheckToken });
|
|
51
73
|
const auth = getAuth(firebaseServerApp);
|
|
52
74
|
await auth.authStateReady();
|
|
53
75
|
return { firebaseServerApp, currentUser: auth.currentUser };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FirebaseAppCheckConfig } from '../../types/types';
|
|
2
|
+
/**
|
|
3
|
+
* Mints a fresh App Check token server-side via a service account, for use
|
|
4
|
+
* when the client-written App Check cookie (see `appCheckTokenCookieName`)
|
|
5
|
+
* is absent — e.g. a cold navigation before `AuthUserProvider` has run and
|
|
6
|
+
* had a chance to write it. Requires `clientEmail`/`privateKey`/`appId` on
|
|
7
|
+
* `firebaseAuth.appCheck`; returns `undefined` (never throws) if the
|
|
8
|
+
* exchange fails, so a caller can always fall back to "no App Check token"
|
|
9
|
+
* exactly as before this existed.
|
|
10
|
+
*
|
|
11
|
+
* Signs a short-lived custom JWT with the service account's private key
|
|
12
|
+
* (`jose`, Edge/WebCrypto-compatible — no `firebase-admin`), then exchanges
|
|
13
|
+
* it for an App Check token via `exchangeCustomToken`. Not cached beyond the
|
|
14
|
+
* caller's own request-scoped `cache()` wrapper — a fresh mint costs one
|
|
15
|
+
* signing operation plus one network round-trip, acceptable per-request but
|
|
16
|
+
* not worth doing more than once per request.
|
|
17
|
+
*/
|
|
18
|
+
export default function mintServerAppCheckToken(projectId: string, appCheck: FirebaseAppCheckConfig | undefined): Promise<string | undefined>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import config from '@intl-config';
|
|
2
|
+
import reportError from '../../error_handling/report_error';
|
|
3
|
+
const APP_CHECK_CUSTOM_TOKEN_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.FirebaseAppCheck';
|
|
4
|
+
const DEFAULT_CUSTOM_TOKEN_LIFETIME = '1h';
|
|
5
|
+
/**
|
|
6
|
+
* Mints a fresh App Check token server-side via a service account, for use
|
|
7
|
+
* when the client-written App Check cookie (see `appCheckTokenCookieName`)
|
|
8
|
+
* is absent — e.g. a cold navigation before `AuthUserProvider` has run and
|
|
9
|
+
* had a chance to write it. Requires `clientEmail`/`privateKey`/`appId` on
|
|
10
|
+
* `firebaseAuth.appCheck`; returns `undefined` (never throws) if the
|
|
11
|
+
* exchange fails, so a caller can always fall back to "no App Check token"
|
|
12
|
+
* exactly as before this existed.
|
|
13
|
+
*
|
|
14
|
+
* Signs a short-lived custom JWT with the service account's private key
|
|
15
|
+
* (`jose`, Edge/WebCrypto-compatible — no `firebase-admin`), then exchanges
|
|
16
|
+
* it for an App Check token via `exchangeCustomToken`. Not cached beyond the
|
|
17
|
+
* caller's own request-scoped `cache()` wrapper — a fresh mint costs one
|
|
18
|
+
* signing operation plus one network round-trip, acceptable per-request but
|
|
19
|
+
* not worth doing more than once per request.
|
|
20
|
+
*/
|
|
21
|
+
export default async function mintServerAppCheckToken(projectId, appCheck) {
|
|
22
|
+
if (!appCheck?.clientEmail || !appCheck.privateKey || !appCheck.appId)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const { SignJWT, importPKCS8 } = await import('jose');
|
|
26
|
+
const privateKey = await importPKCS8(appCheck.privateKey.replace(/\\n/g, '\n'), 'RS256');
|
|
27
|
+
const customToken = await new SignJWT({ app_id: appCheck.appId })
|
|
28
|
+
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
|
|
29
|
+
.setIssuer(appCheck.clientEmail)
|
|
30
|
+
.setSubject(appCheck.clientEmail)
|
|
31
|
+
.setAudience(APP_CHECK_CUSTOM_TOKEN_AUDIENCE)
|
|
32
|
+
.setIssuedAt()
|
|
33
|
+
.setExpirationTime(appCheck.customTokenLifetime ?? DEFAULT_CUSTOM_TOKEN_LIFETIME)
|
|
34
|
+
.sign(privateKey);
|
|
35
|
+
const url = `https://firebaseappcheck.googleapis.com/v1/projects/${projectId}/apps/${appCheck.appId}:exchangeCustomToken`;
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ customToken }),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
await reportError(config, {
|
|
43
|
+
error: new Error(`exchangeCustomToken failed: ${res.status} ${await res.text()}`),
|
|
44
|
+
classOrMethodName: 'mintServerAppCheckToken',
|
|
45
|
+
});
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
return data.token;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
await reportError(config, { error, classOrMethodName: 'mintServerAppCheckToken' });
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -509,6 +509,22 @@ export interface FirebaseAuthRoutingConfig {
|
|
|
509
509
|
refreshTokenCookieName?: string;
|
|
510
510
|
/** Email-verified hint cookie name. Defaults to `'__fa_email_verified_hint__'`. Client-written, non-httpOnly; lets the middleware avoid an unnecessary token refresh when its view already matches the client's. */
|
|
511
511
|
emailVerifiedHintCookieName?: string;
|
|
512
|
+
/**
|
|
513
|
+
* App Check token cookie name. Defaults to `'__fa_app_check_token__'`.
|
|
514
|
+
* Client-written, non-httpOnly; carries the client's live App Check
|
|
515
|
+
* token to the server so `getAuthUser()`/`getAuthenticatedAppForUser`
|
|
516
|
+
* can pass it to `initializeServerApp`. Only relevant when `appCheck`
|
|
517
|
+
* is configured AND App Check enforcement is turned on for Auth in the
|
|
518
|
+
* Firebase console — otherwise `initializeServerApp` rejects every
|
|
519
|
+
* request with `auth/firebase-app-check-token-is-invalid`.
|
|
520
|
+
*/
|
|
521
|
+
appCheckTokenCookieName?: string;
|
|
522
|
+
/**
|
|
523
|
+
* App Check token cookie max-age in seconds. Defaults to 1 hour (3600) —
|
|
524
|
+
* matches the App Check token's own default lifetime, so the cookie
|
|
525
|
+
* doesn't outlive the token it holds.
|
|
526
|
+
*/
|
|
527
|
+
appCheckTokenCookieMaxAge?: number;
|
|
512
528
|
/**
|
|
513
529
|
* Called once, the moment `AuthUserProvider` observes a real sign-in
|
|
514
530
|
* (a `null → user` transition) — never on a plain token refresh of an
|
|
@@ -555,6 +571,42 @@ export interface FirebaseAppCheckConfig {
|
|
|
555
571
|
debugToken?: boolean | string;
|
|
556
572
|
/** Forwarded to `initializeAppCheck`'s `isTokenAutoRefreshEnabled`. Defaults to `true`. */
|
|
557
573
|
isTokenAutoRefreshEnabled?: boolean;
|
|
574
|
+
/**
|
|
575
|
+
* Service account client email, used ONLY server-side to mint an App
|
|
576
|
+
* Check token when the client-written App Check cookie is absent (e.g.
|
|
577
|
+
* a cold navigation before `AuthUserProvider` has run — see
|
|
578
|
+
* `appCheckTokenCookieName`). Required alongside `privateKey` and
|
|
579
|
+
* `appId` for server-side minting. Never sent to the client — read only
|
|
580
|
+
* by `firebase_server.ts`.
|
|
581
|
+
*/
|
|
582
|
+
clientEmail: string;
|
|
583
|
+
/**
|
|
584
|
+
* Service account private key (PEM), paired with `clientEmail` for
|
|
585
|
+
* server-side App Check token minting. Same server-only, secret-bearing
|
|
586
|
+
* field as `clientEmail` — set from an untrusted-by-the-client env var
|
|
587
|
+
* (e.g. `process.env.FIREBASE_PRIVATE_KEY`), never exposed to the
|
|
588
|
+
* browser. Escaped `\n` sequences (common when stored in a single-line
|
|
589
|
+
* env var) are unescaped automatically before use.
|
|
590
|
+
*/
|
|
591
|
+
privateKey: string;
|
|
592
|
+
/**
|
|
593
|
+
* Firebase App Check app ID (e.g. `"1:1234567890:web:abcdef123456"`),
|
|
594
|
+
* required alongside `clientEmail`/`privateKey` for server-side minting.
|
|
595
|
+
* Distinct from the Firebase Auth `appId` on `FirebaseAuthRoutingConfig`
|
|
596
|
+
* itself — App Check registers apps separately.
|
|
597
|
+
*/
|
|
598
|
+
appId: string;
|
|
599
|
+
/**
|
|
600
|
+
* Lifetime of the custom JWT signed for the `exchangeCustomToken`
|
|
601
|
+
* server-side mint, as a `jose` `setExpirationTime` duration string
|
|
602
|
+
* (e.g. `'1h'`, `'30m'`, `'7d'`). Defaults to `'1h'`. This is the custom
|
|
603
|
+
* token's own lifetime, not the resulting App Check token's — Firebase
|
|
604
|
+
* controls that separately. Google's custom-token minting generally
|
|
605
|
+
* rejects lifetimes beyond 1 hour regardless of what's set here, so
|
|
606
|
+
* values longer than `'1h'` are unlikely to have any practical effect —
|
|
607
|
+
* kept configurable in case that constraint changes.
|
|
608
|
+
*/
|
|
609
|
+
customTokenLifetime?: string;
|
|
558
610
|
}
|
|
559
611
|
export interface CookieAttributes {
|
|
560
612
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
4
4
|
"description": "Optimized Next Intl Package Special for App Router and Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -211,7 +211,8 @@
|
|
|
211
211
|
"homepage": "https://github.com/demian-ilnytskyi/cloudflare-next-intl#readme",
|
|
212
212
|
"dependencies": {
|
|
213
213
|
"@microsoft/clarity": "^1.0.2",
|
|
214
|
-
"firebase": "^12.17.0"
|
|
214
|
+
"firebase": "^12.17.0",
|
|
215
|
+
"jose": "^6.2.8"
|
|
215
216
|
},
|
|
216
217
|
"peerDependencies": {
|
|
217
218
|
"next": ">=12.0.0",
|