Security News
RubyGems.org Adds New Maintainer Role
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
@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. If your application needs to exchange Identity Service JWTs to XSUAA JWTs, make this call first using the provided API of the new Service
subclasses and then open a SecurityContext with that JWT. To check for which service a JWT is issued, you can use the new acceptsToken
method on Service
instances:let jwt = ... // extract JWT from req object
const token = new Token(jwt);
if(!xsuaaService.acceptsToken(token) && identityService.acceptsToken(token)) {
// in this case, some hybrid systems want to exchange the Identity Service JWT for an XSUAA JWT before opening a SecurityContext
try {
jwt = (await xsuaaService.fetchJwtBearerToken(jwt)).access_token;
} catch(err) {
LOG.error("Error during token exchange:", err);
}
}
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, 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 });
// user is authenticated -> tie the SecurityContext to this req object
req.securityContext = 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.securityContext.checkLocalScope('read')) {
return res.sendStatus(403);
}
// access token information via SecurityContext
return res.send("Hello " + req.securityContext.token.givenName);
};
As an alternative to writing the middleware manually, you can use the provided XssecPassportStrategy
for passport:
const { XssecPassportStrategy, XsuaaService } = 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));
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 { 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.
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:
IdentityService
XsuaaService
XsaService
(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 { 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);
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.
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>
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 StringOptional 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 formatThe 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
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"] }));
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
}
}
});
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
Analyze 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.
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/xssec
Unfortunately, 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 41,592 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
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.
Security News
Research
Socket's threat research team has detected five malicious npm packages targeting Roblox developers, deploying malware to steal credentials and personal data.