@intuitionrobotics/user-account
Version:
131 lines • 5.81 kB
JavaScript
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