114 lines
3.3 KiB
TypeScript
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,
|
|
});
|
|
});
|