- src/server/nessa-auth.ts: replace jose HS256 sign/verify with Clerk session JWT verification via @clerk/backend verifyToken (RS256/JWKS). signNessaToken removed — frontend now supplies Clerk session tokens. - src/server/api/utils.ts: createTRPCContext verifies Clerk JWT, resolves ctx.nessaUserId via SELECT id FROM users WHERE clerkUserId=? on the shared NessaConnectionFactory. Lookup miss throws typed UNAUTHORIZED (webhook has not run yet). Invalid/expired tokens are swallowed; the enforceNessaUser middleware rejects null nessaUserId. - src/server/api/routers/nessa-community-authz.test.ts: add clerkUserId lookup tests (seeded match, missing row, mismatched id, local≠clerk). - src/server/nessa-auth.test.ts: verifyNessaToken unit tests with mocked @clerk/backend (valid sub, missing sub, malformed/expired/wrong-signature rejection) plus static audit that signNessaToken is gone. - src/server/clerk-user-webhook.ts + src/routes/api/clerk-webhook.ts: Clerk user.created/user.updated webhook handler (Svix signature verification, idempotent upsert by clerkUserId, lazy ALTER TABLE migration) with full test suite. - src/server/api/routers/nessa.ts: remove legacy register/login/google/ apple sign-in mutations (Clerk is now the sole identity provider). - src/env/server.ts: add NESSA_CLERK_SECRET, NESSA_CLERK_JWT_ISSUER, NESSA_CLERK_WEBHOOK_SECRET; NESSA_JWT_SECRET moved to optional. - package.json: add @clerk/backend, svix; lineage/auth.test.ts and nessa-ownership.test.ts: add Clerk env vars to env mocks. - .env.example: document Clerk config vars and rotation. - delete nessa-google-oauth.test.ts (Google auth removed). ctx.nessaUserId remains the local users.id — router bodies are untouched.
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
// ───────────────────────────────────────────────────────────────────────
|
|
// Nessa auth — Clerk session JWT verification (RS256 / JWKS)
|
|
//
|
|
// Migrated from self-signed HS256 tokens to Clerk session token
|
|
// verification. Incoming `Authorization: Bearer <token>` headers are
|
|
// verified against Clerk's JWKS endpoint via `@clerk/backend`.
|
|
//
|
|
// Public API is unchanged so callers need not be modified:
|
|
// * verifyNessaToken(token) → { sub, exp?, iat? }
|
|
// * NessaAuthPayload type
|
|
//
|
|
// signNessaToken was removed — the frontend now supplies Clerk session
|
|
// tokens directly; the backend only verifies.
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
|
|
import { verifyToken } from "@clerk/backend";
|
|
import { env } from "~/env/server";
|
|
|
|
export type NessaAuthPayload = {
|
|
sub: string; // Clerk user id
|
|
exp?: number;
|
|
iat?: number;
|
|
};
|
|
|
|
/**
|
|
* Verify a Clerk session JWT and return the subject (user id).
|
|
*
|
|
* Uses the Clerk Backend API secret key to fetch the JWKS and verify the
|
|
* RS256 signature. Rejects expired, malformed, or improperly signed tokens.
|
|
*/
|
|
export async function verifyNessaToken(
|
|
token: string
|
|
): Promise<NessaAuthPayload> {
|
|
const payload = await verifyToken(token, {
|
|
secretKey: env.NESSA_CLERK_SECRET,
|
|
// Optional: restrict to specific issuers / apps
|
|
// audience: env.NESSA_CLERK_JWT_ISSUER,
|
|
});
|
|
|
|
if (!payload.sub) {
|
|
throw new Error("Missing subject in Clerk session token");
|
|
}
|
|
|
|
return {
|
|
sub: payload.sub,
|
|
exp: payload.exp,
|
|
iat: payload.iat,
|
|
};
|
|
}
|