mirror of
https://github.com/tiennm99/llmapikey.git
synced 2026-09-03 18:16:57 +00:00
feat(auth): replace Supabase Auth with app-native GitHub OAuth
Self-contained GitHub OAuth (Arctic) with a stateless HS256 signed-cookie session (jose); Supabase is downgraded to the Postgres host only. - Origin-derived callback (no redirect-uri env); read:user scope; access token read once at callback and discarded (no token storage). - CSRF via single-use state cookie; open-redirect guard on next. - getCurrentGithubIdentity() now reads the session cookie, preserving the numeric provider_id identity contract for admin/dashboard/mint. - Remove @supabase/ssr + @supabase/supabase-js, middleware, and the supabase-dependent rls test; delete lib/supabase clients.
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
/**
|
||||
* Real-Supabase verification: the anon role must NOT be able to read api_keys.
|
||||
* Because `llmapikey` is unexposed to PostgREST, the anon client should get a
|
||||
* schema/permission error (or, defensively, zero rows) — never real data.
|
||||
*
|
||||
* Skips when Supabase env is absent so `npm test` passes in CI/local without
|
||||
* credentials. Run against a real project to actually exercise the invariant.
|
||||
*/
|
||||
test(
|
||||
"anon client cannot read llmapikey.api_keys",
|
||||
{ skip: !url || !anonKey ? "Supabase env not set" : false },
|
||||
async () => {
|
||||
const supabase = createClient(url, anonKey, {
|
||||
auth: { persistSession: false },
|
||||
});
|
||||
const { data, error } = await supabase
|
||||
.schema("llmapikey")
|
||||
.from("api_keys")
|
||||
.select("id");
|
||||
|
||||
// The schema is unexposed to PostgREST, so the anon client MUST get an
|
||||
// error (e.g. PGRST106). An empty array is NOT acceptable — that would mean
|
||||
// the schema became reachable, just with no rows visible.
|
||||
assert.notEqual(error, null, "anon must receive a hard error, not a result set");
|
||||
assert.equal(data, null, "anon must not receive any data array");
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
encodeSecret,
|
||||
signSessionToken,
|
||||
verifySessionToken,
|
||||
} from "../lib/auth/session-token.js";
|
||||
|
||||
const SECRET = encodeSecret("x".repeat(48));
|
||||
const IDENTITY = { githubUserId: "12345", githubUsername: "octocat" };
|
||||
|
||||
test("session token round-trips identity", async () => {
|
||||
const token = await signSessionToken(IDENTITY, SECRET);
|
||||
const decoded = await verifySessionToken(token, SECRET);
|
||||
assert.deepEqual(decoded, IDENTITY);
|
||||
});
|
||||
|
||||
test("tampered token verifies to null", async () => {
|
||||
const token = await signSessionToken(IDENTITY, SECRET);
|
||||
const tampered = token.slice(0, -2) + (token.endsWith("a") ? "bb" : "aa");
|
||||
assert.equal(await verifySessionToken(tampered, SECRET), null);
|
||||
});
|
||||
|
||||
test("token signed with a different secret verifies to null", async () => {
|
||||
const token = await signSessionToken(IDENTITY, SECRET);
|
||||
const otherSecret = encodeSecret("y".repeat(48));
|
||||
assert.equal(await verifySessionToken(token, otherSecret), null);
|
||||
});
|
||||
|
||||
test("non-numeric subject is rejected", async () => {
|
||||
const token = await signSessionToken(
|
||||
{ githubUserId: "octocat", githubUsername: "octocat" },
|
||||
SECRET,
|
||||
);
|
||||
assert.equal(await verifySessionToken(token, SECRET), null);
|
||||
});
|
||||
|
||||
test("expired token verifies to null", async () => {
|
||||
const { SignJWT } = await import("jose");
|
||||
const expired = await new SignJWT({ login: "octocat" })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject("12345")
|
||||
.setIssuedAt(0)
|
||||
.setExpirationTime(1) // epoch+1s — long past
|
||||
.sign(SECRET);
|
||||
assert.equal(await verifySessionToken(expired, SECRET), null);
|
||||
});
|
||||
|
||||
test("missing token verifies to null", async () => {
|
||||
assert.equal(await verifySessionToken(undefined, SECRET), null);
|
||||
assert.equal(await verifySessionToken("", SECRET), null);
|
||||
});
|
||||
|
||||
test("encodeSecret rejects short/missing secrets", () => {
|
||||
assert.throws(() => encodeSecret(undefined));
|
||||
assert.throws(() => encodeSecret("tooshort"));
|
||||
assert.doesNotThrow(() => encodeSecret("z".repeat(32)));
|
||||
});
|
||||
Reference in New Issue
Block a user