Authentication
Setup
To use authentication with the Kindly SDK, you can initialize the SDK by adding an argument to the start method. Here's an example of how to set the authTokenCallback using the start method:
KindlySDK.start(botKey: String, market: String, authTokenCallback: (_ chatId: String, _ promise: Promise<String>) -> Void)
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
// Generate JWT token
// On success, call
promise.fulfill("JWT_TOKEN")
// On error
promise.reject(ERROR)
}Inside the authTokenCallback closure, you can generate a JWT token and fulfill the promise with the token on success. If there is an error, you can reject the promise with the appropriate error.
Additionally, there is another way to set the authTokenCallback using the KindlySDK.config.getAuthToken property. Here's an example of how to do it:
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET")
KindlySDK.config.getAuthToken = { chatId, promise in
// Generate JWT token
// On success, call
promise.fulfill("JWT_TOKEN")
// On error
promise.reject(ERROR)
}In this example, you set the botKey directly in the start method. Then, you assign a closure to the KindlySDK.config.getAuthToken property. Inside the closure, you can generate the JWT token and fulfill the promise with the token. You can also reject the promise with an error if needed.
Remember to replace BOT_KEY with your actual bot key.
Manually Save Authentication Token
If you need to manually save an authentication token (for example, if you retrieve it from another source):
let success = KindlySDK.saveAuthToken("YOUR_JWT_TOKEN")The method returns a boolean indicating whether the token was successfully saved to the keychain.
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 JWT from your identity provider (optionally paired with an access token), or an opaque bearer token validated by introspection.
The first is the single-string case above. For the other two, fulfil the promise with a KindlyAuthCredentials instead:
KindlySDK.start(botKey: String, market: String, authCredentialsCallback: (_ chatId: String, _ promise: Promise<KindlyAuthCredentials>) -> Void)
KindlySDK.start(botKey: "BOT_KEY", market: "YOUR_MARKET") { chatId, promise in
promise.fulfill(
KindlyAuthCredentials(
idToken: idToken,
accessToken: accessToken,
issuer: "https://idp.example.com/",
expiresAtUnixSeconds: expiresAt
)
)
}Field | Sent as | When you need it |
|---|---|---|
idToken | Authorization: Bearer, when set | The OIDC id token, if your provider issues one. For introspection this is the opaque token itself |
accessToken | X-Access-Token next to a different id token, or Authorization: Bearer when it is the only token you have | Dual-token provider setups, and providers that issue no id token at all. Ignored by the Kindly JWT strategy in its companion form |
issuer | X-Auth-Issuer | Bots with more than one provider configured, and any opaque token |
expiresAt | not sent | Opaque tokens, where there is no exp claim for the SDK to read |
Every field is optional, but at least one of idToken / accessToken has to be set. Credentials carrying neither cannot authenticate anything, and the chat stays anonymous.
Which token becomes the bearer
Authorization is mandatory on Kindly's auth route, so whichever token you have goes there: idToken first, falling back to accessToken. A provider that returns only an access token is a normal, supported case rather than a workaround. That access token becomes the bearer, and Kindly verifies it by whichever strategy the bot is configured for.
Three read-only accessors say what the SDK will actually send:
credentials.bearerToken // idToken ?? accessToken, first non-empty. nil if neither is set
credentials.hasToken // bearerToken != nil, the real "can this authenticate?" check
credentials.companionAccessToken // the value that will be sent as X-Access-Token, or nilcompanionAccessToken is the access token in its companion role only. It is nil when the access token is itself the bearer, and nil when it merely duplicates the id token. Kindly verifies the companion against the id token's at_hash, so sending a duplicate would fail an auth that would otherwise have succeeded. The SDK drops it for you.
X-Access-Token and X-Auth-Issuer are read on auth/chat, message and trigger only.
A single-JWT integration never needs this type, so keep returning a string. The credentials callback can also be set after start():
KindlySDK.config.getAuthCredentials = { chatId, promise in /* … */ }getAuthCredentials takes precedence over getAuthToken when both are set.
Credentials can also be saved directly, the same way a token can:
let success = KindlySDK.saveAuthCredentials(
KindlyAuthCredentials(idToken: "YOUR_ID_TOKEN", accessToken: "YOUR_ACCESS_TOKEN")
)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 token, when it is a JWT. That is the id token when you supplied one, and the access token when it is the only token you have
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: there is no claim to read, so supply expiresAt or the SDK will keep using it until the backend rejects it.
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 callback 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 Promise<Void>.