feat: zitadel token auth middleware
This commit is contained in:
59
server/utils/auth.ts
Normal file
59
server/utils/auth.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createRemoteJWKSet, jwtVerify, type JWTPayload, type JWTVerifyGetKey } from "jose";
|
||||
|
||||
type KeyInput = Parameters<typeof jwtVerify>[1];
|
||||
|
||||
export interface VerifierConfig {
|
||||
issuer: string;
|
||||
/** Token is accepted if its `aud` includes at least one of these. */
|
||||
audiences: string[];
|
||||
/** Token `sub` must be one of these (the single owner). */
|
||||
allowedSubs: string[];
|
||||
/** Test seam: resolve the verification key directly instead of fetching JWKS. */
|
||||
keyResolver?: (token: string) => Promise<KeyInput>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Zitadel access-token verifier. Verifies signature + issuer + expiry via JWKS,
|
||||
* then enforces audience and owner-sub allowlists. Throws on any failure.
|
||||
*/
|
||||
export function makeVerifier(cfg: VerifierConfig) {
|
||||
const jwks: JWTVerifyGetKey | null = cfg.keyResolver
|
||||
? null
|
||||
: createRemoteJWKSet(new URL(`${cfg.issuer}/oauth/v2/keys`));
|
||||
|
||||
return async function verify(token: string): Promise<JWTPayload> {
|
||||
const key: KeyInput = cfg.keyResolver ? await cfg.keyResolver(token) : (jwks as JWTVerifyGetKey);
|
||||
const { payload } = await jwtVerify(token, key, { issuer: cfg.issuer });
|
||||
|
||||
const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
|
||||
if (!cfg.audiences.some((a) => aud.includes(a))) {
|
||||
throw new Error("token audience not accepted");
|
||||
}
|
||||
if (!cfg.allowedSubs.includes(String(payload.sub))) {
|
||||
throw new Error("subject not allowed");
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
}
|
||||
|
||||
function splitCsv(v: unknown): string[] {
|
||||
return String(v ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
let _cached: ReturnType<typeof makeVerifier> | null = null;
|
||||
|
||||
/** Process-wide verifier built from runtime config (Nitro server context). */
|
||||
export function getVerifier() {
|
||||
if (!_cached) {
|
||||
const c = useRuntimeConfig();
|
||||
_cached = makeVerifier({
|
||||
issuer: c.zitadelIssuer,
|
||||
audiences: splitCsv(c.zitadelAudience),
|
||||
allowedSubs: splitCsv(c.allowedUserIds),
|
||||
});
|
||||
}
|
||||
return _cached;
|
||||
}
|
||||
Reference in New Issue
Block a user