195 lines
5.5 KiB
TypeScript
195 lines
5.5 KiB
TypeScript
/**
|
|
* TODO:
|
|
* Está demasiado acoplado a objenious, hay que sacar un servicio jwt general para
|
|
* el cliente HTTP
|
|
*/
|
|
|
|
import fs from "fs"
|
|
|
|
import {
|
|
JWTToken,
|
|
JWTHeader,
|
|
IJWTService,
|
|
JWTPayload
|
|
} from "sim-shared/domain/JWT.js"
|
|
import axios, { AxiosError } from "axios";
|
|
|
|
export type GrantAccessRequestBody = {
|
|
grant_type: string,
|
|
client_id: string,
|
|
client_assertion_type: string,
|
|
client_assertion: string
|
|
}
|
|
|
|
export type TokensRequestResponse = {
|
|
"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
|
|
}
|
|
|
|
export type ObjeniousTokenBody = any
|
|
|
|
/**
|
|
* El servicio gestiona un par de tokens auth - refresh para las
|
|
* operaciones de Objenious.
|
|
* Se puede partir de tokens existentes.
|
|
*
|
|
* Debe tener un cliente HTTP propio para que no le afecten los
|
|
* interceptores, sino puede haber bucles de refresco de token
|
|
*/
|
|
export class JWTService implements IJWTService<ObjeniousTokenBody> {
|
|
public isRefreshing: boolean = false;
|
|
public authToken: JWTToken<ObjeniousTokenBody> | undefined;
|
|
private refreshToken?: JWTToken<ObjeniousTokenBody> | undefined;
|
|
|
|
// http
|
|
private transformHeaders?: (_: Object) => JWTHeader;
|
|
private defaultHttpHeaders: Record<string, string>;
|
|
private defaultBody: Record<string, string>;
|
|
|
|
// jwt
|
|
private defaultJWTHeaders: JWTHeader;
|
|
private defaultJWTPayload: JWTPayload<any>;
|
|
private privateKeyPath: string;
|
|
private tokenUrl: string;
|
|
private refreshTokenUrl: string;
|
|
|
|
|
|
constructor(args: {
|
|
token?: string // si se partiese de un token existente,
|
|
refreshToken?: string,
|
|
transformJWTHeaders?: (_: Object) => JWTHeader,
|
|
defaultHeaders: Record<string, string>,
|
|
defaultBody: Record<string, string>,
|
|
defaultJWTHeaders: JWTHeader,
|
|
defaultJWTPayload: JWTPayload<any>,
|
|
privateKeyPath: string,
|
|
tokenUrl: string,
|
|
refreshTokenUrl: string
|
|
}) {
|
|
if (args?.token != undefined) this.authToken = new JWTToken(args.token)
|
|
if (args?.refreshToken != undefined) this.refreshToken = new JWTToken(args.refreshToken)
|
|
if (args?.transformJWTHeaders != undefined) this.transformHeaders = args.transformJWTHeaders
|
|
|
|
this.defaultHttpHeaders = args.defaultHeaders;
|
|
this.defaultBody = args.defaultBody;
|
|
|
|
this.defaultJWTHeaders = args.defaultJWTHeaders;
|
|
this.defaultJWTPayload = args.defaultJWTPayload;
|
|
this.privateKeyPath = args.privateKeyPath;
|
|
|
|
this.tokenUrl = args.tokenUrl;
|
|
this.refreshTokenUrl = args.refreshTokenUrl;
|
|
}
|
|
|
|
private buildJwtBody() {
|
|
const jwtHeaders = this.defaultJWTHeaders
|
|
|
|
const jwtData = (this.transformHeaders) ?
|
|
this.transformHeaders(this.defaultJWTPayload) :
|
|
this.defaultJWTPayload;
|
|
|
|
const key = fs.readFileSync(this.privateKeyPath, "utf8")
|
|
const token = JWTToken.fromParts({
|
|
header: jwtHeaders,
|
|
payload: jwtData,
|
|
sigantureData: {
|
|
algorythm: "sha256",
|
|
privateKey: key
|
|
}
|
|
})
|
|
return token
|
|
}
|
|
|
|
public async getNewAuthToken() {
|
|
const bodyWithtoken = {
|
|
...this.defaultBody,
|
|
client_assertion: this.buildJwtBody()
|
|
}
|
|
|
|
const headers = (this.transformHeaders) ? this.transformHeaders(this.defaultHttpHeaders) : this.defaultHttpHeaders;
|
|
|
|
const req = axios.post(this.tokenUrl,
|
|
bodyWithtoken,
|
|
{
|
|
headers: headers
|
|
}
|
|
)
|
|
|
|
this.isRefreshing = true;
|
|
let res;
|
|
try {
|
|
res = (await req).data as TokensRequestResponse;
|
|
this.authToken = new JWTToken<ObjeniousTokenBody>(res.access_token)
|
|
this.refreshToken = new JWTToken<ObjeniousTokenBody>(res.refresh_token)
|
|
return this.authToken
|
|
} catch (e) {
|
|
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
|
console.error(errorString, (e as AxiosError).response?.data)
|
|
throw new Error(errorString)
|
|
} finally {
|
|
this.isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
public async getAccessToken() {
|
|
// Caso 1: El token actual es valido
|
|
if (this.authToken != undefined && !this.authToken.isExpired()) {
|
|
return this.authToken
|
|
}
|
|
|
|
// Caso 2: El token ha expirado pero se puede refrescar
|
|
if (this.authToken?.isExpired() && this.refreshToken != undefined && !this.refreshToken.isExpired()) {
|
|
await this.tryRefreshToken()
|
|
}
|
|
|
|
// Caso 3: Ningún token es valido
|
|
await this.getNewAuthToken()
|
|
|
|
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
|
|
|
|
return this.authToken
|
|
}
|
|
|
|
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 refreshBody = {
|
|
...this.defaultBody,
|
|
grant_type: "refresh_token",
|
|
}
|
|
|
|
const body = {
|
|
...refreshBody,
|
|
client_assertion: this.buildJwtBody(),
|
|
refresh_token: this.refreshToken.rawToken
|
|
}
|
|
|
|
const req = axios.post(this.refreshTokenUrl,
|
|
body,
|
|
{
|
|
headers: this.defaultHttpHeaders
|
|
}
|
|
)
|
|
|
|
let res;
|
|
try {
|
|
res = (await req).data as TokensRequestResponse;
|
|
this.authToken = new JWTToken<ObjeniousTokenBody>(res.access_token)
|
|
this.refreshToken = new JWTToken<ObjeniousTokenBody>(res.refresh_token)
|
|
return this.authToken
|
|
} catch (e) {
|
|
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
|
console.error(errorString, e)
|
|
throw new Error(errorString)
|
|
}
|
|
}
|
|
|
|
}
|