137 lines
3.7 KiB
JavaScript
137 lines
3.7 KiB
JavaScript
/**
|
|
* JWT Authentication Utilities
|
|
*
|
|
* Provides functions for creating, validating, and decoding JWT tokens
|
|
* Used by both backend OAuth proxy and frontend storage/validation
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Try to use native crypto, fallback to import
|
|
let jwt;
|
|
try {
|
|
jwt = await import('jsonwebtoken');
|
|
} catch (err) {
|
|
console.warn('jsonwebtoken not installed, using mock implementation');
|
|
jwt = createMockJWT();
|
|
}
|
|
|
|
function createMockJWT() {
|
|
return {
|
|
sign: (payload, secret, options) => {
|
|
// Mock JWT: base64(header).base64(payload).base64(signature)
|
|
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString(
|
|
'base64'
|
|
);
|
|
const body = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
const signature = Buffer.from('mock-signature').toString('base64');
|
|
return `${header}.${body}.${signature}`;
|
|
},
|
|
verify: (token, secret) => {
|
|
try {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) throw new Error('Invalid token format');
|
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
|
|
// Check expiration
|
|
if (payload.exp && Date.now() >= payload.exp * 1000) {
|
|
throw new Error('Token expired');
|
|
}
|
|
|
|
return payload;
|
|
} catch (err) {
|
|
throw new Error(`Invalid token: ${err.message}`);
|
|
}
|
|
},
|
|
decode: (token) => {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return null;
|
|
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a JWT token for admin access
|
|
* @param {Object} payload - Token payload (user data, permissions, etc.)
|
|
* @param {string} secret - Secret key for signing
|
|
* @param {number} expiresIn - Expiration time in seconds (default: 7 days)
|
|
* @returns {string} JWT token
|
|
*/
|
|
export function createToken(payload, secret, expiresIn = 7 * 24 * 60 * 60) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
return jwt.sign(
|
|
{
|
|
...payload,
|
|
iat: now,
|
|
exp: now + expiresIn
|
|
},
|
|
secret,
|
|
{ algorithm: 'HS256' }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify and decode a JWT token
|
|
* @param {string} token - JWT token to verify
|
|
* @param {string} secret - Secret key for verification
|
|
* @returns {Object} Decoded token payload
|
|
* @throws {Error} If token is invalid or expired
|
|
*/
|
|
export function verifyToken(token, secret) {
|
|
return jwt.verify(token, secret);
|
|
}
|
|
|
|
/**
|
|
* Decode a JWT token without verification (use with caution)
|
|
* @param {string} token - JWT token to decode
|
|
* @returns {Object|null} Decoded token payload or null if invalid
|
|
*/
|
|
export function decodeToken(token) {
|
|
return jwt.decode(token);
|
|
}
|
|
|
|
/**
|
|
* Check if a token is expired
|
|
* @param {string} token - JWT token
|
|
* @returns {boolean} True if token is expired
|
|
*/
|
|
export function isTokenExpired(token) {
|
|
try {
|
|
const decoded = jwt.decode(token);
|
|
if (!decoded || !decoded.exp) return true;
|
|
return Date.now() >= decoded.exp * 1000;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get remaining time before token expiration
|
|
* @param {string} token - JWT token
|
|
* @returns {number} Milliseconds until expiration, or 0 if expired
|
|
*/
|
|
export function getTokenExpiresIn(token) {
|
|
try {
|
|
const decoded = jwt.decode(token);
|
|
if (!decoded || !decoded.exp) return 0;
|
|
const remaining = decoded.exp * 1000 - Date.now();
|
|
return Math.max(0, remaining);
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a random secret for testing (NOT for production)
|
|
* @returns {string} Random secret
|
|
*/
|
|
export function generateSecret() {
|
|
return Buffer.from(Math.random().toString()).toString('base64');
|
|
}
|