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

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ node_modules
#!.yarn/cache
.pnp.*
*.pem

View File

@@ -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]

View File

@@ -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",

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])
}

View File

@@ -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"