CodamAIDocs
Topicdone

From login to the data

A person logs in and opens a list. Every step through browser, BFF, Keycloak, CIAS and CDMS to the database and back, in both operating modes.

Variants
embedded (one process)standalone (two services)first request after signing inlater request from a running sessionaccess token expiredrefusal at every station

What this is about

This is the path every request in a CodamAI application takes. A person signs in, clicks “Customers” and sees a list. Six programs sit in between, and each one does exactly one thing.

Once you have understood this page, you can read every other page: all other flows are branches off this one.

Who has which color and who is involved at all is on The parties in a request. This page plays the path through once, from front to back.

The two halves

The flow falls into two parts that have nothing to do with each other — except that the second one needs the first:

1. Signing in
once per session
  • browser, BFF and Keycloak
  • ends with the BFF holding three tokens
  • CDMS and CIAS are not involved
2. Fetching data
on every click
  • browser, BFF, CIAS, CDMS, database
  • starts with the BFF taking out the access token
  • Keycloak is asked at most for the token exchange

Part 1: signing in

sequenceDiagram
    autonumber
    participant B as Browser
    participant F as BFF
    participant K as Keycloak
    B->>F: opens /customers
    F-->>B: no session, redirect to /login
    B->>K: Keycloak login page
    K-->>B: password, one-time code if configured
    B->>K: entries
    K-->>B: redirect back with a one-time code
    B->>F: /api/auth/callback/keycloak?code=…
    F->>K: exchanges the code for tokens
    K-->>F: access, refresh and ID token
    F-->>B: encrypted session cookie, back to /customers

Three things from this carry the rest of the page:

  • Only Keycloak ever sees the password. The application never gets it. Details on Sign in in the browser.
  • The tokens stay in the BFF; the browser only holds an encrypted cookie. Details on Session in the BFF and cookies.
  • The access token is the badge for everything that follows. It is short lived (5 minutes by default) and renewed before it expires, see Renew the token.

Part 2: the click on “Customers”

Now for the path this page is about. The person is signed in and clicks “Customers” in the interface.

sequenceDiagram
    autonumber
    participant U as User
    participant B as Browser
    participant F as BFF
    participant K as Keycloak
    participant FK as Filter chain (CIAS)
    participant T as Tenant gate (CIAS)
    participant C as CDMS
    participant DB as Tenant DB
    U->>B: clicks "Customers"
    B->>F: GET /api/customers, session cookie travels along
    F->>F: decrypt the cookie, take out the access token
    opt token expires soon
        F->>K: POST /token, grant_type=refresh_token
        K-->>F: new access token
    end
    F->>FK: POST /api/rest/crm/customer/query<br/>Authorization: Bearer …
    FK->>FK: check signature and expiry
    FK->>K: exchange the token (or take it from the cache)
    K-->>FK: token for the CIAS client
    FK->>FK: read the identity, resolve the tenant
    FK->>T: is nordbau served?
    T-->>FK: yes (remembered for 30 s)
    FK->>FK: build the effective roles and attributes
    FK->>C: RequestContext filled, hand on
    C->>C: resolve response (which fields?)
    C->>C: check the read role for customer
    C->>C: attach the security filters (owner, attributes, custom)
    C->>DB: SELECT of the allowed columns and rows
    DB-->>C: rows
    C->>C: READ hook per row, then turn into DTOs
    C-->>F: 200 with data and meta
    F-->>B: the list as JSON of its own route
    B-->>U: the table appears

That is the whole path. The following sections walk it again slowly, in four stages.

Stage 1: browser and BFF

The browser never calls CDMS or CIAS directly. It calls a route of its own frontend, and that route lives on the BFF, the server part of the interface.

From the click to the outgoing request
  1. 1
    Browser→BFF
    calls a route of its own application, for example GET /api/customers. The browser sends the session cookie automatically
  2. 2
    BFF
    decrypts the cookie with the secret of the portal and takes out the access token
  3. 3
    BFF→Browser
    no access token, or an error in the session → 401, the interface starts a new login
  4. 4
    BFF→Keycloak
    token about to expire? Then fetch a new one first
    The portals renew 90 seconds before expiry. The hub additionally checks before every API call whether the token has already expired, and then renews immediately.
  5. 5
    BFF→CDMS
    sends POST /api/rest/crm/customer/query with Authorization: Bearer <access token> and a body made of response and parameter
    Result: Only the BFF knows the token. The browser has never seen it.

Why the detour? Because in the browser every script on the page could read the token. In the BFF it cannot. The full reasoning is on Session in the BFF and cookies.

