Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Token lifecycle

An Aduki ID sign-in gives a 10-minute access token for mail, a single-use refresh token and a session. The SDK renews the access token itself; apps only sign out.

Automatic renewal

When mail answers 401 to a request sent with a Bearer token, the SDK renews once and retries the request:

POST /v1/tokens HTTP/1.1
Host: id.aduki.pro
Content-Type: application/json

{ "refresh": "…", "audience": "mail" }
{ "success": true, "data": { "access": "…", "refresh": "…", "expires": 600 } }

Renewals are serialized through the Id client. Refresh tokens rotate, and presenting a spent one makes Aduki ID revoke the whole session, so concurrent requests that fail together share one renewal, and a refresh token that Aduki ID refused is dropped and never presented again. API keys are never renewed.

refresh

suspend fun Aduki.refresh(): Boolean

Renews ahead of time. Returns false when there is no refresh token or Aduki ID refused it (sign in again).

logout

suspend fun Aduki.logout(): Boolean

Revokes the session at Aduki ID, which ends every token issued from it, and clears client.session. Aduki ID only accepts its own tokens for this, so the SDK first swaps the refresh token for an id-audience token:

POST /v1/tokens            { "refresh": "…", "audience": "id" }
DELETE /v1/sessions/{hex}  Authorization: Bearer <id token>

Returns false if the session could not be revoked. The swap spent the old refresh token, so the SDK then keeps the session with the rotated one: call logout() again to retry, or session.clear() to forget it locally. Clients built from an API key have no session and return false.

A client passed to Builder.http(...) gets the same renewal unless it has its own authenticator. Aduki ID calls use its connection settings without its interceptors, so they never carry the mail token.

Session state

client.session.tokens      // StateFlow<Tokens?>
client.session.token()     // current access token
client.session.refresh()   // current refresh token

Tokens.session is kept across renewals.

Rights stream

When an account’s rights change (a role edit, a revoked grant, an unlink), Aduki Mail sends event: rights with {"@type": "Rights", "epoch": N} on its JMAP EventSource (a key rotation sends {"@type": "Rights", "keys": true}), and then ends the stream. The access token should be renewed at once, before the next request is refused with auth.stale.

The SDK can do this for you:

client.watchRights()    // subscribe to {mail host}/jmap/eventsource
// ...
client.unwatchRights()  // also done by logout()

watchRights(host = <REST endpoint without /v1>) opens GET {host}/jmap/eventsource?types=*&ping=30 on the authenticated connection and renews the token on every rights event. After a rights event the stream is reopened immediately; after a failure it reconnects with decorrelated jitter backoff (1 s up to 60 s). Calling it again replaces the previous subscription.

If your app receives the same payload another way (web push, FCM), hand the body to client.rights(payload); it renews for {"@type": "Rights", ...} and ignores anything else, returning whether a token was renewed.

scope.launch { client.rights(data) }

The push only saves a round trip. A stale token is refused and then renewed by the 401 path whether or not a push arrived. The stream source exists for audience mail only; a client with no mail audience needs its own product’s event.