What this is about
Some permissions only apply for a while: for the length of a project, while someone covers for a colleague on vacation, from the first working day. Keycloak does not know anything like this. There, a person has a role or not. That is why CIAS keeps the validity window of a grant itself and brings Keycloak up to date when the time has come.
A grant can have a start (validFrom) and an end (validUntil), both as a point in time with a time of day, both optional. Without either, it is an ordinary grant without a time limit, and that is the normal case.
The four states
stateDiagram-v2
direction LR
[*] --> SCHEDULED: granted, start in the future
[*] --> ACTIVE: granted, no start or start reached
SCHEDULED --> ACTIVE: start reached (timer)
ACTIVE --> EXPIRED: end passed (timer)
SCHEDULED --> EXPIRED: end passed before it started
SCHEDULED --> REVOKED: revoked
ACTIVE --> REVOKED: revoked
EXPIRED --> [*]
REVOKED --> [*]
| State | Meaning | Entered in Keycloak |
|---|---|---|
SCHEDULED | stored, start is in the future | no |
ACTIVE | applies | yes |
EXPIRED | end passed, final | no |
REVOKED | revoked earlier, final | no |
EXPIRED and REVOKED are different states, even though the person no longer has the role in both cases. An audit must be able to answer why someone lost a permission: because the time ran out or because someone revoked it. That is why an expired grant can no longer be revoked afterwards: 409 cias.authorization.invalid-state.
The timeline
gantt
dateFormat YYYY-MM-DD
axisFormat %d.%m.
section Ben covering
granted, SCHEDULED :done, 2026-09-22, 2026-10-01
ACTIVE, role in Keycloak :active, 2026-10-01, 2026-10-15
EXPIRED :crit, 2026-10-15, 2026-10-20
POST /cias/admin/role-assignments
{
"userId": "8c1d…",
"roleClient": "cdms-backend",
"roleKey": "invoice-approve",
"tenantKey": "nordbau",
"validFrom": "2026-10-01T06:00:00Z",
"validUntil": "2026-10-15T18:00:00Z",
"reason": "vacation cover for Anna"
}HTTP 200
{ "id": "d207…", "state": "SCHEDULED", … }Both limits are inclusive: exactly at the point in time validUntil, the grant still applies, after that it does not. CIAS rejects an end before the start: 400 cias.authorization.invalid-request. An end that has already passed as well: 422 cias.authorization.validity-window-ended. Nothing is stored then, and nothing reaches Keycloak.
Granting again: changing the window
Per person, role and tenant there is at most one running grant (SCHEDULED or ACTIVE). Granting the same role again therefore does not create a second one but changes the existing one:
| Window and reason | State before | What happens |
|---|---|---|
| the same | any | nothing; CIAS returns the existing grant, without a new event |
| different | ACTIVE | the grant takes over window and reason; event WindowChanged |
| start now in the future | ACTIVE | first removed from Keycloak, then SCHEDULED; the timer puts it back in at the start |
| start now reached | SCHEDULED | ACTIVE and entered in Keycloak, provided the account is in service; otherwise it stays scheduled |
| end already passed | any | 422 cias.authorization.validity-window-ended, the grant stays as it was |
This is how you extend a stand-in: grant the same role again with the new validUntil. grantedBy stays the original grant; who changed the window is in the event.
The timer
A timer is a job that runs at fixed intervals. Here it looks for all grants whose state no longer matches the clock and puts them in order.
When: SCHEDULED, and validFrom has passed
-
1CIASsets the grant to
ACTIVE -
2CIAS→Keycloakenters the role
Result: Event Activated. First store, then Keycloak: if Keycloak fails, this tends to refuse, and the next run catches up.
When: ACTIVE or SCHEDULED, and validUntil has passed
-
1CIAS→Keycloakremoves the role, if it was entered
-
2CIASsets the grant to
EXPIRED
Result: Event Expired. First Keycloak, then store: the person loses the role even if something fails afterwards.
When: Keycloak cannot be reached for this one grant.
The timer notes it and continues with the next one. On the next run it tries again.
Result: The log contains a warning with the grants that were not finished. Each of them is a permission that someone still has and should no longer have.
| Setting | Default | Meaning |
|---|---|---|
codamai.cias.authorization.synchronization.enabled | off in the starter, on in the standalone CIAS | whether the timer runs |
codamai.cias.authorization.synchronization.interval | PT5M | pause between two runs |
If several instances run, the timer may run on all of them. If two hit the same grant, one loses the lock on the record, and the next run does the rest. The calls to Keycloak can be repeated safely. If you want exactly one run, turn the timer off and call the reconciliation from your own schedule.
When it reaches the token
Between the point in time in the record and what an application sees, there are two delays:
- up to one interval of the timer, 5 minutes by default
- until the person’s token is renewed: the old token still carries the old state
So a grant that ends at 6:00 PM can still take effect for a few minutes after 6:00 PM. See Why revoking permissions takes effect with a delay.
Pitfalls
Next
- Grant a role
- Revoke a role
- For comparison: the validity window of a tenant, Suspend and close tenants, validity