Server-side Validation (Node.js)
#
Complete SWT Validation
#
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
function validateSWT(token, payload, secret) {
try {
// Decode and validate JWT with algorithm allowlist
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256', 'HS384', 'HS512'] // Algorithm allowlist
});
// Verify token type is SWT
const header = jwt.decode(token, { complete: true }).header;
if (header.typ !== 'SWT') {
throw new Error('Invalid token type - must be SWT');
}
// Check required claims
if (!decoded.webhook || !decoded.webhook.event || !decoded.iss ||
!decoded.exp || !decoded.nbf || !decoded.iat || !decoded.jti) {
throw new Error('Missing required claims');
}
// Time validation with clock skew tolerance (~60 seconds)
const now = Math.floor(Date.now() / 1000);
const clockSkew = 60;
if (decoded.exp <= (now - clockSkew)) {
throw new Error('Token has expired');
}
if (decoded.nbf > (now + clockSkew)) {
throw new Error('Token not yet valid');
}
// Hash validation based on body presence
const hasBody = payload && Object.keys(payload).length > 0;
if (hasBody) {
// Non-empty body: hash field is REQUIRED
if (!decoded.webhook.hash) {
throw new Error('Hash field required for non-empty body');
}
const [algorithm, expectedHash] = decoded.webhook.hash.split(':');
// Convert algorithm name (e.g., 'sha-256' to 'sha256' for Node.js)
const nodeAlgorithm = algorithm.replace('-', '');
const actualHash = crypto.createHash(nodeAlgorithm)
.update(JSON.stringify(payload))
.digest('hex');
if (actualHash !== expectedHash) {
throw new Error('Payload hash mismatch');
}
} else {
// Empty body: hash field MUST be absent
if (decoded.webhook.hash) {
throw new Error('Hash field must be absent for empty body');
}
}
return decoded;
} catch (error) {
throw new Error(`SWT validation failed: ${error.message}`);
}
}
Token Creation
#
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
function createSWT(event, payload, secret, options = {}) {
const now = Math.floor(Date.now() / 1000);
const claims = {
webhook: {
event: event
},
iss: options.issuer || 'webhook-service.example.com',
exp: now + (options.expiresIn || 300), // Default: 5 minutes
nbf: now,
iat: now,
jti: crypto.randomUUID()
};
// Add optional subject
if (options.subject) {
claims.sub = options.subject;
}
// Add optional retry_count
if (options.retryCount !== undefined) {
claims.webhook.retry_count = options.retryCount;
}
// Add payload hash if body is non-empty (REQUIRED by spec)
if (payload && Object.keys(payload).length > 0) {
const hash = crypto.createHash('sha256')
.update(JSON.stringify(payload))
.digest('hex');
// Use standardized algorithm name with hyphen
claims.webhook.hash = `sha-256:${hash}`;
}
// Sign with typ: 'SWT' in header
return jwt.sign(claims, secret, {
algorithm: 'HS256',
header: { typ: 'SWT' }
});
}