The BFF is also where a business route (“give me the customers”) becomes a CDMS request: it assembles the field selection (response) and the search parameters. See Structure of a search.

Stage 2: the filter chain of CIAS

The request arrives at the backend — and there it runs through the filter chain of CIAS first, before a single line of CDMS code runs.

POST /api/rest/crm/customer/query with a bearer token
  1. Filter chain
    Public path?
    A few paths need no token: OPTIONS requests from the browser and whatever a module explicitly publishes. /v3/api-docs and /swagger-ui have a login of their own with user name and password. Everything else needs one
    ↳ no 403 when no token is present
  2. Filter chain
    Check the token
    Is the signature valid against a key of the realm, and is the token not expired? Keycloak is not asked for this
    ↳ no 401 with WWW-Authenticate: Bearer error="invalid_token"
  3. CIAS
    Exchange the token
    Keycloak exchanges the token for one for the CIAS client. Only that one carries all roles and attributes
    ↳ no the request continues without an identity, every role check refuses from then on
  4. CIAS
    Resolve the tenant
    Which tenant is meant? Exactly one, or deliberately none?
    ↳ no 403 cias.authentication.tenant-unresolved
  5. CIAS
    Tenant gate
    Is this tenant served today?
    ↳ no 403 cias.authentication.tenant-not-served
  6. CIAS
    Roles and attributes
    Which roles and which attribute values apply in exactly this tenant?
    ↳ no attribute values not retrievable: 403 cias.authentication.tenant-not-served
  7. CIAS
    Tenant required?
    Does the installation separate data per tenant, is the person known, but has no tenant?
    ↳ no 403 cias.authentication.tenant-required
  8. The application receives the request with a filled RequestContext

The result is called the RequestContext: a note pad that applies to this one request only. It holds user id, name, tenant, allowed tenants, roles, groups, attributes — plus the IP address and the browser identification the filter chain read from the request. Everything after that reads only there, never from the token again.

Two points that are easy to miss:

  • The filter chain is always part of the CDMS process, even when CIAS runs as a separate service. It is a jar, not a server.
  • It clears the note pad after the response. The next call on the same thread starts empty.

Every station in detail is on What happens with the token on every request, the tenant question on The tenant check in both operating modes.

Stage 3: CDMS

Only now is CDMS up. It sees no token, only the RequestContext.

Inside CDMS
  1. 1
    CDMS
    REST layer: reads the body, resolves response — which fields, which references, which lists should come back?
    Without response, CDMS does not know what to deliver and refuses. See Field selection with response.
  2. 2
    CDMS
    System layer: does one of the effective roles allow reading customer?
  3. 3
    CDMS→BFF
    no matching role → 403 missing-permission|<role>
  4. 4
    CDMS
    attaches the security filters: owner filter, attribute filters and the custom filters of the project
    The client's filter and the security filters end up in one AND clause together. So no OR from the client can bypass them.
  5. 5
    CDMS→Tenant DB
    Persistence: picks the database and reads only the requested columns of the allowed rows
    System models go to the system database, tenant and user models to the database of the tenant from the RequestContext.
  6. 6
    Hook
    READ hook for every loaded object, before it becomes part of the response
  7. 7
    CDMS→BFF
    answers with data (the list) and meta (hit count, page)
    Result: No hits is not an error: 200 with an empty list.

The stations inside CDMS have their own page: The path of a request through the layers. Which rows a person sees is on The three levels at a glance.

Stage 4: back to the screen

The way back is short, but it has two peculiarities:

  • Before writing the response, CDMS commits its transaction. On a read that does not show; on a write it is the decisive point — see A write across all layers.
  • The BFF does not pass the data on raw. It reshapes it into what the interface needs, and it translates errors: a missing-permission|project becomes “you are missing a role for this model”, a tenant-required becomes “your account belongs to no tenant”. That sends somebody to the right place instead of to the login screen.

The same path in both operating modes

CDMS and CIAS can run in one process or as two services. The path of the request is the same — only the question “is this tenant served?” takes a different route.

The "list of customers", embedded and standalone

When: CDMS and CIAS run in the same process, like the hub backend.

sequenceDiagram
    autonumber
    participant F as BFF
    participant FK as Filter chain (CIAS)
    participant T as Tenant gate (CIAS)
    participant U as cias-user
    participant C as CDMS
    participant DB as Tenant DB
    F->>FK: POST /api/rest/crm/customer/query + token
    FK->>FK: check and exchange the token, resolve the tenant
    FK->>T: may nordbau be served?
    Note over FK,T: method call, no network
    T-->>FK: yes (remembered for 30 s)
    FK->>U: which attribute values apply here?
    U-->>FK: regions = [north]
    FK->>C: RequestContext filled
    C->>DB: SELECT of the allowed rows
    DB-->>C: rows
    C-->>F: data + meta

