UNPKG

@sap/xssec

Version:

XS Advanced Container Security API for node.js

252 lines (210 loc) 10 kB
'use strict'; const Service = require('./Service'); const SecurityContext = require('../context/SecurityContext'); const XsuaaSecurityContext = require('../context/XsuaaSecurityContext'); const XsuaaToken = require('../token/XsuaaToken'); const Jwks = require('../jwks/Jwks'); const { jsonRequest } = require('../util/jsonRequest'); const { HTTPS_SCHEME, ZID_QUERY_PARAMETER, ZID_HEADER } = require('../util/constants'); const { createCacheKey } = require('../util/util'); const { ResponseError, WrongAudienceError } = require('../error'); /** * @typedef {import('../util/Types').ServiceCredentials} ServiceCredentials * @typedef {import('../util/Types').XsuaaServiceCredentials} XsuaaServiceCredentials * @typedef {import('../util/Types').ServiceConfig} ServiceConfig * @typedef {import('../util/Types').SecurityContextConfig} SecurityContextConfig * @typedef {import('../util/Types').TokenFetchOptions} TokenFetchOptions * @typedef {import('../util/Types').XsuaaTokenFetchOptions} XsuaaTokenFetchOptions * @typedef {import('../util/Types').TokenFetchResponse} TokenFetchResponse * @typedef {import('../util/Types').RefreshableTokenFetchResponse} RefreshableTokenFetchResponse * @typedef {import('../util/Types').GrantType} GrantType */ /** * New SAP BTP applications should start with SAP Identity Services instead of XSUAA! See README for details.\ * This {@link Service} class is constructed from XSUAA credentials to provide an API with selected functionality against that XSUAA service instance, e.g. token validation and token fetches. */ class XsuaaService extends Service { #jwksBaseUrl; // base URL from which the JWKS is fetched /** * @param {ServiceCredentials & XsuaaServiceCredentials} credentials * @param {ServiceConfig} [serviceConfig={}] */ constructor(credentials, serviceConfig) { serviceConfig ??= {}; serviceConfig.endpoints ??= {}; serviceConfig.endpoints.oidc_info ??= "/.well-known/openid-configuration"; serviceConfig.endpoints.jwks ??= "/token_keys"; serviceConfig.endpoints.token ??= "/oauth/token"; super(credentials, serviceConfig); } /** * @overrides * @inheritdoc */ acceptsTokenAudience(token) { this.validateCredentials("validate token audience", "clientid", "xsappname"); if (!(token instanceof XsuaaToken)) { // cast to XsuaaToken, so token.scopes getter exists for the checks below token = new XsuaaToken(null, { header: token.header, payload: token.payload }); } // XSUAA tokens with grant_type === 'user_token' might not have audiences filled, so a fallback to scopes is needed let audiencesToConsider; if(token.audiences?.length > 0) { audiencesToConsider = [...token.audiences]; } else if (token.scopes) { audiencesToConsider = [...token.scopes]; } else { audiencesToConsider = []; } if (token.payload.cid) { audiencesToConsider.push(token.payload.cid); } return audiencesToConsider.some(a => a === this.credentials.clientid || a.startsWith(`${this.credentials.clientid}.`)) || audiencesToConsider.some(a => a === this.credentials.xsappname || a.startsWith(`${this.credentials.xsappname}.`)) // broker plan logic below || this.credentials.clientid.includes("!b") && audiencesToConsider.some(a => a.endsWith(`|${this.credentials.xsappname}`)); } /** * @override * @param {String|XsuaaToken} token as JWT or XsuaaToken object * @param {SecurityContextConfig} contextConfig * @returns {Promise<XsuaaSecurityContext} */ async createSecurityContext(token, contextConfig = {}) { if (typeof token === "string") token = new XsuaaToken(token); SecurityContext.buildContextConfig(contextConfig); // cast generic token const xsuaaToken = new XsuaaToken(token.jwt, { header: token.header, payload: token.payload }); if(contextConfig.skipValidation !== true) { await this.validateToken(xsuaaToken, contextConfig); } const ctx = new XsuaaSecurityContext(this, xsuaaToken, contextConfig); for (let extension of this.config.context?.extensions || []) { await extension.extendSecurityContext(ctx); } return ctx; } async getJwks(token, contextConfig) { const jwksParams = { zid: token.zid } const keyParts = { url: this.jwksBaseUrl, ...jwksParams }; const cacheKey = createCacheKey(keyParts); const buildJwksRequest = () => { return async (correlationId) => { const jwksResponse = await this.fetchJwks({ ...jwksParams, correlationId }); return new Jwks(jwksResponse.keys); } } let jwks; try { jwks = await this.jwksCache.getOrRequest(cacheKey, buildJwksRequest, { correlationId: contextConfig.correlationId }); } catch (error) { if (error instanceof ResponseError && error.responseCode === 400) { /** * Online validation in JWKS endpoint may have negative result for the provided header / query parameters. * In this case, the response will have status 400 and should result in a WrongAudienceError instead of a (subclass of) NetworkError. */ throw new WrongAudienceError(token, this, `The online validation in the JWKS endpoint of the XSUAA server responded with 400: '${error.responseText}'. This indicates that the token should not be accepted by this service.`); } throw error; } return jwks; } /** * @internal * Returns the base URL (https protocol + uaadomain from the credentials) that can be used for JWKS fetches. * @returns {String} base URL for JWKS fetches */ get jwksBaseUrl() { if (!this.#jwksBaseUrl) { this.validateCredentials("fetch JWKS", "uaadomain"); const { uaadomain } = this.credentials; if (uaadomain.startsWith(HTTPS_SCHEME)) { this.#jwksBaseUrl = uaadomain; } else { this.#jwksBaseUrl = `${HTTPS_SCHEME}${uaadomain}`; } } return this.#jwksBaseUrl; } async fetchJwks(jwksParams, correlationId) { const jwksUrl = new URL(this.jwksBaseUrl + this.endpoints.jwks); if (jwksParams.zid) { jwksUrl.searchParams.append(ZID_QUERY_PARAMETER, jwksParams.zid); } const request = this.buildRequest({ method: 'GET', }); return jsonRequest(jwksUrl, request, { requestName: `${this.constructor.name}.fetchJwks`, correlationId }); } // Re-declare JSDoc for token fetches with detailed options and return object properties /** * Fetches a token from this service with this service's client credentials. * @param {TokenFetchOptions & XsuaaTokenFetchOptions} options * @returns {Promise<TokenFetchResponse>} response */ async fetchClientCredentialsToken(options = {}) { return super.fetchClientCredentialsToken(options); } /** * Fetches a user token from this service with the given username and password. * @param {String} username * @param {String} password * @param {TokenFetchOptions & XsuaaTokenFetchOptions} options * @returns {Promise<TokenFetchResponse & RefreshableTokenFetchResponse>} response */ async fetchPasswordToken(username, password, options = {}) { return super.fetchPasswordToken(username, password, options); } /** * Fetches a JWT bearer token from this service with the given user token as assertion. * @param {String} assertion JWT bearer token used as assertion * @param {TokenFetchOptions & XsuaaTokenFetchOptions} options * @returns {Promise<TokenFetchResponse & RefreshableTokenFetchResponse>} response */ async fetchJwtBearerToken(assertion, options = {}) { return super.fetchJwtBearerToken(assertion, options); } /** @override */ buildTokenRequest(grant_type, options) { const request = super.buildTokenRequest(grant_type, options); if (options.scope) { request.body.append("scope", options.scope); } if (options.authorities) { request.body.append("authorities", JSON.stringify({ az_attr: options.authorities })); } if (options.zid) { request.headers ??= {}; request.headers[ZID_HEADER] = options.zid; } return request; } /** * Determines the URL that can be used for fetching tokens from this service, optionally adjusted for a tenant in the same subaccount. * @override * @inheritdoc * @param {GrantType} grant_type * @param {String} options.tenant */ async getTokenUrl(grant_type, options = {}) { const { tenant } = options; let baseUrl; if (tenant) { this.validateCredentials("build token fetch URL for custom tenant subdomain", "uaadomain"); const uaaDomain = this.credentials.certificate ? this.credentials.uaadomain.replace("authentication.", "authentication.cert.") : this.credentials.uaadomain; baseUrl = `${HTTPS_SCHEME}${tenant}.${uaaDomain}`; } else { if (this.credentials.certificate) { this.validateCredentials("fetch token via certificate authentication", "certurl"); baseUrl = this.credentials.certurl; } else { this.validateCredentials("fetch token via client secret authentication", "url"); baseUrl = this.credentials.url; } } return new URL(this.endpoints.token, baseUrl); } } module.exports = XsuaaService;