From 1d8af66564c5dd0a5d82fe7ebc998eb8e268fc7f Mon Sep 17 00:00:00 2001 From: Alvar San Martin Date: Mon, 26 Jan 2026 15:04:17 +0100 Subject: [PATCH] Base de JWT de Objenious --- .gitignore | 2 + README.md | 18 ++- package.json | 1 + packages/shared/domain/JWT.ts | 72 +++++++++++ packages/shared/infrastructure/HTTPClient.ts | 7 ++ packages/sim-consumidor-activaciones/.env | 6 + .../aplication/JWT.service.ts | 118 ++++++++++++++++++ .../config/env/index.ts | 12 +- .../aplication/Sim.controller.ts | 24 +++- .../aplication/Sim.usecases.ts | 3 +- yarn.lock | 31 ++++- 11 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 packages/shared/domain/JWT.ts create mode 100644 packages/shared/infrastructure/HTTPClient.ts create mode 100644 packages/sim-consumidor-activaciones/aplication/JWT.service.ts diff --git a/.gitignore b/.gitignore index f03eaec..24998cf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ node_modules #!.yarn/cache .pnp.* + +*.pem diff --git a/README.md b/README.md index b182332..d6210f6 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,19 @@ tener una compañía especificada. definir cada cola en el worker que la consuma para poder añadir workers sin parar el RMQ. - [ ] Versionado de la API. -- [ ] Metodo para sacar la compañia a partir del iccid, o bucar en la - bdd si no es posible. +- [ ] Método para sacar la compañía a partir del iccid, o buscar en la + BDD si no es posible. +- [ ] Cola de mensajes que no se han podido procesar. Distinguir según + error de red; se reintenta; o error del propio mensaje; se envía + a la cola de errores. -## Version con consumidores basados en la compañia +## Versión con consumidores basados en la compañia -El servicio que recibe las peticiones tiene que encargarse de difrenciar -las compañias, en principio se podría sin consultar la bdd si los caracteres -5 y 6 son consistentes para las compañias. +El servicio que recibe las peticiones tiene que encargarse de diferenciar +las compañías, en principio se podría sin consultar la bdd si los caracteres +5 y 6 son consistentes para las compañías. + +ALAI: (34)9090 +NOS: (35)1031 [./imgs/diagrama-servicios-sim-v2.png] diff --git a/package.json b/package.json index 9364828..d7def61 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@tsconfig/node22": "^22.0.5", + "axios": "^1.13.3", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", diff --git a/packages/shared/domain/JWT.ts b/packages/shared/domain/JWT.ts new file mode 100644 index 0000000..9a45040 --- /dev/null +++ b/packages/shared/domain/JWT.ts @@ -0,0 +1,72 @@ +export type JWTHeader = { + alg: string, + typ: string, + kid: string +} + +export type JWTPayload = { + /** (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 = { + header: JWTHeader, + payload?: JWTPayload, + signature: JWTSignature +} + +export class JWTToken { + + public rawToken: string + private decodedPayload: JWTPayload | undefined + + constructor( + token: string + ) { + this.rawToken = token + this.decodedPayload = this.decodePayload() + } + + private decodePayload(): JWTPayload { + 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 + } +} diff --git a/packages/shared/infrastructure/HTTPClient.ts b/packages/shared/infrastructure/HTTPClient.ts new file mode 100644 index 0000000..f8c164e --- /dev/null +++ b/packages/shared/infrastructure/HTTPClient.ts @@ -0,0 +1,7 @@ +export class HTTPClient { + + constructor() { + // JWT? + } + +} diff --git a/packages/sim-consumidor-activaciones/.env b/packages/sim-consumidor-activaciones/.env index 28a8d8c..79929fa 100644 --- a/packages/sim-consumidor-activaciones/.env +++ b/packages/sim-consumidor-activaciones/.env @@ -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 diff --git a/packages/sim-consumidor-activaciones/aplication/JWT.service.ts b/packages/sim-consumidor-activaciones/aplication/JWT.service.ts new file mode 100644 index 0000000..fefa638 --- /dev/null +++ b/packages/sim-consumidor-activaciones/aplication/JWT.service.ts @@ -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) + } + } + +} diff --git a/packages/sim-consumidor-activaciones/config/env/index.ts b/packages/sim-consumidor-activaciones/config/env/index.ts index f803690..4756960 100644 --- a/packages/sim-consumidor-activaciones/config/env/index.ts +++ b/packages/sim-consumidor-activaciones/config/env/index.ts @@ -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) + }; diff --git a/packages/sim-entrada-eventos/aplication/Sim.controller.ts b/packages/sim-entrada-eventos/aplication/Sim.controller.ts index 769a317..dcc7c59 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.controller.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.controller.ts @@ -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( + [ + ["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 + } } diff --git a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts index 8ab957d..39e6504 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts @@ -36,13 +36,12 @@ export class SimUsecases { async activation(args: { iccid: string, compañia: string }) { const activationEvent = { - key: "sim.nos.activate", + key: `sim.${args.compañia}.activate`, payload: { iccid: args.iccid } } - console.log("publicando", activationEvent) return this.eventBus.publish([activationEvent]) } diff --git a/yarn.lock b/yarn.lock index c8a7724..df32760 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,6 +780,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.13.3": + version: 1.13.3 + resolution: "axios@npm:1.13.3" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/2ceca9215671f9c2bcd5d8a0a1a667e9a35f9f7cfae88f25bba773ed9612de6cac50b2bf8be5e6918cbd2db601b4431ca87a00bffd9682939a8b85da9c89345a + languageName: node + linkType: hard + "body-parser@npm:^2.2.1": version: 2.2.2 resolution: "body-parser@npm:2.2.2" @@ -1321,7 +1332,17 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0, form-data@npm:^4.0.5": +"follow-redirects@npm:^1.15.6": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + languageName: node + linkType: hard + +"form-data@npm:^4.0.0, form-data@npm:^4.0.4, form-data@npm:^4.0.5": version: 4.0.5 resolution: "form-data@npm:4.0.5" dependencies: @@ -1993,6 +2014,13 @@ __metadata: languageName: node linkType: hard +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 + languageName: node + linkType: hard + "qs@npm:^6.14.0, qs@npm:^6.14.1": version: 6.14.1 resolution: "qs@npm:6.14.1" @@ -2359,6 +2387,7 @@ __metadata: "@types/express": "npm:^5.0.6" "@types/node": "npm:^25.0.3" "@types/supertest": "npm:^6.0.3" + axios: "npm:^1.13.3" concurrently: "npm:^9.2.1" cors: "npm:^2.8.5" dotenv: "npm:^17.2.3"