fix(csp): handle base64-encoded Clerk publishable keys
Some checks are pending
CI / build (push) Waiting to run
Some checks are pending
CI / build (push) Waiting to run
Investigation with the running container revealed the previous fix
was correct on the deployed server but didn't help the user because
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in their .env has the placeholder
'pk_test_...' from .env.example, not the readable form like
'pk_test_useful-louse-74-O42m8W' I tested against. The actual key is
in Clerk's older "encoded" format:
pk_test_dXNlZnVsLWxvdXNlLTc0LmNsZXJrLmFjY291bnRzLmRldiQ
The base64 portion decodes to the literal FAPI host
'useful-louse-74.clerk.accounts.dev' (with a trailing '$' separator),
which is exactly the host shown in the error message. So script-src
needs to allow exactly that host, and my previous regex only knew
the readable form.
clerkFrontendApiHost() now handles both formats:
Form 1 (encoded): pk_test_<base64slug>\$
/-> decode b64 /-> <slug>.clerk.accounts.dev
(or .clerk.services for ?)
Note: the encoded payload always carries the
literal hostname regardless of test/live; we
accept either well-known TLD suffix on the
decoded string.
Form 2 (readable): pk_test_<slug>-<randomSuffix>
/-> <slug>.clerk.accounts.dev
Captured greedily (slug may contain digits and
hyphens) — kept as a fallback.
Defensive fall-throughs ensure a string that decodes to garbage
(e.g. a readable-form key passed through the b64 regex) doesn't
silently return null — it falls through to form 2.
Verified against four cases:
pk_test_dXNlZnVsLWxvdXNlLTc0... -> useful-louse-74.clerk.accounts.dev ✓
pk_test_useful-louse-74-O42m8W -> useful-louse-74.clerk.accounts.dev ✓
pk_live_dXNlZnVsLWxvdXNlLTc0... -> useful-louse-74.clerk.accounts.dev ✓
pk_test_invalid-garbage -> invalid.clerk.accounts.dev (form 2)
The user must rebuild and redeploy for the new CSP header to take
effect — the previously-served header is cached in the running
container's standalone bundle and won't refresh until container
restart with the new build.
This commit is contained in:
parent
59988a597e
commit
dd0dd9c2dc
@ -5,15 +5,43 @@ const s3Host = process.env.S3_ENDPOINT
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Derive the Clerk frontend API host from the publishable key so the
|
// Derive the Clerk frontend API host from the publishable key so the
|
||||||
// CSP allowlist always matches the active environment.
|
// CSP allowlist always matches the active environment. Clerk supports
|
||||||
// Clerk key format is:
|
// two publishable key formats:
|
||||||
// pk_test_<slug>-<randomSuffix> -> <slug>.clerk.accounts.dev
|
//
|
||||||
// pk_live_<slug>-<randomSuffix> -> <slug>.clerk.services
|
// 1) "Encoded" form (older): pk_test_<base64slug>$
|
||||||
// The slug itself may contain hyphens and digits, so the suffix is
|
// The base64 portion decodes to "<slug>.clerk.accounts.dev" (test)
|
||||||
// only the final dash-group. The regex greedily captures the slug.
|
// or "<slug>.clerk.services" (live). Trailing '$' is a separator.
|
||||||
|
//
|
||||||
|
// 2) "Readable" form (newer): pk_test_<slug>-<randomSuffix>
|
||||||
|
// -> <slug>.clerk.accounts.dev (test) or <slug>.clerk.services.
|
||||||
|
// The slug may itself contain hyphens and digits, so only the
|
||||||
|
// final dash-group is captured as the suffix.
|
||||||
function clerkFrontendApiHost(): string | null {
|
function clerkFrontendApiHost(): string | null {
|
||||||
const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
|
|
||||||
|
// Form 1: base64-encoded FAPI URL. Match everything between the
|
||||||
|
// 'pk_test_'/'pk_live_' prefix and an optional trailing '$'.
|
||||||
|
const enc = key.match(/^pk_(test|live)_([A-Za-z0-9+/=_-]+)\$?$/);
|
||||||
|
if (enc) {
|
||||||
|
const b64 = enc[2].replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
if (/^[A-Za-z0-9+/=]+$/.test(b64)) {
|
||||||
|
try {
|
||||||
|
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
||||||
|
// If the decoded string doesn't look like a Clerk FAPI host
|
||||||
|
// (e.g. it's garbage from decoding a non-base64 readable-form
|
||||||
|
// key), fall through to form 2 rather than returning null.
|
||||||
|
const fapi = decodeFapiHost(decoded);
|
||||||
|
if (fapi) return fapi;
|
||||||
|
} catch {
|
||||||
|
// fall through to form 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fall through to form 2 if the slug is non-base64 (e.g. readable form)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form 2: readable slug + random suffix.
|
||||||
const m = key.match(/^pk_(test|live)_(.+?)-([a-z0-9]+)$/i);
|
const m = key.match(/^pk_(test|live)_(.+?)-([a-z0-9]+)$/i);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
const slug = m[2].toLowerCase();
|
const slug = m[2].toLowerCase();
|
||||||
@ -22,6 +50,20 @@ function clerkFrontendApiHost(): string | null {
|
|||||||
: `${slug}.clerk.services`;
|
: `${slug}.clerk.services`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeFapiHost(decoded: string): string | null {
|
||||||
|
// The decoded string is the FAPI host (e.g.
|
||||||
|
// 'useful-louse-74.clerk.accounts.dev$'). Note: the encoded base64
|
||||||
|
// payload always carries the literal FAPI host regardless of test vs
|
||||||
|
// live mode — both `pk_test_...` and `pk_live_...` can decode to an
|
||||||
|
// '.accounts.dev' host when the deployment is on the test endpoint.
|
||||||
|
// We accept either well-known Clerk FAPI host pattern.
|
||||||
|
const host = decoded.trim().replace(/\$$/, "").trim().toLowerCase();
|
||||||
|
if (host.endsWith(".clerk.accounts.dev") || host.endsWith(".clerk.services")) {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const clerkFapiHost = clerkFrontendApiHost();
|
const clerkFapiHost = clerkFrontendApiHost();
|
||||||
|
|
||||||
const csp = [
|
const csp = [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user