/**
* Token-based authentication
*
* Supports multiple modes:
* 1. Simple tokens (for local/dev use): base64(JSON({webId, iat, exp})) + HMAC signature
* 2. Solid-OIDC DPoP tokens (for federation): verified via external IdP JWKS
* 3. Nostr NIP-98 tokens: Schnorr signatures, returns did:nostr identity
*/
import crypto from 'crypto';
import { verifySolidOidc, hasSolidOidcAuth } from './solid-oidc.js';
import { verifyNostrAuth, hasNostrAuth } from './nostr.js';
import { webIdTlsAuth, hasClientCertificate } from './webid-tls.js';
import { resolveTokenSecret } from './token-secret.js';
// Initialize secret once at module load. See token-secret.js for the
// resolution order (env ~/.jss/token.secret exit-or-ephemeral).
const SECRET = resolveTokenSecret();
/**
* Create a simple token for a WebID
* @param {string} webId - The WebID to create token for
* @param {number} [expiresIn] - Expiration time in seconds (default: no expiry)
* @returns {string} Token string
*/
export function createToken(webId, expiresIn) {
const payload = {
webId,
iat: Math.floor(Date.now() / 1000),
};
if (expiresIn !== undefined && expiresIn > 0) {
payload.exp = Math.floor(Date.now() / 1000) + expiresIn;
}
const data = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signature = crypto
.createHmac('sha256', SECRET)
.update(data)
.digest('base64url');
return `${data}.${signature}`;
}
/**
* Verify and decode a simple token (2-part HMAC-signed)
*
* SECURITY: Only accepts 2-part simple tokens signed with HMAC.
* JWT tokens (3-part) require async verification via verifyTokenAsync().
*
* @param {string} token - The token to verify
* @returns {{webId: string, iat: number, exp?: number} | null} Decoded payload or null
*/
export function verifyToken(token) {
if (!token || typeof token !== 'string') {
return null;
}
const parts = token.split('.');
// JWT tokens (3 parts) require async verification - reject in sync function
if (parts.length === 3) {
return null;
}
if (parts.length !== 2) {
return null;
}
const [data, signature] = parts;
// Verify HMAC signature
const expectedSig = crypto
.createHmac('sha256', SECRET)
.update(data)
.digest('base64url');
// Constant-time comparison to prevent timing attacks
try {
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
return null;
}
} catch {
// If lengths don't match, timingSafeEqual throws
return null;
}
// Decode payload
try {
const payload = JSON.parse(Buffer.from(data, 'base64url').toString());
// Check expiration
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
return null;
}
return payload;
} catch {
return null;
}
}
/**
* Verify a JWT token from the credentials endpoint
* Properly verifies signature against IdP's JWKS
*
* @param {string} token - JWT token
* @returns {Promise}
*/
async function verifyJwtFromIdp(token) {
try {
// Dynamically import to avoid circular dependencies
const { getPublicJwks } = await import('../idp/keys.js');
const jose = await import('jose');
const jwks = await getPublicJwks();
if (!jwks || !jwks.keys || jwks.keys.length === 0) {
return null;
}
// Create JWKS for verification
const keySet = jose.createLocalJWKSet(jwks);
// Verify the token
const { payload } = await jose.jwtVerify(token, keySet, {
// Allow some clock skew
clockTolerance: 60,
});
// Extract webid claim
const webId = payload.webid || payload.webId || payload.sub;
if (!webId) {
return null;
}
return {
webId,
iat: payload.iat,
exp: payload.exp
};
} catch (err) {
// Verification failed - invalid signature, expired, etc.
return null;
}
}
/**
* Extract token from Authorization header
* @param {string} authHeader - Authorization header value
* @returns {string | null} Token or null
*/
export function extractToken(authHeader) {
if (!authHeader || typeof authHeader !== 'string') {
return null;
}
// Support "Bearer " format
if (authHeader.startsWith('Bearer ')) {
return authHeader.slice(7);
}
// Also support raw token
return authHeader;
}
/**
* Extract WebID from request (sync version for simple tokens only)
* @param {object} request - Fastify request object
* @returns {string | null} WebID or null if not authenticated
*/
export function getWebIdFromRequest(request) {
const authHeader = request.headers.authorization;
// Skip DPoP tokens - use async version for those
if (authHeader && authHeader.startsWith('DPoP ')) {
return null;
}
// Skip Nostr tokens - use async version for those
if (authHeader && authHeader.startsWith('Nostr ')) {
return null;
}
const token = extractToken(authHeader);
if (!token) {
return null;
}
const payload = verifyToken(token);
return payload?.webId || null;
}
/**
* Extract WebID from request (async version supporting Solid-OIDC)
* @param {object} request - Fastify request object
* @returns {Promise}
*/
export async function getWebIdFromRequestAsync(request) {
const authHeader = request.headers.authorization;
// Try Authorization header methods first
if (authHeader) {
// Try Solid-OIDC first (DPoP tokens)
if (hasSolidOidcAuth(request)) {
return verifySolidOidc(request);
}
// Try Nostr NIP-98 (Schnorr signatures)
if (hasNostrAuth(request)) {
return verifyNostrAuth(request);
}
// Fall back to Bearer tokens
const token = extractToken(authHeader);
if (token) {
// Try simple 2-part token first
const payload = verifyToken(token);
if (payload?.webId) {
return { webId: payload.webId, error: null };
}
// If 3-part JWT, verify against IdP's JWKS
const parts = token.split('.');
if (parts.length === 3) {
const jwtPayload = await verifyJwtFromIdp(token);
if (jwtPayload?.webId) {
return { webId: jwtPayload.webId, error: null };
}
return { webId: null, error: 'Invalid or unverifiable JWT token' };
}
return { webId: null, error: 'Invalid token' };
}
}
// Try WebID-TLS (client certificate authentication)
// This works even without Authorization header
if (hasClientCertificate(request)) {
try {
const webId = await webIdTlsAuth(request);
if (webId) {
return { webId, error: null };
}
// Certificate present but verification failed
return { webId: null, error: 'WebID-TLS certificate verification failed' };
} catch (err) {
return { webId: null, error: `WebID-TLS error: ${err.message}` };
}
}
return { webId: null, error: null };
}