@celastrina/http
Version:
HTTP Function Package for Celastrina
1,234 lines (1,233 loc) • 94.7 kB
JavaScript
/*
* Copyright (c) 2021, KRI, LLC.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* @author Robert R Murrell
* @copyright Robert R Murrell
* @license MIT
*/
"use strict";
const axios = require("axios").default;
const {v4: uuidv4} = require("uuid");
const moment = require("moment");
const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const cookie = require("cookie");
const {CelastrinaError, CelastrinaValidationError, AddOn,
LOG_LEVEL, Configuration, Subject, Sentry, Algorithm, AES256Algorithm, Cryptography, RoleFactory,
RoleFactoryParser, Context, BaseFunction, ValueMatch, AttributeParser, ConfigLoader, Authenticator,
instanceOfCelastrinaType, AddOnEvent} = require("@celastrina/core");
/**
* @typedef _AzureRequestBinging
* @property {string} originalUrl
* @property {string} method
* @property {Object} query
* @property {Object} headers
* @property {Object} params
* @property {Object} body
* @property {string} rawBody
*/
/**
* @typedef _AzureResponseBinging
* @property {Object} headers
* @property {number} status
* @property {Object} body
* @property {string} rawBody
* @property {Array.<Object>} cookies
*/
/**
* @typedef _AZLogger
* @function error
* @function warn
* @function info
* @function verbose
*/
/**
* @typedef _TraceContext
* @property {string} traceparent
*/
/**
* @typedef _ExecutionContext
* @property {string} invocationId
* @property {string} functionName
* @property {string} functionDirectory
*/
/**
* @typedef _AzureFunctionContext
* @property {_ExecutionContext} executionContext
* @property {_TraceContext} traceContext
* @property {_AZLogger} log
* @property {Object} bindings
* @property {_AzureRequestBinging} req
* @property {_AzureResponseBinging} res
* @property {Object} bindingData
*/
/**
* @typedef _jwtpayload
* @property {string} aud
* @property {string} sub
* @property {string} oid
* @property {string} iss
* @property {number} iat
* @property {number} exp
* @property {string} nonce
*/
/**
* @typedef _jwt
* @property {_jwtpayload} payload
* @typedef _ClaimsPayload
* @property {moment.Moment} issued
* @property {moment.Moment} expires
* @property {string} token
* @property {string} audience
* @property {string} subject
* @property {string} issuer
*/
/**
* @typedef {Object} JWKSKEY
* @property {string} [kid]
* @property {string} [kty]
* @property {string} [x5c]
* @property {string} [e]
* @property {string} [n]
* @property {string} [x]
* @property {string} [y]
* @property {string} [crv]
*/
/**
* @typedef {Object} JWKS
* @property {(null|string)} [issuer]
* @property {string} type
* @property {JWKSKEY} key
*/
/**
* Cookie
* @author Robert R Murrell
*/
class Cookie {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Cookie#",
type: "celastrinajs.http.Cookie"};}
/**
* @param {string} name
* @param {(null|string)} [value=null]
* @param {Object} [options={}]
* @param {boolean} [dirty=false]
*/
constructor(name, value = null, options = {}, dirty = false, ) {
if(typeof name !== "string" || name.trim().length === 0)
throw CelastrinaValidationError.newValidationError("Invalid String. Attribute 'name' cannot be undefined, null, or zero length.", "cookie.name");
this._name = name.trim();
this._value = value;
this._options = options;
this._dirty = dirty;
}
/**@return{boolean}*/get doSetCookie() {return this._dirty};
/**@return{string}*/get name() {return this._name;}
/**@return{string}*/get parseValue() {
if(this._value == null)
return "";
else
return this._value;
}
/**@return{null|string}*/get value() {return this._value;}
/**@param{null|string}value*/set value(value) {
this._value = value;
this._dirty = true;
}
/**@return{Object}*/get options() {return this._options;}
/**@param{Object}options*/set options(options) {
if(options == null || typeof options === "undefined")
options = {};
this._options = options;
this._dirty = true;
}
/**
* @param {string} name
* @param {*} value
*/
setOption(name, value) {
this._options[name] = value;
this._dirty = true;
}
/**
* @returns {string}
*/
serialize() {
return cookie.serialize(this._name, this.parseValue, this._options);
}
/**
* @return {Promise<{name: string, value: string}>}
*/
async toAzureCookie() {
let _obj = {name: this._name, value: this.parseValue};
Object.assign(_obj, this._options);
return _obj;
}
/**@param{number}age*/set maxAge(age) {this.setOption("maxAge", age);}
/**@param{Date}date*/set expires(date) {this.setOption("expires", date);}
/**@param{boolean}http*/set httpOnly(http) {this.setOption("httpOnly", http);}
/**@param{string}domain*/set domain(domain) {this.setOption("domain", domain);}
/**@param{string}path*/set path(path) {this.setOption("path", path);}
/**@param{boolean}secure*/set secure(secure) {this.setOption("secure", secure);}
/**@param("lax"|"none"|"strict")value*/set sameSite(value) {this.setOption("sameSite", value)};
/**@return{number}*/get maxAge() {return this._options["maxAge"];}
/**@return{Date}*/get expires() {return this._options["expires"];}
/**@return{boolean}*/get httpOnly() {return this._options["httpOnly"];}
/**@return{string}*/get domain() {return this._options["domain"];}
/**@return{string}*/get path() {return this._options["path"];}
/**@return{boolean}*/get secure() {return this._options["secure"];}
/**@return("lax"|"none"|"strict")*/get sameSite() {return this._options["sameSite"];};
/**@param {string} value*/encodeStringToValue(value) {this.value = Buffer.from(value).toString("base64");}
/**@param {Object} _object*/encodeObjectToValue(_object) {this.encodeStringToValue(JSON.stringify(_object));}
/**@return{string}*/decodeStringFromValue() {return Buffer.from(this.value).toString("ascii");}
/**@return{any}*/decodeObjectFromValue() {return JSON.parse(this.decodeStringFromValue());}
delete() {
this.value = null;
let _epoch = moment("1970-01-01T00:00:00Z");
this.expires = _epoch.utc().toDate();
}
/**
* @param {string} name
* @param {(null|string)} [value=null]
* @param {Object} [options={}]
* @returns {Cookie} A new cookie whos dirty marker is set to 'true', such that doSerializeCookie will generte a
* value to the Set-Cookie header.
*/
static newCookie(name, value = null, options = {}) {
return new Cookie(name, value, options, true);
}
/**
* @param {string} name
* @param {(null|string)} [value=null]
* @param {Object} [options={}]
* @returns {Promise<Cookie>} A new cookie whos dirty marker is set to 'false', such that doSerializeCookie will
* NOT generte a value to the Set-Cookie header.
*/
static async loadCookie(name, value = null, options = {}) {
return new Cookie(name, value, options);
}
/**
* @param {string} value
* @param {Array.<Cookie>} [results=[]];
* @returns {Promise<Array.<Cookie>>} A new cookie whos dirty marker is set to 'false', such that doSerializeCookie
* will NOT generte a value to the Set-Cookie header.
*/
static async parseCookies(value,results = []) {
let _cookies = cookie.parse(value);
for(let _name in _cookies) {
if(_cookies.hasOwnProperty(_name)) {
results.unshift(new Cookie(_name, _cookies[_name]));
}
}
return results;
}
}
/**
* JwtSubject
* @author Robert R murrell
*/
class JwtSubject {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/JwtSubject#",
type: "celastrinajs.http.JwtSubject"};}
static PROP_JWT_HEADER = "celastrinajs.jwt.header";
static PROP_JWT_SIGNATURE = "celastrinajs.jwt.signature";
static PROP_JWT_NONCE = "nonce";
static PROP_JWT_TOKEN = "celastrinajs.jwt";
static PROP_JWT_AUD = "aud";
static PROP_JWT_ISS = "iss";
static PROP_JWT_ISSUED = "iat";
static PROP_JWT_NOTBEFORE = "nbf";
static PROP_JWT_EXP = "exp";
/**
* @param {Subject} subject
*/
constructor(subject) {
/**@type{Subject}*/this._subject = subject
}
/**@return{Subject}*/get subject() {return this._subject;}
/**@return{Object}*/get header() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_HEADER);}
/**@return{Object}*/get signature() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_SIGNATURE);}
/**@return{string}*/get token() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_TOKEN);}
/**@return{string}*/get nonce(){return this._subject.getClaimSync(JwtSubject.PROP_JWT_NONCE);}
/**@return{string}*/get audience() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_AUD);}
/**@return{string}*/get issuer(){return this._subject.getClaimSync(JwtSubject.PROP_JWT_ISS);}
/**@return{moment.Moment}*/get issued(){return moment.unix(this._subject.getClaimSync(JwtSubject.PROP_JWT_ISSUED));}
/**@return{moment.Moment}*/get notBefore(){ //optional payload so we must check.
let _nbf = this._subject.getClaimSync(JwtSubject.PROP_JWT_NOTBEFORE);
if(_nbf != null) _nbf = moment.unix(_nbf);
return _nbf;
}
/**@return{moment.Moment}*/get expires(){return moment.unix(this._subject.getClaimSync(JwtSubject.PROP_JWT_EXP));}
/**@return{boolean}*/get expired(){ return moment().isSameOrAfter(this.expires);}
/**
* @param {undefined|null|object}headers
* @param {string}[name="Authorization]
* @param {string}[scheme="Bearer "]
* @return {Promise<object>}
*/
async setAuthorizationHeader(headers, name = "Authorization", scheme = "Bearer ") {
if(typeof headers === "undefined" || headers == null) headers = {};
headers[name] = scheme + this._subject.getClaimSync(JwtSubject.PROP_JWT_TOKEN);
return headers;
}
/**
* @param {string}name
* @param {null|string}defaultValue
* @return {Promise<number|string|Array.<string>>}
*/
async getHeader(name, defaultValue = null) {
let header = this.header[name];
if(typeof header === "undefined" || header == null) header = defaultValue;
return header;
}
/**
* @param {Subject} subject
* @param {string} token
* @return {Promise<JwtSubject>}
*/
static async decode(subject, token) {return JwtSubject.decodeSync(subject, token);}
/**
* @param {Subject} subject
* @param {string} token
* @return {JwtSubject}
*/
static decodeSync(subject, token) {
if(typeof token !== "string" || token.trim().length === 0)
throw CelastrinaError.newError("Not Authorized.", 401);
/** @type {null|Object} */let decoded = /** @type {null|Object} */jwt.decode(token, {complete: true});
if(typeof decoded === "undefined" || decoded == null)
throw CelastrinaError.newError("Not Authorized.", 401);
subject.addClaims(decoded.payload);
subject.addClaim(JwtSubject.PROP_JWT_HEADER, decoded.header);
subject.addClaim(JwtSubject.PROP_JWT_SIGNATURE, decoded.signature);
subject.addClaim(JwtSubject.PROP_JWT_TOKEN, token);
return new JwtSubject(subject);
}
/**
* @param {Subject} subject
* @param {Object} decoded
* @param {String} token
* @return {Promise<JwtSubject>}
*/
static async wrap(subject, decoded, token) {
subject.addClaims(decoded.payload);
subject.addClaim(JwtSubject.PROP_JWT_HEADER, decoded.header);
subject.addClaim(JwtSubject.PROP_JWT_SIGNATURE, decoded.signature);
subject.addClaim(JwtSubject.PROP_JWT_TOKEN, token);
return new JwtSubject(subject);
}
}
/**
* Issuer
* @abstract
* @author Robert R Murrell
*/
class Issuer {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Issuer#",
type: "celastrinajs.http.Issuer"};}
/**
* @param {null|string} issuer
* @param {(Array.<string>|null)} [audiences=null]
* @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token
* is valid for this issuer.
* @param {boolean} [validateNonce=false]
*/
constructor(issuer = null, audiences = null, assignments = null,
validateNonce = false) {
this._issuer = issuer;
this._audiences = audiences;
this._assignments = assignments;
this._validateNonce = validateNonce;
}
/**@return{string}*/get issuer(){return this._issuer;}
/**@param{string}issuer*/set issuer(issuer){this._issuer = issuer;}
/**@return{Array.<string>}*/get audiences(){return this._audiences;}
/**@param{Array.<string>}audience*/set audiences(audience){this._audiences = audience;}
/**@return{Array<string>}*/get assignments(){return this._assignments;}
/**@param{Array<string>}assignments*/set assignments(assignments){this._assignments = assignments;}
/**@return{boolean}*/get validateNonce() {return this._validateNonce;}
/**@param{boolean}validate*/set validateNonce(validate) {return this._validateNonce = validate;}
/**
* @param {HTTPContext} context
* @param {JwtSubject} subject
* @return {Promise<*>}
* @abstract
*/
async getKey(context, subject) {throw CelastrinaError.newError("Not Implemented", 501);}
/**
* @param {HTTPContext} context
* @param {JwtSubject} subject
* @return {Promise<(null|string)>}
*/
async getNonce(context, subject) {return null;}
/**
* @param {HTTPContext} context
* @param {JwtSubject} _jwt
* @return {Promise<{verified:boolean,assignments?:(null|Array<string>)}>}
*/
async verify(context, _jwt) {
if(_jwt.issuer === this._issuer) {
try {
let decoded = jwt.verify(_jwt.token, await this.getKey(context, _jwt), {algorithm: "RSA"});
if(typeof decoded === "undefined" || decoded == null) {
context.log("Failed to verify token.", LOG_LEVEL.THREAT,
"Issuer.verify(context, _jwt)");
return {verified: false};
}
if(this._audiences != null) {
if(!this._audiences.includes(_jwt.audience)) {
context.log("Subject '" + _jwt.subject.id + "' with audience '" + _jwt.audience +
"' failed to match audiences.", LOG_LEVEL.THREAT, "Issuer.verify(context, _jwt)");
return {verified: false};
}
}
if(this._validateNonce) {
let nonce = await this.getNonce(context, _jwt);
if(typeof nonce === "string" && nonce.trim().length > 0) {
if(_jwt.nonce !== nonce) {
context.log("Subject '" + _jwt.subject.id + "' failed to pass nonce validation.", LOG_LEVEL.THREAT,
"Issuer.verify(context, _jwt)");
return {verified: false};
}
}
}
return {verified: true, assignments: this._assignments};
}
catch(exception) {
context.log("Exception authenticating JWT: " + exception, LOG_LEVEL.THREAT,
"Issuer.verify(context, _jwt)");
return {verified: false};
}
}
else return {verified: false};
}
}
/**
* LocalJwtIssuer
* @author Robert R Murrell
*/
class LocalJwtIssuer extends Issuer {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/LocalJwtIssuer#",
type: "celastrinajs.http.LocalJwtIssuer"};}
/**
* @param {(null|string)} issuer
* @param {(null|string)} key
* @param {(Array.<string>|null)} [audiences=null]
* @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token
* is valid for this issuer.
* @param {boolean} [validateNonce=false]
*/
constructor(issuer = null, key = null, audiences = null,
assignments = null, validateNonce = false) {
super(issuer, audiences, assignments, validateNonce);
this._key = key;
}
/**
* @param {HTTPContext} context
* @param {JwtSubject} subject
* @return {Promise<*>}
*/
async getKey(context, subject) {
return this._key;
}
/**@return{string}*/get key() {return this._key;}
/**@param{string}key*/set key(key) {this._key = key;}
}
/**
* OpenIDJwtValidator
* @description Use the following OpenID IDP's for OpenIDJwtValidator
* <ul>
* <li>Microsoft Azure AD IDP:
* https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration</li>
* <li>Microsoft Azure ADB2C IDP: https://[tenant name].b2clogin.com/[tenant
* name].onmicrosoft.com/{claim[tfp]}/v2.0/.well-known/openid-configuration</li>
* </ul>
* All values in curly-brace {} will be replaced with a value from the claim name {claim} in the decoded
* JWT.
* @author Robert R Murrell
*/
class OpenIDJwtIssuer extends Issuer {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/OpenIDJwtIssuer#",
type: "celastrinajs.http.OpenIDJwtIssuer"};}
/**
* @param {null|string} issuer
* @param {null|string} configUrl
* @param {(Array.<string>|null)} [audiences=null]
* @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token
* is valid for this issuer.
* @param {boolean} [validateNonce=false]
*/
constructor(issuer = null, configUrl = null, audiences = null,
assignments = null, validateNonce = false) {
super(issuer, audiences, assignments, validateNonce);
this._configUrl = configUrl;
}
get configURL() {return this._configUrl;}
set configURL(url) {this._configUrl = url;}
/**
* @param {HTTPContext} context
* @param {JwtSubject} _jwt
* @param {string} url
* @return {Promise<string>}
*/
static async _replaceURLEndpoint(context, _jwt, url) {
/**@type {RegExp}*/let regex = RegExp(/{([^}]*)}/g);
let match;
let matches = new Set();
while((match = regex.exec(url)) !== null) {
matches.add(match[1]);
}
for(const match of matches) {
let value = _jwt.subject.getClaimSync(match);
if(value == null) {
context.log("Claim '" + match + "' not found for subject '" + _jwt.subject.id + "' while building OpenID configuration URL.",
LOG_LEVEL.THREAT, "OpenIDJwtIssuer._replaceURLEndpoint(context, _jwt, url)");
throw CelastrinaError.newError("Not Authorized.", 401);
}
if(Array.isArray(value)) value = value[0];
url = url.split("{" + match + "}").join(/**@type{string}*/value);
}
return url;
}
/**
* @param {HTTPContext} context
* @param {JwtSubject} _jwt
* @param {string} configUrl
* @return {Promise<(null|JWKS)>}
* @private
*/
static async _getKey(context, _jwt, configUrl) {
let _endpoint = await OpenIDJwtIssuer._replaceURLEndpoint(context, _jwt, configUrl);
try {
let _response = await axios.get(_endpoint);
let _issuer = _response.data["issuer"];
_endpoint = _response.data["jwks_uri"];
_response = await axios.get(_endpoint);
/**@type{(null|JWKS)}*/let _key = null;
for (const key of _response.data.keys) {
if(key.kid === _jwt.header.kid) {
_key = {issuer: _issuer, type: key.kty, key: key};
break;
}
}
return _key;
}
catch(exception) {
context.log("Exception getting OpenID configuration for subject " + _jwt.subject.id + ": " + exception,
LOG_LEVEL.ERROR, "OpenIDJwtIssuer._getKey(subject, context)");
CelastrinaError.newError("Exception authenticating user.", 401);
}
}
/**
* @param {(null|JWKS)} key
* @param {HTTPContext} context
* @return {Promise<string>}
* @private
*/
async _getPemX5C(key, context) {
return "-----BEGIN PUBLIC KEY-----\n" + key.key.x5c + "\n-----END PUBLIC KEY-----\n";
}
/**
* @param {HTTPContext} context
* @param {JwtSubject} _jwt
* @return {Promise<void>}
* @private
*/
async getKey(context, _jwt) {
/**@type{(null|JWKS)}*/let key = await OpenIDJwtIssuer._getKey(context, _jwt, this._configUrl);
let pem;
if(typeof key.key.x5c === "undefined" || key.key.x5c == null)
pem = jwkToPem(key.key);
else
pem = await this._getPemX5C(key, context);
return pem;
}
}
/**
* IssuerParser
* @author Robert R Murrell
* @abstract
*/
class IssuerParser extends AttributeParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/IssuerParser#",
type: "celastrinajs.http.IssuerParser"};}
/**
* @param {AttributeParser} [link=null]
* @param {string} [type="Object"]
* @param {string} [version="1.0.0"]
*/
constructor(link = null, type = "BaseIssuer", version = "1.0.0") {
super(type, link, version);
}
/**
* @param {Object} _Issuer
* @param {Issuer} _issuer
*/
_loadIssuer(_Issuer, _issuer) {
if(!(_Issuer.hasOwnProperty("issuer")) || (typeof _Issuer.issuer !== "string") || _Issuer.issuer.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"[IssuerParser._loadIssuer(_Issuer, _issuer)][_Issuer.issuer]: Issuer cannot be null, undefined, or empty.", "_Issuer.issuer");
if(!(_Issuer.hasOwnProperty("audiences")) || !(Array.isArray(_Issuer.audiences)) || _Issuer.audiences.length === 0)
throw CelastrinaValidationError.newValidationError(
"[IssuerParser._loadIssuer(_Issuer, _issuer)][_Issuer.audiences]: Audiences cannot be null.", "_Issuer.audiences");
let assignments = [];
if((_Issuer.hasOwnProperty("assignments")) && (Array.isArray(_Issuer.assignments)) && _Issuer.assignments.length > 0)
assignments = _Issuer.assignments;
let _validate = false;
if(_Issuer.hasOwnProperty("validateNonce") && (typeof _Issuer.validateNonce === "boolean"))
_validate = _Issuer.validateNonce;
_issuer.issuer = _Issuer.issuer.trim();
_issuer.audiences = _Issuer.audiences;
_issuer.assignments = assignments;
_issuer.validateNonce = _validate;
}
}
/**
* LocalJwtIssuerParser
* @author Robert R Murrell
*/
class LocalJwtIssuerParser extends IssuerParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/LocalJwtIssuerParser#",
type: "celastrinajs.http.LocalJwtIssuerParser"};}
/**
* @param {AttributeParser} [link=null]
* @param {string} [version="1.0.0"]
*/
constructor(link = null, version = "1.0.0") {
super(link, "LocalJwtIssuer", version);
}
/**
* @param {Object} _LocalJwtIssuer
* @param {PropertyManager} pm
* @return {Promise<LocalJwtIssuer>}
* @private
*/
async _create(_LocalJwtIssuer, pm) {
let _issuer = new LocalJwtIssuer();
await this._loadIssuer(_LocalJwtIssuer, _issuer);
if(!(_LocalJwtIssuer.hasOwnProperty("key")) || (typeof _LocalJwtIssuer.key !== "string") || _LocalJwtIssuer.key.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"[LocalJwtIssuerParser._create(_LocalJwtIssuer)][_LocalJwtIssuer.key]: ",
"_LocalJwtIssuer.key");
_issuer.key = _LocalJwtIssuer.key.trim();
return _issuer;
}
}
/**
* LocalJwtIssuerParser
* @author Robert R Murrell
*/
class OpenIDJwtIssuerParser extends IssuerParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/OpenIDJwtIssuerParser#",
type: "celastrinajs.http.OpenIDJwtIssuerParser"};}
/**
* @param {AttributeParser} [link=null]
* @param {string} [version="1.0.0"]
*/
constructor(link = null, version = "1.0.0") {
super(link, "OpenIDJwtIssuer", version);
}
/**
* @param {Object} _OpenIDJwtIssuer
* @param {PropertyManager} pm
* @return {Promise<OpenIDJwtIssuer>}
* @private
*/
async _create(_OpenIDJwtIssuer, pm) {
let _issuer = new OpenIDJwtIssuer();
await this._loadIssuer(_OpenIDJwtIssuer, _issuer);
if(!(_OpenIDJwtIssuer.hasOwnProperty("configURL")) || (typeof _OpenIDJwtIssuer.configURL !== "string") || _OpenIDJwtIssuer.configURL.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"[OpenIDJwtIssuerParser._create(_OpenIDJwtIssuer)][_OpenIDJwtIssuer.configURL]: configURL cannot be null or empty.",
"_OpenIDJwtIssuer.configURL");
_issuer.configURL = _OpenIDJwtIssuer.configURL.trim();
return _issuer;
}
}
/**
* HTTPParameter
* @abstract
* @author Robert R Murrell
*/
class HTTPParameter {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HTTPParameter#",
type: "celastrinajs.http.HTTPParameter"};}
/**
* @param{string}[type]
* @param{boolean}[readOnly]
*/
constructor(type = "HTTPParameter", readOnly = false) {
this._type = type;
this._readOnly = readOnly
}
/**@return{string}*/get type() {return this._type;}
/**@return{boolean}*/get readOnly() {return this._readOnly;}
/**
* @param {HTTPContext} context
* @param {string} key
* @return {*}
* @abstract
*/
async _getParameter(context, key) {
throw CelastrinaError.newError("Not Implemented.", 501);
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {*} [defaultValue]
* @return {Promise<*>}
*/
async getParameter(context, key, defaultValue = null) {
let _value = await this._getParameter(context, key);
if(typeof _value === "undefined" || _value == null) _value = defaultValue;
return _value;
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {*} [value = null]
* @abstract
*/
async _setParameter(context, key, value = null) {}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {*} [value = null]
* @return {Promise<void>}
*/
async setParameter(context, key, value = null) {
if(this._readOnly)
throw CelastrinaError.newError("Set Parameter not supported.");
await this._setParameter(context, key, value);
}
}
/**
* HeaderParameter
* @author Robert R Murrell
*/
class HeaderParameter extends HTTPParameter {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HeaderParameter#",
type: "celastrinajs.http.HeaderParameter"};}
constructor(type = "header"){super(type);}
/**
* @param {HTTPContext} context
* @param {string} key
* @return {(null|string)}
*/
async _getParameter(context, key) {
return context.getRequestHeader(key);
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {(null|string)} [value = null]
*/
async _setParameter(context, key, value = null) {
await context.setResponseHeader(key, value);
}
}
/**
* CookieParameter
* @author Robert R Murrell
*/
class CookieParameter extends HTTPParameter {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/CookieParameter#",
type: "celastrinajs.http.CookieParameter"};}
constructor(type = "cookie"){super(type);}
/**
* @param {HTTPContext} context
* @param {string} key
* @return {Cookie}
*/
async _getParameter(context, key) {
let cookie = await context.getCookie(key, null);
if(cookie != null) cookie = cookie.value;
return cookie;
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {null|string} [value = null]
*/
async _setParameter(context, key, value = null) {
let cookie = await context.getCookie(key, null);
if(cookie == null)
cookie = Cookie.newCookie(key, value);
else
cookie.value = value;
await context.setCookie(cookie);
}
}
/**
* QueryParameter
* @author Robert R Murrell
*/
class QueryParameter extends HTTPParameter {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/QueryParameter#",
type: "celastrinajs.http.QueryParameter"};}
constructor(type = "query"){super(type, true);}
/**
* @param {HTTPContext} context
* @param {string} key
* @return {(null|string)}
*/
async _getParameter(context, key) {
return context.getQuery(key);
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {(null|string)} [value = null]
*/
async _setParameter(context, key, value = null) {
throw CelastrinaError.newError("QueryParameter.setParameter not supported.", 501);
}
}
/**
* BodyParameter
* @author Robert R Murrell
*/
class BodyParameter extends HTTPParameter {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/BodyParameter#",
type: "celastrinajs.http.BodyParameter"};}
constructor(type = "body"){super(type);}
/**
* @param {HTTPContext} context
* @param {string} key
* @return {*}
*/
async _getParameter(context, key) {
let _value = context.requestBody;
/**@type{Array<string>}*/let _attrs = key.split(".");
for(const _attr of _attrs) {
_value = _value[_attr];
}
return _value;
}
/**
* @param {HTTPContext} context
* @param {string} key
* @param {*} [value = null]
*/
async _setParameter(context, key, value = null) {
let _value = context.responseBody;
/**@type{Array<string>}*/let _attrs = key.trim().split(".");
for(let idx = 0; idx < _attrs.length - 2; ++idx) {
_value = _value[_attrs[idx]];
if(typeof _value === "undefined" || _value == null)
throw CelastrinaError.newError("Invalid object path '" + key + "'.");
}
_value[_attrs[_attrs.length - 1]] = value;
}
}
/**
* HTTPParameterParser
* @author Robert R Murrell
*/
class HTTPParameterParser extends AttributeParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HTTPParameterParser#",
type: "celastrinajs.http.HTTPParameterParser"};}
/**
* @param {AttributeParser} [link=null]
* @param {string} [version="1.0.0"]
*/
constructor(link = null, version = "1.0.0") {
super("HTTPParameter", link, version);
}
/**
* @param {Object} _HTTPParameter
* @param {PropertyManager} pm
* @return {Promise<HTTPParameter>}
*/
async _create(_HTTPParameter, pm) {
let _parameter = "header";
if(_HTTPParameter.hasOwnProperty("parameter") && (typeof _HTTPParameter.parameter === "string") &&
_HTTPParameter.parameter.trim().length > 0)
_parameter = _HTTPParameter.parameter.trim();
return HTTPParameterParser.createHTTPParameter(_parameter);
}
/**
* @param {string} type
* @return {HTTPParameter}
*/
static createHTTPParameter(type) {
switch(type) {
case "header":
return new HeaderParameter();
case "cookie":
return new CookieParameter();
case "query":
return new QueryParameter();
case "body":
return new BodyParameter();
default:
throw CelastrinaValidationError.newValidationError(
"[HTTPParameterParser.getHTTPParameter(type)][type]: '" + type + "' is not supported.",
"type");
}
}
}
/**
* Session
* @author Robert R Murrell
*/
class Session {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Session#",
type: "celastrinajs.http.Session"};}
/**
* @param {Object} [values = {}]
* @param {boolean} [isNew = false]
* @param {(null|string)} [id = null]
*/
constructor(values = {}, isNew = false, id = uuidv4()) {
if(typeof values.id === "undefined" || values.id == null) values.id = id;
this._values = values;
/**@type{boolean}*/this._dirty = isNew;
/**@type{boolean}*/this._new = isNew;
}
/**@return{string}*/get id() {return this._values.id;}
/**@return{boolean}*/get isNew() {return this._new;}
/**
* @param {string} name
* @param {*} defaultValue
* @return {Promise<*>}
*/
async getProperty(name, defaultValue = null) {
let _value = this._values[name];
if(typeof _value === "undefined" || _value == null)
return defaultValue;
else
return _value;
}
/**
* @param {string} name
* @param {*} value
* @return {Promise<void>}
*/
async setProperty(name, value) {
this._values[name] = value;
this._dirty = true;
}
/**
* @param {string} name
* @return {Promise<void>}
*/
async deleteProperty(name) {delete this._values[name]; this._dirty = true;}
/**@type{boolean}*/get doWriteSession() {return this._dirty;}
/**
* @param {Object} values
*/
static load(values) {
return new Session(values);
}
}
/**
* SessionManager
* @author Robert R Murrell
*/
class SessionManager {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionManager#",
type: "celastrinajs.http.SessionManager"};}
/**
* @param {HTTPParameter} parameter
* @param {string} [name = "celastrinajs_session"]
* @param {boolean} [createNew = true]
*/
constructor(parameter, name = "celastrinajs_session", createNew = true) {
if(typeof parameter === "undefined" || parameter == null)
throw CelastrinaValidationError.newValidationError("Argument 'parameter' cannot be null.", "parameter");
if(typeof name !== "string" || name.trim().length === 0)
throw CelastrinaValidationError.newValidationError("Argument 'name' cannot be null or empty.", "name");
this._parameter = parameter;
this._name = name.trim();
this._createNew = createNew;
}
/**@return{HTTPParameter}*/get parameter() {return this._parameter;}
/**@return{string}*/get name() {return this._name;}
/**@return{boolean}*/get createNew() {return this._createNew;}
/**
* @param azcontext
* @param {Object} config
* @return {Promise<void>}
*/
async initialize(azcontext, config) {}
/**
* @return {Promise<Session>}
*/
async newSession() {this._session = new Session({}, true); return this._session;}
/**
* @param {*} session
* @param {HTTPContext} context
* @return {(null|string)}
*/
async _loadSession(session, context) {return session;}
/**
* @param {HTTPContext} context
* @return {Promise<Session>}
*/
async loadSession(context) {
let _session = await this._parameter.getParameter(context, this._name);
if((typeof _session === "undefined" || _session == null)) {
if(this._createNew)
_session = this.newSession();
else
return null;
}
else {
/**@type{string}*/let _obj = await this._loadSession(_session, context);
if(typeof _obj == "undefined" || _obj == null || _obj.trim().length === 0) {
if(this._createNew)
_session = await this.newSession();
else
return null;
}
else
_session = Session.load(JSON.parse(_obj));
}
return _session;
}
/**
* @param {string} session
* @param {HTTPContext} context
* @return {(null|string)}
*/
async _saveSession(session, context) {return session;}
/**
* @param {Session} [session = null]
* @param {HTTPContext} context
* @return {Promise<void>}
*/
async saveSession(session = null, context) {
if(instanceOfCelastrinaType(Session, session)) {
if(session.doWriteSession && !this._parameter.readOnly)
await this._parameter.setParameter(context, this._name, await this._saveSession(JSON.stringify(session), context));
}
}
}
/**
* SecureSessionManager
* @author Robert R Murrell
*/
class SecureSessionManager extends SessionManager {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SecureSessionManager#",
type: "celastrinajs.http.SecureSessionManager"};}
/**
* @param {Algorithm} algorithm
* @param {HTTPParameter} parameter
* @param {string} [name = "celastrinajs_session"]
* @param {boolean} [createNew = true]
*/
constructor(algorithm, parameter, name = "celastrinajs_session", createNew = true) {
super(parameter, name, createNew);
this._crypto = new Cryptography(algorithm);
}
/**@return{Cryptography}*/get cryptography() {return this._crypto;}
/**
* @param azcontext
* @param {Object} config
* @return {Promise<void>}
*/
async initialize(azcontext, config) {
await super.initialize(azcontext, config);
await this._crypto.initialize();
}
/**
* @param {*} session
* @param {HTTPContext} context
* @return {(null|string)}
*/
async _loadSession(session, context) {
return this._crypto.decrypt(session);
}
/**
* @param {string} session
* @param {HTTPContext} context
* @return {(null|string)}
*/
async _saveSession(session, context) {
return this._crypto.encrypt(session);
}
}
/**
* AESSessionManager
* @author Robert R Murrell
*/
class AESSessionManager extends SecureSessionManager {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/AESSessionManager#",
type: "celastrinajs.http.AESSessionManager"};}
/**
* @param {(undefined|null|{key:string,iv:string})} options
* @param {HTTPParameter} parameter
* @param {string} [name = "celastrinajs_session"]
* @param {boolean} [createNew = true]
*/
constructor(options, parameter, name = "celastrinajs_session",
createNew = true) {
if(typeof options === "undefined" || options == null)
throw CelastrinaValidationError.newValidationError(
"Argement 'options' cannot be undefined or null", "options");
if(typeof options.key !== "string" || options.key.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"Argement 'key' cannot be undefined, null or zero length.", "options.key");
if(typeof options.iv !== "string" || options.iv.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"Argement 'iv' cannot be undefined, null or zero length.", "options.iv");
super(AES256Algorithm.create(options), parameter, name, createNew);
}
}
/**
* SessionRoleFactory
* @author Robert R Murrell
*/
class SessionRoleFactory extends RoleFactory {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionRoleFactory#",
type: "celastrinajs.http.SessionRoleFactory"};}
/**
* @param {string} [key="roles"]
*/
constructor(key = "roles") {
super();
this._key = key;
}
/**@return{string}*/get key() {return this._key;}
/**
* @param {Context|HTTPContext} context
* @param {Subject} subject
* @return {Promise<Array.<string>>}
*/
async getSubjectRoles(context, subject) {
let _roles = await context.session.getProperty(this._key, []);
if(!Array.isArray(_roles)) throw CelastrinaError.newError("Invalid role assignments for session key '" + this._key + "'.");
return _roles;
}
}
/**
* SessionRoleFactoryParser
* @author Robert R Murrell
*/
class SessionRoleFactoryParser extends RoleFactoryParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionRoleFactoryParser#",
type: "celastrinajs.http.SessionRoleFactoryParser"};}
/**
* @param {AttributeParser} link
* @param {string} version
*/
constructor(link = null, version = "1.0.0") {
super(link, "SessionRoleFactory", version);
}
/**
* @param {Object} _SessionRoleFactory
* @param {PropertyManager} pm
* @return {Promise<SessionRoleFactory>}
*/
async _create(_SessionRoleFactory, pm) {
let _key = "roles";
if(_SessionRoleFactory.hasOwnProperty("key") && (typeof _SessionRoleFactory.key === "string"))
_key = _SessionRoleFactory.key;
return new SessionRoleFactory(_key);
}
}
/**
* AESSessionManagerParser
* @author Robert R Murrell
*/
class AESSessionManagerParser extends AttributeParser {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/AESSessionManagerParser#",
type: "celastrinajs.http.AESSessionManagerParser"};}
/**
* @param {AttributeParser} [link=null]
* @param {string} [version="1.0.0"]
*/
constructor(link = null, version = "1.0.0") {
super("AESSessionManager", link, version);
}
/**
* @param {Object} _AESSessionManager
* @param {PropertyManager} pm
* @return {Promise<AESSessionManager>}
*/
async _create(_AESSessionManager, pm) {
let _paramtype = "cookie";
let _paramname = "celastrinajs_session";
if(_AESSessionManager.hasOwnProperty("parameter") && (typeof _AESSessionManager.parameter === "string"))
_paramtype = _AESSessionManager.parameter;
if(_AESSessionManager.hasOwnProperty("name") && (typeof _AESSessionManager.name === "string"))
_paramname = _AESSessionManager.name;
let _createnew = true;
if(_AESSessionManager.hasOwnProperty("createNew") && (typeof _AESSessionManager.createNew === "boolean"))
_createnew = _AESSessionManager.createNew;
let _options = null;
if(_AESSessionManager.hasOwnProperty("options") && (typeof _AESSessionManager.options === "object") &&
_AESSessionManager.options != null)
_options = _AESSessionManager.options;
else {
throw CelastrinaValidationError.newValidationError(
"[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options]: Argument 'optiosn' cannot be null or undefined.",
"AESSessionManager.options");
}
if(!(_options.hasOwnProperty("iv")) || (typeof _options.iv !== "string") || _options.iv.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options.iv]: Aregument 'iv' cannot be null or empty.",
"AESSessionManager.options.iv");
if(!(_options.hasOwnProperty("key")) || (typeof _options.key !== "string") || _options.key.trim().length === 0)
throw CelastrinaValidationError.newValidationError(
"[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options.key]: Argument 'key' cannot be null or empty.",
"AESSessionManager.options.key");
return new AESSessionManager(_options, HTTPParameterParser.createHTTPParameter(_paramtype), _paramname, _createnew);
}
}
/**
* HMAC
* @author Robert R Murrell
*/
class HMAC {
/**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HMAC#",
type: "celastrinajs.http.HMAC"};}
/**
* @param {string} secret
* @param {HTTPParameter} parameter
* @param {string} name
* @param {string} algorithm
* @param {BinaryToTextEncoding|string} encoding
* @param {Array<string>} assignments
*/
constructor(secret, parameter = new HeaderParameter(), name = "x-ms-content-sha256",
algorithm = "sha256", encoding = "hex", assignments = []) {
this._parameter = parameter;
this._name = name;
this._secret = secret;
this._algorithm = algorithm;
/**@type{Array<string>}*/this._assignments = assignments;
/**@type{BinaryToTextEncoding}*/this._encoding = encoding;
}
/**@return{string}*/get name() {return this._name;}
/**@return{string}*/get secret() {return this._secret;}
/**@return{string}*/get algorithm() {return this._algorithm;}
/**@return{BinaryToTextEncoding}*/get encoding() {return this._encoding;}
/**@return{HTTPParameter}*/get parameter() {return this._parameter;}
/**@type{Array<string>}*/get assignments() {return this._assignments;}
/**@param{Array<string>}assignments*/set assignments(assignments) {return this._assignments = assignments;}
}
/***
* @typedef HTTPAddOnEvent
* @ex