Authentication
Setup
To use authenticated chat sessions, pass an authTokenProvider to the start method. KindlyAuthTokenProvider is a fun interface with a single suspend function, so you can implement it with a lambda:
import no.kindly.chatsdk.chat.KindlyAuthTokenProvider
KindlySDK.start(
application = this,
botKey = "BOT_KEY",
languageCode = "en",
market = "YOUR_MARKET",
authTokenProvider = KindlyAuthTokenProvider { request ->
// Suspend code â e.g. call your backend to mint a JWT for request.chatId
fetchJwtFromBackend(chatId = request.chatId)
}
)The SDK calls fetchToken(request) from its own coroutine whenever it needs a token â you don't need to manage a CoroutineScope, Deferred, or threading yourself. Return the JWT string, or throw an exception to signal failure.
AuthTokenRequest
The request tells you which chat session the token is for and why the SDK is asking:
Property | Meaning |
|---|---|
chatId | The current chat session ID. Pass it to your backend so the token is scoped to this session. |
reason | INITIAL_CONNECT â the first token for a new session. EXPIRED â the cached token is expired (or about to expire); mint a fresh one, never return a cached token. |
Behavior
- Token fetches are capped by a 15-second SDK-side timeout. If your provider takes longer, the fetch is abandoned and the chat continues unauthenticated.
- Tokens that arrive already expired are discarded, so make sure your backend always mints a fresh token â especially when reason == EXPIRED.
- JWTs without an exp claim are treated as valid and are never proactively refreshed.
Migrating from authTokenCallback (deprecated)
Earlier SDK versions used a Deferred-based callback:
KindlySDK.start(
application = this,
botKey = "BOT_KEY",
languageCode = "en",
market = "YOUR_MARKET",
authTokenCallback = { chatId ->
val deferredToken = CompletableDeferred<String>()
// Async code, then on completion:
// deferredToken.complete(token)
deferredToken
}
)This continues to work â existing integrations do not break â but it is deprecated. To migrate, replace the callback with a provider and drop the Deferred plumbing:
// Before
authTokenCallback = { chatId ->
CoroutineScope(Dispatchers.IO).async { fetchJwtFromBackend(chatId) }
}
// After
authTokenProvider = KindlyAuthTokenProvider { request ->
fetchJwtFromBackend(request.chatId)
}Identity Providers That Issue More Than One Token
The Kindly platform verifies an authenticated chat in one of three ways: a JWT your own backend signs, a token from your identity provider (an id token, an access token, or the pair), or an opaque bearer token validated by introspection.
The first is the single-string case above. For the other two, use authCredentialsProvider:
import no.kindly.chatsdk.chat.KindlyAuthCredentials
import no.kindly.chatsdk.chat.KindlyAuthCredentialsProvider
KindlySDK.start(
application = this,
botKey = "BOT_KEY",
market = "YOUR_MARKET",
authCredentialsProvider = KindlyAuthCredentialsProvider { request ->
val tokens = fetchTokens(request.chatId)
KindlyAuthCredentials(
idToken = tokens.idToken, // optional, the preferred bearer
accessToken = tokens.accessToken, // optional, companion or bearer
issuer = "https://idp.example.com/", // optional
expiresAt = tokens.expiresAtUnixSeconds, // optional, Unix seconds
)
},
)Field | Required | When you need it |
|---|---|---|
idToken | no | The OIDC id token, when your provider issues one. The preferred bearer |
accessToken | no | The access token, when your provider issues one. Companion next to an id token, bearer on its own |
issuer | no | Bots with more than one provider configured, and any opaque token. Sent as X-Auth-Issuer |
expiresAt | no | Opaque tokens, where there is no exp claim for the SDK to read. Unix seconds |
Every field is optional. What the SDK requires is that at least one of idToken / accessToken is set, because the backend's auth route mandates an Authorization header and nothing else. Credentials carrying neither cannot authenticate, and the chat stays anonymous.
What goes on the wire
Three derived properties spell out the rule, so you never have to re-derive it:
Property | Value | Header |
|---|---|---|
bearerToken | idToken ?: accessToken, first non-empty, else null | Authorization: Bearer |
hasToken | bearerToken != null. The validity check, never idToken != null | none |
companionAccessToken | The access token, but only when an id token is also set and differs from it | X-Access-Token |
X-Access-Token and X-Auth-Issuer are read by the backend on auth/chat, message and trigger only. The companion is deliberately null when the access token is itself the bearer, and when it merely duplicates the id token. That second case has teeth: the backend verifies the companion against the id token's at_hash and rejects a mismatched pair, so sending a duplicate would fail an auth that would otherwise have succeeded. The SDK drops it for you.
A provider that issues only an access token
Normal and supported, not a workaround. Leave idToken unset and the access token becomes the bearer:
KindlyAuthCredentials(
accessToken = tokens.accessToken,
issuer = "https://idp.example.com/",
expiresAt = tokens.expiresAtUnixSeconds,
)A single-JWT integration never needs this type. When more than one auth parameter is passed to start(...), precedence is authCredentialsProvider > authTokenProvider > authTokenCallback (deprecated).
Token Refresh
The SDK refreshes credentials on its own, 30 seconds before they expire. Expiry can come from three places, and whichever is earliest wins:
- expires_at returned by the backend when it bound the credentials
- expiresAt you supplied on KindlyAuthCredentials
- The exp claim of the bearer, when it is a JWT. The bearer, not the id token specifically, so a credential carrying only an access token still refreshes on that token's own claim
If none of the three is known the credentials are treated as non-expiring and never proactively refreshed. That is correct for a JWT with no exp claim, but it is a trap for an opaque token. Supply expiresAt or the SDK will keep using it until the backend rejects it.
A token the SDK cannot parse means expiry unknown, never expired. Opaque bearers are a supported credential and are not discarded on arrival for being undecodable; the SDK just has no way to guess when they die, which is what expiresAt is for.
Re-authenticating Mid-Session
Expiry is handled for you. These two are for when the identity itself changes, which the SDK cannot detect:
KindlySDK.authenticate() // user signed in, or switched account
KindlySDK.deauthenticate() // user signed outauthenticate() asks your provider for fresh credentials and re-binds them to the chat that is already open, keeping the conversation and its history.
deauthenticate() unbinds the identity on the backend and clears the stored credentials. The chat carries on anonymously with its history intact. Use endChat() instead when the conversation itself should end. It is safe to call when the chat is already anonymous.
Both return a Deferred<Unit>; awaiting is optional, and worth doing when you need to know the backend agreed. deauthenticate() is idempotent, so calling it on an already anonymous chat completes immediately.