Entwicklerdokumentation: Personal Access Tokens
Überblick
Die Personal-Access-Token-(PAT)-Umsetzung ergänzt den CAS um eine kleine, optional aktivierbare REST-API. Über diese API kann ein vertrauenswürdiges Backend PATs für einen fachlichen Benutzer erzeugen, auflisten, lesen und löschen.
Die aktuelle Umsetzung umfasst Verwaltung, sichere Ablage und Anmeldung mit PATs. PATAuthenticationHandler prüft Token und Eigentümer; PATServiceTicketFactory begrenzt Service-Tickets anhand des gespeicherten Scopes. Ein Beispiel für den vollständigen API-Zugriff folgt im Abschnitt „Praktischer Anmeldefall: Usermgt-API mit PAT“.
Die wichtigsten Eigenschaften sind:
- Die Funktion ist standardmäßig deaktiviert.
- Die API wird mit HTTP Basic abgesichert und zustandslos betrieben.
- Der aufrufende technische Account und der fachliche Eigentümer eines PATs sind getrennte Identitäten.
- Ein Klartext-Token wird genau einmal in der Antwort auf seine Erzeugung ausgegeben.
- Persistiert wird nur ein SHA-256-Fingerprint des vollständigen Tokens.
- Lese- und Löschoperationen sind in der Persistenz immer an die Benutzer-ID gebunden.
- SQLite ist die aktuell implementierte Datenbank; die Persistenz ist für weitere Datenbankanbieter vorbereitet.
- Flyway verwaltet das PAT-Schema unabhängig von der übrigen CAS-Konfiguration.
Systemkontext und Vertrauensgrenze
Die PAT-API ist nicht als direkte Endbenutzer-API gedacht. Vorgesehen ist ein Backend wie das User Management, das bereits eine Benutzersitzung besitzt und daraus die fachliche Benutzer-ID ermittelt.
Endbenutzer
│ authentifiziert sich am User Management
▼
User-Management-Backend
│ HTTP Basic + fachliche userId im URL-Pfad
▼
CAS PAT-API
│
├── erzeugt Token und Metadaten
├── protokolliert technischen Aufrufer und Eigentümer getrennt
└── persistiert Fingerprint und Metadaten
│
▼
dedizierte PAT-DatenbankDer technische Aufrufer authentifiziert sich über die global konfigurierten Spring-Security-Credentials (spring.security.user.name und spring.security.user.password). Die userId im Pfad bezeichnet dagegen den Eigentümer des PATs.
Diese Trennung bildet zugleich die wesentliche Vertrauensgrenze: Der CAS prüft, ob der Aufrufer gültig per HTTP Basic authentifiziert ist. Er prüft aber nicht, ob dieser Aufrufer die im Pfad angegebene userId verwalten darf. Die korrekte Zuordnung muss daher vom aufrufenden Backend gewährleistet werden. Jeder Account, der von dieser Security-Konfiguration akzeptiert wird, kann grundsätzlich PATs für beliebige Benutzer-IDs verwalten.
Die Pfade werden wie folgt registriert
/cas/api/users/{userId}/pats
/cas/api/users/{userId}/pats/{id}Aufbau der Implementierung
Der PAT-Code liegt hauptsächlich unter app/src/main/java/de/triology/cas/pat. Die Pakete sind nach ihrer Aufgabe getrennt:
| Bereich | Aufgabe |
|---|---|
config |
Bedingte Aktivierung, Bean-Aufbau, Security-Filterkette und Datenbankanbindung |
controller |
REST-Endpunkte und Übersetzung von Fehlern in den HTTP-Vertrag |
model |
Request-, Response-, Metadaten- und Persistenzmodelle |
service |
Fachlicher Ablauf, Validierung, Token-Erzeugung und Audit-Ereignisse |
repository |
Eigentümergebundene JDBC-Zugriffe |
config.persistence |
Abstraktion und aktuelle SQLite-Implementierung der Datenbankanbindung |
Außerhalb dieses Pakets ergänzt de.triology.cas.logging.PATTokenRewritePolicy die Schutzmaßnahmen gegen versehentlich protokollierte Tokenwerte. Das initiale Datenbankschema liegt unter app/src/main/resources/db/pat/migration/sqlite.
Die zentrale Auto-Configuration ist PATServiceConfiguration. Sie baut die Anwendungsschichten in dieser Reihenfolge auf:
PATDatabaseProvider
│
▼
patDataSource ──► patFlyway ──► patJdbcTemplate
│
▼
PATRepository
│
SecurePATGenerator + Clock ──────────────┤
▼
PATService
│
▼
PATControllerAPI-Vertrag
Die API bietet vier Operationen:
| Methode und Pfad | Verhalten | Erfolgsstatus |
|---|---|---|
POST /api/users/{userId}/pats |
Erzeugt ein PAT und liefert den Klartext einmalig aus | 201 Created |
GET /api/users/{userId}/pats |
Liefert alle Metadaten des Benutzers, neueste zuerst | 200 OK |
GET /api/users/{userId}/pats/{id} |
Liefert einen Metadatensatz innerhalb der Eigentümergrenze | 200 OK |
DELETE /api/users/{userId}/pats/{id} |
Löscht den Datensatz physisch | 204 No Content |
PATs können nicht aktualisiert werden. Nicht unterstützte Methoden werden für PAT-Pfade als 405 Method Not Allowed mit einem Allow-Header und dem PAT-Fehlerformat beantwortet.
Erzeugungsdaten
Der Create-Request besteht aus:
| Feld | Bedeutung |
|---|---|
displayName |
Pflichtfeld und lesbarer Name, maximal 255 Zeichen |
expiresAt |
Optionaler Ablaufzeitpunkt als Instant; muss nach dem Erzeugungszeitpunkt liegen |
scope |
Optionale, kommagetrennte Service-Pfade bis 1000 Zeichen; leer oder fehlend wird zu /* |
Unbekannte JSON-Felder werden abgelehnt. Auch eine leere oder mehr als 255 Zeichen lange userId ist ungültig. Der Scope darf maximal 1000 Zeichen lang sein.
scope wird beim Erstellen nicht gegen eine Liste erlaubter Werte geprüft. Bei der Ausstellung eines Service-Tickets werden die enthaltenen Pfade ausgewertet. Ein fehlender Ablaufzeitpunkt erzeugt ein nicht ablaufendes PAT.
List- und Einzelantworten enthalten ausschließlich Metadaten. Sie enthalten insbesondere weder das Klartext-Token noch dessen Fingerprint.
Fehlerformat
Fehler werden als JSON-Objekt mit code, message und timestamp ausgegeben. Die wichtigsten Abbildungen sind:
| Status | Code | Typischer Grund |
|---|---|---|
400 |
INVALID_REQUEST |
Validierungsfehler, ungültige UUID, unbekanntes Feld oder nicht lesbares JSON |
401 |
UNAUTHORIZED |
Fehlende oder ungültige Basic-Auth-Credentials |
404 |
PAT_NOT_FOUND |
PAT existiert nicht oder gehört zu einer anderen Benutzer-ID |
405 |
METHOD_NOT_ALLOWED |
Versuch, ein PAT zu aktualisieren oder eine andere nicht unterstützte Methode zu verwenden |
503 |
SERVICE_UNAVAILABLE |
Temporärer Ausfall der PAT-Persistenz |
500 |
INTERNAL_ERROR |
Unerwarteter interner Fehler oder inkonsistente Daten |
Die 401-Antwort enthält zusätzlich WWW-Authenticate: Basic realm="PAT API". Interne Exceptions und Datenbankdetails werden nicht an Clients weitergegeben.
Ablauf beim Erzeugen eines PATs
Der sicherheitsrelevante Hauptablauf sieht folgendermaßen aus:
sequenceDiagram
participant UM as User Management
participant SEC as Security-Filterkette
participant API as PATController
participant SVC as PATService
participant GEN as SecurePATGenerator
participant DB as PATRepository
UM->>SEC: POST mit Basic Auth, userId und Metadaten
SEC->>API: authentifizierter Principal
API->>SVC: create(userId, request, principal)
SVC->>SVC: Eingaben und Ablaufzeit prüfen
SVC->>GEN: Token erzeugen
GEN-->>SVC: Klartext-PAT + SHA-256-Fingerprint
SVC->>DB: Fingerprint und Metadaten speichern
DB-->>SVC: erfolgreich
SVC-->>API: CreatePATResponse mit Klartext-PAT
API-->>UM: 201, Location, no-storeDer Generator liest 32 Zufallsbytes aus SecureRandom und codiert sie URL-sicher sowie ohne Base64-Padding. Das sichtbare Token beginnt mit pat_. Anschließend wird über das vollständige Token einschließlich Präfix ein SHA-256-Fingerprint gebildet.
Der Service kombiniert das Ergebnis mit einer zufälligen UUID, der Eigentümer-ID, den Metadaten und einem UTC-Zeitpunkt. Nur der Fingerprint und die Metadaten werden an das Repository übergeben. Nach erfolgreicher Speicherung liefert die Create-Antwort das Klartext-Token zurück. Die Antwort setzt Cache-Control: no-store und Pragma: no-cache.
Praktischer Anmeldefall: Usermgt-API mit PAT
Dieses Beispiel liest das eigene Benutzerkonto über GET /usermgt/api/account. Es benötigt keine Administratorrolle und prüft den vollständigen Weg vom API-Client über CAS bis zur Antwort des Dogus. Die Befehle werden im CAS-Repository in derselben Bash-Sitzung ausgeführt. Das vorhandene Skript heißt pat-tests.sh.
1. Voraussetzungen
CAS benötigt pat/enabled=true, einen erreichbaren LDAP-Benutzer und die technischen API-Zugangsdaten. Im Dogu-Template stammen diese aus den verschlüsselten Schlüsseln experimental/totp/api_user_name und experimental/totp/api_user_password, auch wenn TOTP deaktiviert ist. Nach Änderungen CAS neu starten. Usermgt muss erreichbar und seine konfigurierte CAS-Service-URL in CAS zugelassen sein; deren Pfad muss vom Scope /usermgt abgedeckt werden.
Ohne 2FA gilt experimental/totp/activate=false. Mit 2FA gilt experimental/totp/activate=true; das CAS-Template setzt dann den GAuth-Bypass auf das Principal-Attribut patAuthentication mit Wert true. Die PAT-Anmeldung benötigt dadurch keinen TOTP-Code. Ein frischer regulärer Browser-Login mit Benutzerpasswort verlangt weiterhin TOTP. Die API-Credentials in Usermgt selbst sind für die Verwaltung von PATs über die Oberfläche nötig, nicht für diesen Basic-Auth-Aufruf mit einem bereits erstellten PAT.
2. Token mit dem Verwaltungsskript erstellen
export CAS_URL='https://ces.example.org/cas'
export USERMGT_URL='https://ces.example.org/usermgt'
export USER_ID='<LDAP-BENUTZERNAME>'
export SA_USER='<TECHNISCHER-API-BENUTZER>'
read -r -s -p 'Technisches API-Passwort: ' SA_PASSWORD
export SA_PASSWORD
bash docs/development/pat-tests.sh create \
--scope /usermgt --displayname 'Usermgt API Login'Erwartet wird 201 Created. Aus dem JSON die id für die spätere Löschung und den vollständigen token einschließlich pat_ übernehmen. USER_ID muss exakt dem Token-Eigentümer entsprechen. Das Skript benötigt Bash, curl und jq; es verwendet für Entwicklungsinstanzen curl -k und deaktiviert damit die TLS-Zertifikatsprüfung. Die folgenden direkten curl-Aufrufe prüfen das Zertifikat; bei einer privaten CA --cacert /pfad/zur/ca.pem ergänzen.
export PAT_ID='<ID-AUS-DER-CREATE-ANTWORT>'
read -r -s -p 'Vollständiges PAT: ' PAT_TOKEN3. Tatsächlichen API-Aufruf mit PAT ausführen
curl --fail-with-body --silent --show-error --include \
--basic --user "$USER_ID:$PAT_TOKEN" \
--header 'Accept: application/json' \
"$USERMGT_URL/api/account"Hier ist das Passwortfeld das PAT, und der Benutzername ist der LDAP-Benutzer. SA_USER und SA_PASSWORD werden ausschließlich für die Verwaltung verwendet. Ein Authorization: Bearer-Header ist für diesen Anmeldeweg nicht vorgesehen. Der Aufruf sendet keine vorhandenen Sitzungscookies und folgt keinen Login-Redirects.
Erwartet wird 200 OK mit dem eigenen Benutzerkonto als JSON. Eine HTML-Loginseite oder ein Redirect ist kein erfolgreicher API-Login. Intern passiert Folgendes:
- Usermgt übernimmt die Basic-Auth-Zugangsdaten in
CasRestAuthenticationRealm. CasRestClientsendetusernameund das PAT alspasswordanPOST /cas/v1/tickets. CAS prüft Fingerprint, Ablauf und Eigentümer und löst den Benutzer über LDAP auf. Die erfolgreiche Antwort enthält201und die TGT-URL imLocation-Header.- Usermgt fordert mit diesem TGT ein Service-Ticket für seine konfigurierte
CasConfiguration.service-URL an. CAS prüft deren Pfad gegenpatScopeund liefert bei Erfolg200mit dem Service-Ticket. - Usermgt validiert das Ticket über
Saml11TicketValidator, übernimmt den bestätigten Principal und liefert das eigene Benutzerkonto aus.
Der Scope gilt für die konfigurierte CAS-Service-URL, nicht für jeden REST-Endpunkt einzeln. /usermgt deckt diesen Pfad und Unterpfade ab, aber nicht /usermgt-other. Es ersetzt keine Benutzerrechte: Ein Aufruf von /usermgt/api/users benötigt beispielsweise weiterhin die Administratorrolle. Deshalb verwendet dieses Beispiel /api/account.
4. Ablehnung und Löschung prüfen
Für einen Scope-Negativtest ein zweites PAT mit --scope /redmine erstellen und denselben Usermgt-Aufruf damit wiederholen. Die Ausstellung des Usermgt-Service-Tickets muss scheitern. Ebenfalls testen: falscher Benutzername und abgelaufenes PAT. Den konkreten Fehlerstatus anhand der Dogu-Antwort prüfen; das JSON-Fehlerformat der PAT-Verwaltungs-API gilt nicht automatisch für diese Anmeldestrecke.
Anschließend das ursprüngliche PAT löschen und denselben API-Aufruf ohne Sitzungscookies wiederholen:
bash docs/development/pat-tests.sh delete "$PAT_ID"
# Erwartet: kein erfolgreicher Zugriff auf das Benutzerkonto mehr.
curl --fail-with-body --silent --show-error --include \
--basic --user "$USER_ID:$PAT_TOKEN" \
--header 'Accept: application/json' \
"$USERMGT_URL/api/account"
unset PAT_TOKEN SA_PASSWORDDas Löschen liefert 204. Die neue Anmeldung muss scheitern; bereits ausgestellte Tickets oder bestehende Sitzungen werden durch die Löschung nicht aktiv widerrufen. Den vollständigen Fall jeweils mit deaktiviertem und aktiviertem TOTP testen. pat-tests.sh invalid-auth prüft dagegen nur falsche technische Zugangsdaten an der Verwaltungs-API.
Lesen, Ownership und Löschen
Die Eigentümergrenze wird im Repository umgesetzt. Einzelabfrage und Löschung verwenden immer die Kombination aus user_id und id:
WHERE user_id = ? AND id = ?Dadurch kann eine bekannte PAT-ID nicht über den Pfad eines anderen Benutzers gelesen oder gelöscht werden. Für beide Fälle liefert die API dieselbe 404-Antwort, unabhängig davon, ob die ID nicht existiert oder einem anderen Benutzer gehört.
Die Listenoperation filtert ebenfalls nach user_id und sortiert nach created_at DESC. Sie ist aktuell nicht paginiert und filtert abgelaufene PATs nicht heraus.
Ein Delete entfernt den Datensatz direkt aus der Datenbank. Es gibt weder Soft Delete noch einen separaten Status für widerrufene Tokens.
Persistenz und Migrationen
Logisches Datenmodell
Die Tabelle personal_access_tokens enthält:
| Spalte | Inhalt |
|---|---|
id |
UUID des PAT-Datensatzes als Text und Primärschlüssel |
user_id |
Fachlicher Eigentümer |
display_name |
Anzeigename |
token_fingerprint |
32 Byte großer SHA-256-Fingerprint |
created_at |
UTC-Erzeugungszeitpunkt als Text |
expires_at |
Optionaler UTC-Ablaufzeitpunkt als Text |
scope |
Kommagetrennte Service-Pfade |
Ein Index auf (user_id, created_at DESC) unterstützt die Listenoperation. Ein weiterer Index auf token_fingerprint unterstützt die Tokenauflösung bei der PAT-Anmeldung.
SQLite-Anbindung
SQLite ist über SQLitePATDatabaseProvider angebunden. Der Provider erkennt JDBC-URLs mit dem Präfix jdbc:sqlite:, erstellt die DataSource und verweist auf die passenden Flyway-Migrationen. Die Verbindung verwendet:
- WAL-Journalmodus,
- aktivierte Foreign-Key-Prüfung und
- ein Busy Timeout von fünf Sekunden.
Beim Start wählt die Konfiguration anhand der JDBC-URL genau einen PATDatabaseProvider. Kein oder mehr als ein passender Provider führt zu einem Startfehler. Danach migriert die dedizierte Flyway-Instanz die Datenbank, bevor das JdbcTemplate und das Repository verwendet werden können.
Die standardmäßige Datenbankdatei /var/ces/config/pats.db liegt im persistenten und backuprelevanten CAS-Volume. Für konsistente Sicherungen einer laufenden SQLite-Datenbank ist wegen des WAL-Modus auch der WAL-Zustand zu berücksichtigen.
Security und Umgang mit Secrets
Die PAT-Pfade besitzen eine eigene Spring-Security-Filterkette. Sie wird vor der allgemeinen Basic-Auth-Konfiguration eingeordnet und hat folgende Eigenschaften:
- HTTP Basic als Authentifizierungsverfahren,
- keine serverseitige Session (
STATELESS), - CSRF-Schutz ausschließlich für diese zustandslose API deaktiviert,
- Zugriff nur für authentifizierte Requests und
- JSON-Antwort statt HTML-Redirect bei fehlender Authentifizierung.
Beim Aktivieren prüft die Konfiguration, dass ein Spring-Security-Benutzername und ein Passwort vorhanden sind. Sie erzwingt derzeit keine PAT-spezifische Rolle oder Authority.
Die Verwaltungs-API liefert den Klartext eines PATs ausschließlich in der Create-Antwort aus. Für die spätere Anmeldung übergibt der Client diesen Klartext als Passwort; er darf dabei nicht protokolliert werden. Mehrere Schutzschichten reduzieren das Risiko einer versehentlichen Protokollierung:
- Die sicherheitsrelevanten Modelle maskieren Token und Fingerprint in
toString(). - Der Logger für Spring MVCs Request-/Response-Body-Verarbeitung ist deaktiviert.
- PAT-Logger laufen über eine Log4j-Rewrite-Policy, die PAT-Muster und Tokenfelder maskiert.
- Audit-Ereignisse enthalten IDs und Akteure, aber keine Tokenwerte oder Fingerprints.
Die Rewrite-Policy ist eine zusätzliche Absicherung und kein Ersatz für secret-sicheren Code. Neue Log-Ausgaben, Tracing-Integrationen oder Fehlerobjekte dürfen niemals den Request-/Response-Body oder das Klartext-Token übernehmen.
Auditierung und Diagnose
Die Umsetzung verwendet den Logger de.triology.cas.pat.audit. Er zeichnet insbesondere folgende Ereignisse auf:
- erfolgreiche Erzeugung mit PAT-ID, Eigentümer und technischem Principal,
- fehlgeschlagene Erzeugung bei nicht verfügbarer Persistenz,
- erfolgreiche Löschung,
- fehlgeschlagene Löschung eines nicht gefundenen PATs,
- ungültige Requests und
- nicht authentifizierte Zugriffe.
userId und principal haben dabei bewusst unterschiedliche Bedeutungen. Die userId ist der fachliche Eigentümer aus dem Request-Pfad; principal ist der authentifizierte technische Aufrufer. Diese Trennung sollte bei neuen Operationen beibehalten werden.
Unerwartete Fehler werden serverseitig mit Stacktrace protokolliert, während der Client nur eine generische Meldung erhält. Bei Erweiterungen ist deshalb darauf zu achten, dass Exceptions keine Klartext-Tokens in ihrer Nachricht oder ihren Feldern tragen.
Konfiguration und Aktivierung
Die Spring-Properties lauten:
personal-acces-token-service.enabled=false
personal-acces-token-service.database-url=jdbc:sqlite:/var/ces/config/pats.dbIn der Dogu-Konfiguration werden sie aus folgenden Schlüsseln erzeugt:
| Dogu-/Helm-Konfiguration | Spring-Property | Standardwert |
|---|---|---|
pat/enabled beziehungsweise configuration.normal.pat.enabled |
personal-acces-token-service.enabled |
false |
pat/database_url beziehungsweise configuration.normal.pat.database_url |
personal-acces-token-service.database-url |
jdbc:sqlite:/var/ces/config/pats.db |
Zusätzlich müssen gültige Werte für spring.security.user.name und spring.security.user.password aus der jeweiligen Laufzeitkonfiguration vorhanden sein. Fehlen sie bei aktiviertem PAT-Service, bricht der Aufbau der Security-Filterkette mit einem Startfehler ab.
Erweiterungspunkte
Weitere Datenbank unterstützen
Eine neue Datenbank wird über einen weiteren PATDatabaseProvider ergänzt. Der Provider muss die JDBC-URL eindeutig erkennen, eine passende DataSource aufbauen und ein eigenes Flyway-Verzeichnis angeben. Das logische Schema und die Semantik des Repositories müssen dabei gleich bleiben. Controller und Service sollen keine datenbankspezifischen Abhängigkeiten erhalten.
Authentifizierungsstrecke erweitern
PATAuthenticationHandler löst den Fingerprint auf, prüft den Ablaufzeitpunkt und den angegebenen Eigentümer und lädt den Principal über LDAP. Er setzt patScope und patAuthentication=true. PATServiceTicketFactory prüft den Service-URL-Pfad gegen den Scope im TGT. Tokenwerte dürfen auch in diesem Pfad nicht in Logs, Metriken oder Traces erscheinen.
Bei Anpassungen an der Principal-Auflösung müssen die PAT-Attribute erhalten bleiben: Ohne patScope als Liste mit einem String an erster Stelle delegiert die Ticket-Factory ohne PAT-Scope-Prüfung an CAS. Löschen oder Ablauf verhindert neue PAT-Anmeldungen, widerruft aber keine bestehenden Tickets oder Dogu-Sitzungen. Bei der Service-Ticket-Ausstellung wird die Gültigkeit des Tokens nicht erneut in der Datenbank geprüft.