AI translation
This page was translated from German with AI. If in doubt, the German original applies.
Sessions and tokens¶
The auth model — three-tier, with rotation of the refresh token on every access request — is described under Getting started → The auth model and applies equally to REST, gRPC, and the SDK. This section only covers what is specific to the SDK.
The three 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));
// 3) ClientId / ClientSecret — for unattended processes
await client.CreateSession(
clientId: new ClientId(clientIdJwt),
clientSecret: clientSecret,
appToken: new AppToken(appTokenJwt));
AppToken, ClientId, AccessToken, and RefreshToken live in Zedal.Sdk.Jwt. All four are wrappers around a JWT; the constructor takes the encoded string.
The UserId variant is fragile
The pattern is <subscriber>-<account>@<provider>:<password>, and the SDK parses it by splitting at -, @, and :. If a component — especially the password — itself contains one of these characters, more than four parts result and the call fails with RpcException (InvalidArgument, “Input does not match the expected pattern”) without any network call ever being made. For anything automated, use variant 1 or 3.
With variant 1, you specify the providerId yourself. Variants 2 and 3 derive it: from the provider part of the UserId or from the claim in the ClientId, respectively. ContinueSession reads it from the refresh token. If the resolved provider ID is 0, the call aborts with RpcException (InvalidArgument, “active provider not set”).
Token rotation and the callback¶
SetAccessTokenChanged registers a callback that receives the current pair (AccessToken accessToken, RefreshToken refreshToken) on every rotation. It is the only way to learn the currently valid refresh token.
client.SetAccessTokenChanged(tokens =>
SecureStorage.Save("zedal-refresh-token", tokens.refreshToken.Jwt.EncodedToken));
The encoded JWT string is in Jwt.EncodedToken; ToByteArray() returns the same string as UTF-8 bytes. If you want to store the token in binary form, IRefreshTokenStore (SaveTokenAsync(byte[]), LoadTokenAsync(), DeleteTokenAsync()) is a suitable interface to implement yourself.
CreateSession with ClientId/ClientSecret does not trigger the callback
Of the three login paths, only two invoke the AccessTokenChanged callback. The overload with ClientId/ClientSecret sets the tokens internally without firing the callback; ContinueSession, the credential login, and every automatic refresh fire it.
Symptom: A machine integration that persists its refresh token exclusively in the callback stores nothing after login. Only the next automatic refresh delivers a value — and if the application exits before that, storage still holds the token from the previous run, which has long since been invalidated. On restart, ContinueSession fails.
How to handle it: After CreateSession(clientId, …), actively fetch the token once and store it instead of waiting for the callback:
client.SetAccessTokenChanged(tokens =>
SecureStorage.Save("zedal-refresh-token", tokens.refreshToken.Jwt.EncodedToken));
await client.CreateSession(new ClientId(clientIdJwt), clientSecret, appToken);
// This line is not optional for this login path.
SecureStorage.Save("zedal-refresh-token",
client.CoreApi.Auth.RefreshToken.Jwt.EncodedToken);
For the other two paths, the rule still applies: register the callback before login, otherwise it misses the first rotation.
Token renewal¶
There is no lifetime stored in the client, and you shouldn’t assume one either. Both tokens are JWTs and carry their expiration time in the exp claim; in the SDK, you read it via token.Jwt.ValidTo.
| Question | Answer |
|---|---|
| When does the SDK renew on its own? | As soon as the remaining lifetime of the access token drops below 5 minutes — checked before every call that needs a token. |
| Who does that? | DefaultTokenRefresher, the standard implementation of IJwtTokenRefresh. A semaphore ensures that only one renewal runs at a time within a client. |
What does AccessToken.IsValid(scopeInMinutes) return? |
true if the token is still valid in scopeInMinutes. The default is 2 minutes; the refresher asks with 5. |
| Can I replace this? | Yes — pass your own IJwtTokenRefresh implementation to CreateClient(..., tokenRefresh: …). The default implementation checks only the expiration date; it does not handle revoked or locked access. |
A failed refresh looks like an auth error
If the renewal fails, DefaultTokenRefresher returns an empty string as the token instead of passing on the cause. The actual call then starts without a usable token and comes back as RpcException with Unauthenticated — the original cause (network error, rotated token, revoked session) is not included. If you need to distinguish these cases, implement IJwtTokenRefresh yourself.
For parallel processes, the rule remains unchanged: a refresh token belongs to exactly one process. Two clients with the same stored token invalidate each other — see Token rotation.
Ending a session¶
EndSession tries to revoke the refresh token on the server and then clears the local state in any case (AccessToken, RefreshToken, ActiveProvider).
EndSession does not report failure
If the server-side revocation fails — network down, deadline exceeded — the error is swallowed. The call returns normally, and the session is gone locally but possibly not on the server. If you need to be sure that a token is no longer valid, use the auth operations directly: client.CoreApi.Auth.RevokeRefreshTokenByToken(...) or RevokeRefreshTokenByFamilyId(...), and handle the exception.
Session management via CoreApi.Auth¶
Whatever the facade doesn’t cover is on client.CoreApi.Auth:
| Method | Purpose |
|---|---|
IssueClientId(providerId, subscriberId, accountId, password, description) |
Creates a client_id/client_secret pair for unattended processes. Requires the usermgt scope on the app token. The secret is returned only here. |
ListClientIds() |
Lists the issued client IDs — without secrets. |
RevokeClientId(clientId) / RevokeClientIdByTokenId(tokenId) |
Revokes a client ID; running sessions of this machine client end as a result. |
ListSessions() |
Lists the active sessions of the account. |
RevokeRefreshTokenByToken(token) / RevokeRefreshTokenByFamilyId(familyId) |
Revokes a single token or an entire token family. |
RefreshAccessToken() |
Renews immediately, without waiting for the 5-minute threshold. |
EnsureLoggedIn() |
Throws RpcException (Unauthenticated) if no refresh token is present; fetches a missing access token. |
AccessToken, RefreshToken, ActiveProvider |
The current session state as properties. |