Use req.user.id, roles, is(), attr, tenant, and tokenInfo. Prevent privilege escalation by never setting security fields from req.data.
req.user and how to access itCustom handlers sometimes need to make auth decisions that CDS annotations can't express — combining multiple attributes, checking group membership, or calling a permission service. Knowing the full req.user API and how to access raw token claims gives you that flexibility.
this.on('*', '*', (req) => {
// Identity
req.user.id // 'alice@company.com' (user_name from JWT)
req.user.name // 'Alice Smith' (given_name + family_name)
req.user.email // 'alice@company.com' (email from JWT)
req.user.locale // 'en-US' (locale from JWT or Accept-Language)
// Authorization
req.user.roles // ['Admin'] — scopes stripped of app prefix
req.user.is('Admin') // true if 'bookshop.Admin' in JWT scope
req.user.is('Admin', 'Buyer') // true if EITHER role
req.user.hasScope('bookshop.Admin') // exact scope check (with prefix)
// Attributes
req.user.attr.region // ['EMEA'] — from xs.user.attributes
req.user.attr.costCenter // ['CC-1001']
// Tenant
req.user.tenant // 'abc-123' — zid from JWT
// Raw token
req.user.tokenInfo.getTokenInfo().userInfo // full JWT payload
})
const tokenInfo = req.user.tokenInfo
if (tokenInfo) {
const claims = tokenInfo.getTokenInfo()
const groups = claims.userInfo['xs.system.attributes']?.['xs.saml.groups'] || []
const grantType = claims.userInfo['grant_type'] // 'client_credentials' for service tokens
}
this.on('READ', 'Orders', async (req) => {
const { user } = req
// Multi-condition authorization
if (user.is('Admin')) {
return SELECT.from('Orders') // admins see all
}
if (user.is('RegionalManager')) {
const regions = user.attr.region || []
return SELECT.from('Orders').where({ region: { in: regions } })
}
if (user.is('Buyer')) {
return SELECT.from('Orders').where({ buyer_ID: user.id })
}
// No matching role
return req.reject(403, 'Insufficient permissions')
})
this.before('*', '*', (req) => {
// Service tokens have grant_type = 'client_credentials'
// and no personal user info
const isServiceCall = !req.user.id || req.user.id === '$system'
if (isServiceCall) {
// Audit differently for service calls
console.log('Service call:', req.event, 'on', req.target?.name)
}
})
// When CAP calls another service, it needs to propagate the user context
const bpService = await cds.connect.to('BusinessPartnerAPI')
this.on('READ', 'Suppliers', async (req) => {
// CAP automatically forwards the JWT when using req.query delegation
return bpService.run(req.query)
// The JWT is attached to the outbound request automatically
})
// When you need to call an HTTP endpoint directly (not via cds.connect.to)
const { tokenInfo } = req.user
const destService = await cds.connect.to('destination-service')
const options = await destService.getOptions({
destinationName: 'S4HANA_SYSTEM',
tokenExchangeType: 'UserTokenToIdToken',
userToken: tokenInfo.getTokenValue()
})
const response = await axios.get(`${options.url}/api/endpoint`, {
headers: options.headers // includes exchanged token
})
// WRONG — never trust client-provided user data
this.before('CREATE', 'Orders', (req) => {
req.data.buyer_ID = req.data.buyer_ID // client can fake this!
})
// CORRECT — always use token-verified identity
this.before('CREATE', 'Orders', (req) => {
req.data.buyer_ID = req.user.id // from validated JWT
})
// WRONG — never log JWT tokens
console.log('Token:', req.user.tokenInfo) // logs secret
// WRONG — never expose tokenInfo to clients
return req.user.tokenInfo // leaks internal token
req.user.attr.region and filters Orders by regionreq.user.id) and log them separatelyreq.user.idreq.user.is('Role') checks scopes (stripped of app prefix)req.user.attr.attrName reads user attributes from the JWTreq.user — never from request bodycds.connect.to() + bpService.run(req.query)