Result: One process, no network call between CDMS and CIAS. CIAS cannot fail on its own.

When: CIAS runs as its own service (cias-runtime), CDMS as a second one.

sequenceDiagram
    autonumber
    participant F as BFF
    participant FK as Filter chain (in the CDMS service)
    participant TC as cias-tenancy-client
    participant S as CIAS service
    participant C as CDMS
    participant DB as Tenant DB
    F->>FK: POST /api/rest/crm/customer/query + token
    FK->>FK: check and exchange the token, resolve the tenant
    FK->>TC: may nordbau be served?
    TC->>S: GET /cias/lookup/tenants/nordbau<br/>with the service token of CDMS
    S-->>TC: 200 served: true
    TC-->>FK: yes (remembered for 30 s)
    FK->>TC: which attribute values apply here?
    TC->>S: GET /cias/lookup/users/{id}/attributes?tenantKey=nordbau
    S-->>TC: regions = [north]
    TC-->>FK: values
    FK->>C: RequestContext filled
    C->>DB: SELECT of the allowed rows
    DB-->>C: rows
    C-->>F: data + meta

Result: Two extra HTTP calls — but only when nothing is remembered. The token for them is a service token of CDMS, never the person's.

The differences in full are on Embedded and standalone compared. For the request path what counts most is: the filter chain runs in the CDMS process in both cases, and the answer of both lookups is remembered for 30 seconds. What happens when the CIAS service stays silent is on When CIAS is not reachable.

What travels in which request

Three requests in a row, and each one looks different:

SectionWhat travels alongWho checks it
Browser → BFFsession cookie (encrypted, httpOnly)the BFF, with the secret of the portal
BFF → Keycloak (only when renewing)refresh token, client id, client secretKeycloak
BFF → CDMSAuthorization: Bearer <access token>, plus response and parameter in the bodythe filter chain of CIAS, then CDMS
CDMS → CIAS service (standalone only)a service token of its own, belonging to CDMSthe CIAS service, through a dedicated read role
CDMS → databaseno credentials of the person, but the connection of the tenantthe database itself

Where the path ends when something is missing

Why the list does not arrive
Session in the BFFToken validTenant admittedRead role for the modelWhat the person sees
no–––The interface sends them to sign in
yesno––401 – the token is renewed, the request repeated
yesyesno–403 from the filter chain, with error: cias.authentication.…
yesyesyesno403 from CDMS, with messageKey: missing-permission|<role>
yesyesyesyes200 with the rows the filters let through — possibly none

So there are two kinds of 403, and they look different: the filter chain writes { "error": …, "message": … }, CDMS writes { "messageKey": …, "code": …, "layer": … }. The field name tells you immediately who refused. See 401, 403 or 404?.

An empty list, by the way, is not an error and looks exactly like “there is nothing”. That is deliberate: someone who may not see a row should not be able to conclude that it exists. See Why invisible objects return 404.

Traps

Where to go next

Sources in the code and the knowledge base
  • hub-frontend – server/api/auth/[...].ts, server/utils/sessionToken.ts, refreshToken.ts, backendFetch.ts, ciasFetch.ts, useCmsApi.ts, server/api/hub/projects/index.get.ts
  • CDMS/frontend, CIAS/cias-frontend – server/utils/useCmsApi.ts, app/middleware/auth.global.ts
  • CIAS/cias-authentication – SessionConfig (filter chain behind the BearerTokenAuthenticationFilter, public paths), JwtSessionFilter (IP, user agent, headers `tenant`/`user`, RequestContext), TokenParser.tokenParser/admit, TenantGate, EffectiveRoles, EffectiveAttributes, AttributeLookup, ContextSwitch
  • CIAS/cias-tenancy – TenantLookupController; CIAS/cias-tenancy-client – RemoteTenantLookupAdapter
  • CDMS/cdms-rest-api – AbstractRestApi, Expander, RequestTransactionCommitter
  • CDMS/cdms-system-layer – AbstractSystemLayer.queryObjects, AbstractLayer.recursiveQuery (role check, security filters in one AND clause, READ hook)
  • CDMS/cdms-authorization – AbstractAuthorizationLayer, AbstractAttributeFilter
  • commons-persistence – DatabaseRequestContext.getEntityManager, resolveTenant
Search