Base de JWT de Objenious

This commit is contained in:
2026-01-26 15:04:17 +01:00
parent d445faab99
commit 1d8af66564
11 changed files with 283 additions and 11 deletions

View File

@@ -0,0 +1,72 @@
export type JWTHeader = {
alg: string,
typ: string,
kid: string
}
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
}
export class JWTToken<T> {
public rawToken: string
private decodedPayload: JWTPayload<T> | undefined
constructor(
token: string
) {
this.rawToken = token
this.decodedPayload = this.decodePayload()
}
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
}
}

View File

@@ -0,0 +1,7 @@
export class HTTPClient {
constructor() {
// JWT?
}
}

View File

@@ -20,3 +20,9 @@ POSTGRES_PORT=5432
DEV_POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234
# claves de Objenious
OBJ_PEM_PATH=./obj.pem
OBJ_AUTHORIZATION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ
OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ
OBJ_CLIENT_ID=savefamily_rest_ws

View File

@@ -0,0 +1,118 @@
// PEM ?
import { env } from "#config/env";
import {
JWTToken
} from "#shared/domain/JWT"
import axios from "axios";
import { throwDeprecation } from "node:process";
type GrantAccessRequestBody = {
grant_type: string,
client_id: string,
client_assertion_type: string,
client_assertion: string
}
type GrantAccessRequestResponse = {
"access_token": string,
"expires_in": number,
"refresh_token": string
"refresh_expires_in": number,
"token_type": "Bearer" | string,
"not-before-policy": number,
"session_state": string,
"scope": string
}
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const REFRESH_BODY = {
...DEFAULT_BODY,
grant_type: "refresh_token",
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
/**
* El servicio gestiona un par de tokens auth - refresh para las
* operaciones de Objenious.
* Se puede partir de tokens existentes.
*/
export class JWTService {
// Igual no deberia mantener estado
private authToken?: JWTToken<{}>
private refreshToken?: JWTToken<{}>
constructor(args?: {
token?: string // si se partiese de un token existente,
refreshToken?: string
}) {
if (args?.token != undefined) this.authToken = new JWTToken(args.token)
if (args?.refreshToken != undefined) this.refreshToken = new JWTToken(args.refreshToken)
}
public async getAccessToken() {
if (this.authToken != undefined && !this.authToken.isExpired()) {
console.warn("Se está intentado conseguir un token sin expirar el anterior")
}
const req = axios.post(GET_TOKEN_URL,
DEFAULT_BODY,
{
headers: DEFAULT_HEADERS
}
)
let res;
try {
res = (await req).data as GrantAccessRequestResponse;
} catch (e) {
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
console.error(errorString, e)
throw new Error(errorString)
}
}
public async tryRefreshToken() {
if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido")
if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado")
const body = {
...REFRESH_BODY,
refresh_token: this.refreshToken.rawToken
}
const req = axios.post(REFRESH_TOKEN_URL,
body,
{
headers: DEFAULT_HEADERS
}
)
let res;
try {
res = (await req).data as GrantAccessRequestResponse;
} catch (e) {
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
console.error(errorString, e)
throw new Error(errorString)
}
}
}

View File

@@ -1,5 +1,8 @@
import { loadEnvFile } from "node:process";
loadEnvFile("../../.env")
loadEnvFile("../../../../.env") // Global
loadEnvFile("../../.env") // Especifica del servicio
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
@@ -18,5 +21,12 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
// ESPECIFICO DE OBJENIOUS
OBJ_PEM_PATH: String(process.env.OBJ_PEM_PATH),
OBJ_AUTHOIZATION: String(process.env.OBJ_ATHORIZATION),
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID)
};

View File

@@ -1,6 +1,16 @@
import { Request, Response } from "express"
import { SimUsecases } from "aplication/Sim.usecases"
// Partiendo del caracter 3 2 de pais + 2 de compañia
// Metiendolo a la BDD podria ser mas dinamico pero perderia
// tiempo de query
// Puede que esté bien crear un endpoint para administrarlo
const COMPAÑIASICCID = new Map<string, string>(
[
["3490", "alai"],
["3510", "nos"]
])
export class SimController {
private simUseCases: SimUsecases
@@ -18,7 +28,7 @@ export class SimController {
if (valido == false) return; // Si no es valido ya se ha enviado el error
const { iccid } = req.body
const compañia = "nos" // esto deberia ser un servcio
const compañia = this.compañiaFromIccid(iccid)
try {
await this.simUseCases.activation({ iccid, compañia })
@@ -159,4 +169,16 @@ export class SimController {
return valid;
}
/**
* A partir del iccid completo devuelve la compañia a la que pertenece
* @throws Error si no hay una compañia definida en COMPAÑIASICCID con el codigo
*/
private compañiaFromIccid(iccid: string) {
const caracteresCommpañia = iccid.slice(2, 6)
const compañia = COMPAÑIASICCID.get(caracteresCommpañia)
if (compañia == undefined) throw new Error("El la compañia es desconocida: " + caracteresCommpañia)
return compañia
}
}

View File

@@ -36,13 +36,12 @@ export class SimUsecases {
async activation(args: { iccid: string, compañia: string }) {
const activationEvent = <SimEvents.general>{
key: "sim.nos.activate",
key: `sim.${args.compañia}.activate`,
payload: {
iccid: args.iccid
}
}
console.log("publicando", activationEvent)
return this.eventBus.publish([activationEvent])
}