UNPKG

@tidecloak/verify

Version:

A lightweight utility for server-side verification of TideCloak-issued JSON Web Tokens (JWTs).

76 lines (75 loc) 4.31 kB
import { jwtVerify, createLocalJWKSet, createRemoteJWKSet } from "jose"; /** * Signature algorithms TideCloak realms issue tokens with. Pinning these prevents * algorithm-confusion attacks: without an explicit allowlist `jose` accepts any * algorithm a key in the set can validate (and falls back to permissive behaviour * for keys that omit an `alg`). Override via `config.tokenSignatureAlgorithms`. */ const DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512", "EdDSA"]; /** * Verify a TideCloak-issued JWT on the server side using your imported config object. * * @param {object} config - Imported TideCloak configuration (parsed JSON). May also * carry optional `tokenSignatureAlgorithms` (string[]) and `clockTolerance` * (number of seconds, or a jose duration string) to tune verification. * @param {string} token - access token to verify. * @param {string[]} [allowedRoles] - Array of Tidecloak realm or client roles; user must have at least one. * @returns {Promise<object|null>} - The token payload if valid and role-check passes, otherwise null. */ export async function verifyTideCloakToken(config, token, allowedRoles = []) { var _a, _b, _c, _d; try { // Ensure token is provided if (!token) { throw new Error("No token provided"); } // Ensure config is provided if (!config || Object.keys(config).length === 0) { throw new Error("Could not load TideCloak configuration"); } // Construct issuer URL (ensure slash before 'realms') const baseUrl = config["auth-server-url"]; const sep = baseUrl.endsWith("/") ? "" : "/"; const issuer = `${baseUrl}${sep}realms/${config.realm}`; // Determine JWK set (use local JWKs if provided, otherwise fetch remotely) const jwkSet = config.jwk ? createLocalJWKSet(config.jwk) : createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`)); // Verify signature (with a pinned algorithm allowlist), issuer and time claims. // `clockTolerance` allows a small amount of clock drift between issuer and verifier. const algorithms = Array.isArray(config.tokenSignatureAlgorithms) && config.tokenSignatureAlgorithms.length > 0 ? config.tokenSignatureAlgorithms : DEFAULT_ALLOWED_ALGORITHMS; const { payload } = await jwtVerify(token, jwkSet, { issuer, algorithms, clockTolerance: (_a = config.clockTolerance) !== null && _a !== void 0 ? _a : "5s", }); // Verify authorized party (client). Only enforced when a `resource` (client id) // is configured; without this guard an undefined client would reject every token. const client = config["resource"]; if (client !== undefined && client !== null && payload.azp !== client) { throw new Error(`AZP mismatch: expected '${client}', got '${payload.azp}'`); } // Gather all user roles from realm and client roles for the specified resource from the config. const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || []; const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[client]) === null || _d === void 0 ? void 0 : _d.roles) || []; const allRoles = new Set([...realmRoles, ...clientRoles]); // If allowedRoles specified, ensure at least one match if (allowedRoles.length > 0) { const hasAllowed = allowedRoles.some(role => allRoles.has(role)); if (!hasAllowed) { throw new Error(`Role match failed: user roles [${[...allRoles].join(", ")}] do not include any of [${allowedRoles.join(", ")}]`); } } return payload; } catch (err) { // Log only the message (not the error object, which can echo token-derived // data). Note: this collapses both invalid tokens and infrastructure failures // (e.g. an unreachable JWKS endpoint) into a `null` result, so callers cannot // distinguish "forbidden" from "auth backend down". console.error("[TideJWT] Token verification failed:", err instanceof Error ? err.message : err); return null; } }