Forward verified bearer identity to backend
This commit is contained in:
@@ -343,7 +343,7 @@ export function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") &&
|
backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") &&
|
||||||
backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_EMAIL_HEADER") &&
|
backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_EMAIL_HEADER") &&
|
||||||
backendAuthSource.indexOf("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") >
|
backendAuthSource.lastIndexOf("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") >
|
||||||
backendAuthSource.indexOf("const sessionUser = session?.user"),
|
backendAuthSource.indexOf("const sessionUser = session?.user"),
|
||||||
"backend proxy must forward Supabase session user id/email with the backend token so Python can skip duplicate /auth/v1/user validation",
|
"backend proxy must forward Supabase session user id/email with the backend token so Python can skip duplicate /auth/v1/user validation",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,4 +37,17 @@ export function runTests() {
|
|||||||
helperSource.includes('result.set("Content-Type", "application/json")'),
|
helperSource.includes('result.set("Content-Type", "application/json")'),
|
||||||
"backend auth must expose a safe JSON header builder",
|
"backend auth must expose a safe JSON header builder",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
helperSource.includes("getVerifiedBearerIdentity") &&
|
||||||
|
helperSource.includes("/auth/v1/user") &&
|
||||||
|
helperSource.includes("apikey: anonKey") &&
|
||||||
|
helperSource.includes("incomingAuth") &&
|
||||||
|
helperSource.includes("const identity = await getVerifiedBearerIdentity(incomingAuth)") &&
|
||||||
|
helperSource.includes("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, identity.userId)") &&
|
||||||
|
helperSource.includes("headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, identity.email)") &&
|
||||||
|
helperSource.includes("authUserId: identity?.userId || null") &&
|
||||||
|
helperSource.includes("authEmail: identity?.email || null"),
|
||||||
|
"backend auth helper must verify incoming Supabase bearer tokens and forward trusted user identity headers to backend services",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ type HeaderBuildOptions = {
|
|||||||
includeSupabaseIdentity?: boolean;
|
includeSupabaseIdentity?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type VerifiedBearerIdentity = {
|
||||||
|
email: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
function extractBearerToken(headerValue: string | null) {
|
function extractBearerToken(headerValue: string | null) {
|
||||||
if (!headerValue) return "";
|
if (!headerValue) return "";
|
||||||
const parts = headerValue.trim().split(/\s+/);
|
const parts = headerValue.trim().split(/\s+/);
|
||||||
@@ -27,6 +32,40 @@ function extractBearerToken(headerValue: string | null) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getVerifiedBearerIdentity(
|
||||||
|
accessToken: string,
|
||||||
|
): Promise<VerifiedBearerIdentity | null> {
|
||||||
|
const token = String(accessToken || "").trim();
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim();
|
||||||
|
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim();
|
||||||
|
if (!token || !supabaseUrl || !anonKey) return null;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${supabaseUrl.replace(/\/+$/, "")}/auth/v1/user`, {
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
apikey: anonKey,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const user = await res.json();
|
||||||
|
const userId = String(user?.id || "").trim();
|
||||||
|
if (!userId) return null;
|
||||||
|
return {
|
||||||
|
email: String(user?.email || "").trim(),
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasSupabaseSessionCookie(request: NextRequest) {
|
function hasSupabaseSessionCookie(request: NextRequest) {
|
||||||
return request.cookies.getAll().some((cookie) => {
|
return request.cookies.getAll().some((cookie) => {
|
||||||
const name = cookie.name.toLowerCase();
|
const name = cookie.name.toLowerCase();
|
||||||
@@ -54,11 +93,18 @@ export async function buildBackendRequestHeaders(
|
|||||||
const incomingAuth = extractBearerToken(request.headers.get("authorization"));
|
const incomingAuth = extractBearerToken(request.headers.get("authorization"));
|
||||||
if (incomingAuth) {
|
if (incomingAuth) {
|
||||||
headers.set("Authorization", `Bearer ${incomingAuth}`);
|
headers.set("Authorization", `Bearer ${incomingAuth}`);
|
||||||
|
const identity = await getVerifiedBearerIdentity(incomingAuth);
|
||||||
|
if (identity?.userId) {
|
||||||
|
headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, identity.userId);
|
||||||
|
if (identity.email) {
|
||||||
|
headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, identity.email);
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
headers,
|
headers,
|
||||||
response: null,
|
response: null,
|
||||||
authUserId: null,
|
authUserId: identity?.userId || null,
|
||||||
authEmail: null,
|
authEmail: identity?.email || null,
|
||||||
hasBearerAuth: true,
|
hasBearerAuth: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user