Use CDS security annotations to require authentication and restrict access by scope, inspect req.user in handlers, and test locally with mock users.
CAP reads the Authorization: Bearer <token> header and exposes a typed req.user object in every handler — no middleware code needed.
// srv/cat-service.cds
using bookshop from '../db/schema';
@requires: 'authenticated-user'
service CatalogService {
entity Books as projection on bookshop.Books;
entity Orders as projection on bookshop.Orders;
}
Any request without a valid JWT token gets HTTP 401 automatically.
@requires: 'authenticated-user'
service CatalogService {
// Anyone logged in can read
@readonly
entity Books as projection on bookshop.Books;
// Only users with the 'admin' scope can create/update/delete
@restrict: [
{ grant: 'READ', to: 'viewer' },
{ grant: ['WRITE', 'DELETE'], to: 'admin' }
]
entity Orders as projection on bookshop.Orders;
}
The scope names (viewer, admin) must match the role-template names in xs-security.json (CAP strips the $XSAPPNAME. prefix automatically).
// srv/cat-service.js
module.exports = class CatalogService extends cds.ApplicationService {
init() {
this.before('CREATE', 'Orders', req => {
const { id, name, roles } = req.user;
console.log(`Order placed by: ${name} (${id})`);
console.log(`Has admin scope: ${req.user.is('admin')}`);
// Stamp the order with the creator's ID
req.data.createdBy = id;
});
return super.init();
}
};
| Property | Value |
|---|---|
req.user.id |
Subject claim from JWT (unique user ID) |
req.user.name |
Display name |
req.user.attr |
Custom XSUAA attributes |
req.user.is('scope') |
Returns true if the token contains that scope |
req.user.roles |
Array of all scopes in the token |
cds watch activates mock authentication automatically in development.
You can configure mock users in your package.json:
{
"cds": {
"requires": {
"auth": {
"kind": "mocked",
"users": {
"alice": { "roles": ["admin"] },
"bob": { "roles": ["viewer"] },
"carol": {}
}
}
}
}
}
Pass the user as a Basic Auth header (mock only — never in production):
# Test as alice (admin)
curl -u alice: http://localhost:4004/catalog/Orders
# Test as carol (no roles) — should get 403 on restricted entities
curl -u carol: http://localhost:4004/catalog/Orders
Or with the VS Code REST Client:
### Read books (any logged-in user)
GET http://localhost:4004/catalog/Books
Authorization: Basic alice:
### Create order (admin only)
POST http://localhost:4004/catalog/Orders
Authorization: Basic alice:
Content-Type: application/json
{ "bookId": 1, "quantity": 2 }
Once you have a service key from your BTP XSUAA instance, you can test locally against real auth:
# Bind the service key for local use
cds bind xsuaa --to bookshop-xsuaa --kind xsuaa
# Run with the real XSUAA
cds watch --profile hybrid
With --profile hybrid, CAP validates real JWTs from XSUAA instead of mock tokens.
Get a token with the OAuth2 password grant (dev only):
curl -X POST https://your-tenant.authentication.eu10.hana.ondemand.com/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&username=your@email.com&password=yourpw' \
-u '<clientid>:<clientsecret>'
Adding @requires to the service but forgetting to add xsuaa to the requires in package.json:
{
"cds": {
"requires": {
"auth": { "kind": "xsuaa" }
}
}
}
Without this, CAP uses the mocked auth even in production and every user appears as anonymous.