2026-02-10 13:20:39 +01:00
|
|
|
/**
|
|
|
|
|
* Herramientas para gestionar todos los trabajos con JWT, los tipos
|
|
|
|
|
* definidos aquí deben de ser sufcientemente generales para usarse
|
|
|
|
|
* en todos los proyectos.
|
|
|
|
|
*
|
|
|
|
|
* Las cabeceras de un token y la firma son standard para todos, pero
|
|
|
|
|
* el body puede variar con contenido nuevo.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-27 12:12:40 +01:00
|
|
|
import { sign } from "node:crypto"
|
|
|
|
|
|
2026-02-10 13:20:39 +01:00
|
|
|
export interface IJWTService<T> {
|
|
|
|
|
/* Obtener un token de auth sin comprobar nada */
|
|
|
|
|
getNewAuthToken: () => Promise<JWTToken<T>>,
|
|
|
|
|
/* Obtener un token valido -> sino refrescar -> sin pillar un token nuevo */
|
|
|
|
|
getAccessToken: () => Promise<JWTToken<T>>,
|
|
|
|
|
/* Intenta refrescar el token actual */
|
|
|
|
|
tryRefreshToken: () => Promise<JWTToken<T>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 15:04:17 +01:00
|
|
|
export type JWTHeader = {
|
2026-02-10 13:20:39 +01:00
|
|
|
/* algoritmo de firma */
|
2026-01-26 15:04:17 +01:00
|
|
|
alg: string,
|
2026-02-10 13:20:39 +01:00
|
|
|
/* tipo de token */
|
2026-01-26 15:04:17 +01:00
|
|
|
typ: string,
|
2026-02-10 13:20:39 +01:00
|
|
|
/* key ID */
|
|
|
|
|
kid?: string
|
|
|
|
|
/* content type */
|
|
|
|
|
cty?: string
|
|
|
|
|
/**/
|
|
|
|
|
iss?: string,
|
|
|
|
|
sub?: string,
|
|
|
|
|
aud?: string,
|
|
|
|
|
jti?: string,
|
|
|
|
|
iat?: number,
|
|
|
|
|
exp?: number,
|
2026-01-26 15:04:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:20:39 +01:00
|
|
|
/*
|
|
|
|
|
* Define los campos basicos del payload de un token, en <T> se
|
|
|
|
|
* añaden los campos espeficos de la comunicacion
|
|
|
|
|
* */
|
2026-01-26 15:04:17 +01:00
|
|
|
export type JWTPayload<T> = {
|
|
|
|
|
/** (Issuer) Quién emitió el token */
|
|
|
|
|
iss?: string;
|
|
|
|
|
|
|
|
|
|
/** (Subject) De quién trata el token (ej. user_id) */
|
|
|
|
|
sub?: string;
|
|
|
|
|
|
|
|
|
|
/** (Audience) Destinatarios del token */
|
|
|
|
|
aud?: string | string[];
|
|
|
|
|
|
|
|
|
|
/** (Expiration Time) Fecha de expiración (Unix timestamp) */
|
|
|
|
|
exp?: number;
|
|
|
|
|
|
|
|
|
|
/** (Not Before) No válido antes de esta fecha (Unix timestamp) */
|
|
|
|
|
nbf?: number;
|
|
|
|
|
|
|
|
|
|
/** (Issued At) Cuándo fue emitido (Unix timestamp) */
|
|
|
|
|
iat?: number;
|
|
|
|
|
|
|
|
|
|
/** (JWT ID) Identificador único para este token */
|
|
|
|
|
jti?: string;
|
|
|
|
|
|
|
|
|
|
/** (Authentication Context Class) */
|
|
|
|
|
acr?: string
|
|
|
|
|
} & T
|
|
|
|
|
|
|
|
|
|
export type JWTSignature = {
|
|
|
|
|
e?: string,
|
|
|
|
|
ktv?: string,
|
|
|
|
|
n?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type JWT<T> = {
|
|
|
|
|
header: JWTHeader,
|
|
|
|
|
payload?: JWTPayload<T>,
|
|
|
|
|
signature: JWTSignature
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 12:12:40 +01:00
|
|
|
// todo pasar a la clase JWT
|
2026-02-10 13:20:39 +01:00
|
|
|
function signJWT(args: SignatureOptions) {
|
2026-01-27 12:12:40 +01:00
|
|
|
const signature = sign(
|
|
|
|
|
args.algorythm,
|
|
|
|
|
Buffer.from(args.data),
|
|
|
|
|
args.privateKey
|
|
|
|
|
)
|
2026-02-10 13:20:39 +01:00
|
|
|
return signature.toString("base64url")
|
2026-01-27 12:12:40 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:20:39 +01:00
|
|
|
export type SignatureOptions = {
|
|
|
|
|
algorythm: "sha256" | string,
|
|
|
|
|
data: string,
|
|
|
|
|
privateKey: string,
|
|
|
|
|
}
|
2026-01-26 15:04:17 +01:00
|
|
|
|
2026-02-10 13:20:39 +01:00
|
|
|
export class JWTToken<T> {
|
2026-01-26 15:04:17 +01:00
|
|
|
public rawToken: string
|
|
|
|
|
private decodedPayload: JWTPayload<T> | undefined
|
2026-02-10 13:20:39 +01:00
|
|
|
private signatureFunc = signJWT
|
2026-01-26 15:04:17 +01:00
|
|
|
|
|
|
|
|
constructor(
|
2026-02-10 13:20:39 +01:00
|
|
|
token: string,
|
|
|
|
|
externalSignatureFunc?: (args: SignatureOptions) => string
|
2026-01-26 15:04:17 +01:00
|
|
|
) {
|
|
|
|
|
this.rawToken = token
|
|
|
|
|
this.decodedPayload = this.decodePayload()
|
2026-02-10 13:20:39 +01:00
|
|
|
if (externalSignatureFunc != undefined) this.signatureFunc = externalSignatureFunc
|
2026-01-26 15:04:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-27 12:12:40 +01:00
|
|
|
public static fromParts<T>(args: {
|
|
|
|
|
header: JWTHeader,
|
|
|
|
|
payload?: JWTPayload<T>,
|
|
|
|
|
sigantureData?: {
|
|
|
|
|
privateKey: string,
|
|
|
|
|
algorythm: string
|
2026-02-10 13:20:39 +01:00
|
|
|
},
|
|
|
|
|
signatureFunc?: (args: SignatureOptions) => string
|
2026-01-27 12:12:40 +01:00
|
|
|
}) {
|
|
|
|
|
const strHeader = JSON.stringify(args.header)
|
|
|
|
|
const base64Header = Buffer.from(strHeader).toString("base64url")
|
2026-02-10 13:20:39 +01:00
|
|
|
const signatureFunc = args.signatureFunc ?? signJWT
|
|
|
|
|
|
2026-01-27 12:31:37 +01:00
|
|
|
let token = base64Header
|
2026-01-27 12:12:40 +01:00
|
|
|
|
|
|
|
|
if (args.payload != undefined) {
|
|
|
|
|
const strPayload = JSON.stringify(args.payload)
|
|
|
|
|
const base64payload = Buffer.from(strPayload).toString("base64url")
|
2026-01-27 12:31:37 +01:00
|
|
|
token += ("." + base64payload)
|
2026-01-27 12:12:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (args.sigantureData != undefined) {
|
2026-02-10 13:20:39 +01:00
|
|
|
const base64signature = signatureFunc({
|
2026-01-27 12:12:40 +01:00
|
|
|
algorythm: args.sigantureData.algorythm,
|
|
|
|
|
privateKey: args.sigantureData.privateKey,
|
2026-02-10 13:20:39 +01:00
|
|
|
data: token,
|
|
|
|
|
})
|
2026-01-27 12:31:37 +01:00
|
|
|
token += ("." + base64signature)
|
2026-01-27 12:12:40 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-27 12:31:37 +01:00
|
|
|
return token
|
2026-01-27 12:12:40 +01:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 15:04:17 +01:00
|
|
|
private decodePayload(): JWTPayload<T> {
|
|
|
|
|
if (this.rawToken == undefined) throw new Error("La clase no tiene un token definido")
|
|
|
|
|
const rawTokenPayload = this.rawToken.split(".")[1]
|
|
|
|
|
if (rawTokenPayload == undefined) throw new Error("El token no tiene payload")
|
|
|
|
|
return JSON.parse(Buffer.from(rawTokenPayload, "base64").toString("utf8"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public isExpired() {
|
|
|
|
|
if (this.decodedPayload == undefined) throw new Error("Error leyendo el payload del token")
|
|
|
|
|
const now = new Date()
|
|
|
|
|
const expirationDate = this.decodedPayload.exp
|
|
|
|
|
if (expirationDate == undefined) return false // un token sin fecha de expiracion no expira
|
|
|
|
|
if (expirationDate * 1000 <= now.getTime()) return true
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-02-10 13:20:39 +01:00
|
|
|
|
|
|
|
|
public isValid() {
|
|
|
|
|
throw new Error("No implementado")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public verifySignature() {
|
|
|
|
|
throw new Error("No implementado")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 15:04:17 +01:00
|
|
|
}
|
2026-02-10 13:20:39 +01:00
|
|
|
|