Roadbound-BRR/supabase/functions/auth-debug/index.ts

114 lines
3.3 KiB
TypeScript

import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { handleOptions, json } from "../_shared/http.ts";
const supabaseUrl = Deno.env.get("SUPABASE_URL");
const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY");
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error("SUPABASE_URL and SUPABASE_ANON_KEY are required");
}
type JwtClaims = {
sub?: string;
aud?: string | string[];
role?: string;
email?: string;
exp?: number;
iat?: number;
nbf?: number;
iss?: string;
[key: string]: unknown;
};
function parseJwtClaims(token: string): {
claims: JwtClaims | null;
decodeError: string | null;
partsCount: number;
} {
const parts = token.split(".");
if (parts.length !== 3) {
return {
claims: null,
decodeError: `JWT must have 3 parts, got ${parts.length}`,
partsCount: parts.length,
};
}
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padLen = (4 - (b64.length % 4)) % 4;
const padded = b64 + "=".repeat(padLen);
const decoded = atob(padded);
const claims = JSON.parse(decoded) as JwtClaims;
return { claims, decodeError: null, partsCount: parts.length };
} catch (error) {
return {
claims: null,
decodeError: error instanceof Error ? error.message : "Unknown decode error",
partsCount: parts.length,
};
}
}
function unixNow() {
return Math.floor(Date.now() / 1000);
}
Deno.serve(async (req) => {
const preflight = handleOptions(req);
if (preflight) return preflight;
if (req.method !== "GET" && req.method !== "POST") {
return json({ error: "Method not allowed" }, 405);
}
const authHeader = req.headers.get("Authorization");
const bearerPrefixOk = authHeader?.startsWith("Bearer ") ?? false;
const token = bearerPrefixOk ? authHeader!.slice(7).trim() : "";
const tokenPresent = token.length > 0;
const jwtParse = tokenPresent
? parseJwtClaims(token)
: { claims: null, decodeError: "Missing bearer token", partsCount: 0 };
const claims = jwtParse.claims;
const now = unixNow();
const authClient = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader ?? "" } },
});
const { data: userData, error: userError } = await authClient.auth.getUser();
const checks = {
authorizationHeaderPresent: authHeader != null,
bearerPrefixOk,
tokenPresent,
tokenLength: token.length,
tokenPreview: tokenPresent ? `${token.slice(0, 16)}...` : null,
jwtPartsCount: jwtParse.partsCount,
jwtDecodeError: jwtParse.decodeError,
claimSub: claims?.sub ?? null,
claimRole: claims?.role ?? null,
claimAud: claims?.aud ?? null,
claimEmail: claims?.email ?? null,
claimIssuer: claims?.iss ?? null,
claimExp: claims?.exp ?? null,
claimIat: claims?.iat ?? null,
claimNbf: claims?.nbf ?? null,
nowUnix: now,
isExpired:
typeof claims?.exp === "number" ? claims.exp <= now : null,
notYetValid:
typeof claims?.nbf === "number" ? claims.nbf > now : null,
authGetUserOk: !!userData.user && !userError,
authGetUserError: userError?.message ?? null,
authGetUserUserId: userData.user?.id ?? null,
authGetUserEmail: userData.user?.email ?? null,
};
return json({
function: "auth-debug",
verifyJwtDisabled: true,
checks,
});
});