PackagePackagetypeA library you install and call yourself.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

@arc-mcp/xsuaa-auth

The client-to-server authentication stack for an MCP server on SAP BTP: an XSUAA OAuth proxy, dynamic client registration, a chained bearer verifier, and a separate entrypoint for per-user principal propagation.

An MCP server deployed to Cloud Foundry has XSUAA at the other end of the client's OAuth flow, and the awkward parts are the ones nobody wants to write twice: the /authorize redirect-URI shim, XSUAA's un-encoded + in the OAuth state, and RFC 7591 dynamic client registration that survives a restart. setupHttpAuth composes those onto an Express application and returns the bearer middleware for the /mcp route, and the same primitives are exported for a server that wires them itself. Verification is a chain of XSUAA, OIDC and API key, each optional, and the scope policy is injected as a hook that the package never owns. The ./btp entrypoint is where a verified token becomes SAP identity: it resolves a destination with the user's own JWT and returns the credentials and a proxy descriptor for the Cloud Connector, leaving the header assembly to the caller. It was extracted from arc-1's auth stack, which the README names beside calmcp and LISA as the servers it was shaped to fit.

Facts

Latestv1.2.0ReleasedLast pushLicenceMIT

Where it sits

Lives in
Provides
AuthenticationBTPAuthenticationcapabilityEstablishing who is calling: validating credentials or tokens, resolving the user, and making that identity available to the application. [Authorization](capability:authorization) decides what that identity may do; [identity provisioning](capability:identity-provisioning) manages its lifecycle across systems.
Runs
Cloud Foundry
Requires
SAP Authorization and Trust Management serviceSAP Authorization and Trust Management serviceartifactthe bound XSUAA service instance whose credentials and application URL the facade reads from the environment, and the authorization server the OAuth proxy fronts@sap/xssec@sap/xssecartifactXSUAA token validation and security context, which the package consumes internally for its XSUAA verifier.@sap-cloud-sdk/connectivity@sap-cloud-sdk/connectivityartifactdestination lookup and the connectivity proxy that the package's BTP entrypoint builds on.
Optional
SAP Destination serviceSAP Destination serviceartifactthe destination the entrypoint looks up with the verified user JWT to obtain the per-user tokensCloud ConnectorCloud Connectorartifactthe route from Cloud Foundry into the customer network that the returned BTPProxyConfig points atSAP Connectivity serviceSAP Connectivity serviceartifactreading the connectivity binding from VCAP_SERVICES so the Cloud Connector proxy descriptor can be built.

Who is behind it

ARC MCPARC MCPOrganisationMaintains this
In the atlas: maintains 9
@arc-mcp
Marian ZeisMarian ZeisPersonMaintains this
In the atlas: maintains 35, contributes to 86, wrote 25
@marianfoo
maintainersGeert-Jan KlapsPersonContributes to this
In the atlas: maintains 4, contributes to 11, wrote 4
@geert-janklaps
1 contributor

Before choosing this

Conditions

PracticalFunctional boundary

It stops at the AuthInfo and the raw bearer token, and the README lists what stays with the consuming server: rate limiting, the scope and tool policy, the MCP transport, the SAP HTTP client's headers, and helmet-style hardening. The one piece of hardening it constrains rather than defers is the cross-origin opener policy, which it deliberately leaves unset because popup-based OAuth breaks under it. It is ESM-only on Node 22 or later and shares the host's Express and MCP SDK instances as peer dependencies, so a server on Express 4 cannot take it.

What it declares

Every artifact

Type
Package
Origin
Independently maintained
Runtime
Node.js
Runs on
Cloud Foundry

As a package

Where it runs
does not say
Distributed through
npm

For authentication

Supports
Token with no userPasses the user onCorporate IdP behind it
property statedstated nonot said

Tasks this contributes to1Beta

Reach a system in the customer's own network as the signed-in userAn application on SAP BTP calls a system inside the customer's network and that system sees the person who signed in, not a shared service user.Coversper-user propagation for an MCP server
What it does

Does the cloud half of this for an MCP server: it resolves a destination with the verified user's own JWT and returns the per-user credentials and a proxy descriptor for the Cloud Connector, so the call leaves as that user rather than as a service account.

Remaining work

Assemble the outgoing request. Its README leaves the headers, the cookies and the forward-proxy call to the consuming server, and the destination and the connector still have to be configured as above.

All options on the Authentication page →

Connections9

