@sap/xssec
Version:
XS Advanced Container Security API for node.js
154 lines (132 loc) • 5.94 kB
JavaScript
;
const SecurityContext = require('../context/SecurityContext');
const XsaSecurityContext = require('../context/XsaSecurityContext');
const XsaToken = require('../token/XsaToken');
const Jwk = require('../jwks/Jwk');
const MissingKidError = require('../error/validation/MissingKidError');
const MissingVerificationKeyError = require('../error/validation/MissingVerificationKeyError');
const { getLogger } = require('../util/logging');
const XsuaaService = require('./XsuaaService');
const Token = require('../token/Token');
const LOG = getLogger("XsaService.js");
/**
* @typedef {import('../util/Types').ServiceCredentials} ServiceCredentials
* @typedef {import('../util/Types').XsaServiceCredentials} XsaServiceCredentials
* @typedef {import('../util/Types').ServiceConfig} ServiceConfig
* @typedef {import('../util/Types').SecurityContextConfig} SecurityContextConfig
* @typedef {import('../util/Types').TokenFetchOptions} TokenFetchOptions
* @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 XSA! See README for details.\
* This {@link Service} class is constructed from XSA credentials to provide an API with selected functionality against that XSA service instance, e.g. token validation and token fetches.
*/
class XsaService extends XsuaaService {
/**
* @param {ServiceCredentials & XsaServiceCredentials} credentials
* @param {ServiceConfig} [serviceConfig={}]
*/
constructor(credentials, serviceConfig) {
super(credentials, serviceConfig);
}
/**
* @override
* @param {String|XsaToken} token token as JWT or XsaToken object
* @param {SecurityContextConfig} contextConfig
* @returns {Promise<XsaSecurityContext>}
*/
async createSecurityContext(token, contextConfig = {}) {
if (typeof token === "string") {
token = new XsaToken(token);
} else if (token instanceof Token && !(token instanceof XsaToken)) {
token = new XsaToken(token.jwt, { header: token.header, payload: token.payload });
}
SecurityContext.buildContextConfig(contextConfig);
if (contextConfig.skipValidation !== true && this.config.validation.enabled !== false) {
await this.validateToken(token, contextConfig);
}
let ctx = new XsaSecurityContext(this, token, contextConfig);
for (let extension of this.contextExtensions) {
ctx = await extension.extendSecurityContext(ctx) ?? ctx;
}
return ctx;
}
async validateTokenSignature(token, contextConfig) {
const pemKeyFromConfig = this.credentials.verificationkey;
if (!token.header.jku || !token.header.kid || token.header.kid == 'legacy-token-key') {
LOG.info("Token header contained no JKU or KID or the KID was 'legacy-token-key'");
return this.#validateTokenSignatureWithFallback(token, pemKeyFromConfig);
}
try {
await super.validateTokenSignature(token, contextConfig);
} catch (error) {
if (error instanceof MissingKidError) {
LOG.info("JWKS did not contain kid.");
return this.#validateTokenSignatureWithFallback(token, pemKeyFromConfig);
} else {
throw error;
}
}
}
#validateTokenSignatureWithFallback(token, pemKeyFromConfig) {
if (!pemKeyFromConfig) {
throw new MissingVerificationKeyError();
} else {
LOG.info("Validating token signature with verificationkey from service configuration.");
return Jwk.fromPEM(pemKeyFromConfig).validateSignature(token);
}
}
/**
* Fetches a token from this service with this service's client credentials.
* @param {TokenFetchOptions} 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} 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} options
* @returns {Promise<TokenFetchResponse & RefreshableTokenFetchResponse>} response
*/
async fetchJwtBearerToken(assertion, options = {}) {
return super.fetchJwtBearerToken(assertion, options);
}
/**
* Determines the URL that can be used for fetching tokens from this service.
* @param {GrantType} grant_type
*/
async getTokenUrl(grant_type) {
let baseUrl;
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);
}
/**
* @override
* @inheritdoc
*/
get jwksBaseUrl() {
this.validateCredentials("fetch JWKS", "url");
return this.credentials.url;
}
}
module.exports = XsaService;