UNPKG

@intuitionrobotics/user-account

Version:
131 lines 5.81 kB
import { __stringify, BadImplementationException, currentTimeMillies, Day, Module } from "@intuitionrobotics/ts-common"; import { ApiException } from "@intuitionrobotics/thunderstorm/backend"; import * as jws from "jws"; import {} from "jws"; import { EXPIRES_AT, JWTBuilder } from "./JWTBuilder.js"; import { AUTHENTICATION_KEY, AUTHENTICATION_PREFIX } from "../../index.js"; export class TokenExpiredException extends ApiException { constructor(message, cause) { super(401, message, cause); } } export class SecretsModule_Class extends Module { DEFAULT_ISS = "TOOLS"; AUTHENTICATION_PREFIX = AUTHENTICATION_PREFIX; AUTHENTICATION_KEY = AUTHENTICATION_KEY; constructor() { super("SecretsModule"); this.setDefaultConfig({ validateKeyId: "AUTH_SECRET" }); } getSecret(k) { const secret = process.env?.[k] || this.getConfig()?.[k]; if (!secret) throw new BadImplementationException(`Missing secret with key ${k} in SecretsModule`); return { kid: k, value: secret }; } getAuthSecret = (kid) => { return this.getSecret(kid); }; getConfig = () => { if (!this.config) throw new BadImplementationException(`Missing config, check SecretsModule's config`); if (!this.config.secrets) throw new BadImplementationException(`Missing 'secrets' key in config, check SecretsModule's config`); return this.config.secrets; }; validateRequestAndCheckExpiration(request, scopes) { const token = this.validateRequest(request, scopes); if (this.isExpired(token)) { const cause = `The JWT passed is not valid, check. With payload: ${__stringify(token.payload)}. The JWT passed is expired`; throw new TokenExpiredException(cause); } return token.payload; } // Specify a kid to force the usage of it validateRequest(request, scopes) { const authToken = this.extractAuthToken(request); const token = this.decodeJwt(authToken); if (!token) throw new ApiException(401, "Could not decode token " + authToken); const kid = token.header.kid || this.config.validateKeyId; if (!kid) throw new ApiException(401, "Could not deduce which key to use in order to verify the token, please specify a key ID"); const secret = this.getAuthSecret(kid); const verified = jws.verify(authToken, token.header.alg, secret.value); let cause = `The JWT passed is not valid, check. With payload: ${__stringify(token.payload)} and header ${__stringify(token.header)}.`; if (!verified) throw new ApiException(401, cause); if (!token.payload?.[EXPIRES_AT]) { cause += ` The JWT is missing the expiration claim`; throw new ApiException(401, cause); } const scopesToValidate = token.payload?.scopes; if (scopesToValidate) this.validateScopes(scopesToValidate, scopes); return token; } validateScopes(_scopesToValidate, scopes) { const scopesToValidate = _scopesToValidate.split(","); const isValidScope = scopesToValidate.some(s => scopes.includes(s)); if (!isValidScope) throw new ApiException(403, `User doesn't have valid scopes. It needs ${scopes.join(",")} but it has ${_scopesToValidate}`); } extractAuthToken(request) { const authHead = request.header(this.AUTHENTICATION_KEY); if (authHead === undefined) throw new BadImplementationException("Missing Authorization header"); if (!authHead) throw new BadImplementationException('The Authorization header is empty'); const parts = authHead.split(" "); if (parts.length !== 2 || parts[0] !== this.AUTHENTICATION_PREFIX) throw new BadImplementationException(`The Authorization header is malformed` + "\n" + `Value: ${authHead}` + "\n" + `Expected Value: ${this.AUTHENTICATION_PREFIX} [token]`); const authToken = parts[1].trim(); if (!authToken) throw new BadImplementationException(`The token provided is empty`); return authToken; } isExpired = (token) => { return this.getExpiration(token) < currentTimeMillies(); }; getExpiration(token) { let exp = token.payload[EXPIRES_AT]; if (!exp) return exp; const now = currentTimeMillies(); const cutOff = 100000000000; // 3-3-1973 in milliseconds const isInSeconds = exp < cutOff; if (isInSeconds) exp = exp * 1000; const year = 365 * Day; if (exp > now + year) throw new ApiException(401, `The JWT passed is not valid. Payload: ${__stringify(token.payload)}.` + `Malformed JWT, expiry date is not valid, check the exp format, assumed to be in ${isInSeconds ? "seconds" : "milliseconds"}`); return exp; } generateJwt = (payload, kid, algorithm = "HS256") => { const secret = this.getAuthSecret(kid); return new JWTBuilder(algorithm) // This is a default that can be overwritten by the claims .setExpiration(Math.floor((currentTimeMillies() + Day) / 1000)) .addClaims(payload) .setIssuer(this.getIss()) .setKeyID(secret.kid) .build(secret.value); }; getIss = () => { const issuer = this.config.issuer; if (!issuer) return this.DEFAULT_ISS; return issuer; }; decodeJwt = (jwt) => { return jws.decode(jwt); }; } export const SecretsModule = new SecretsModule_Class(); //# sourceMappingURL=SecretsModule.js.map