
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@sap/xssec
Advanced tools
This module allows Node.js applications to authenticate users via JWT tokens issued by SAP Business Technology Platform (BTP) security services (SAP Cloud Identity Services and XSUAA). It also provides an API for fetching tokens from these services.
We recommend developing new applications for SAP BTP with the SAP Cloud Identity Service using Authorization Policies (Details).
Version 4 represents a major rework of the module with the following changes and benefits:
v3 packagev3 package. While we strongly recommend to migrate to the new API, backward-compatibility should be achievable by changing the import to this package:const { createSecurityContext, requests, constants, TokenInfo, JWTStrategy } = require("@sap/xssec").v3;
isTokenIssuedByXSUAA, getConfigType have been removed. Replace with class check:securityContext.token instanceof XsuaaToken // or: IdentityServiceToken, XsaToken, UaaToken
getHdbToken has been removed. It should be replaceable with the following code:const hdbToken = securityContext.token.payload.ext_cxt?.['hdb.nameduser.saml'] || securityContext.token.payload['hdb.nameduser.saml'] || securityContext.token.jwt;
IAS_XSUAA_XCHANGE_ENABLED has been removed because it was unclear from which service instance to which service instance the exchange should be made when multiple instances of the same type were present. Applications can implement the exchange themselves with the new API as described here.disableCache has been removed. It is also not supported by implementations from the v3 compatibility package.isInForeignMode has been removed.Add a dependency to @sap/xssec to your package.json, e.g. via:
npm i @sap/xssec
We strongly recommend to declare the version of this dependency with a ^ (caret) to consume future minor and hotfix releases when you update your dependencies (also see Maintenance):
"dependencies": {
"@sap/xssec": "^4"
}
Important: @sap/xssec requires at least Node.js 18 which is the current LTS version.
Keep the version of this dependency up-to-date as it is a crucial part of your application's security, e.g. by regularly running:
npm update @sap/xssec # or: npm update
This is especially important when you deploy your application with a package-lock.json that locks the version that gets installed to a fixed version, until the next npm update.
When in doubt, check which version of the module is installed via
npm list @sap/xssec
This will print a dependency tree that shows which versions of the module are installed, including transitive dependencies.
The following example gives an overview of the most important APIs of this module for user authentication in express:
const { createSecurityContext, XsuaaService, SECURITY_CONTEXT, errors: { ValidationError }} = require("@sap/xssec");
const credentials = { clientid, ... } // access service credentials, e.g. via @sap/xsenv
const authService = new XsuaaService(credentials) // or: IdentityService, XsaService, UaaService ...
async function authMiddleware(req, res, next) {
try {
const secContext = await createSecurityContext(authService, { req });
// or: const secContext = await createSecurityContext([xsuaaService, identityService]], { req }); for hybrid authentication
// user is authenticated -> tie the SecurityContext to this req object via the dedicated Symbol
req[SECURITY_CONTEXT] = secContext;
return next();
} catch (e) {
// user could not be authenticated
if(e instanceof ValidationError) {
// request has invalid authentication (e.g. JWT expired, wrong audience, ...)
LOG.debug("Unauthenticated request: ", e);
return res.sendStatus(401);
} else {
// authentication could not be validated due to Error
LOG.error("Error while authenticating user: ", e);
return res.sendStatus(500);
}
}
}
app.use(authMiddleware);
// access SecurityContext in endpoint handlers
app.get('/helloWorld', (req, res) => {
if (!req[SECURITY_CONTEXT].checkLocalScope('read')) {
return res.sendStatus(403);
}
// access token information via SecurityContext
return res.send("Hello " + req[SECURITY_CONTEXT].token.givenName);
});
As an alternative to writing the middleware manually, you can use the provided XssecPassportStrategy for passport:
const { XssecPassportStrategy, XsuaaService, SECURITY_CONTEXT } = require("@sap/xssec");
const credentials = { clientid, ... } // access service credentials, e.g. via @sap/xsenv
const authService = new XsuaaService(credentials) // or: IdentityService, XsaService, UaaService ...
passport.use(new XssecPassportStrategy(authService, SECURITY_CONTEXT));
// or: passport.use(new XssecPassportStrategy([xsuaaService, identityService], SECURITY_CONTEXT)); for hybrid authentication
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
app.get('/helloWorld', (req, res) => {
if (!req.authInfo.checkLocalScope('read')) { // access SecurityContext via req.authInfo
return res.sendStatus(403);
}
return res.send("Hello " + req.authInfo.token.givenName) // access token via SecurityContext or ...
// return res.send("Hello " + req.user.name.givenName); // access passport user via req.user
};
However: Once you need access to ValidationErrors for logging or analyzing requests with invalid authentication, using the passport strategy becomes more or less the same effort as writing a middleware yourself:
const { errors: { XssecError, ValidationError }} = require("@sap/xssec");
...
// configure passport to failWithError to be able to catch ValidationErrors.
// Otherwise it will swallow the ValidationError and directly send a 401/403 response
app.use(passport.authenticate('JWT', { session: false, failWithError: true }));
// in your express error handler, check for errors passed on by XssecPassportStrategy and handle accordingly
app.use((err, req, res, next) => {
if(e instanceof ValidationError) {
// request with invalid authentication (e.g. JWT expired, wrong audience, ...)
LOG.debug("Unauthenticated request: ", e);
return res.sendStatus(401);
} else if (e instanceof XssecError) {
// authentication could not be validated due to an Error
LOG.error("Error while authenticating user: ", e);
return res.sendStatus(500);
} else {
// ToDo: handle other errors ...
}
});
Unless you have a reason to use passport, we suggest to write the middleware yourself. This gives you full control with comparable effort.
For new SAP BTP applications, we recommend to directly start with SAP Cloud Identity Services and Authorization Policies instead of the other services supported by this module to get the following benefits:
Major new features will only be made available for SAP Cloud Identity Services. Also, it allows consumption of XSUAA-based services that have not yet migrated.
Migration for older applications from XSUAA to SAP Cloud Identity Services with Authorization Policies is currently in the pilot phase. There will be guides for different migration stages available soon.
The XsuaaLegacyExtension is the first official solution to support hybrid authentication during migration from XSUAA to SAP Cloud Identity Services.
As a basis for all usage scenarios, instantiate a new Service instance of one of the following classes exported by the module that corresponds to the authentication service your application is bound to:
IdentityServiceXsuaaServiceXsaService (legacy systems)UaaService (legacy systems)Pass the service credentials as parsed object:
const { IdentityService } = require("@sap/xssec");
const credentials = { clientid ...} // access service credentials, e.g. via @sap/xsenv
const authService = new IdentityService(credentials);
If your application is bound to more than one authentication service, create multiple instances using the corresponding class and credentials for each service.
To authenticate users, use the createSecurityContext function.
try {
const secContext = await createSecurityContext(authService, { req });
// user is authenticated
} catch (e) {
// user could not be authenticated
if(e instanceof XssecError) {
// e was thrown intentionally by this module with details for the cause of the failure
}
}
:warning: To prevent information overlap from one user's SecurityContext to another's, do not re-use the same contextConfig object for multiple invocations of createSecurityContext!
Create a new object each time, as in the sample code. Changing the req property and then passing the same object a second time is not enough.
Since version 4.0.1, the module has mechanisms in place to avoid these issues even when used wrongly but you should not willingly trust it.
The createSecurityContext function resolves with a service-specific SecurityContext object, e.g. XsuaaSecurityContext.
The SecurityContext will always have the token property that contains a service-specific Token object, e.g. XsuaaToken that provides access to the decoded information of the token.
The library can also be used for authorization checks.
An XsuaaSecurityContext provides different methods to check if the token contains a specific scope.
The recommended way to perform authorization for tokens issued by SAP Cloud Identity Service, is to use Authorization Policies. After authenticating requests with this module, the authorization checks are done with a dedicated module called @sap/ams.
Each Service subclass has dedicated methods that can be used to fetch tokens via the following flows:
fetchClientCredentialsToken({ options })fetchPasswordToken(username, password, { options })fetchJwtBearerToken(assertion, { options })The API of this module throws instances of XssecError or hierarchical subclasses thereof that can be identified like so:
try {
await xsuaaService.fetchClientCredentialsToken()
} catch(e) {
if(e instanceof XssecError) {
// it is an error thrown by this module
}
}
There are three important subclasses of XssecError that divide the Errors into different categories like so:
XssecError
├── ConfigurationError
├── InvalidCredentialsError
├── ...
├── NetworkError
│ ├── ResponseError
├── ...
├── ValidationError
├── ExpiredTokenError
├── ...
ConfigurationError indicates that your application is not using this library correctly or the authentication service is not correctly configured. You should not encounter this Error outside of development as these Errors require fixing.Service constructor that are missing mandatory propertiesNetworkError means the authentication server was unreachable or responded with an unexpected error. This is not necessarily an issue of this module.ValidationError occurs during authentication when the request did not contain valid authentication information. This is the most common type of Error thrown by this module. It has many subclasses that provide detailed information about the reason why the validation result was negative.
Examples:
The Error classes contain error-specific properties as details:
Example:
const { errors: { UnsupportedAlgorithmError }} = require("@sap/xssec");
catch(e) {
if(e instanceof UnsupportedAlgorithmError) {
// e.token contains the token whose algorithm is not supported
// e.alg contains the algorithm of the token that is not supported, e.g. "HS256"
}
}
We discourage application developers from basing their integration tests against BTP Security Services on a local setup that feeds this module with self-signed JWTs and a self-hosted JWKS on localhost. This was a common strategy in the past but has several problems:
If you need to weaken the validation mechanisms of this module for testing, you should commit fully to this strategy.
Consider using a simpler authentication strategy like Basic Auth with mocked users or creating mocked SecurityContext or Token instances of this module for local tests.
Cloud application frameworks typically offer mechanisms to configure different strategies in test and production profiles (CAP Example).
In addition, complement your local tests with proper integration tests against real service instances on a dedicated test cloud landscape.
This section describes the public API of v4 of this module.
:warning: Accessing other properties and methods is discouraged as they may change during minor and hotfix releases of this module, even if they are not marked private via # (ECMAScript private fields/methods) or @private (JsDoc)
Instances of Service(credentials, serviceConfig) are created from parsed service credentials and an optional configuration:
const { IdentityService } = require("@sap/xssec");
const credentials = { clientid ...} // access service credentials, e.g. via @sap/xsenv
const serviceConfig = {
// optional, service-specific configuration object...
validation: {
x5t: {
enabled: true // enables token ownership validation via x5t signature thumbprint
}
}
};
const identityService = new IdentityService(credentials, serviceConfig);
It is possible to update the cert and key of a Service instance by calling Service#setCertificateAndKey(cert, key) with PEM-encoded values that will be used in subsequent calls to the server.
Note: Updating the cert and key property of the credentials object passed in the Service constructor is not enough because the Service makes a copy of the credentials for internal use and then discards the original reference.
This method is useful in different scenarios, e.g:
cert and key, the application can update the credentials of the Service instance via this method.Service instance.Applications that expect their credentials to be rotated with other properties besides cert and key changing, must construct a fresh new Service instance whenever new credentials are received.
The subclasses of Service provide the following service-specific methods that can be used to fetch tokens:
fetchClientCredentialsToken({ options })fetchPasswordToken(username, password, { options })fetchJwtBearerToken(assertion, { options })They resolve with the parsed JSON response from the authentication server. Typically, the desired token is the one under property access_token. However, for user-specific IdentityService tokens (Password, JWT Bearer), the desired token is typically id_token:
const accessTokenJwt = (await xsuaaService.fetchClientCredentialsToken()).access_token;
const idTokenJwt = (await identityService.fetchPasswordToken(username, password)).id_token;
Of course, the other properties of the response, for example refresh_token, are also accessible.
Cached versions of the token flow methods are available as well:
getClientCredentialsToken({ options })getPasswordToken(username, password, { options })getJwtBearerToken(assertion, { options })These methods use the token cache configured on the service instance (see Token Cache). Token responses are cached by request URL, parameters and headers. Cached tokens are re-used for subsequent requests with the same parameters. The minimum guaranteed lifetime of returned tokens is 5 minutes. When the token cache is disabled on the service instance, these methods behave identically to their fetch* counterparts.
const xsuaaService = new XsuaaService(credentials); // token cache is enabled by default
const response1 = await xsuaaService.getClientCredentialsToken({ zid: "zone1" }); // fetches token from server and caches it
const response2 = await xsuaaService.getClientCredentialsToken({ zid: "zone1" }); // returns cached token if still valid for >= 5 minutes, otherwise fetches a new token from the server
The methods are annotated with service-specific JSDoc type definitions that should provide IDE-support for the function signatures and following options.
The options parameter is optional. It supports:
correlationId (string) a correlation id that allows tracing debug logs and Errors thrown by this module to the request
token_format (jwt|opaque) can be used to explicitly specify token format
timeout (number) maximum time in ms to wait for a response
It also supports the following service-specific options:
XSUAA:
scope (string|string[]) requested scope(s)
tenant (string) the subdomain of a tenant on the same subaccount from which to fetch a token. Note: this parameter does not accept a zone ID. Use the zid parameter instead to pass a zone ID.
zid (string) the zone id from which to fetch a token
authorities (object) additional authorities that can be freely chosen during token fetch and will be put into the token under az_attr claim
Identity Service:
resource (string|string[]) name(s) of API dependency to another application that shall be consumed with this token in the format urn:sap:identity:application:provider:name:\<dependencyName\>
refresh_expiry (number) Can be used to reduce the expiry time of the refresh token. If set to 0, no refresh token will be issued.
Furthermore, each subclass of Service offers the method acceptsToken(token) which checks if a Token is accepted by this service instance:
const jwt = ... // extract JWT from req object
const token = new Token(jwt);
if(identityService.acceptsToken(token)) {
// the JWT was issued for an application bound to this Identity Service instance but it MUST still be validated!
}
createSecurityContext(service, contextConfig) can be called with a single Service instance or an array of Service instances as first parameter. In the latter case, the service from which the Security Context is created, will be determined based on the audience of the token.
The second parameter is a unique (!) contextConfig object that is used to pass information for the creation of this specific context.
Mandatory properties:
req request object from which the JWT will be extracted as Bearer token from req.headers.authorization. Additionally, if present, the client's certificate will be extracted from req.headers["x-forwarded-client-cert"] where it is typically put by Cloud Foundry after SSL termination.or:
jwt manually provided JWT token as Stringor:
token manually provided pre-decoded Token instance. This prevents costly double-decoding of the jwt token when it has already been decoded before.Optional properties:
correlationId a correlation id that allows tracing debug logs and Errors thrown by this module to the requestclientCertificatePem manually provided client certificate in PEM formatskipValidation if true, the SecurityContext is created without validating the token. Caution! This flag MUST NOT BE ENABLED, except for testing or when the token has already been validated before, e.g. in DwC contexts.The client certificate is only required in features such as x5t validation or proof token validation for SAP Cloud Identity Service.
SecurityContext instances are returned by createSecurityContext. They have the following properties:
TokenInfo) instance with getters for its header, payload and most important claimscontextConfig from which the SecurityContext was createdgetAppToken(), getUserInfo()The service-specific subclasses of SecurityContext extend this class as follows.
Instances of XsuaaSecurityContext additionally have methods for authorization checks:
checkScope(scope) Checks if the scopes of the token include the given scope exactly as provided to the function. As the scopes of the token begin with the xsappname of the Xsuaa service instance, this means, the provided scope parameter must include this prefix:if(secContext.checkScope(`${authService.credentials.xsappname}.READ`)) {
// user is authorized
}
checkLocalScope(scope) Checks if the scopes of the token include <xsappname>.<scope>, where the xsappname comes from the credentials of the service that was used to create the SecurityContext. This is a quality-of-life function to check for scopes without passing this xsappname to the function:if(secContext.checkLocalScope("READ")) {
// user is authorized
}
checkFollowingInstanceScope(scope) Checks if the scopes of the token include <followingInstanceXsAppName>.<scope>, where followingInstanceXsAppName is the xsappname of the XSUAA clone instance for which the token was issued. This is a quality-of-life function to check for scopes of following XSUAA instances.In production, only trust information of Token instances returned by createSecurityContext (see Authentication).
However, for the purposes of testing, instances of Token can be constructed directly from a raw jwt or a combination of parsed header and payload like so:
const jwt = "eyJraWQiOi...."
let token = new IdentityServiceToken(jwt);
const header = { alg: "RS256" };
const payload = { foo: "bar" };
token = new IdentityServiceToken(null, { header, payload });
:warning: The resulting token object is created without validating the jwt for authenticity, integrity, expiration etc.
The Token class provides easier access to the most common claims of the jwt, as well as its parsed header and payload:
Token instance was createdgetAudiencesArray(), getTokenValue()Instances of IdentityServiceToken additionally have
Instances of XsuaaToken, XsaToken, UaaToken additionally have
The library makes use of caching to improve performance and reduce latency for certain operations. This includes caching network requests, as well as caching results of computationally expensive operations.
The caching strategy ensures that the library is not any less secure when using caches. The only exception to this is that a JWKS rotation reaches the application after a (configurable) delay. However, this is a well-established trade-off to make offline token validation feasible.
The library uses the following caches for responses from the authentication servers:
These caches are crucial to achieve acceptable latency in the application, so they are enabled by default.
Optionally, caches for the results of the following CPU-intensive operations can be enabled:
These caches can drastically improve latency on subsequent requests with the same token. This is especially useful in applications where the user client makes many requests in parallel or in quick succession with the same token. Experiments measured up to 500% throughput (req/s) on a ping endpoint with both caches enabled compared to the default configuration with no caching. Of course, the effect on latency gets relatively smaller in endpoints with more complex processing logic, e.g. due to database access.
For backward-compatibility, those caches are disabled by default until the next major version but we generally recommend their usage already today.
It is possible to configure the XssecPassportStrategy with scope(s) such that incoming requests must have at least one of them. Otherwise, a response with code 403 is returned:
app.use(passport.authenticate('JWT', { session: false, scope: "read" }));
app.use(passport.authenticate('JWT', { session: false, scope: ["read", "write"] }));
The default configuration for requests against a Service instance can be overridden via the requests property in the Service configuration.
{
"timeout": 2000
}
If you frequently encounter TimeoutErrors during createSecurityContext or upon OIDC configuration retrieval before a token request, you can increase the default timeout of requests:
const authService = new IdentityService(identityServiceCredentials,
{
requests: {
timeout: 5000 // maximum possible value is 10000ms
}
});
Consequently, this timeout value will be used for fetching the OIDC configuration from the .well-known endpoint as well as fetching the JWKS.
For token flows, you can pass an individual timeout for the token request itself that takes priority over the default timeout of the Service instance.
To verify the validity of a token, the library needs to ensure that it was signed with a private key belonging to one of the public keys from the authentication server's JWKS (JSON Web Key Set). The application retrieves the JWKS via HTTP from the authentication server. It is cached to reduce both the load on the server and the latency of requests introduced by the signature validation.
Please note that the JWKS endpoint is parameterized and does additional service-specific validations based on those parameters. For this reason, among others, more than one JWKS is typically cached and individually refreshed under different cache keys that include those parameters.
There are three values that are used to control the cache:
expiration time (integer) when a JWKS is needed for validation whose cache entry has expired (time since last refresh > expiration_time), a refresh of the JWKS is performed (if not already in progress) and the token validation of the request needs to wait synchronously until the JWKS has been succesfully refreshed.refresh period (integer) When a JWKS is needed for validation whose cache entry is within the refresh period (time until expiration < refresh_period), the cached JWKS will be used for validation (unless it has expired completely) and the JWKS will be refreshed asynchronously in the background.shared (boolean) when true, shares the cache with all Service instances of the same subclass (e.g. IdentityService) created with shared=true.shared=true!Only one HTTP request at a time will be performed to refresh the JWKS.
In effect, productive systems with regular incoming requests should not experience delays from refreshing a JWKS after the initial fetch of that JWKS. Delays will only happen when the JWKS could not be refreshed during the refresh period, e.g. due to a prolonged outage of the JWKS endpoint or when no requests were received during the refresh period that would have triggered an asynchronous refresh.
{
"shared": false,
"expirationTime": 1800000, // 1800000ms = 30min
"refreshPeriod": 900000, // 900000ms = 15min
}
In rare situations you might need to change the cache configuration.
The expiration time is important to support key rotation scenarios and should not be too high. Otherwise, the security of the application is impacted.
:exclamation: Normally you don't need to overwrite the default values!
To overwrite cache parameters, you need to specify them as key/value pairs in <serviceConfiguration>.validation.jwks:
const authService = new IdentityService(identityServiceCredentials,
{
// override one default value or many
validation: {
jwks: {
expirationTime: 3600000,
// refreshPeriod: 1800000,
// shared: true
}
}
});
A Token FetchCache is a cache for the responses of token fetch requests. It is used by the cached token getter methods (getClientCredentialsToken, getJwtBearerToken, getPasswordToken) to prevent unnecessary performance overhead and reduce the number of requests to the authentication server for repeated token fetches with the same parameters.
Each cache entry consists of the token fetch response object and the timestamp at which the cached token expires. Tokens with a remaining lifetime of less than 5 minutes are not returned from the cache, ensuring callers always receive a token with at least 5 minutes remaining lifetime.
The token fetch cache is a per-instance cache that is enabled by default with a built-in LRU cache of size 100. Since the cache is lazily initialized, there is zero overhead for applications that only use the fetch* methods and never call the get* methods.
{
"enabled": true,
"size": 100
}
To disable or customize the token fetch cache, specify the tokenfetch.cache property in the Service configuration. You can use the simple, built-in LRU cache or alternatively provide any Node.js cache implementation with the standard get/set signature.
The following configuration snippets are examples that use the built-in LRU cache:
const authService = new XsuaaService(xsuaaCredentials,
{
tokenfetch: {
cache: {
size: 1000 // built-in LRU cache with a custom size of 1000 entries
}
}
}
);
// disable the token fetch cache
const authService = new IdentityService(identityServiceCredentials,
{
tokenfetch: {
cache: {
enabled: false // env var: CDS_REQUIRES_<BINDING>_TOKENFETCH_CACHE_ENABLED=false
}
}
}
);
The following configuration snippet is an example that uses the well-known lru-cache module as a custom cache implementation:
const LRUCache = require("lru-cache");
const tokenCache = new LRUCache({ max: 100 });
const authService = new IdentityService(identityServiceCredentials,
{
tokenfetch: {
cache: {
impl: tokenCache
}
}
}
);
If you create a second service instance later and want to share the token fetch cache with an existing service instance, use the tokenFetchCache getter:
const service1 = new XsuaaService(credentials);
// Re-use service1's token fetch cache for service2 by passing it via the impl option
const service2 = new XsuaaService(newCredentials, {
tokenfetch: { cache: { impl: service1.tokenFetchCache } }
});
A Signature Cache is a cache for the results of the cryptographic signature validation of a JWT token. It is used to improve the latency of subsequent requests with the same token (see Caching CPU intensive operations).
Each cache entry consists of the size of the cached JWT as string plus the boolean for the result.
To enable this cache, you can enable the simple, built-in LRU cache or alternatively, provide any Node.js cache implementation with the standard get/set signature. The entries of the cache do not need to be timed out with a TTL for secure operation because a signature that is (in)valid now will always be (in)valid in the future. The validation result is only used when the JWKS still contains a valid key for the token's kid, so JWKS rotation can still be used to invalidate tokens by removing the corresponding public key.
The following configuration snippet is an example that uses the built-in LRU cache:
const authService = new IdentityService(identityServiceCredentials,
{
validation: {
signatureCache: {
enabled: true // enables the built-in LRU cache with default size 100
}
}
}
);
const authService = new XsuaaService(xsuaaCredentials,
{
validation: {
signatureCache: {
size: 1000 // enables the built-in LRU cache with a custom size of 1000 entries
}
}
}
);
The following configuration snippet is an example that uses the well-known lru-cache module as a custom cache implementation:
const LRUCache = require("lru-cache");
const signatureCache = new LRUCache({ max: 100 });
const authService = new IdentityService(identityServiceCredentials,
{
validation: {
signatureCache: {
impl: signatureCache
}
}
}
);
The Token Decode Cache is a cache for the base64 decoded header/payload objects of a JWT token. It is used to improve the latency of subsequent requests with the same token (see Caching CPU intensive operations).
Each cache entry consists of the size of the cached JWT as string plus the header/payload objects.
To enable this cache, you can enable the simple, built-in LRU cache or alternatively, provide any Node.js cache implementation with the standard get/set signature.
The following configuration snippets are examples that use the built-in LRU cache:
const { Token } = require("@sap/xssec");
Token.enableDecodeCache(); // enables the built-in LRU cache with default size 100
Token.enableDecodeCache({
size: 1000 // enables the built-in LRU cache with a custom size of 1000 entries
});
The following configuration snippet is an example that uses the well-known lru-cache module as a custom cache implementation:
const LRUCache = require("lru-cache");
const tokenDecodeCache = new LRUCache({ max: 100 });
Token.enableDecodeCache({
impl: tokenDecodeCache // enables the built-in LRU cache with a custom implementation
});
The library supports a configurable retry mechanism for network requests, such as fetching tokens or JWKS. This ensures resilience against temporary network issues or service unavailability.
The retry logic is based on an exponential backoff strategy with the following configurable parameters:
{
"strategy": "exponential", // The retry strategy (currently only "exponential" is supported)
"retries": 3, // Maximum number of retry attempts
"initialDelay": 500, // Initial delay in milliseconds before the first retry
"factor": 3, // Multiplier for the delay after each retry
"maxDelay": 4000 // Maximum delay in milliseconds between retries
}
initialDelay (e.g., 500ms).factor (e.g., 500ms → 1500ms → 4000ms).maxDelay (e.g., 4000ms).With the default configuration:
maxDelay)If all retries fail, the operation will throw a RetryError object, containing all errors during the attempt.
:exclamation: Note: The retry logic is only applied to network-related errors (e.g., timeouts or unreachable endpoints). It also applies to HTTP error status codes in the range 500–599, as well as 429 (Too Many Requests) and 408 (Request Timeout).
Errors such as invalid configurations or authentication failures are not retried.
const authService = new IdentityService(identityServiceCredentials,
{
requests: {
retry: {
"strategy": "exponential", // The retry strategy (currently only "exponential" is supported)
"retries": 3, // Maximum number of retry attempts
"initialDelay": 500, // Initial delay in milliseconds before the first retry
"factor": 3, // Multiplier for the delay after each retry
"maxDelay": 4000 // Maximum delay in milliseconds between retries
}
}
}
);
//for the default configuration you can also just set it to true
const authService = new IdentityService(identityServiceCredentials,
{
requests: {
retry: true //take the default configuration
}
}
);
JWKS rotation is supported out-of-the-box and you do not need to configure anything for this:
If your authentication service instance is configured to manage certificates and keys on its own, there will be certificate and key properties in the service credentials that work out-of-the-box with this module when you use it for authenticated requests to the authentication service, e.g. when fetching tokens.
If your authentication service instance is configured to use an externally managed certificate/key you might need to add them to the service credentials before passing the credentials to the Service constructor:
const { XsuaaService } = require("@sap/xssec");
const credentials = { clientid, ... } // access service credentials, e.g. via @sap/xsenv
credentials.key = <yourExternallyManagedKey> // in PEM format
credentials.certificate = <yourExternallyManagedCertificate> // in PEM format
const authService = new XsuaaService(credentials) // or IdentityService ...
The following are Identity Service specific configuration options.
The library optionally supports token ownership validation via x5t thumbprint (RFC 8705) for tokens issues by SAP Cloud Identity Service.
:grey_exclamation: x5t token validation should only be enabled for applications using mTLS because the x5t validation will fail when there is no client certificate used for the request. SAP BTP will automatically put the client certificate in the x-forwarded-client-cert header of requests performed against cert application routes. From there it will be picked up by this lib to do the validation against the fingerprint claim from the token payload.
To enable x5t validation, set <serviceConfiguration>.validation.x5t.enabled> flag to true:
const authService = new IdentityService(identityServiceCredentials,
{
validation: {
x5t: {
enabled: true
}
}
});
To enable proof token validation, set <serviceConfiguration>.validation.proofToken.enabled> flag to true:
const authService = new IdentityService(identityServiceCredentials,
{
validation: {
proofToken: {
enabled: true
}
}
});
After creating a SecurityContext on a service with proof token validation enabled, the service plans of the caller can be retrieved:
const securityContext = await createSecurityContext(identityService, { reqWithForwardedClientCertificate });
// service plans are available via securityContext.servicePlans
The XsuaaLegacyExtension enables hybrid authentication during migration from XSUAA to SAP Cloud Identity Services. It automatically exchanges IAS user tokens for XSUAA tokens, allowing applications to support both authentication methods.
Applications migrating from XSUAA-based authentication to SAP Cloud Identity Services may need to:
The extension handles this by automatically fetching an XSUAA token after an IAS user token has been successfully validated, giving the application full flexibility which token to use.
Enable the extension in the IdentityService configuration:
const { IdentityService, XsuaaService, createSecurityContext } = require("@sap/xssec");
const xsuaaService = new XsuaaService(xsuaaCredentials);
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: true
});
// The extension is now active with default settings (IAS-primary mode)
// Both services must be passed to createSecurityContext
const securityContext = await createSecurityContext([identityService, xsuaaService], { req });
The extension fetches the XSUAA token from the first XsuaaService instance from the services array passed to createSecurityContext.
The extension supports two modes, controlled by the primaryContextType configuration property:
IAS-Primary Mode (Default) - For applications adopting AMS authorization
const { IdentityService, XsuaaService, XsuaaLegacyExtension, createSecurityContext } = require("@sap/xssec");
const xsuaaService = new XsuaaService(xsuaaCredentials);
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: true // defaults to IAS-primary
});
// Or explicitly:
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: {
primaryContextType: XsuaaLegacyExtension.IDENTITY_SERVICE_SECURITY_CONTEXT
// or: primaryContextType: "IdentityServiceSecurityContext"
}
});
const securityContext = await createSecurityContext([identityService, xsuaaService], { req });
In this mode, createSecurityContext for IAS user tokens returns an IdentityServiceSecurityContext with the XSUAA context embedded:
IdentityServiceSecurityContext
└─ xsuaaContext: XsuaaSecurityContext
This mode is recommended for applications that have adopted @sap/ams for authorization checks. It is possible to configure the AMS library so that in addition to the policies assigned to users in the IAS directory, additional base policies are granted based on XSUAA scopes. This allows tenants a graceful migration path where existing XSUAA scopes continue to provide equivalent authorization via base policies while the administrator is not yet finished with policy assignments in the directory.
XSUAA-Primary Mode - For applications keeping XSUAA authorization
const { IdentityService, XsuaaService, XsuaaLegacyExtension, createSecurityContext } = require("@sap/xssec");
const xsuaaService = new XsuaaService(xsuaaCredentials);
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: {
primaryContextType: XsuaaLegacyExtension.XSUAA_SECURITY_CONTEXT
// or: primaryContextType: "XsuaaSecurityContext"
}
});
const securityContext = await createSecurityContext([identityService, xsuaaService], { req });
In this mode, createSecurityContext for IAS user tokens returns an XsuaaSecurityContext with the IAS context embedded:
XsuaaSecurityContext
└─ iasContext: IdentityServiceSecurityContext
This mode allows applications to continue using existing XSUAA-based authorization checks such as securityContext.checkLocalScope without further code changes.
Applications following this approach must ensure that IAS technical user tokens are either rejected after token validation or authorized with custom logic as they would still result in an IdentityServiceSecurityContext without the expected methods for checking scopes.
The extension only applies to user tokens by default (not technical/client credentials tokens). You can customize this behavior by subclassing and overriding the appliesTo method:
const { IdentityService, XsuaaService, XsuaaLegacyExtension, createSecurityContext } = require("@sap/xssec");
class TenantSpecificXsuaaLegacyExtension extends XsuaaLegacyExtension {
async appliesTo(iasCtx) {
// First check the default condition (user token)
if (!await super.appliesTo(iasCtx)) {
return false;
}
// Additional custom logic, e.g., tenant allowlist
const allowedTenants = ["tenant1", "tenant2", "tenant3"];
return allowedTenants.includes(iasCtx.token.appTid);
}
}
// Register the custom extension using context.extensions
const xsuaaService = new XsuaaService(xsuaaCredentials);
const identityService = new IdentityService(identityServiceCredentials, {
context: {
extensions: [
new TenantSpecificXsuaaLegacyExtension({
primaryContextType: XsuaaLegacyExtension.XSUAA_SECURITY_CONTEXT
})
]
}
});
const securityContext = await createSecurityContext([identityService, xsuaaService], { req });
The extension caches fetched XSUAA contexts to improve performance. By default, it uses an in-memory LRU cache with a maximum size of 100 entries. When accessed, cached XSUAA contexts with less than 5 minutes token lifetime are removed from the cache and a fresh token is fetched.
Default cache:
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: true // Uses default: LRU cache with size 100
});
Custom cache size:
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: {
cache: { size: 500 }
}
});
Disable caching:
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: {
cache: { enabled: false }
}
});
Custom cache implementation:
const customCache = {
get: (key) => { /* your implementation */ },
set: (key, value) => { /* your implementation */ }
};
const identityService = new IdentityService(identityServiceCredentials, {
xsuaaLegacyExtension: {
cache: { impl: customCache }
}
});
XsuaaSecurityContext without modification)createSecurityContext returns an IdentityServiceSecurityContext even if primaryContextType is set to XsuaaSecurityContextIdentityService and XsuaaService instances to createSecurityContext as an arrayAnalyze the ValidationError thrown by createSecurityContext or passed on by the passport strategy to find the cause for this, e.g. by logging its message or if possible, by inspecting it in the debugger.
@sap/xssec 4 uses ECMAScript 2020 syntax. This means it requires at least Node.js 18 which is the current LTS version. Version 4 does not support Node.js versions that are out of maintenance. If you try, you will encounter SyntaxErrors such as
SyntaxError: Unexpected token '??='
To enable debug logging, set the environment variable DEBUG as follows when starting your application: DEBUG=xssec. Note that DEBUG=xssec:* works only for xssec 3.
:warning: Do not enable in production unless absolutely necessarily to analyze a problem. Sensitive information might be logged when debug logs are enabled, including (but not necessary limited to):
TokenValidationError to console)Please use official SAP support channels to get support under component BC-CP-CF-SEC-LIB or Security Client Libraries.
Before opening support tickets, please check the Troubleshooting section first.
Make sure to include the following mandatory information:
npm list @sap/xssecUnfortunately, we can NOT offer consulting via support channels.
FAQs
XS Advanced Container Security API for node.js
The npm package @sap/xssec receives a total of 184,622 weekly downloads. As such, @sap/xssec popularity was classified as popular.
We found that @sap/xssec demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.