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:
- browser, BFF and Keycloak
- ends with the BFF holding three tokens
- CDMS and CIAS are not involved
- 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.
-
1Browser→BFFcalls a route of its own application, for example
GET /api/customers. The browser sends the session cookie automatically -
2BFFdecrypts the cookie with the secret of the portal and takes out the access token
-
3BFF→Browserno access token, or an error in the session → 401, the interface starts a new login
-
4BFF→Keycloaktoken about to expire? Then fetch a new one firstThe portals renew 90 seconds before expiry. The hub additionally checks before every API call whether the token has already expired, and then renews immediately.
-
5BFF→CDMSsends
POST /api/rest/crm/customer/querywithAuthorization: Bearer <access token>and a body made ofresponseandparameterResult: 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.
-
Filter chainPublic path?A few paths need no token:
OPTIONSrequests from the browser and whatever a module explicitly publishes./v3/api-docsand/swagger-uihave a login of their own with user name and password. Everything else needs one↳ no 403 when no token is present -
Filter chainCheck the tokenIs 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" -
CIASExchange the tokenKeycloak 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
-
CIASResolve the tenantWhich tenant is meant? Exactly one, or deliberately none?↳ no 403
cias.authentication.tenant-unresolved -
CIASTenant gateIs this tenant served today?↳ no 403
cias.authentication.tenant-not-served -
CIASRoles and attributesWhich roles and which attribute values apply in exactly this tenant?↳ no attribute values not retrievable: 403
cias.authentication.tenant-not-served -
CIASTenant required?Does the installation separate data per tenant, is the person known, but has no tenant?↳ no 403
cias.authentication.tenant-required - 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.
-
1CDMSREST layer: reads the body, resolves
response— which fields, which references, which lists should come back?Withoutresponse, CDMS does not know what to deliver and refuses. See Field selection withresponse. -
2CDMSSystem layer: does one of the effective roles allow reading
customer? -
3CDMS→BFFno matching role → 403
missing-permission|<role> -
4CDMSattaches the security filters: owner filter, attribute filters and the custom filters of the projectThe client's filter and the security filters end up in one AND clause together. So no
ORfrom the client can bypass them. -
5CDMS→Tenant DBPersistence: picks the database and reads only the requested columns of the allowed rowsSystem models go to the system database, tenant and user models to the database of the tenant from the RequestContext.
-
6HookREAD hook for every loaded object, before it becomes part of the response
-
7CDMS→BFFanswers with
data(the list) andmeta(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|projectbecomes “you are missing a role for this model”, atenant-requiredbecomes “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.
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:
| Section | What travels along | Who checks it |
|---|---|---|
| Browser → BFF | session cookie (encrypted, httpOnly) | the BFF, with the secret of the portal |
| BFF → Keycloak (only when renewing) | refresh token, client id, client secret | Keycloak |
| BFF → CDMS | Authorization: Bearer <access token>, plus response and parameter in the body | the filter chain of CIAS, then CDMS |
| CDMS → CIAS service (standalone only) | a service token of its own, belonging to CDMS | the CIAS service, through a dedicated read role |
| CDMS → database | no credentials of the person, but the connection of the tenant | the database itself |
Where the path ends when something is missing
| Session in the BFF | Token valid | Tenant admitted | Read role for the model | What the person sees |
|---|---|---|---|---|
| no | – | – | – | The interface sends them to sign in |
| yes | no | – | – | 401 – the token is renewed, the request repeated |
| yes | yes | no | – | 403 from the filter chain, with error: cias.authentication.… |
| yes | yes | yes | no | 403 from CDMS, with messageKey: missing-permission|<role> |
| yes | yes | yes | yes | 200 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
- The path of the token: how tenant, roles and attributes get into the token
- The tenant check in both operating modes
- A write across all layers: the same path, but with saving
- When CIAS is not reachable
- The path of a request through the layers: the CDMS half in detail
- What happens with the token on every request: the CIAS half in detail