@scaffoldly/serverless-util
Version:
Scaffoldly Serverless Helper Functionality
235 lines • 8.87 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authorize = exports.authorizeToken = exports.verifyIssuer = exports.verifyAudience = exports.parseUrn = exports.extractUserId = exports.generateAudience = exports.generateSubject = exports.cookieSameSite = exports.cookieSecure = exports.cookiePrefix = exports.cookieDomain = exports.AUTH_PREFIXES = exports.DEFAULT_PROVIDER = exports.URN_PREFIX = void 0;
const errors_1 = require("./errors");
const jose = __importStar(require("jose"));
const axios_1 = __importDefault(require("axios"));
const crypto_1 = __importDefault(require("crypto"));
const moment_1 = __importDefault(require("moment"));
const http_1 = require("./http");
const constants_1 = require("./constants");
exports.URN_PREFIX = 'urn';
exports.DEFAULT_PROVIDER = 'auth';
exports.AUTH_PREFIXES = ['Bearer', 'jwt', 'Token'];
// TODO: External shared cache
const authCache = {};
const createCacheKey = (token, method, path) => {
const key = {
token,
method: method || '*',
path: path || '*',
};
const sha = crypto_1.default.createHash('sha1');
sha.update(JSON.stringify(key));
return { key: sha.digest('base64'), method: key.method, path: key.path };
};
const cookieDomain = (httpRequest) => {
const host = httpRequest.header('host');
if (!host) {
throw new errors_1.HttpError(400, 'Missing host header');
}
return host.split(':')[0];
};
exports.cookieDomain = cookieDomain;
const cookiePrefix = (name) => {
if (constants_1.STAGE === 'local') {
return name;
}
return `__Secure-${name}`;
};
exports.cookiePrefix = cookiePrefix;
const cookieSecure = () => {
if (constants_1.STAGE === 'local') {
return false;
}
return true;
};
exports.cookieSecure = cookieSecure;
const cookieSameSite = () => {
if (constants_1.STAGE === 'local') {
return 'lax';
}
return 'none';
};
exports.cookieSameSite = cookieSameSite;
const generateSubject = (audience, userId) => `${audience}:${userId}`;
exports.generateSubject = generateSubject;
const generateAudience = (domain, provider) => `${exports.URN_PREFIX}:${provider}:${domain}`;
exports.generateAudience = generateAudience;
const extractUserId = (jwtPayload, defaultOnAbsent) => {
if (!jwtPayload || !jwtPayload.sub) {
if (defaultOnAbsent) {
console.warn(`Missing JWT payload, returning default: ${defaultOnAbsent}`);
return defaultOnAbsent;
}
throw new errors_1.HttpError(400, 'Missing id from JWT payload', jwtPayload);
}
return jwtPayload.sub.split(':').slice(-1)[0];
};
exports.extractUserId = extractUserId;
const parseUrn = (urn) => {
if (!urn) {
console.warn('Missing urn');
return {};
}
const parts = urn.split(':');
if (parts.length < 3) {
console.warn('Unable to parse urn:', parts);
return {};
}
const [prefix, provider, domain] = parts;
if (!prefix) {
console.warn('Unable to find prefix in urn');
return {};
}
if (!provider) {
console.warn('Unable to find provider in urn');
return { prefix };
}
if (!domain) {
console.warn('Unable to find domain in urn');
return { prefix, provider };
}
return { prefix, provider, domain };
};
exports.parseUrn = parseUrn;
const verifyAudience = (providers, domain, aud) => {
if (!aud) {
console.warn('Missing audience');
return false;
}
const { prefix, provider: checkProvider, domain: checkDomain } = exports.parseUrn(aud);
if (prefix !== exports.URN_PREFIX) {
console.warn(`Urn prefix mismatch. Got ${prefix}, expected ${exports.URN_PREFIX}`);
return false;
}
if (providers.length &&
checkProvider &&
!providers.find((provider) => provider.toLowerCase() === checkProvider.toLowerCase())) {
console.warn(`Provider mismatch. Got ${checkProvider}, expected one of ${providers}`);
return false;
}
if (!checkDomain) {
console.warn('Unable to find domain in audience');
return false;
}
if (checkDomain === domain) {
return true;
}
console.warn(`Domain mismatch. Got ${checkDomain}, expected ${domain}`);
return false;
};
exports.verifyAudience = verifyAudience;
const verifyIssuer = (domain, iss) => {
if (!domain) {
console.warn('Missing domain');
return false;
}
if (!iss) {
console.warn('Missing issuer');
return false;
}
const issuerUrl = new URL(iss);
if (issuerUrl.hostname.endsWith(domain)) {
return true;
}
console.warn('Invalid issuer', domain, iss);
return false;
};
exports.verifyIssuer = verifyIssuer;
const authorizeToken = async ({ providers, token, domain, method, path }) => {
let decoded;
try {
decoded = jose.decodeJwt(token);
}
catch (e) {
if (e instanceof Error) {
throw new errors_1.HttpError(401, `Error decoding authentication token: ${e.message}`);
}
else {
throw e;
}
}
if (domain && !exports.verifyAudience(providers, domain, decoded.aud)) {
throw new errors_1.HttpError(401, 'Unauthorized');
}
const cacheKey = createCacheKey(token, method, path);
if (authCache[cacheKey.key]) {
const { expires, payload } = authCache[cacheKey.key];
if (moment_1.default().isBefore(expires)) {
console.log(`Returning cached payload for ${payload.aud} (expires: ${expires}; cacheKey: ${cacheKey})`);
return payload;
}
}
const { iss } = decoded;
if (!iss) {
throw new errors_1.HttpError(400, 'Missing issuer in token payload', decoded);
}
if (domain && !exports.verifyIssuer(domain, iss)) {
throw new errors_1.HttpError(401, 'Unauthorized', { domain, iss });
}
console.log(`Authorizing ${decoded.sub} externally to ${iss}`);
try {
const { data: payload } = await axios_1.default.post(iss, { token });
console.log(`Authorization response`, payload);
const ret = payload;
authCache[cacheKey.key] = { payload, expires: moment_1.default(ret.exp * 1000) };
return authCache[cacheKey.key].payload;
}
catch (e) {
if (axios_1.default.isAxiosError(e) && e.response && e.response.status) {
if (e.response.status === 401 || e.response.status === 403) {
throw new errors_1.HttpError(e.response.status, e.response.status === 401 ? 'Unauthorized' : 'Forbidden', {
url: iss,
status: e.response.status,
message: e.message,
});
}
throw new errors_1.HttpError(500, 'Error authorizing token', { url: iss, status: e.response.status, message: e.message });
}
throw new errors_1.HttpError(500, 'Error authroizing token', { message: e.message });
}
};
exports.authorizeToken = authorizeToken;
// TODO Lambda Authorizer
function authorize(domain, providers = [exports.DEFAULT_PROVIDER]) {
// TODO: Support Scopes
return async (request, securityName, _scopes) => {
if (securityName !== 'jwt') {
throw new Error(`Unsupported Security Name: ${securityName}`);
}
const authorization = http_1.extractAuthorization(request);
if (!authorization) {
throw new errors_1.HttpError(401, 'Unauthorized');
}
const token = http_1.extractToken(authorization);
if (!token) {
throw new errors_1.HttpError(400, 'Unable to extract token');
}
return exports.authorizeToken({ providers, token, domain, method: request.method, path: request.path });
};
}
exports.authorize = authorize;
//# sourceMappingURL=auth.js.map
;