134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
/**
|
|
* Utilitários JWT com verificação de expiração padronizada
|
|
* Clock skew tolerance de 60 segundos para compensar diferenças de tempo
|
|
*/
|
|
|
|
interface JWTPayload {
|
|
exp?: number;
|
|
iat?: number;
|
|
[key: string]: any;
|
|
}
|
|
|
|
const CLOCK_SKEW_SECONDS = 60;
|
|
|
|
/**
|
|
* Parse JWT token payload sem validação de assinatura
|
|
* @param token JWT token
|
|
* @returns Payload decodificado ou null se inválido
|
|
*/
|
|
export function parseJwt(token: string): JWTPayload | null {
|
|
try {
|
|
const base64Url = token.split(".")[1];
|
|
if (!base64Url) return null;
|
|
|
|
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
const jsonPayload = decodeURIComponent(
|
|
atob(base64)
|
|
.split("")
|
|
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
|
.join(""),
|
|
);
|
|
|
|
return JSON.parse(jsonPayload);
|
|
} catch (error) {
|
|
console.warn("[JWT] Erro ao fazer parse do token:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifica se token está expirado com tolerância de clock skew
|
|
* @param token JWT token ou timestamp de expiração em segundos
|
|
* @returns true se expirado
|
|
*/
|
|
export function isExpired(token: string | number): boolean {
|
|
try {
|
|
let expTimestamp: number;
|
|
|
|
if (typeof token === "string") {
|
|
const payload = parseJwt(token);
|
|
if (!payload?.exp) {
|
|
console.warn("[JWT] Token sem claim exp, considerando válido");
|
|
return false;
|
|
}
|
|
expTimestamp = payload.exp;
|
|
} else {
|
|
expTimestamp = token;
|
|
}
|
|
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
const isExpiredValue = nowSeconds >= expTimestamp + CLOCK_SKEW_SECONDS;
|
|
|
|
console.log("[JWT] Verificação de expiração:", {
|
|
nowSeconds,
|
|
expTimestamp,
|
|
clockSkew: CLOCK_SKEW_SECONDS,
|
|
isExpired: isExpiredValue,
|
|
timeUntilExpiry: expTimestamp - nowSeconds,
|
|
});
|
|
|
|
return isExpiredValue;
|
|
} catch (error) {
|
|
console.warn("[JWT] Erro na verificação de expiração:", error);
|
|
return true; // Assumir expirado em caso de erro
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifica se token deve ser renovado (expira em menos de 5 minutos)
|
|
* @param token JWT token ou timestamp de expiração em segundos
|
|
* @returns true se deve renovar
|
|
*/
|
|
export function shouldRefresh(token: string | number): boolean {
|
|
try {
|
|
let expTimestamp: number;
|
|
|
|
if (typeof token === "string") {
|
|
const payload = parseJwt(token);
|
|
if (!payload?.exp) return false;
|
|
expTimestamp = payload.exp;
|
|
} else {
|
|
expTimestamp = token;
|
|
}
|
|
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
const refreshThreshold = 5 * 60; // 5 minutos
|
|
const shouldRefreshValue = nowSeconds >= expTimestamp - refreshThreshold;
|
|
|
|
console.log("[JWT] Verificação de renovação:", {
|
|
nowSeconds,
|
|
expTimestamp,
|
|
refreshThreshold,
|
|
shouldRefresh: shouldRefreshValue,
|
|
timeUntilRefresh: expTimestamp - refreshThreshold - nowSeconds,
|
|
});
|
|
|
|
return shouldRefreshValue;
|
|
} catch (error) {
|
|
console.warn("[JWT] Erro na verificação de renovação:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extrai informações úteis do token
|
|
* @param token JWT token
|
|
* @returns Informações do token ou null
|
|
*/
|
|
export function getTokenInfo(token: string): {
|
|
payload: JWTPayload;
|
|
isExpired: boolean;
|
|
shouldRefresh: boolean;
|
|
expiresAt: Date | null;
|
|
} | null {
|
|
const payload = parseJwt(token);
|
|
if (!payload) return null;
|
|
|
|
return {
|
|
payload,
|
|
isExpired: isExpired(token),
|
|
shouldRefresh: shouldRefresh(token),
|
|
expiresAt: payload.exp ? new Date(payload.exp * 1000) : null,
|
|
};
|
|
}
|