webpods
Version:
Append-only log service with OAuth authentication
151 lines • 5.19 kB
JavaScript
/**
* Hybrid authentication middleware supporting both WebPods and Hydra tokens
*/
import { verifyHydraToken } from "../oauth/jwt-validator.js";
import { createLogger } from "../logger.js";
import { getIpAddress } from "../utils.js";
const logger = createLogger("webpods:auth:hybrid");
/**
* Extract token from request
*/
function extractToken(req, currentPod) {
// For pod requests, prefer pod_token cookie
let token = currentPod
? req.cookies?.pod_token
: req.cookies?.token;
if (!token) {
token = req.cookies?.token; // Fallback to regular token
}
if (!token) {
const authHeader = req.headers.authorization;
if (authHeader) {
if (authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
else if (!authHeader.includes(" ")) {
token = authHeader;
}
}
}
return token;
}
/**
* Hybrid authentication middleware supporting both WebPods and Hydra tokens
*/
export async function authenticateHybrid(req, res, next) {
try {
const currentPod = req.pod_id || undefined;
const token = extractToken(req, currentPod);
if (!token) {
res.status(401).json({
error: {
code: "UNAUTHORIZED",
message: "Authentication required",
},
});
return;
}
req.ip_address = getIpAddress(req);
// Only support Hydra tokens now
logger.debug("Attempting to verify token", {
tokenPrefix: token.substring(0, 50),
pod: currentPod,
});
const hydraResult = await verifyHydraToken(token);
if (!hydraResult.success) {
logger.warn("Invalid token", { error: hydraResult.error });
res.status(401).json({ error: hydraResult.error });
return;
}
const payload = hydraResult.data;
// Check pod permissions if on a pod subdomain
if (currentPod) {
// Check both audience and ext.pods claims
// Handle nested ext structure from Hydra (ext.ext.pods)
const allowedPods = payload.ext?.pods || payload.ext?.ext?.pods || [];
const audience = payload.aud || [];
// For testing/development, accept both localhost and webpods.com audiences
const possibleAudiences = [
`https://${currentPod}.webpods.com`,
`http://${currentPod}.localhost:3000`,
`http://${currentPod}.localhost`,
];
// Token is valid if either:
// 1. The pod is in the ext.pods claim, OR
// 2. Any of the expected audiences is in the aud claim
const isAuthorized = allowedPods.includes(currentPod) ||
audience.some((aud) => possibleAudiences.includes(aud));
if (!isAuthorized) {
logger.warn("Hydra token not authorized for pod", {
currentPod,
allowedPods,
audience,
possibleAudiences,
});
res.status(403).json({
error: {
code: "POD_FORBIDDEN",
message: `Token not authorized for pod '${currentPod}'`,
},
});
return;
}
}
// Pod-level access means both read and write are allowed
// No need to check for specific permissions
// Attach Hydra auth info
req.auth = {
user_id: payload.sub,
client_id: payload.client_id,
pods: payload.ext?.pods,
scope: payload.scope,
};
req.auth_type = "hydra";
logger.debug("Hydra token authenticated", {
userId: payload.sub,
clientId: payload.client_id,
pods: payload.ext?.pods,
});
next();
}
catch (error) {
logger.error("Authentication error", { error });
res.status(500).json({
error: {
code: "INTERNAL_ERROR",
message: "Internal server error",
},
});
}
}
/**
* Optional hybrid authentication - doesn't require auth but extracts it if present
*/
export async function optionalAuthHybrid(req, _res, next) {
try {
req.ip_address = getIpAddress(req);
const token = extractToken(req);
if (!token) {
next();
return;
}
// Only support Hydra tokens
const hydraResult = await verifyHydraToken(token);
if (hydraResult.success) {
const payload = hydraResult.data;
req.auth = {
user_id: payload.sub,
client_id: payload.client_id,
pods: payload.ext?.pods,
scope: payload.scope,
};
req.auth_type = "hydra";
}
next();
}
catch (error) {
logger.error("Optional auth error", { error });
next();
}
}
//# sourceMappingURL=hybrid-auth.js.map