Understand IAS vs XSUAA, OAuth 2.0 flows, JWT structure, and the req.user API in CAP.
req.user is populated from the tokenSecurity in BTP is a layer cake. Misunderstanding one layer causes either a broken app (users can't log in) or an insecure one (the wrong users can). Starting with the architecture means you can debug auth issues systematically instead of guessing.
┌─────────────────────────────────────────────────────────┐
│ BTP Identity Flow │
│ │
│ Browser ──login──► IAS ──SAML──► XSUAA ──JWT──► CAP │
│ │
│ IAS: Corporate identity provider (SSO, MFA) │
│ XSUAA: BTP authorization server (OAuth 2.0, roles) │
│ CAP: Reads JWT, populates req.user │
└─────────────────────────────────────────────────────────┘
| Flow | Use case | Who calls it |
|---|---|---|
| Authorization Code + PKCE | Browser SPA (Fiori) | Fiori app / AppRouter |
| Client Credentials | Service-to-service | CAP → external service |
| JWT Bearer | Principal propagation | CAP → S/4HANA |
| Password | Legacy / testing only | CLI tools |
{
"iss": "https://mysubdomain.authentication.eu10.hana.ondemand.com",
"sub": "user-uuid-abc123",
"exp": 1726524000,
"iat": 1726520400,
"jti": "unique-token-id",
"user_name": "alice@company.com",
"given_name": "Alice",
"family_name": "Smith",
"email": "alice@company.com",
"zid": "tenant-id", // tenant identifier
"scope": ["bookshop.Admin", "bookshop.Buyer", "openid"],
"xs.user.attributes": {
"region": ["EMEA"],
"costCenter": ["CC-1001"]
}
}
this.on('READ', 'Orders', (req) => {
console.log(req.user.id) // 'alice@company.com'
console.log(req.user.name) // 'Alice Smith'
console.log(req.user.locale) // 'en-US'
console.log(req.user.tenant) // 'my-subaccount-id'
console.log(req.user.tokenInfo) // raw JWT claims (when needed)
console.log(req.user.is('Admin')) // true if scope includes bookshop.Admin
console.log(req.user.is('Buyer')) // true if scope includes bookshop.Buyer
console.log(req.user.attr.region) // ['EMEA']
console.log(req.user.attr.costCenter) // ['CC-1001']
})
When a request arrives:
1. CAP extracts Authorization: Bearer <token> header
2. Validates signature using XSUAA's public key (fetched from JWKS endpoint)
3. Checks expiry (exp claim)
4. Populates req.user from validated claims
5. Rejects with 401 if token is missing/invalid
// .cdsrc.json — enables JWT validation
{
"requires": {
"auth": {
"[production]": {
"kind": "xsuaa"
},
"[development]": {
"kind": "mocked",
"users": {
"alice": { "roles": ["Admin"] },
"bob": { "roles": ["Buyer"] }
}
}
}
}
}
Browser ──► AppRouter (Node.js) ──► XSUAA (auth code flow)
│
└──► CAP backend (attaches JWT, forwards request)
The AppRouter handles:
- Redirecting unauthenticated users to XSUAA login page
- Exchanging auth code for JWT
- Attaching JWT to upstream requests to CAP
- Session management (stores JWT in server-side session)
# 1. Decode the JWT (without verifying signature)
echo '<jwt>' | cut -d '.' -f 2 | base64 -d | jq .
# 2. Check XSUAA JWKS endpoint is reachable
curl https://mysubdomain.authentication.eu10.hana.ondemand.com/.well-known/openid-configuration
# 3. Check CAP logs for auth errors
cf logs bookshop-srv --recent | grep -i 'auth\|401\|403'
# 4. Inspect req.user in a handler
this.before('*', '*', (req) => console.log('User:', JSON.stringify(req.user)))
"auth": { "[development]": { "kind": "mocked", ... } } to .cdsrc.json with 3 userscds watch and call an endpoint with Authorization: Basic alice:alicethis.before('*', '*', ...) that logs req.user.id and req.user.rolesreq.user.is('RoleName') checks if the scope contains the app's role prefix