UNPKG

@authress/sdk

Version:

Client SDK for Authress authorization as a service. Provides managed authorization api to secure service resources including user data.

135 lines (112 loc) 5.64 kB
const { jwtVerify, importJWK } = require('jose'); const base64url = require('base64url'); const axios = require('axios'); const { DateTime } = require('luxon'); const { URL } = require('url'); const ServiceClientTokenProvider = require('./serviceClientTokenProvider'); const TokenVerificationError = require('./tokenVerificationError'); const { sanitizeUrl } = require('./util'); const packageInfo = require('../package.json'); const keyMap = {}; const client = axios.create(); // Avoid breaking SDK usages in UIs that depend on this library, since we aren't allowed to set User-Agent in a browser context if (process && process.versions && process.versions.node) { client.defaults.headers.common['User-Agent'] = `Authress SDK; Javascript; ${packageInfo.version};`; } function decode(token) { try { return token && { header: JSON.parse(base64url.decode(token.split('.')[0])), payload: JSON.parse(base64url.decode(token.split('.')[1])) }; } catch (error) { return null; } } async function getPublicKey(httpClient, jwkKeyListUrl, kid) { const getKeyUnCached = async () => { const result = await httpClient.get(jwkKeyListUrl, { Authorization: undefined }); const jwk = result.data.keys.find(key => key.kid === kid); if (jwk) { return jwk; } throw new TokenVerificationError('The associated public key for the specified access token is not available. For Service Client Access Keys, it may not be valid yet or has been deleted. For new Access Keys just created, key validation is cached and can take up to 5 minutes before new keys can be used.'); }; const hashKey = JSON.stringify({ jwkKeyListUrl, kid }); try { const keyData = keyMap[hashKey]; if (keyData && DateTime.utc() < keyData.expiry) { const key = await keyData.asyncKey; return key; } } catch (error) { keyMap[hashKey] = null; } keyMap[hashKey] = { asyncKey: getKeyUnCached(), expiry: DateTime.utc().plus({ hours: 1 }) }; return keyMap[hashKey].asyncKey; } function getSanitizedIssuerUrl(rawUrlString) { let sanitizedUrl = rawUrlString; if (!sanitizedUrl.startsWith('http')) { sanitizedUrl = sanitizedUrl.match(/^(localhost|authress.localhost.localstack.cloud:4566$)/) ? `http://${sanitizedUrl}` : `https://${sanitizedUrl}`; } const url = new URL(sanitizedUrl); const domainBaseUrlMatch = url.host.match(/^([a-z0-9-]+)[.][a-z0-9-]+[.]authress[.]io$/); if (domainBaseUrlMatch) { url.host = `${domainBaseUrlMatch[1]}.login.authress.io`; sanitizedUrl = url.toString(); } return sanitizedUrl.replace(/[/]+$/, ''); } module.exports = async function(authressCustomDomainOrHttpClient, requestToken, options = { expectedPublicKey: null, verifierOptions: {} }) { const httpClient = typeof authressCustomDomainOrHttpClient === 'object' ? authressCustomDomainOrHttpClient : client; const authressCustomDomain = typeof authressCustomDomainOrHttpClient === 'object' ? authressCustomDomainOrHttpClient.baseUrl : authressCustomDomainOrHttpClient; if (!requestToken) { throw new TokenVerificationError('Unauthorized: Token not specified'); } let authenticationToken = requestToken; let unverifiedToken = decode(requestToken); if (!unverifiedToken) { // Check if the token is a client secret and then create a token dynamically from that try { const replacementToken = await (new ServiceClientTokenProvider(requestToken, authressCustomDomain))(); authenticationToken = replacementToken; unverifiedToken = decode(replacementToken); } catch (tokenError) { throw new TokenVerificationError('Unauthorized: Invalid token'); } } const kid = unverifiedToken && unverifiedToken.header && unverifiedToken.header.kid; if (!kid) { throw new TokenVerificationError('Unauthorized: No KID in token'); } const issuer = unverifiedToken && unverifiedToken.payload && unverifiedToken.payload.iss; if (!issuer) { throw new TokenVerificationError('Unauthorized: No Issuer found'); } const completeIssuerUrlOrigin = new URL(getSanitizedIssuerUrl(authressCustomDomain)).origin; const altIssuerUrlOrigin = new URL(sanitizeUrl(authressCustomDomain)).origin; const altGlobalIssuerUrlOrigin = new URL(sanitizeUrl(authressCustomDomain)).origin.replace(/^https:\/\/([a-z0-9-]+)[.][a-z0-9-]+[.]authress[.]io$/, 'https://$1.api.authress.io'); try { if (new URL(issuer).origin !== completeIssuerUrlOrigin && new URL(issuer).origin !== altIssuerUrlOrigin && new URL(issuer).origin !== altGlobalIssuerUrlOrigin) { throw new TokenVerificationError(`Unauthorized: Invalid Issuer: ${issuer}, Expected: ${completeIssuerUrlOrigin}`); } } catch (error) { throw new TokenVerificationError(`Unauthorized: Invalid Issuer: ${issuer}, Expected: ${completeIssuerUrlOrigin}`); } // Handle service client checking const clientIdMatcher = new URL(issuer).pathname.match(/^\/v\d\/clients\/([^/]+)$/); if (clientIdMatcher && clientIdMatcher[1] !== unverifiedToken.payload.sub) { throw new TokenVerificationError(`Unauthorized: Invalid Sub found for service client token: ${unverifiedToken.payload.sub}`); } const key = options.expectedPublicKey || await getPublicKey(httpClient, `${issuer}/.well-known/openid-configuration/jwks?kid=${kid}`, kid); try { const verifiedToken = await jwtVerify(authenticationToken, await importJWK(key), { algorithms: ['EdDSA', 'RS512'], issuer, ...options.verifierOptions }); return verifiedToken.payload; } catch (verifierError) { throw new TokenVerificationError(`Unauthorized: ${verifierError.message}`); } };