---
title: Authentication
slug: 3L20-authentication
docTags: 
createdAt: 2026-09-14T23:58:02.565Z
---

## 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:

```kotlin
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:

```kotlin
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:

```kotlin
// 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`:

```kotlin
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:

```kotlin
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:

1. `expires_at` returned by the backend when it bound the credentials
2. `expiresAt` you supplied on `KindlyAuthCredentials`
3. 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:

```kotlin
KindlySDK.authenticate()     // user signed in, or switched account
KindlySDK.deauthenticate()   // user signed out
```

`authenticate()` 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.
