Worum es geht
Ein Token wird immer für einen bestimmten Client ausgestellt. Meldet sich jemand im Hub an, bekommt der BFF ein Token für den Client des Hubs. Darin steht, was der Hub-Client sehen darf, und nicht unbedingt alles, was CIAS braucht: die Rollen des Backend-Clients, die Organisationen, die Attribute für den Mandanten.
Deshalb tauscht CIAS jedes eingehende Token bei Keycloak gegen eines für seinen eigenen Client ein, etwa cias-backend oder cdms-backend. Das Verfahren heißt Token-Exchange und ist ein Standard von OAuth.
Keycloak tauscht nur, wenn das eingehende Token den Client von CIAS in seiner Audience (aud) nennt. Die einzige Ausnahme: Der Client tauscht sein eigenes Token. Genau diese Regel prüft CIAS schon beim Prüfen des Tokens, sodass ein Token für einen fremden Client gar nicht erst bis zum Tausch kommt. Das getauschte Token muss als azp den Client von CIAS nennen, sonst nimmt CIAS es nicht an.
Der Vorteil: Das Frontend muss nicht wissen, welche Rollen und Claims das Backend braucht. Es schickt sein Token, CIAS besorgt sich den Rest.
Der Ablauf
sequenceDiagram
participant C as Client
participant S as Filterkette (CIAS)
participant M as Cache
participant K as Keycloak
C->>S: Anfrage mit Token (jti = abc)
S->>M: gibt es ein getauschtes Token für abc?
alt Treffer
M-->>S: getauschtes Token
else kein Treffer
S->>K: POST /token<br/>grant_type=token-exchange<br/>subject_token=…
K-->>S: Token für den CIAS-Client
S->>M: merken unter abc
end
S->>S: Identität aus dem getauschten Token lesen
jti ist die eindeutige ID eines Tokens, ein Claim, den Keycloak in jedes Token schreibt. Sie ist der Schlüssel des Caches.
Die Anfrage an Keycloak
| Feld | Wert |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
client_id, client_secret | der Client von CIAS (cias_client, cias_client_secret) |
subject_token | das Token, das der Client geschickt hat |
subject_token_type | urn:ietf:params:oauth:token-type:access_token |
audience | nur wenn restrict-audience: true, dann der eigene Client |
Es ist derselbe Client, dessen Rollen CIAS danach aus resource_access.<client>.roles liest. So passen getauschtes Token und gelesene Rollen immer zusammen.
Die Varianten
Wann: Dasselbe Token (gleiche jti) kam vor kurzem schon einmal.
CIAS nimmt das gemerkte getauschte Token. Keycloak wird nicht gefragt.
Ergebnis: Der schnelle Normalfall, denn ein Client schickt dasselbe Token bei vielen Anfragen hintereinander.
Wann: Ein neues Token, zum Beispiel direkt nach einem Refresh.
CIAS tauscht bei Keycloak und merkt sich das Ergebnis. Es bleibt höchstens 5 Minuten im Cache und nie länger als bis 10 Sekunden vor Ablauf des getauschten Tokens.
Ergebnis: Ein Aufruf an Keycloak, danach wieder Treffer.
Wann: Das Token hat keine jti.
Ohne Schlüssel kann CIAS nichts merken. Jede Anfrage mit diesem Token wird bei Keycloak neu getauscht.
Ergebnis: Funktioniert, kostet aber bei jeder Anfrage einen Aufruf an Keycloak.
Wann: Keycloak lehnt den Tausch ab, etwa weil der Tausch am Client nicht eingeschaltet ist.
-
1CIAS→Keycloakbittet um den Tausch
-
2Keycloak→CIAS400
invalid_request: Standard token exchange is not enabled… -
3CIASschreibt einen Fehler mit Status und Antwort von Keycloak ins Log
-
4CIASbeendet die Anfrage mit 401
cias.authentication.token-rejected
Ergebnis: Die Anfrage erreicht die Anwendung nicht. Sie läuft nie ohne Identität weiter.
Wann: Keycloak antwortet nicht oder mit einem Serverfehler (5xx).
CIAS schreibt den Grund ins Log und antwortet mit 503 cias.authentication.identity-provider-unavailable. Am Token liegt es nicht.
Ergebnis: Die Anfrage kann später gelingen.
Einstellungen
codamai:
cias:
token-exchange:
ttl: 5m # CIAS_TOKEN_EXCHANGE_TTL
expiry-margin: 10s # CIAS_TOKEN_EXCHANGE_MARGIN
max-entries: 10000 # CIAS_TOKEN_EXCHANGE_MAX_ENTRIES
restrict-audience: false| Einstellung | Standard | Bedeutung |
|---|---|---|
ttl | 5 Minuten | wie lange ein getauschtes Token höchstens gemerkt wird |
expiry-margin | 10 Sekunden | Abstand zum Ablauf des getauschten Tokens, ab dem es nicht mehr aus dem Cache kommt |
max-entries | 10 000 | Obergrenze des Caches. Ist er voll, fliegen erst abgelaufene Einträge, sonst wird er geleert |
restrict-audience | false | ob audience mitgeschickt wird |