Base de JWT de Objenious
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ node_modules
|
||||
|
||||
#!.yarn/cache
|
||||
.pnp.*
|
||||
|
||||
*.pem
|
||||
|
||||
18
README.md
18
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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
72
packages/shared/domain/JWT.ts
Normal file
72
packages/shared/domain/JWT.ts
Normal 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
|
||||
}
|
||||
}
|
||||
7
packages/shared/infrastructure/HTTPClient.ts
Normal file
7
packages/shared/infrastructure/HTTPClient.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class HTTPClient {
|
||||
|
||||
constructor() {
|
||||
// JWT?
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
118
packages/sim-consumidor-activaciones/aplication/JWT.service.ts
Normal file
118
packages/sim-consumidor-activaciones/aplication/JWT.service.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
31
yarn.lock
31
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"
|
||||
|
||||
Reference in New Issue
Block a user