f38014099e
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m16s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
133 lines
3.5 KiB
JavaScript
133 lines
3.5 KiB
JavaScript
/**
|
|
* Token Revocation System
|
|
* Provides ability to invalidate tokens before expiration
|
|
*/
|
|
|
|
const { db } = require('../database/db');
|
|
const logger = require('./logger');
|
|
|
|
/**
|
|
* Add a token to the revocation list
|
|
* @param {string} token - JWT token to revoke
|
|
* @param {string} reason - Reason for revocation
|
|
* @param {Object} metadata - Additional metadata
|
|
*/
|
|
async function revokeToken(token, reason, metadata = {}) {
|
|
try {
|
|
// Extract token info without full verification (it might be compromised)
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) {
|
|
throw new Error('Invalid token format');
|
|
}
|
|
|
|
// Decode payload
|
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
|
|
await db('revoked_tokens').insert({
|
|
token_id: payload.jti || `${payload.id}-${payload.iat}`, // JWT ID or fallback
|
|
user_id: payload.id,
|
|
token_type: payload.type,
|
|
revoked_at: new Date().toISOString(),
|
|
expires_at: new Date(payload.exp * 1000).toISOString(),
|
|
reason,
|
|
metadata: JSON.stringify(metadata)
|
|
});
|
|
|
|
logger.info('Token revoked', {
|
|
userId: payload.id,
|
|
tokenType: payload.type,
|
|
reason
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Failed to revoke token', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a token is revoked
|
|
* @param {Object} decodedToken - Decoded JWT payload
|
|
* @returns {boolean} - True if token is revoked
|
|
*/
|
|
async function isTokenRevoked(decodedToken) {
|
|
try {
|
|
const tokenId = decodedToken.jti || `${decodedToken.id}-${decodedToken.iat}`;
|
|
|
|
const revoked = await db('revoked_tokens')
|
|
.where('token_id', tokenId)
|
|
.orWhere((builder) => {
|
|
builder
|
|
.where('user_id', decodedToken.id)
|
|
.where('revoked_at', '<=', new Date(decodedToken.iat * 1000).toISOString());
|
|
})
|
|
.first();
|
|
|
|
return !!revoked;
|
|
} catch (error) {
|
|
logger.error('Failed to check token revocation', error);
|
|
// Fail closed - treat as revoked if we can't check
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Revoke all tokens for a user
|
|
* @param {number} userId - User ID
|
|
* @param {string} reason - Reason for revocation
|
|
*/
|
|
async function revokeAllUserTokens(userId, reason) {
|
|
try {
|
|
// This effectively revokes all tokens by setting a revocation time
|
|
// Any token issued before this time will be considered revoked
|
|
await db('user_token_revocations').insert({
|
|
user_id: userId,
|
|
revoked_at: new Date().toISOString(),
|
|
reason
|
|
}).onConflict('user_id').merge();
|
|
|
|
logger.info('All user tokens revoked', { userId, reason });
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Failed to revoke user tokens', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up expired revoked tokens
|
|
* Should be run periodically
|
|
*/
|
|
async function cleanupExpiredRevocations() {
|
|
try {
|
|
const deleted = await db('revoked_tokens')
|
|
.where('expires_at', '<', new Date().toISOString())
|
|
.delete();
|
|
|
|
if (deleted > 0) {
|
|
logger.info(`Cleaned up ${deleted} expired token revocations`);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to cleanup revoked tokens', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize cleanup job for expired revocations
|
|
*/
|
|
function initializeRevocationCleanup() {
|
|
// Run cleanup every 6 hours
|
|
setInterval(cleanupExpiredRevocations, 6 * 60 * 60 * 1000);
|
|
|
|
// Run initial cleanup
|
|
cleanupExpiredRevocations();
|
|
}
|
|
|
|
module.exports = {
|
|
revokeToken,
|
|
isTokenRevoked,
|
|
revokeAllUserTokens,
|
|
cleanupExpiredRevocations,
|
|
initializeRevocationCleanup
|
|
}; |