UNPKG

@infomaker/service-authorization-lib

Version:

IMID Service Authorization Library

370 lines (330 loc) 12.5 kB
/* eslint-env node */ 'use-strict' const jwt = require('jsonwebtoken') const errors = require('./errors') const log = require('@infomaker/json-log')(__filename) const validators = require('./validators') const authorizationModes = require('./authorization-modes') /** @module Authorize */ /** * The result object returnd if the authorization was successful * @typedef authorizationSuccessResult * @param {Object} result * @param {Object} result.credentials * @param {Object} result.credentials.serviceToken - Decoded service token used to authorize the request * @param {Object} result.artifacts * @param {string} result.artifacts.reason - Why the request was authorized * @param {Object[]} result.artifacts.authenticationParameters - The built authorization parameters used to authorize the request * @param {Object[]} result.artifacts.matchingAccessRules - The built access rules that matched the provided token * @param {Object} result.artifacts.matchingServiceAdmin - If the token matched a service admin rule */ /** * The error object returned if the authorization failed. * * Returns either Unauthoried, AccessDenied or ConfigError * @typedef authorizationErrorResult * @param {Object} result * @param {(Errors~Unauthorized|Errors~AccessDenied|Errors~ConfigError)} result.err - The error thrown during authorization */ /** * * Main authorization function * * Consists of the following steps: * 1. If token is missing and endpoint is open, authorize request. * 2. Validate and decode service token. * 3. If servieToken exists and endpoint is open, authorize request. * 4. Try to authorize using serviceAdmin. * 5. Build auth params and check if service token exists. * 6. Authorize organization. * 7. Authorize using optional accessRules. * * @async * @function authorize * @param {Object} params * @param {AccessRule} params.authParams - Object with auth parameters from the request * @param {string} params.unverifiedServiceToken - Unverified service token in JWT format * @param {string} params.serviceTokenSignSecret - The secret the unverified token should be validated with * @param {Object} params.request - The request object to be made availbable in authParams * * @returns {(authorizationSuccessResult|authorizationErrorResult)} Either returns an authorizeSuccessResult object or an authorizationErrorResult object */ const authorize = async ({authParams, unverifiedServiceToken, serviceTokenSignSecret, imidToken, request}) => { _removeAppendedImidHeadersFromRequest(request) try { const validEndpointAuthParams = await _validateAuthParams(authParams) /** * Step 1: If token is missing and endpoint is open, authorize request */ if (!unverifiedServiceToken && validEndpointAuthParams === authorizationModes.openEndpoint) { return _authorizeRequest({ reason: 'Open endpoint and no serviceToken', requestAuthParams: validEndpointAuthParams }) } /** * Step 2: Validate and decode service token * @private */ const serviceToken = await _verifyAndDecodeServiceToken(unverifiedServiceToken, serviceTokenSignSecret) /** * Step 3: If servieToken exists and endpoint is open, authorize request */ if (validEndpointAuthParams === authorizationModes.openEndpoint) { return _authorizeRequest({ reason: 'Authorized: Valid serviceToken for open endpoint', serviceToken, imidToken, requestAuthParams: validEndpointAuthParams }) } if (serviceToken && validEndpointAuthParams === authorizationModes.authenticatedEndpoint) { return _authorizeRequest({ reason: 'Authorized: Valid serviceToken for authenticated endpoint', serviceToken, imidToken, requestAuthParams: validEndpointAuthParams }) } /** * Step 4: Try to authorize using serviceAdmin * @private */ const matchingServiceAdmin = await _authorizeUsingServiceAdmin(serviceToken) if (matchingServiceAdmin !== null) { return _authorizeRequest({ reason: 'Authorized: Service admin rule matched', matchingServiceAdmin, serviceToken, imidToken, requestAuthParams: validEndpointAuthParams, }) } /** * Step 5: Build auth params and check if service token exists * @private */ const requestAuthParams = await _buildAuthParamsForThisRequest(validEndpointAuthParams, request) if (!serviceToken) { throw new errors.Unauthorized({ reason: 'Invalid or missing service token for locked down endpoint', serviceToken, requestAuthParams, loginRedirectOrg: requestAuthParams.org }) } /** * Step 6: Authorize organization */ if (Object.prototype.hasOwnProperty.call(requestAuthParams, 'org') && requestAuthParams.org !== false) { if (requestAuthParams.org === serviceToken.org) { if (!Object.prototype.hasOwnProperty.call(requestAuthParams,'accessRules')) { return _authorizeRequest({ reason: 'Authorized: Org matched org and no accessRules exists (locked down endpoint)', serviceToken, imidToken, requestAuthParams }) // means that anyone in org is allowed } } else { throw new errors.Unauthorized({ reason: 'Token org does not match required org for locked down endpoint', serviceToken, requestAuthParams, loginRedirectOrg: requestAuthParams.org }) } } /** * Step 7: Authorize using optional accessRules */ if (Object.prototype.hasOwnProperty.call(requestAuthParams,'accessRules')) { const matchingAccessRules = await _authorizeUsingAccessRules(serviceToken, requestAuthParams) if (matchingAccessRules.length) { const reason = requestAuthParams.org ? 'Authorized: Matching org and access rule (for locked down endpoint)' : 'Authorized: Matching access rule and false org (for locked down endpoint)' return _authorizeRequest({ reason, matchingAccessRules, serviceToken, imidToken, requestAuthParams }) } } throw new errors.AccessDenied({ reason: 'Neither serviceAdmin nor org/accessRules matched for locked down endpoint', serviceToken, requestAuthParams, }) } catch (err) { // We don't want to throw execptions, instead return a object holding the error return { err: err } } } const _removeAppendedImidHeadersFromRequest = (req) => { const rawRequest = (req.raw && req.raw.req) ? req.raw.req : req /* istanbul ignore if */ if (!Object.prototype.hasOwnProperty.call(rawRequest,'headers')) { log.warn('Request contains no headers, skipping cleaning', rawRequest) return } const appendedHeaders = [ 'authorization', 'x-imid-req-id', 'x-imid-token' ] appendedHeaders.forEach(appendedHeader => { delete rawRequest.headers[appendedHeader] }) } const _authorizeRequest = ({reason, serviceToken, imidToken, requestAuthParams, matchingAccessRules=[], matchingServiceAdmin=null}) => { return { credentials: { serviceToken, imidToken }, artifacts: { reason, authenticationParameters: requestAuthParams, matchingAccessRules, matchingServiceAdmin } } } const _authorizeUsingServiceAdmin = async (serviceToken) => { if (serviceToken !== null && serviceToken.service_admin === true) { return { serviceAdmin: true } } return null } const _authorizeUsingAccessRules = async (serviceToken, requestAuthParams) => { const accessRules = await validators.accessRules.validateAsync(requestAuthParams.accessRules) const matchingAccessRules = accessRules.filter(accessRule => _accessRuleMatcher(accessRule, serviceToken)) return matchingAccessRules } const _accessRuleMatcher = (rule, serviceToken) => { let permissionsToCheck = serviceToken.permissions.org.slice() const unitPermissions = serviceToken.permissions.units[rule.unit] if (Object.prototype.hasOwnProperty.call(rule,'unit')) { if (Array.isArray(unitPermissions)) { permissionsToCheck = permissionsToCheck.concat(unitPermissions) } else { return false // Token does not belong to unit } } if (Object.prototype.hasOwnProperty.call(rule,'permission') && !permissionsToCheck.includes(rule.permission)) { return false // Token does not contain correct permission } if (Object.prototype.hasOwnProperty.call(rule,'sub') && serviceToken.sub !== rule.sub) { return false // Token does not contain correct sub } return true } const _verifyAndDecodeServiceToken = (serviceToken, serviceTokenSignSecret) => { return new Promise(resolve => { jwt.verify(serviceToken, serviceTokenSignSecret, {}, (err, decodedToken) => { err ? resolve(null) : resolve(decodedToken) }) }) } const _validateAuthParams = async (authParams) => { try { return await validators.authParams.validateAsync(authParams) } catch (err) { throw new errors.ConfigError({ msg: 'Authentication parameters are misconfigured', originalError: err, stack: err.stack }) } } const _buildAuthParamsForThisRequest = async (endpointAuthParams, request) => { log.debug('SAL: Building auth params from request', { endpointAuthParams }) if (endpointAuthParams === authorizationModes.serviceAdminEndpoint) { return authorizationModes.serviceAdminEndpoint // Special handling for service admin enpoints } if (endpointAuthParams === authorizationModes.authenticatedEndpoint) { return authorizationModes.authenticatedEndpoint // Special handling for authenticated enpoints } let requestAuthParams = {} await _buildOnPreAuth(endpointAuthParams.onPreAuth, request) requestAuthParams.org = await _buildOrg(endpointAuthParams.org, request) if (endpointAuthParams.accessRules) { requestAuthParams.accessRules = await _buildAccessRules(endpointAuthParams.accessRules, request) } return requestAuthParams } const _buildOnPreAuth = async (onPreAuth, request) => { try { typeof onPreAuth === 'function' ? await onPreAuth(request) : onPreAuth } catch (err) { // Propagate all IM-errors if (err.isImError) { throw err } // Wrap all others as config error throw new errors.ConfigError({ msg: 'Error thrown while building onPreAuth', originalError: err, onPreAuth, stack: err.stack }) } } const _buildOrg = async (org, request) => { try { const builtOrg = typeof org === 'function' ? await org(request) : org if (builtOrg === undefined) { throw new Error('Org function returned undefined') } return await validators.requiredStringOrFalse.validateAsync(builtOrg) // ensure resulting org is string or false } catch (err) { // Propagate all IM-errors if (err.isImError) { throw err } // Wrap all others as config error throw new errors.ConfigError({ msg: 'Error thrown while building org', originalError: err, org, stack: err.stack }) } } const _buildAccessRules = async (accessRules, request) => { try { const builtAccessRules = accessRules.map(async authorizationRule => { const builtAccessRule = {} const builtAccessRulePromise = Object.keys(authorizationRule).map(async key => { const valueOrFunToGenValue = authorizationRule[key] builtAccessRule[key] = typeof valueOrFunToGenValue === 'function' ? await valueOrFunToGenValue(request) : valueOrFunToGenValue if (builtAccessRule[key] === undefined) { throw new Error(`Access rule function for key ${key} returned undefined`) } return builtAccessRule[key] }) await Promise.all(builtAccessRulePromise) await validators.builtAccessRule.error(() => `Built access rule "${builtAccessRule}" failed validation`).validateAsync(builtAccessRule) return builtAccessRule }) return await Promise.all(builtAccessRules) } catch (err) { // Propagate all IM-errors if (err.isImError) { throw err } // Wrap all others as config error throw new errors.ConfigError({ msg: 'Error thrown while building access rules', originalError: err, accessRules, stack: err.stack }) } } module.exports = authorize