UNPKG

@scaffoldly/serverless-util

Version:
235 lines 8.87 kB
"use strict"; 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