Sessions und Tokens¶
Das Auth-Modell — dreistufig, mit Rotation des Refresh-Tokens bei jedem Access-Request, für Prozesse ohne Benutzer künftig direkt mit API-Key — ist unter Erste Schritte → Das Auth-Modell beschrieben und gilt für REST, gRPC und SDK gleich. Hier stehen nur die SDK-Besonderheiten.
Die Logins¶
// 1) Separate fields
await client.CreateSession(
providerId: 106,
subscriberId: "mysubscriber",
accountId: "myaccount",
password: "mypassword",
appToken: new AppToken(appTokenJwt));
// 2) Combined UserId
await client.CreateSession(
userId: "mysubscriber-myaccount@106:mypassword",
appToken: new AppToken(appTokenJwt));
AppToken, AccessToken und RefreshToken liegen in Zedal.Sdk.Jwt. Sie sind Hüllen um ein JWT; der Konstruktor nimmt die kodierte Zeichenkette.
Die UserId-Variante verträgt keine Trennzeichen in den Bestandteilen
Das Muster ist <subscriber>-<account>@<provider>:<password>; das SDK zerlegt es an -, @ und :. Enthält ein Bestandteil — insbesondere das Passwort — eines dieser Zeichen, scheitert der Aufruf mit RpcException (InvalidArgument, „Input does not match the expected pattern”), bevor ein Netzwerkaufruf stattfindet. Für alles Automatisierte nimm Variante 1.
Bei Variante 1 gibst du die providerId selbst an. Variante 2 leitet sie aus dem Provider-Teil der UserId ab. ContinueSession liest sie aus dem Refresh-Token. Ist die aufgelöste Provider-Id 0, bricht der Aufruf mit RpcException (InvalidArgument, „active provider not set”) ab.
Token-Rotation und der Callback¶
SetAccessTokenChanged registriert einen Callback, der bei jeder Rotation das aktuelle Paar (AccessToken accessToken, RefreshToken refreshToken) bekommt. Er ist der einzige Weg, das jeweils gültige Refresh-Token zu erfahren.
client.SetAccessTokenChanged(tokens =>
SecureStorage.Save("zedal-refresh-token", tokens.refreshToken.Jwt.EncodedToken));
Die kodierte JWT-Zeichenkette steht unter Jwt.EncodedToken; ToByteArray() liefert dieselbe Zeichenkette als UTF-8-Bytes. Wer den Token binär ablegen will, findet mit IRefreshTokenStore (SaveTokenAsync(byte[]), LoadTokenAsync(), DeleteTokenAsync()) eine passende Schnittstelle zum Selbstimplementieren.
Registriere den Callback vor dem Login und vor ContinueSession, sonst verpasst er die erste Rotation.
Token-Erneuerung¶
Es gibt keine im Client hinterlegte Lebensdauer, und du sollst auch keine annehmen. Beide Token sind JWTs und tragen ihre Ablaufzeit im exp-Claim; im SDK liest du sie über token.Jwt.ValidTo.
| Frage | Antwort |
|---|---|
| Wann erneuert das SDK von selbst? | Sobald die Restlaufzeit des Access-Tokens unter 5 Minuten liegt — geprüft vor jedem Aufruf, der ein Token braucht. |
| Wer macht das? | DefaultTokenRefresher, die Standardimplementierung von IJwtTokenRefresh. Ein Semaphor stellt sicher, dass innerhalb eines Clients nur eine Erneuerung gleichzeitig läuft. |
Was liefert AccessToken.IsValid(scopeInMinutes)? |
true, wenn das Token in scopeInMinutes noch gültig ist. Default sind 2 Minuten; der Refresher fragt mit 5. |
| Kann ich das ersetzen? | Ja — eigene IJwtTokenRefresh-Implementierung an CreateClient(..., tokenRefresh: …). Die Default-Implementierung prüft ausschließlich das Ablaufdatum; entzogene oder gesperrte Zugänge behandelt sie nicht. |
Ein fehlgeschlagener Refresh endet als Unauthenticated
Scheitert die Erneuerung, liefert DefaultTokenRefresher ein leeres Token. Der eigentliche Aufruf endet dann mit RpcException (Unauthenticated); die Ursache (Netzfehler, rotiertes Token, widerrufene Session) ist darin nicht enthalten. Wenn du diese Fälle unterscheiden musst, implementiere IJwtTokenRefresh selbst.
Für parallele Prozesse gilt unverändert: Ein Refresh-Token gehört genau einem Prozess. Zwei Clients mit demselben gespeicherten Token entwerten sich gegenseitig — siehe Token-Rotation.
Sessions mit API-Key¶
API-Keys folgen
API-Keys stehen noch nicht bereit. ApiKey, die Session-Methoden mit Key und NotGrantedScopes sind nicht Teil von Zedal.Sdk 1.3.0; sie folgen mit einem späteren Release, sobald ZEDAL die API-Keys freischaltet. Wann es so weit ist, erfährst du beim ZEDAL-Support (erp-tools@zedal.de).
Ein API-Key ersetzt für Prozesse ohne Benutzer — Integrationen, ERP-Anbindungen, Skripte — den Login mit Passwort. Das SDK tauscht ihn zusammen mit dem App-Token direkt gegen ein Access-Token; ein Refresh-Token entsteht dabei nicht. Es gibt also nichts zu rotieren und nach einem Refresh nichts zu speichern: Der Prozess hält nur den Key.
Key anlegen¶
API-Keys legst du im ZEDAL-Portal unter „Mein ZEDAL“ → „API-Keys“ an, für den dort angemeldeten Account; das SDK legt keine an. Zum Anlegen fragt das Portal noch einmal das Passwort des Accounts ab, denn der Key gilt über die Sitzung hinaus. Beim Anlegen wählst du die Scopes, die der Key gewähren darf — core, dataexport, wastemanager, nichts ist implizit —, und optional ein Ablaufdatum. Das Portal zeigt den Key nur direkt nach dem Anlegen; leg ihn so geschützt ab wie ein Passwort.
Der Key gehört dem Account, an dem er angelegt wurde, und handelt mit dessen Rechten. Jeder Account verwaltet nur seine eigenen Keys, auch der Hauptaccount. Soll der Key nicht am Zugang einer Person hängen, leg ihn an einem eigenen Account für die Integration an.
Mit dem Key anmelden¶
await client.CreateSession(
apiKey: new ApiKey(SecureStorage.Load("zedal-api-key")),
appToken: new AppToken(appTokenJwt));
var missing = client.CoreApi.Auth.NotGrantedScopes;
if (missing.Count > 0)
{
Console.WriteLine("API key does not grant: " + string.Join(", ", missing));
}
ApiKey liegt in Zedal.Sdk.Jwt, ist aber kein JWT: Der Konstruktor nimmt den Key so, wie ihn das Portal ausgibt (zedal_ak_…), und wirft ArgumentException, wenn der Wert keiner ist — ohne ihn in der Meldung zu wiederholen. ToString() zeigt den Key ohne sein Geheimnis.
Das Access-Token trägt nur die Scopes, die Key und App-Token haben; du liest sie mit client.CoreApi.Auth.AccessToken.GetScope(). NotGrantedScopes nennt die Scopes des App-Tokens, die ein Key gewähren könnte, dieser aber nicht. Ob deine Anwendung sie braucht, hängt an den Aufrufen, die sie macht.
| Frage | Antwort |
|---|---|
| Wie wird erneuert? | Wie bei jeder Session, sobald die Restlaufzeit des Access-Tokens unter 5 Minuten liegt. Statt eines Refresh-Tokens tauscht das SDK dann den Key erneut. |
| Was bekommt der Callback? | AccessTokenChanged feuert bei jedem Tausch, mit refreshToken == null. |
| Dürfen mehrere Prozesse denselben Key nutzen? | Ja, ein Key rotiert nicht. Jeder Tausch zählt aber gegen die Grenze des Keys (siehe unten). |
| Was passiert bei einem ungültigen Key? | Ein unbekannter, widerrufener oder abgelaufener Key ergibt RpcException mit Unauthenticated, ebenso ein gesperrter Account. |
Den Key nicht vor jedem Aufruf tauschen
Der Server begrenzt, wie oft ein Key in einer Zeitspanne getauscht werden kann; darüber hinaus antwortet er mit RpcException, Status ResourceExhausted. Das SDK verwendet das Access-Token bis kurz vor seinem Ablauf weiter. Wer für jeden Aufruf einen neuen Client anlegt und CreateSession ruft, läuft in die Grenze. Halte einen Client je Prozess und lass ihn selbst erneuern — oder behalte das Access-Token zwischen den Läufen (nächster Abschnitt).
Access-Token zwischen Programmläufen behalten¶
Startet deine Anwendung für jeden Lauf neu — ein Kommandozeilenwerkzeug, ein Cron-Job —, kostet jeder Lauf einen Tausch. Behalte dann das Access-Token und setze die Session damit fort:
client.SetAccessTokenChanged(tokens =>
SecureStorage.Save("zedal-access-token", tokens.accessToken.Jwt.EncodedToken));
var kept = SecureStorage.Load("zedal-access-token");
await client.ContinueSession(
apiKey: new ApiKey(SecureStorage.Load("zedal-api-key")),
appToken: new AppToken(appTokenJwt),
accessToken: kept == null ? null : new AccessToken(kept));
ContinueSession verwendet das mitgebrachte Token ohne Tausch, solange es für diesen Key und dieses App-Token ausgestellt wurde und nicht abgelaufen ist. Sonst tauscht es den Key wie CreateSession und meldet das neue Token über den Callback. Ohne Tausch bleibt NotGrantedScopes leer. Lege das Access-Token so geschützt ab wie den Key: Bis zu seinem exp lässt sich damit alles tun, was der Key erlaubt.
Key widerrufen¶
Widerrufen wird im Portal, an derselben Stelle. Danach gibt es mit dem Key kein neues Access-Token mehr; bereits ausgegebene Access-Tokens können bis zu ihrem Ablauf gültig bleiben. In einer Session mit API-Key vergisst EndSession nur Key und Access-Token; der Key selbst bleibt gültig, bis du ihn im Portal widerrufst.
Session beenden¶
EndSession versucht, das Refresh-Token serverseitig zu widerrufen, und räumt danach in jedem Fall den lokalen Zustand ab (AccessToken, RefreshToken, ActiveProvider).
EndSession meldet keinen Fehlschlag
Schlägt der serverseitige Widerruf fehl, etwa ohne Netz oder bei überschrittener Deadline, kehrt EndSession trotzdem normal zurück: Lokal ist die Session beendet, serverseitig möglicherweise nicht. Wenn du sicher sein musst, dass ein Token nicht mehr gilt, nimm die Auth-Operationen direkt: client.CoreApi.Auth.RevokeRefreshTokenByToken(...) oder RevokeRefreshTokenByFamilyId(...) und behandle die Exception.
Session-Verwaltung über CoreApi.Auth¶
Was die Fassade nicht abdeckt, liegt auf client.CoreApi.Auth:
| Methode | Zweck |
|---|---|
ListSessions() |
Listet die aktiven Sessions des Accounts. |
RevokeRefreshTokenByToken(token) / RevokeRefreshTokenByFamilyId(familyId) |
Widerruft ein einzelnes Token oder eine ganze Token-Familie. |
RefreshAccessToken() |
Erneuert sofort, ohne auf die 5-Minuten-Schwelle zu warten. |
EnsureLoggedIn() |
Wirft RpcException (Unauthenticated), wenn kein Refresh-Token vorliegt; holt ein fehlendes Access-Token nach. |
AccessToken, RefreshToken, ActiveProvider |
Der aktuelle Session-Zustand als Properties. |