webpods
Version:
Append-only log service with OAuth authentication
111 lines • 3.59 kB
JavaScript
/**
* JWT validation for Hydra-issued tokens
*/
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
import { getJwksUrl, getHydraPublicUrl } from "./hydra-client.js";
import { createLogger } from "../logger.js";
const logger = createLogger("webpods:oauth:jwt");
// Create JWKS client with caching
const client = jwksClient({
jwksUri: getJwksUrl(),
cache: true,
cacheMaxAge: 600000, // 10 minutes
rateLimit: true,
jwksRequestsPerMinute: 10,
});
// Promisify the getSigningKey function
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
logger.error("Failed to get signing key", {
error: err,
kid: header.kid,
});
return callback(err);
}
const signingKey = key?.getPublicKey();
callback(null, signingKey);
});
}
/**
* Verify a Hydra-issued JWT token
*/
export async function verifyHydraToken(token) {
return new Promise((resolve) => {
jwt.verify(token, getKey, {
algorithms: ["RS256"],
issuer: getHydraPublicUrl() + "/",
}, (err, decoded) => {
if (err) {
logger.error("Token verification failed", {
error: err.message,
name: err.name,
issuer: getHydraPublicUrl() + "/",
});
if (err.name === "TokenExpiredError") {
resolve({
success: false,
error: {
code: "TOKEN_EXPIRED",
message: "Token has expired",
},
});
}
else if (err.name === "JsonWebTokenError") {
resolve({
success: false,
error: {
code: "INVALID_TOKEN",
message: "Invalid token",
},
});
}
else {
resolve({
success: false,
error: {
code: "TOKEN_ERROR",
message: err.message || "Token verification failed",
},
});
}
}
else {
const payload = decoded;
logger.debug("Token verified successfully", {
sub: payload.sub,
client_id: payload.client_id,
pods: payload.ext?.pods,
});
resolve({
success: true,
data: payload,
});
}
});
});
}
/**
* Check if a token is a Hydra JWT (vs WebPods JWT)
*/
export function isHydraToken(token) {
try {
// Decode without verification to check issuer
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
return false;
}
// Check if issuer is Hydra
const hydraUrl = getHydraPublicUrl();
const iss = decoded.payload?.iss;
// The token issuer is "http://localhost:4444/" but hydraUrl is "http://localhost:4444"
// We need to check both with and without trailing slash
const isHydra = iss?.startsWith(hydraUrl);
return isHydra;
}
catch {
return false;
}
}
//# sourceMappingURL=jwt-validator.js.map