@arc-mcp/xsuaa-auth connections@arc-mcp/xsuaa-authPROVIDES→ provides1provides stated Puts the XSUAA OAuth flow, dynamic client registration and bearer verification in front of an MCP server's HTTP route, and maps the verified user to their own SAP identity through a destination rather than a shared service account. arc-mcp/xsuaa-auth — README: https://raw.githubusercontent.com/arc-mcp/xsuaa-auth/main/README.md arc-mcp/xsuaa-auth — README: https://raw.githubusercontent.com/arc-mcp/xsuaa-auth/main/README.mdAuthenticationUSED BY← needed by1needed by derived the XSUAA OAuth proxy and BTP principal-propagation layer the server authenticates callers with and reaches SAP through Through: inverted from its requires edge ClementRingot/LISA — README: https://raw.githubusercontent.com/ClementRingot/LISA/main/README.mdLISA← optionally used by1optionally used by derived If: XSUAA auth mode is active implementing the MCP OAuth 2.1 proxy and JWT verification used by [@arc-mcp/xsuaa-auth](artifact:arc-mcp-xsuaa-auth) in XSUAA mode Through: inverted from its requires edge rosa: docs/ARCHITECTURE.md: https://raw.githubusercontent.com/ClementRingot/ROSA/main/docs/ARCHITECTURE.mdROSAifNEEDS→ needs3needs stated destination lookup and the connectivity proxy that the package's BTP entrypoint builds on. xsuaa-auth README: https://github.com/arc-mcp/xsuaa-auth@sap-cloud-sdk/connectivityneeds stated XSUAA token validation and security context, which the package consumes internally for its XSUAA verifier. xsuaa-auth README: https://github.com/arc-mcp/xsuaa-auth@sap/xssecneeds stated the bound XSUAA service instance whose credentials and application URL the facade reads from the environment, and the authorization server the OAuth proxy fronts arc-mcp/xsuaa-auth — README: https://raw.githubusercontent.com/arc-mcp/xsuaa-auth/main/README.mdSAP Authorization and Trust Managementservice→ optionally needs3optionally needs stated If: the propagated user reaches an on-premise system, for which the `./btp` entrypoint returns a connectivity proxy descriptor the caller applies the route from Cloud Foundry into the customer network that the returned `BTPProxyConfig` points at arc-mcp/xsuaa-auth — README: https://raw.githubusercontent.com/arc-mcp/xsuaa-auth/main/README.mdCloud Connectorifoptionally needs stated If: the ./btp entrypoint is used for per-user principal propagation via Cloud Connector reading the connectivity binding from VCAP_SERVICES so the Cloud Connector proxy descriptor can be built. xsuaa-auth README: https://github.com/arc-mcp/xsuaa-authSAP Connectivity serviceifoptionally needs stated If: the `./btp` entrypoint is used, which is the per-user principal propagation path; a server that only verifies bearer tokens never reaches it the destination the entrypoint looks up with the verified user JWT to obtain the per-user tokens arc-mcp/xsuaa-auth — README: https://raw.githubusercontent.com/arc-mcp/xsuaa-auth/main/README.mdSAP Destination serviceif

Install

npm add @arc-mcp/xsuaa-auth

Derived from the package name. No recipe recorded; the README has the configuration.

Where it sits in your app

Download
Your application on SAP BTPYour SAP BTP subaccountdocumentedrequiresdocumentedmay needif the `./btp` entrypoint isused, which is the per-userprincipal propagation path; a se…documentedmay needif the propagated user reachesan on-premise system, forwhich the `./btp` entrypoint ret…documentedrequiresdocumentedmay needif the ./btp entrypoint isused for per-user principalpropagation via Cloud ConnectordocumentedrequiresdocumentedoutbounddocumentedHTTP, outbound@arc-mcp/xsuaa-auth, module@arc-mcp/xsuaa-authCloud Connector, moduleCloud Connector@sap/xssec, module@sap/xssec@sap-cloud-sdk/connectivity, module@sap-cloud-sdk/conne…SAP Authorization and Trust Management service, service instanceSAP Authorizationand TrustManagement serviceSAP Destination service, service instanceSAP DestinationserviceSAP Connectivity service, service instanceSAP Connectivityservice
SAPnot SAP's, or the project's ownauthenticationdocumented onlyunder a condition

README

quoted from arc-mcp/xsuaa-auth under MIT, read
arc-mcp/xsuaa-auth/README.mdOn GitHub

@arc-mcp/xsuaa-auth

XSUAA / OAuth authentication + SAP BTP principal propagation for Model Context Protocol servers built on Express and @modelcontextprotocol/sdk.

CI CodeQL OpenSSF Scorecard npm

It gives an MCP server the SAP-native client→server auth stack: an XSUAA OAuth proxy provider, a stateless RFC 7591 Dynamic Client Registration store (HMAC-signed client_ids, restart-resilient), the OAuth-state callback codec that works around XSUAA's un-encoded + in state, a chained bearer verifier (XSUAA → OIDC → API-key, each optional), and a thin setupHttpAuth facade. A separate ./btp entrypoint adds per-user principal propagation via the BTP Destination Service + Cloud Connector.

Two API layers, same package: a plug-and-play facade for the common flow, and the building blocks it composes for full control.

Extracted from arc-1's production auth stack and designed so arc-1, calmcp, and LISA can adopt it with a minimal diff. The full design rationale is frozen in docs/SPEC.md and docs/RESEARCH.md.


Install

npm install @arc-mcp/xsuaa-auth

