52 · JWT & req.user API

Use req.user.id, roles, is(), attr, tenant, and tokenInfo. Prevent privilege escalation by never setting security fields from req.data.

Episode 52 — JWT Token Handling in Handlers

What you'll learn

Why this matters

Custom 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.

req.user API reference

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
})

Reading raw JWT claims

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
}

Implementing role-based logic in handlers

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')
})

Detecting service-to-service calls

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)
  }
})

Forwarding the JWT to downstream services

// 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
})

Manual token exchange (principal propagation)

// 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
})

Security pitfalls

// 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

Hands-on exercise

  1. Add a handler that reads req.user.attr.region and filters Orders by region
  2. Detect service-to-service calls (no req.user.id) and log them separately
  3. Override the buyer_ID in a BEFORE CREATE handler with req.user.id
  4. Write a test that verifies the buyer_ID cannot be spoofed by the client

Checkpoint ✓

Sign in for progress tracking & Q&A →