133 lines
3.5 KiB
TypeScript
133 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
|
|
}
|
|
} |