ESM-only, Node >= 22. You also need these peer dependencies (the package shares the host's Express + MCP SDK instances rather than bundling its own):

PeerRangeRequired?
@modelcontextprotocol/sdk>=1.18.2 <2yes — 1.18.2 is the first version exposing mcpAuthRouter({ resourceServerUrl })
express^5.0.1yes — the SDK hard-depends on Express 5; v4 cannot coexist with its router
jose>=5 <7optional — only for the OIDC verifier; lazy-imported, so non-OIDC consumers can skip it
npm install @modelcontextprotocol/sdk express
npm install jose            # only if you use createOidcVerifier / AuthOptions.oidc

@sap/xssec and @sap-cloud-sdk/connectivity are regular dependencies and install automatically.


Quickstart

Layer 1 — the setupHttpAuth facade (plug-and-play)

The facade composes the standard XSUAA + DCR + callback + bearer flow and returns the bearer middleware for your /mcp route. On Cloud Foundry, loadXsuaaCredentials() and resolveAppUrl() read the bound XSUAA service and the public route straight from the environment — no hand-parsed binding.

import express from 'express';
import { setupHttpAuth, loadXsuaaCredentials, resolveAppUrl } from '@arc-mcp/xsuaa-auth';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const bearer = setupHttpAuth(
  app,
  {
    apiKeys: process.env.API_KEYS,                 // string | ApiKeyEntry[]
    xsuaa: {
      credentials: loadXsuaaCredentials(),         // from VCAP_SERVICES
      appUrl: resolveAppUrl(process.env, { publicUrlEnvVar: 'PUBLIC_URL', port: 8080 }),
      clientIdPrefix: 'myapp-',
      resourceName: 'My MCP Server',
      // requiredScopes: ['Viewer'],               // enforced via requireBearerAuth
    },
    oidc: { issuer: process.env.OIDC_ISSUER!, audience: process.env.OIDC_AUDIENCE! },
    allowedOrigins: ['https://claude.ai'],          // CORS for browser MCP clients
    required: true,                                 // fail closed if nothing is configured
  },
  logger,                                           // optional Logger (default: no-op)
);

// Mount your MCP transport behind the returned middleware.
app.all('/mcp', bearer!, myMcpHandler);

What the facade does in XSUAA mode: applies CORS (if allowedOrigins set) → builds the chained verifier → creates the XSUAA OAuth provider → mounts the pattern-gated /authorize ensureRedirectUri shim, the /oauth/callback proxy, and the SDK mcpAuthRouter (discovery + authorize/token/register/revoke) → returns requireBearerAuth. With no xsuaa block (API-key/OIDC only) it returns bearer middleware without mounting the OAuth router. With no method configured it throws when required: true, else logs a loud warning and returns undefined (open).

It deliberately sets no restrictive Cross-Origin-Opener-Policy — popup-based OAuth (Copilot Studio, claude.ai) breaks under COOP: same-origin. Broader hardening (helmet CSP/HSTS) stays yours to add.

Expired or revoked refresh grants return 400 invalid_grant so clients can sign in again. Other upstream refresh failures remain 500 server_error.

Layer 2 — building blocks (full control)

When you orchestrate the HTTP server yourself (arc-1, LISA), call the same primitives the facade uses:

import {
  createXsuaaOAuthProvider,
  createXsuaaTokenVerifier,
  createOidcVerifier,
  createApiKeyVerifier,
  createChainedTokenVerifier,
  createOAuthCallbackHandler,
  StatelessDcrClientStore,
  OAuthStateCodec,
  validateRedirectUri,
} from '@arc-mcp/xsuaa-auth';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';

// 1. The XSUAA OAuth provider also hands you the DCR store + state codec.
const { provider, clientStore, stateCodec } = createXsuaaOAuthProvider(credentials, appUrl, {
  clientIdPrefix: 'myapp-',
  dcrSigningSecret: process.env.DCR_SIGNING_SECRET, // stabilizes client_ids across restarts
});

// 2. Chain the verifiers (XSUAA → OIDC → api-key; each optional).
//    Pass expandScopes to EACH sub-verifier — that's the layer that applies it
//    (the chain does not re-apply it; see the note below). The chain's own
//    `{ expandScopes }` only configures the api-key verifier it builds internally.
const verifier = createChainedTokenVerifier(
  { apiKeys: process.env.API_KEYS },
  createXsuaaTokenVerifier(credentials, { expandScopes }),
  createOidcVerifier(issuer, audience, { algorithms: ['RS256', 'ES256', 'PS256'], expandScopes }),
  { expandScopes },
);

// 3. Wire it onto your app exactly how you need (callback proxy, /authorize shim, router…).
app.get('/oauth/callback', createOAuthCallbackHandler(stateCodec, clientStore, { logger }));
app.use(mcpAuthRouter({ provider, issuerUrl, baseUrl, resourceServerUrl, scopesSupported, resourceName }));
app.all('/mcp', requireBearerAuth({ verifier: { verifyAccessToken: verifier }, resourceMetadataUrl }), myMcpHandler);

Each verifier accepts an optional injected expandScopes policy hook (default identity), applied exactly once by the sub-verifier that produces the AuthInfo. createChainedTokenVerifier does not re-apply it on top of a sub-verifier's result (so a non-idempotent expander runs once, not twice) — it only uses the hook for the api-key verifier it builds from config.apiKeys. arc-1 passes its authz/policy function so AuthInfo carries expanded scopes; other consumers omit it. The package owns no scope/tool policy — it's injected, never owned.

Each verifier also accepts acceptedScopes (default the arc-1 set ['read','write','data','sql','transports','git','admin']) — the scope-name allowlist applied to a token's claims. Override it (e.g. ['Viewer']) when your scopes differ, or, via the facade, set xsuaa.scopesSupported (which the facade threads to both verifiers).

createOidcVerifier additionally accepts fallbackScopes (default [], fail closed) — the scopes granted when a verified OIDC token carries no accepted scope (no scope/scp claim, or claims that match none of acceptedScopes). The empty default means an IdP misconfigured to drop scope claims grants no access rather than silently falling back to read-only. Opt into the legacy read-only behavior with fallbackScopes: ['read'] (via the facade, oidc.fallbackScopes). It is not run through expandScopes.

Verified user attributes (optional building block)

createXsuaaTokenVerifier accepts userAttributeNames and requireUserToken for applications that authorize individual user principals with XSUAA attributes. Omitting both options preserves the existing verifier behavior and AuthInfo shape. This is not enabled by setupHttpAuth.

const verify = createXsuaaTokenVerifier(credentials, {
  userAttributeNames: ['arc1_targets'],
  requireUserToken: true,
});
const authInfo = await verify(accessToken);
// authInfo.extra.xsuaaUserAttributes: { arc1_targets: ['A4H/001', 'A4H/100'] }
// authInfo.extra.xsuaaUserAttributeStatus: { arc1_targets: 'valid' }

Only allowlisted attributes from the successfully validated SAP security context are copied. The arrays and records are frozen; missing, malformed, and over-limit values carry distinct safe status codes. Target syntax and application policy stay in the consumer. Do not request an OAuth scope named user_attributes to activate this feature.

requireUserToken rejects machine or unknown principals with the exported XsuaaUserTokenRequiredError. It extends the MCP SDK's InsufficientScopeError, so requireBearerAuth returns 403 with this package's forbidden wire code, not an insufficient_scope step-up challenge; ordinary InvalidTokenError remains 401. A custom adapter can instead map its stable XSUAA_USER_TOKEN_REQUIRED code to a generic 403. A machine principal cannot satisfy this policy by requesting more scopes. Do not parse error messages or retry another authentication method after this terminal error; the package's chain preserves the terminal decision in either JWT verifier slot. It unwraps own Error.cause wrappers and normalizes errors from separately loaded package copies to the fixed local error. Ordinary missing-scope challenges remain insufficient_scope. The tested TypeScript SDK clients do not retry OAuth on forbidden; other MCP clients may recover on any 403 and need separate validation.

The chain still tries other verifiers after an ordinary validation/JWKS failure. Do not configure an alternative verifier that accepts the same XSUAA tokens with a weaker principal policy. For a user-only XSUAA route, use the configured XSUAA verifier directly. Attribute extraction alone is not proof of a user principal or application permission; enforce the required local scopes too. See the attribute/principal contract and live-evidence gates.


AuthOptions

The facade's configuration object. All fields are optional except where noted.

FieldTypeDefaultNotes
apiKeysstring | ApiKeyEntry[]—A single static key, or [{ key, scopes?, clientId? }]. Matched in constant time. A bare string key grants scopes: [] — it authenticates but fails any requiredScopes; use [{ key, scopes: [...] }] to grant scopes.
xsuaaobject—Present ⇒ XSUAA OAuth proxy is mounted (see sub-fields below). Omit for API-key/OIDC-only.
xsuaa.credentialsXsuaaCredentialsrequired{ url, clientid, clientsecret, xsappname, uaadomain, verificationkey? }. Use loadXsuaaCredentials().
xsuaa.appUrlstringrequiredPublic URL the OAuth metadata advertises. Use resolveAppUrl().
xsuaa.clientIdPrefixstring'mcp-'Prefix for issued DCR client_ids — set a per-deployment value.
xsuaa.dcrKdfLabelstring'mcp-dcr/v1'Domain-separation label for DCR client_id signing. Bumping it revokes all issued client_ids.
xsuaa.stateKdfLabelstring'mcp-oauth-state/v1'Domain-separation label for OAuth-state tokens.
xsuaa.resourceNamestringSDK defaultresource_name in the protected-resource metadata.
xsuaa.scopesSupportedstring[]—Advertised in OAuth metadata. Also threaded to the XSUAA + OIDC verifiers as acceptedScopes — set it to your own scope names (e.g. ['Viewer']) so non-arc-1 scopes aren't filtered out of verified tokens.
xsuaa.requiredScopesstring[]—Enforced via the SDK's requireBearerAuth({ requiredScopes }).
xsuaa.redirectUriPatternsreadonly string[]XSUAA_DEFAULT_REDIRECT_URI_PATTERNSRedirect allowlist for the pre-registered default client; also permits extra DCR custom schemes. See Redirect validation.
xsuaa.dcrRedirectUriPatternsreadonly string[]unsetOptional allowlist for DCR redirect URIs. Registration rejects non-matching URIs; earlier client_ids keep only matching URIs. [] rejects every DCR redirect. See Redirect validation.
xsuaa.defaultRedirectUrisreadonly string[]XSUAA_DEFAULT_REDIRECT_URISInitial URIs for the default client; each must also match redirectUriPatterns at the callback. Narrow both lists together.
xsuaa.dcrEnabledbooleantrueSet false to remove /register and its discovery metadata. Existing registrations and manual clients remain usable, with browser approval.
xsuaa.dcrTtlSecondsnumber2592000 (30d)DCR client_id lifetime. 0 disables expiry (recommended for clients that don't auto-re-register on invalid_client).
xsuaa.stateTtlSecondsnumber600Legacy codec state lifetime. Browser-approved flows always expire within ten minutes, even when set to 0.
xsuaa.dcrSigningSecretstringXSUAA clientsecretDedicated HMAC secret for DCR client_ids — set a ≥32-byte value so a clientsecret rotation doesn't invalidate cached client_ids.
xsuaa.callbackUrlstring${appUrl}/oauth/callbackThis server's callback URL sent to XSUAA. Must share the configured appUrl origin; HTTPS required except HTTP loopback development. Allow it in xs-security.json redirect-uris; it need not match the client patterns.
oidcobject—{ issuer, audience, clockToleranceSec?, algorithms?, scopeClaim?, acceptedScopes?, fallbackScopes? }. Lazy-imports jose. algorithms defaults to ['RS256','ES256','PS256']; scopeClaim overrides the primary scope-claim name (default scope); acceptedScopes is the scope-name allowlist for OIDC-only deployments with custom scope names (defaults to xsuaa.scopesSupported, else the arc-1 set); fallbackScopes (default [], fail closed) is the scope set granted when a verified token carries no accepted scope — set ['read'] for legacy read-only fallback.
allowedOriginsstring[]—Exact-match CORS allowlist (with credentials) for browser MCP clients. Unset = no CORS.
requiredbooleanfalsetrue ⇒ throw if no method configured; false ⇒ warn + return undefined (open).
expandScopes(scopes: string[]) => string[]identityInjected scope-expansion policy, applied by every verifier.

setupHttpAuth(app, options, logger?) returns the /mcp bearer RequestHandler, or undefined when no method is configured and required is falsy.

Browser approval

Every authorization request presents an approval page before forwarding to XSUAA, including requests from manual clients and previously registered clients. The page shows the registered client name, actual return address, and requested permissions. An existing identity-provider SSO session does not skip this approval.

The page identifies the server using its configured public origin. The application name is explicitly unverified; it is not a trusted publisher identity. Dynamically registered applications supply their own names. Long names are shortened to 100 Unicode code points so they cannot bury the decision details. The full browser-normalized return address remains visible. Neither a familiar name nor a local address proves an application's identity. The page keeps decision details and a brief trust warning visible; longer explanations live in this documentation. Scope identifiers belong to the consuming server, so the library does not translate arbitrary scopes into permission promises. Without an explicit scope request, the page states that access can include all permissions assigned to the account for that server.

Internationalized callback hosts appear in their ASCII form and path characters may be percent-encoded, as in the browser's navigation URL. The original redirect value remains unchanged in the signed form submission.

Users choose Deny or Approve and continue. Approval continues to the configured identity provider, which may reuse an SSO session. Denial cancels this authorization request; it does not revoke tokens from an earlier connection. An expired or invalid request must be restarted from the MCP client in the same browser.

Approval is required for each new authorization flow, not each MCP call, transport reconnect with a valid token, or refresh-token exchange. A new browser flow can occur after signing out, losing cached credentials, an expired/revoked refresh token, or a client choosing to authenticate again. There is no fixed reapproval interval and no remember-approval or disable-approval setting. Disabling DCR does not disable approval.

The page uses native HTML forms and inline CSS, without JavaScript or external assets. It supports keyboard focus, narrow screens, dark mode and forced colors. The error page shares the same layout and gives restart guidance without a retry form. See approval page maintenance for the design rationale and manual accessibility checks.

Approval requires a same-origin form submission bound to a host-only, HttpOnly cookie and the exact OAuth request. A separate ten-minute cookie binds the XSUAA callback to the browser that approved it. Local authorization codes are signed and bound to the client and return address before token exchange; XSUAA continues to enforce PKCE and single use. Concurrent tabs and multiple instances work without a shared session store when their public URL and signing secret agree. Reverse proxies must preserve Cookie and Origin headers and advertise the correct public appUrl, including its path prefix.

Existing client IDs remain valid. Sign-ins in flight during the upgrade must restart: legacy unbound state and raw upstream authorization codes are rejected. Existing refresh tokens are not invalidated by this upgrade. If exposure is suspected, use documented XSUAA incident controls and verify token invalidation independently. Rotating the DCR signing key alone does not revoke upstream tokens. Disabling DCR blocks new registrations, not existing clients or stolen tokens.

Upgrading deployments

Provider construction rejects an appUrl that is not HTTPS, except HTTP on localhost, 127.0.0.1 or [::1]. The callback must share that public origin. Configure the public URL explicitly outside Cloud Foundry; a non-loopback HTTP bind address is not a public OAuth URL. Existing API-key or OIDC authentication does not remove these requirements when XSUAA is enabled alongside it. For HTTP development with the SDK router, use localhost or 127.0.0.1: the currently tested SDK rejects an IPv6 HTTP issuer. IPv6 client callback URLs remain supported.

Use the same canonical public origin in every client and browser. Alternate routes or custom-domain aliases can lose the host-only approval cookie and require sign-in to restart at the configured origin. Proxies must preserve Cookie, Set-Cookie and Origin, and must not overwrite the approval page's Referrer-Policy: origin.

An ordinary sign-in uses two /authorize requests: the approval page and its form submission. Both count toward the existing authorization endpoint limit. Account for shared-IP traffic without disabling that protection. Upgrade every instance before resuming sign-ins; a mixture of old and new OAuth implementations is not supported.

Redirect validation

XSUAA validates the server's callback URL; this package validates client redirects. For the pre-registered default client, redirectUriPatterns gates added URIs and the callback. Keep patterns narrow and align defaultRedirectUris with them.

DCR registration separately accepts HTTPS, loopback HTTP and the known schemes claude:, cursor:, vscode: and vscode-insiders:; patterns can permit extra custom schemes. DCR callbacks must exactly match a URI in the signed registration. Narrowing the patterns does not restrict DCR HTTPS domains or known schemes. To restrict them, set dcrRedirectUriPatterns (same glob syntax; schemes and hosts are case-insensitive, paths and queries are case-sensitive; write hosts in ASCII form). It only narrows the policy above: extra custom schemes still need a redirectUriPatterns entry, and the pre-registered default client keeps its separate policy. If any redirect URI fails to match, the whole registration is rejected with HTTP 400 invalid_client_metadata. Allowlist every URI a client registers; for example, http://127.0.0.1:*/** does not cover port-less http://127.0.0.1/.

Earlier signed client IDs are filtered at lookup. Excluded URIs are ignored while the restriction applies; widening the policy restores them. A client without an allowed URI cannot start authorization. Update its callbacks to allowed addresses and re-register, or adjust the policy deliberately. Rotate the signing secret or dcrKdfLabel to revoke previously issued client IDs permanently. [] rejects every DCR redirect but keeps /register mounted; dcrEnabled: false removes new registration. Unset adds no restriction to the default DCR policy.

DCR registration rejects userinfo and fragments even when the allowlist is unset. Invalid or disallowed redirects now return HTTP 400 invalid_client_metadata instead of HTTP 500 server_error. Direct registerClient callers receive InvalidClientMetadataError with a generic message; do not match the previous error text. Rejection audits include the parsed host and the reason invalid_redirect_uri or redirect_uri_not_allowlisted, without the submitted URL.

Every authorization still requires explicit client approval, including loopback and VS Code return addresses. The allowlist restricts where a client may return; it does not establish the client's identity or the user's consent. It does not revoke already-issued access or refresh tokens. Keep this policy identical on every instance. Upgrade every instance to a supporting library version before enabling the policy; a mixed deployment can still handle a request on an instance using the old policy. Client custom schemes belong in this package's configuration, not xs-security.json.


Principal propagation (./btp)

The ./btp entrypoint maps the authenticated MCP user to their own SAP identity via the BTP Destination Service + Cloud Connector. The handoff from the auth layer is just the raw, already-verified bearer JWT (authInfo.token).

import {
  listDestinationsAtLevel,
  lookupDestinationWithUserToken,
  lookupDestinationWithUserTokenUncached,
  parseVCAPServices,
  resolveBTPDestination,
} from '@arc-mcp/xsuaa-auth/btp';

// Technical (shared) destination — no per-user identity:
const { url, username, password, client, proxy } = await resolveBTPDestination('SAP_TRIAL', logger);

// Per-user principal propagation — pass the verified user JWT:
const btpConfig = parseVCAPServices(process.env)!;
btpConfig.requestTimeoutMs = 15_000; // optional; defaults to 10s and is capped at 60s
const subaccountDestinations = await listDestinationsAtLevel(btpConfig, 'subaccount', logger);
const { destination, authTokens } = await lookupDestinationWithUserToken(
  btpConfig,
  'MY_PP_DESTINATION',
  authInfo.token,    // the verified bearer JWT (guarded: must be a 3-segment JWT, not an API key)
  logger,
);
// authTokens: { sapConnectivityAuth?, bearerToken?, ppProxyAuth?, samlAssertionAuthorization? }

// Drift-sensitive per-request lookup: bypasses both reads and writes of the SDK cache.
await lookupDestinationWithUserTokenUncached(btpConfig, 'MY_PP_DESTINATION', authInfo.token, logger);

The package returns credentials + a proxy descriptor; it never applies them. Your SAP HTTP client owns header assembly (Authorization / SAP-Connectivity-Authentication / Proxy-Authorization) and the forward-proxy request. What to do when no PP token is produced (throw vs. fall back to BasicAuth) is your policy.

Which PerUserAuthTokens field is populated depends on the destination's Authentication type (they're mutually exclusive). Apply whichever one is set:

PerUserAuthTokens fieldDestination AuthenticationApply as
sapConnectivityAuthPrincipalPropagation (Cloud Connector)SAP-Connectivity-Authentication header, alongside Proxy-Authorization from the connectivity proxy
bearerTokenOAuth2UserTokenExchange / OAuth2SAMLBearerAssertionAuthorization: Bearer <bearerToken>
samlAssertionAuthorizationSAMLAssertion (e.g. S/4HANA Public Cloud developer extensibility — the same flow SAP Business Application Studio uses)Authorization: <value> verbatim (already prefixed, e.g. SAML2.0 …), alongside x-sap-security-session: create
ppProxyAuth—Reserved (jwt-bearer "Option 1" → Proxy-Authorization); never produced by lookupDestinationWithUserToken, a consumer assigns it itself
ExportPurpose
parseVCAPServices(env?)Build a BTPConfig from VCAP_SERVICES (XSUAA + destination + connectivity bindings).
listDestinationsAtLevel(cfg, level, logger?)Fetch the complete subaccount or instance collection without the SDK cache. Basic credentials remain only in the explicit User/Password fields; originalProperties excludes known credential, token, authorization-header, and certificate material while preserving non-secret custom properties.
lookupDestination(cfg, name, logger?)Resolve a destination (works with BasicAuth destinations, no user JWT).
lookupDestinationWithUserToken(cfg, name, userJwt, logger?)The PP primitive — per-user destination + PerUserAuthTokens. JWT-only (anti-footgun).
lookupDestinationWithUserTokenUncached(cfg, name, userJwt, logger?)Same PP result with useCache:false; failed or successful resolutions cannot affect a later lookup.
createConnectivityProxy(cfg, locationId?, logger?)A BTPProxyConfig descriptor for the Cloud Connector connectivity proxy.
resolveBTPDestination(name, logger?)Convenience: destination → { url, username, password, client, proxy }.
BTPRequestTimeoutErrorTyped timeout from direct BTP requests that are not intentionally wrapped in a more specific service error. Its timeoutMs field contains the effective bounded timeout.
DEFAULT_BTP_REQUEST_TIMEOUT_MS / MAX_BTP_REQUEST_TIMEOUT_MSPublic 10-second default and 60-second hard cap for consumers that expose the setting.

Direct Destination and Connectivity Service requests use one abortable timeout for the fetch and response body together. Configure it with BTPConfig.requestTimeoutMs; invalid or non-positive values use the 10-second default, and values above 60 seconds are capped. This applies to service-token acquisition, collection/Find calls, Connectivity proxy tokens, and the direct principal-propagation fallback. The SAP Cloud SDK-owned per-user destination lookup manages its own transport and is not covered by this option.


@sap/xssec is CommonJS — interop note

@sap/xssec is pure CommonJS (no ESM entry). This package consumes it with a default import + destructure under esModuleInterop, which is the supported pattern from an ESM module on Node 22+:

import xssec from '@sap/xssec';
const { XsuaaService, createSecurityContext } = xssec;   // NOT `import { XsuaaService } from '@sap/xssec'`

You don't need to do this yourself — the package handles XSUAA validation internally. It's documented here because it's the one interop sharp edge if you extend the package or import @sap/xssec alongside it. A named ESM import (import { XsuaaService } from '@sap/xssec') will fail.


What's not included

By design, the package's job ends at producing AuthInfo + the raw bearer token (and, via ./btp, destination credentials). These stay with the consuming server:

  • Rate limiting — per-IP and per-user limiters are deferred (see docs/SPEC.md §14); each consumer keeps its own for now.
  • Scope / tool policy — expandScopes is an injected hook; the package owns no ACTION_POLICY or scope semantics.
  • The MCP transport — you own /mcp (stdio / Streamable HTTP); the package contributes middleware + the OAuth router.
  • The SAP HTTP client — header assembly, CSRF, cookies, stateful sessions, and the forward-proxy request are yours.
  • Safety ceiling / server config / the MCP tools — entirely consumer-owned.
  • Helmet / CSP / HSTS — broader HTTP hardening is yours (and must keep COOP unset for popup OAuth).

Documentation

  • docs/SPEC.md — the frozen API contract: every public signature, the dependency ranges, the logger contract, the auth↔PP coupling, and the adoption path for each consumer.
  • docs/RESEARCH.md — extraction research, the three-way (arc-1 / calmcp / LISA) reality check, and the Architecture Decision Records.
  • SECURITY.md — vulnerability reporting + the security-relevant configuration knobs.
A note on the logger

Token-exchange failure events log the HTTP status, never the upstream body, which may echo secrets.

The injected Logger uses (message, data?) argument order:

interface Logger {
  debug(message: string, data?: Record<string, unknown>): void;
  info(message: string, data?: Record<string, unknown>): void;
  warn(message: string, data?: Record<string, unknown>): void;
  error(message: string, data?: Record<string, unknown>): void;
  emitAudit?(event: Record<string, unknown>): void;   // optional; always null-guarded
}

It's optional everywhere and defaults to a no-op (noopLogger). pino users ((obj, msg) order) pass a thin adapter:

const adapter: Logger = {
  debug: (m, d) => log.debug(d ?? {}, m),
  info: (m, d) => log.info(d ?? {}, m),
  warn: (m, d) => log.warn(d ?? {}, m),
  error: (m, d) => log.error(d ?? {}, m),
};

License

MIT © Marian Zeis

MIT, read

History

Latest

v1.2.0

Features 1, Fixes 1

Notes
Features
  • oauth: add optional DCR redirect allowlist (#92) (0819530)
Bug Fixes
  • auth: clarify client approval and improve accessibility (#93) (442dd14)
  1. v1.1.1

    Fixes 2

    Notes
    Bug Fixes
    • oauth: answer invalid_grant when XSUAA rejects a refresh token (#85) (5c7029a)
    • oauth: require browser-bound client consent (#90) (3f75258)
  2. v1.1.0

    Features 1

    Notes
    Features
    • add verified XSUAA user attributes and principal enforcement (#70) (f2042c1)
  3. v1.0.2

    Fixes 1

    Notes
    Bug Fixes
    • deps: remediate npm audit findings (#52) (cbcfd9b)
  4. v1.0.1

    Fixes 1

    Notes
    Bug Fixes
    • deps: align Biome schema with installed version (#46) (090144d)
  5. v1.0.0

    Fixes 1

    Notes
    Bug Fixes
    • align Biome schema with installed version (4afc543)
  6. v0.1.8

    Features 4, Fixes 5

    Notes
    Features
    • add uncached and level-specific destination APIs (#31) (a54cbc5)
    • arc-mcp-xsuaa-auth 0.1.0 — XSUAA/OAuth + BTP PP auth for MCP servers (245795d)
    • facade: add oidc.acceptedScopes for OIDC-only deployments (#14) (35a9364)
    • support for saml assertion destinations (#21) (645e998)
    Bug Fixes
    • do not issue client_secret for public DCR clients (auth method "none") (#6) (a756986)
    • exclude package.json from biome so release-please bumps do not break lint (#8) (c03413f)
    • harden Destination Service Find handling (#33) (ba30e8d)
    • harden XSUAA verifier + OAuth callback, export DEFAULT_ACCEPTED_SCOPES (#17) (84f7526)
    • time out BTP service requests (#35) (70196f9)
  7. v0.1.7

    Fixes 1

    Notes
    Bug Fixes
  8. v0.1.6

    Fixes 1

    Notes
    Bug Fixes
    • harden Destination Service Find handling (#33) (ba30e8d)
  9. v0.1.5

    Features 1

    Notes
    Features
    • add uncached and level-specific destination APIs (#31) (a54cbc5)
  10. v0.1.4

    Features 1, Fixes 1

    Notes
    Features
    • support for saml assertion destinations (#21) (645e998)
    Bug Fixes
    • harden XSUAA verifier + OAuth callback, export DEFAULT_ACCEPTED_SCOPES (#17) (84f7526)
  11. v0.1.3

    Features 1

    Notes
    Features
    • facade: add oidc.acceptedScopes for OIDC-only deployments (#14) (35a9364)
  12. v0.1.2

    Fixes 1

    Notes
    Bug Fixes
    • exclude package.json from biome so release-please bumps do not break lint (#8) (c03413f)
  13. v0.1.1

    Features 1, Fixes 1

    Notes
    Features
    • arc-mcp-xsuaa-auth 0.1.0 — XSUAA/OAuth + BTP PP auth for MCP servers (245795d)
    Bug Fixes
    • do not issue client_secret for public DCR clients (auth method "none") (#6) (a756986)
2,621downloads per week, npm
Weekly downloads for @arc-mcp/xsuaa-auth26 complete calendar weeks from 2026-04-20 to 2026-10-18; the latest total is 2621.20 Apr – 26 Apr: 027 Apr – 3 May: 04 May – 10 May: 011 May – 17 May: 018 May – 24 May: 025 May – 31 May: 01 Jun – 7 Jun: 08 Jun – 14 Jun: 015 Jun – 21 Jun: 71322 Jun – 28 Jun: 69629 Jun – 5 Jul: 9016 Jul – 12 Jul: 83213 Jul – 19 Jul: 1,34320 Jul – 26 Jul: 1,77527 Jul – 2 Aug: 2,2103 Aug – 9 Aug: 1,53210 Aug – 16 Aug: 1,24217 Aug – 23 Aug: 1,52424 Aug – 30 Aug: 1,07931 Aug – 6 Sept: 1,0617 Sept – 13 Sept: 57414 Sept – 20 Sept: 1,19421 Sept – 27 Sept: 2,06528 Sept – 4 Oct: 2,0985 Oct – 11 Oct: 1,96812 Oct – 18 Oct: 2,621
Authentication
  1. SAP Authorization and Trust Management serviceOAuth server issuing JWTs and holding scopes for BTP apps
  2. Cloud Identity ServicesIdentity Authentication and Identity Provisioning as one tenant
  3. SAP Customer Identity and Access Management for B2CRegistration, login and consent for customer-facing sites
  4. OAuth 2.0 on SAP BTPOAuth 2.0 grant flows for applications and APIs in Neo regions
  5. abap_cloud_platformTypeScript library reaching ABAP service instances on Cloud Foundry
  6. @marianfoo/sap-mcp-authBrowser login and session module for SAP MCP servers
  7. @sap/approuterSAP's Node.js application router in front of business applications
  8. Cloud ConnectorSAP's on-premises agent linking SAP BTP to customer-network systems
  9. @saptools/cf-xsuaaOAuth2 token fetcher for a Cloud Foundry app's XSUAA binding

Details

Sources4
GitHub APIdiscovered here, arc-mcpchecked
npm registry, @arc-mcp/xsuaa-authversion, releases, installschecked
github.com/git+https://github.com/arc-mcp/xsuaa-auth.gittree, pushes, contributors, CIchecked
README.mdquoted, and what the entry declareschecked

Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.

Compatibility and provenance
runtimesnode
deploymentCloud Foundry
latestv1.2.0
first release
releases and cadence13, ~4d median
discovered viaGitHub API · arc-mcp
first seen in the atlas
ecosystem and domainbtp, security-and-compliance
Contributors1

From the repository’s contributor list, .

History in the atlas2
  1. The BTP door now compares its authentication options on whether the signed-in identity reaches the next system. The README's sentence about the `./btp` entrypoint states it for this package, and it was read again on this date.
  2. First seen.
For your agent
Ref
artifact:arc-mcp-xsuaa-auth
JSON
/artifacts/arc-mcp-xsuaa-auth.json
MCP
Connecting an agent