Securing CAP Services with XSUAA Role-Based Authorization
Learn how CAP services enforce authentication and authorization using the XSUAA service, restrict-based annotations, and scopes, and how this model differs from classic ABAP authorization objects.
Explanation
CAP applications exposed on SAP BTP almost never remain unprotected once they move past a local development sandbox. The moment a service is bound to a real subaccount and consumed by a UI, integration flow, or another CAP/Node/Java application, authentication and authorization become a first-class design concern. CAP addresses this through a declarative security model layered on top of the SAP Authorization and Trust Management service, commonly referred to as XSUAA, and (in Cloud Foundry today, with identity services evolving on BTP) the Identity Authentication service for federated login. At the core of CAP security are two things: authentication requirements and authorization restrictions. Authentication is typically configured at the application router or approuter level, which handles the OAuth2 authorization code flow against XSUAA and forwards a validated JWT token to backend CAP services. CAP itself does not implement the login screen; it trusts the token issued by XSUAA and inspects claims such as scopes, attributes, and user ID. Authorization inside CAP is expressed declaratively in the CDS model using annotations like @requires and @restrict. @requires specifies which authenticated role(s) a user must have to access an entity or service as a whole, while @restrict allows fine-grained control per operation (READ, WRITE, CREATE, UPDATE, DELETE) and can reference dynamic expressions tied to data, such as restricting write access to records where a user field matches the requesting user's ID. This is conceptually similar to authorization checks in ABAP, but instead of authorization objects and PFCG roles, CAP relies on OAuth2 scopes defined in xs-security.json and mapped to XSUAA role templates, which administrators then assign to role collections in the BTP cockpit. A critical design decision is where XSUAA scopes are defined: in the xs-security.json file bound during deployment, which declares scopes, role templates, and attributes. These are not automatically usable until role collections are created in the subaccount and assigned to users or groups. This two-step separation—developer defines scopes and roles in code, administrator assembles role collections in the cockpit—is a recurring point of confusion for teams new to BTP, because it means a correctly coded @restrict annotation will still reject all users until the corresponding role collection is created and assigned. At runtime, the flow is: the browser or client authenticates via approuter, which redirects to XSUAA (or an upstream identity provider federated through Identity Authentication service), receives a JWT, and attaches it to requests forwarded to the CAP service. CAP's middleware validates the token signature and expiry against XSUAA's public keys, extracts scopes, and evaluates them against the annotations in the compiled CDS model before allowing the request to reach the underlying handler logic. If validation fails, CAP responds with 401 (unauthenticated) or 403 (forbidden) depending on whether the token was missing/invalid or valid but insufficiently scoped. Troubleshooting typically starts with checking whether the JWT actually contains the expected scope, since role collection assignment delays or naming mismatches between xs-security.json and CDS annotations are the most common cause of unexpected 403 responses. Local testing with mocked authentication (using CAP's built-in mocked users in package.json cds configuration) is useful during development but must not be mistaken for production security validation, since mocked auth bypasses real XSUAA token verification entirely. On S/4HANA Cloud public edition or on-premise systems integrated with CAP extension applications, the underlying business authorization (for example, restricting which company codes or plants a user can see) is usually still enforced by the backend S/4HANA system's own authorization concept, while CAP-level XSUAA scopes control access to the extension application itself. Conflating these two layers—assuming XSUAA scopes replace S/4HANA authorization checks—is a design mistake that surfaces during security review, since CAP only governs the extension surface, not the system of record's data authorization unless explicitly re-implemented.
Code example
// xs-security.json (relevant excerpt){ "xsappname": "orders-srv", "tenant-mode": "dedicated", "scopes": [ { "name": "$XSAPPNAME.OrdersAdmin", "description": "Full access to orders" }, { "name": "$XSAPPNAME.OrdersRead", "description": "Read-only access to orders" } ], "role-templates": [ { "name": "OrdersAdmin", "scope-references": ["$XSAPPNAME.OrdersAdmin"] }, { "name": "OrdersRead", "scope-references": ["$XSAPPNAME.OrdersRead"] } ]} // srv/orders-service.cdsservice OrdersService { @requires: 'authenticated-user' entity Orders as projection on my.Orders; // Only users with OrdersAdmin scope can create/update/delete @restrict: [ { grant: 'READ', to: ['OrdersRead', 'OrdersAdmin'] }, { grant: ['CREATE','UPDATE','DELETE'], to: 'OrdersAdmin' } ] entity ProtectedOrders as projection on my.Orders;} // package.json (local mock users - development only){ "cds": { "requires": { "auth": { "kind": "mocked", "users": { "alice": { "roles": ["OrdersAdmin"] }, "bob": { "roles": ["OrdersRead"] } } } } }}Real project scenario
A consulting team built a CAP-based order exception management extension for an S/4HANA Cloud public edition customer. During UAT, warehouse staff reported they could see orders but could not correct exception flags, while a supervisor could not see any data at all despite being assigned the admin role. Investigation revealed two issues: the supervisor's role collection had been created in the wrong subaccount (a common mistake when multiple subaccounts exist for dev/test/prod), and the warehouse role template's scope name in xs-security.json had been renamed during a refactor without redeploying the security configuration, so the deployed XSUAA instance still had the old scope name while the CDS model referenced the new one. Resolving it required redeploying the security artifacts, recreating the role collection in the correct subaccount, and re-testing token contents using a JWT decoder before declaring the fix verified.
Common mistakes
• Assuming a correctly written @restrict annotation is enough without creating and assigning the corresponding role collection in the BTP cockpit • Relying on mocked authentication users during development and never validating real XSUAA token behavior before go-live • Renaming scopes in xs-security.json without redeploying the security configuration, causing mismatches between deployed XSUAA scopes and CDS annotations • Conflating CAP-level XSUAA authorization with backend S/4HANA authorization, leaving business data checks unimplemented • Forgetting that scope names must be prefixed correctly to match $XSAPPNAME conventions, causing silent authorization failures • Not testing role collection assignment across all relevant subaccounts (dev, test, prod) separately
Best practices
• Keep xs-security.json scopes and CDS @restrict annotations in sync and redeploy security configuration whenever scope names change • Use distinct role templates for read versus write access rather than a single broad admin scope • Test authorization with real XSUAA tokens in a dev subaccount before relying on mocked users for sign-off • Document which role collections must be created in which subaccounts as part of deployment runbooks • Treat CAP authorization as protecting the extension surface only, and explicitly verify how backend system data authorization is handled • Log authorization failures with enough context (user, requested scope, entity) to speed up production troubleshooting without exposing sensitive token content
Interview angle
Interviewers assess whether a candidate understands that CAP security is a two-part system: developer-defined scopes and restrictions in code versus administrator-managed role collections in the cockpit. Strong answers explain the JWT validation flow, differentiate @requires from @restrict, describe how to debug a 403 by inspecting token scopes, and clearly state that CAP-level authorization does not automatically replace backend system authorization such as S/4HANA authorization objects.