Widget authentication
Kindly supports authenticating your users so they can view or manage their accounts, orders, bookings, etc. from the chat. It also allows your live agents to know who they are talking to.
We support three different ways to configure authentication.
- Kindly JWT: verifies tokens you sign yourself with a PEM public key.
- JWKS provider: verifies tokens issued by an OpenID Connect or OAuth 2.0 identity provider, or any other issuer that publishes a JWKS key set.
- Introspection: validates opaque bearer tokens against your endpoint (RFC 7662).
You can configure more than one, of any type. Each added method appears in the Configured authentication methods table (provider name, type, issuer) and can be edited or deleted from there. All methods result in the same outcome: user details appear in your live agent platform, and the token is forwarded to your webhooks so you can identify the user server-side.
More information about each method type below:
Which method should I use?
īģŋ | Kindly JWT | JWKS provider | Introspection |
|---|---|---|---|
Description | Verifies tokens you sign yourself with a PEM public key | Verifies tokens issued by an OIDC or OAuth 2.0 identity provider, or any other JWKS-compatible issuer | Validates opaque tokens against your endpoint (RFC 7662) |
You already run an OIDC or OAuth 2.0 identity provider | Works, but adds a parallel token scheme | â Easiest option | â Identity providers often provide an introspection endpoint out of the box |
Your access tokens are opaque or non-standard JWTs | â | â (When using an id token AND an access token, the access token can be opaque) | â |
You want Kindly to verify tokens without you managing a key pair | â (you manage the RSA key pair) | â | â |
You want full control over custom claims in the token | â | Limited to your provider's claims | Limited to your endpoint's introspection response |
Authentication strategies
Once a method resolves, how Kindly verifies the user comes down to what you hand the chat client. You don't select a strategy explicitly. Kindly picks it from the token(s) it receives:
īģŋ | What you return | How Kindly verifies it | Set up with |
|---|---|---|---|
Single JWT | One JWT | Kindly JWT: RS256 against your public key. JWKS provider: signature check against your provider's JWKS | Kindly JWT / JWKS provider |
Dual token | An id token (JWT) and a separate access token (which may itself be a JWT or opaque) | Id token via JWKS; access token via JWKS if it's a JWT, or stored unverified alongside the verified id token if it's opaque | JWKS provider |
Single opaque token | One bearer token that isn't a standard JWT (opaque, or a JWT-shaped token only your own endpoint can validate) | Token introspection (RFC 7662) against your endpoint | Introspection |
Each setup section below includes a sequence diagram of its flow: Kindly JWT, JWKS provider, and Introspection. Authentication strategies
Navigate to your workspace's Settings â Kindly Chat â Authentication to set any authentication provider.
Set up "Kindly JWT"
Click Set up on the Kindly JWT card to open the Kindly JWT verifier dialog:
īģŋ | Required | Description |
|---|---|---|
Issuer URL (iss) | Yes | Must match the iss claim your backend puts in the JWT payload (see token format below). You can add multiple Kindly JWT verifiers, each scoped to a different issuer. |
Public key | Yes | The PEM-encoded public key matching the private key your backend signs with. |
The rest of this section covers generating the key pair, minting the JWT, and wiring up the widget callback.
sequenceDiagram
participant U as User
participant C as Kindly Chat Client
participant B as Your integration backend
participant K as Kindly
U->>C: Start a Chat
C->>B: getAuthToken(chatId)
B->>C: JWT signed with your private key
C->>K: Authenticate the chat
K->>K: Verify with your public key
K->>C: avatar_url, full_name
opt Webhook dialogue
C->>K: Trigger a webhook dialogue
K->>B: Webhook request with forwarded JWT
B->>B: Verify JWT with your public key
B->>K: Webhook response
K->>C: Text response
endCreate a private/public key pair
To allow your own server to authenticate webhook requests without the Kindly API interfering, we use a standard private/public key pair.
The private key is used by your server to encode and sign the JWT in your authentication endpoint.
The public key goes into the Public key field of the Kindly JWT verifier above; Kindly uses it to verify that the JWT is valid.
Run the following commands to generate a key pair. On a Linux or Unix-like environment:
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pemThe contents of private.pem should look something like:
and the contents of public.pem should look like this:
Create an authentication endpoint
The responsibility of authenticating your users is yours and is usually done by an HTTP endpoint you implement. In the most common case, the Kindly Chat callback sends a HTTP POST request to an authentication endpoint you implement. If the request is sent from the same domain as the endpoint, CORS headers can be omitted.
Create an authentication endpoint that returns a JWT in the format specified below. Be aware that JWTs are encoded and signed, not encrypted, meaning anyone who has the token can read the content.
Set up an authentication endpoint on your backend that returns a JSON Web Token (JWT) signed using the RS256 algorithm and the private key created in the step above, and the payload must conform to the following required format:
You can implement the auth endpoint however you wish. We recommend using a compatible JWT library from the list at https://jwt.io/.
Token format
īģŋ | Required | Description |
|---|---|---|
iat | Yes | Integer timestamp (epoch time) when the token is generated. |
exp | Yes | Integer timestamp (epoch time) when the token expires. Must be no more than 15 minutes after iat. This is separate from the freshness window below: even a token set to expire in 15 minutes is only accepted within ~10 seconds of iat. |
iss | Yes | String representing the issuer of this token. Must match the Issuer URL (iss) you configured on the Kindly JWT verifier. |
sub | Yes | String representing your id for the logged in user. |
chat | Yes | A JSON object containing the listed keys. Example: { id: "abc123", webhook_domains: ["example.com", "*.example.org"] } |
chat.id | Yes | String value provided by Kindly Chat, representing the current chat. |
chat.webhook_domains | īģŋ | Array of hostnames Kindly is allowed to forward the token to, e.g. example.com, or *.example.org to cover subdomains. Hostnames only (no scheme, port or path), and one malformed entry rejects the whole token. |
sid | īģŋ | String id for the user's session on your side. Only used with backchannel logout (see below), where it lets a logout request clear the one session it names. Without it, a logout clears every chat bound to that sub for this issuer. |
name | īģŋ | String value representing the full name of the user. Displayed in Kindly Chat and Inbox. |
īģŋ | String value representing the email of the user. Displayed in Kindly Chat and Inbox. | |
email_verified | īģŋ | Boolean value indicating whether you have verified the email. The email is stored either way; this flag is recorded alongside it. |
phone_number | īģŋ | String value representing the phone number of the user. Displayed in Kindly Chat and Inbox. |
phone_number_verified | īģŋ | Boolean value indicating whether you have verified the phone number. The phone number is stored either way; this flag is recorded alongside it. |
picture | īģŋ | An URL, which is publicly accessible, pointing to a user avatar. Displayed in Kindly Chat and Inbox. |
You may also include any other property you want to use in your webhooks. You can read more on JWT in the standard here.
Mint a fresh token on every request. A Kindly JWT is only accepted for about 10 seconds after its iat; Kindly rejects a token presented later than that as expired, regardless of the exp you set. This is why getAuthToken is called on demand (and re-run automatically before a session refresh) rather than returning a stored value. Never cache or reuse a Kindly JWT across requests. (This freshness window applies to the Kindly JWT method only; JWKS provider tokens are accepted up to their exp.)
Python example using Django REST framework
import datetime
import jwt
from django.conf import settings
from django.http.response import HttpResponseBadRequest, JsonResponse
from django.utils import timezone
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
@api_view(['POST'])
@permission_classes((IsAuthenticated,))
def auth(request):
chat_id = request.data.get('chat_id')
if not chat_id:
return HttpResponseBadRequest()
now = timezone.now()
iat = int(now.timestamp())
expires = int((now + datetime.timedelta(minutes=1)).timestamp()) # JWT is valid for 1 minute
user = request.user
payload = {
'iat': iat,
'exp': expires,
'iss': settings.CHAT_CLIENT_AUTH_ISSUER, # must match the Issuer URL (iss) on the verifier
'chat': {'id': chat_id},
'sub': str(user.id),
'name': user.get_full_name(),
'email': user.email,
'email_verified': True,
'picture': 'https://example.com/avatar.jpg',
}
# PyJWT >= 2.0 returns a str; on PyJWT < 2.0, append .decode('utf-8')
token = jwt.encode(payload, settings.CHAT_CLIENT_AUTH_PRIVATE_KEY, algorithm='RS256')
return JsonResponse({'token': token}, status=200)Set up JWKS provider
Click Set up on the JWKS provider card to open the JWKS provider dialog. It works with any issuer that publishes a JWKS key set, OpenId Connect or any plain OAuth 2.0 authorization server.
Provider identity: issuer URL used to match incoming JWTs (iss claim).
īģŋ | Required | Description |
|---|---|---|
Issuer URL (iss) | Yes | Your identity provider's issuer, e.g. https://idp.example.com/realms/my-realm. |
Discovery URL | īģŋ | Optional. Accepts an OpenID Connect discovery document or an RFC 8414 OAuth 2.0 Authorization Server Metadata document. Only needed when it isn't at <iss>/.well-known/openid-configuration or <iss>/.well-known/oauth-authorization-server. |
JWKS URI | īģŋ | Optional. An alternative to Discovery URL: fetches the key set directly, skipping metadata discovery. Prefer Discovery URL when your provider has one: a metadata document lets your provider rotate its key set without a config change here. Use JWKS URI for a provider that publishes a key set but no metadata document at all. |
JWT token verification: id/access tokens, verified via JWKS.
īģŋ | Required | Description |
|---|---|---|
Allowed access token audiences | Yes | Audience (aud) on the token you send in Authorization. |
Allowed id token audiences | Only if you also send a separate access token via X-Access-Token (dual token) | Audience(s) your provider puts on the id token, checked when a companion access token makes the Authorization bearer an id token. |
Audiences are matched against the token's aud claim, and there is no way to opt out of the check: a method with no audiences configured rejects every token. Set them to the value your identity provider actually puts in aud, normally your client id. For a single-JWT setup, Allowed access token audiences is all you need; dual-token setups need both fields, because the id token and the access token are checked separately.
Avoid allow-listing a broad, shared audience. Some providers put a built-in audience on nearly every token they issue. Allow-listing one of those accepts tokens minted for any other application at that issuer, not just yours.
Outbound forwarding: domains verified tokens may be forwarded to.
īģŋ | Required | Description |
|---|---|---|
Forward domains | īģŋ | Domains Kindly is allowed to forward the token to in webhook requests, e.g. *.example.com. Unioned with chat.webhook_domains if the token Kindly forwards carries one (that's the access token in dual-token setups, since the id token is never forwarded). |
With a JWKS provider you can send either a single JWT (id token only) or two tokens (id token plus an access token).
The access token can either be a JWT or an opaque token.
See Authentication strategies. A single opaque token instead uses the Introspection method.
Dual token: both tokens must describe the same user. When the access token is a JWT, its sub has to match the id token's sub; Kindly rejects the pair otherwise, leaving the chat as it was. An access token that identifies itself as an id token (a token_use: "id" claim) is rejected for the same reason. Opaque access tokens can't be checked this way, so make sure your backend pairs them per user.
sequenceDiagram
participant U as User
participant C as Kindly Chat Client
participant B as Your integration backend
participant K as Kindly
participant I as Your identity provider
U->>C: Start a Chat
C->>C: getAuthToken(chatId)
C->>I: Obtain token (directly, or via your backend)
I->>C: id token (+ access token for dual token)
C->>K: Authenticate the chat
K->>I: Verify via JWKS
K->>C: avatar_url, full_name
opt Webhook dialogue
C->>K: Trigger a webhook dialogue
K->>B: Webhook request with forwarded token
B->>I: Verify token via JWKS
B->>K: Webhook response
K->>C: Text response
end
Set up an Introspection provider
This is the single opaque token strategy: your backend returns one bearer token that doesn't decode as a JWT (or a non-standard JWT-shaped token only your own endpoint can validate), and Kindly validates it against your introspection endpoint. Click Set up on the Introspection card to open the Introspection provider dialog:
Provider identity
īģŋ | Required | Description |
|---|---|---|
Provider ID (iss) | Yes | An identifier for this config. Opaque tokens don't carry an issuer claim, so your client must send this same value in its issuer field to select this provider. |
Token introspection: OAuth 2.0 Token Introspection (RFC 7662). Works for opaque bearer tokens and non-standard JWT-shaped tokens validated via your introspection endpoint instead of JWKS.
īģŋ | Required | Description |
|---|---|---|
Discovery URL | īģŋ | Optional. Accepts an OpenID Connect discovery document or an RFC 8414 OAuth 2.0 Authorization Server Metadata document. Lets Kindly read introspection_endpoint from it. Only needed when it isn't at <iss>/.well-known/openid-configuration, <iss>/.well-known/oauth-authorization-server, or set directly in Introspection URL below. |
Introspection URL | Only if Provider ID isn't an HTTPS URL and Discovery URL is unset | Your OAuth 2.0 introspection endpoint, e.g. https://auth.example.com/oauth/introspect. |
Introspection client ID | Yes | Client ID Kindly authenticates with when calling your introspection endpoint. |
Introspection client secret | Yes | Client secret for the above. |
JWT token verification: id/access tokens, verified via introspection claim.
īģŋ | Required | Description |
|---|---|---|
Allowed access token audiences | Yes | The introspection response's aud claim is checked against this list. |
Backchannel logout: optional. When not set, the key set is discovered via Discovery URL instead. Needed only if you plan to send Kindly backchannel logout notifications.
īģŋ | Required | Description |
|---|---|---|
JWKS URI | īģŋ | Used to verify the signature of backchannel-logout tokens. |
Outbound forwarding
īģŋ | Required | Description |
|---|---|---|
Forward domains | īģŋ | Domains Kindly is allowed to forward the token to in webhook requests, e.g. *.example.com. |
Kindly calls your introspection endpoint with the bearer token (POST, form-encoded, with your client id and secret as HTTP Basic credentials) and requires active: true, a sub (the user's id), and an aud matching Allowed access token audiences in the response before treating the user as authenticated. Identity fields such as username and email are read from the same response.
If your response includes an iss, it must match the Provider ID (iss) you configured. Watch for this if you pick a non-URL Provider ID while your authorization server returns its real issuer URL; the two won't line up and every authentication will be rejected. Either use the issuer URL as the Provider ID, or omit iss from the response.
Unlike the JWT-based strategies, there's no signature to check locally for the token itself: Kindly validates it by calling your authorization server. Discovery URL and JWKS URI are the exception: they exist only so Kindly can verify the signature of a backchannel logout token, which your provider mints for that notification itself rather than issuing to a user.
sequenceDiagram
participant U as User
participant C as Kindly Chat Client
participant B as Your integration backend
participant K as Kindly
participant I as Your authorization server
U->>C: Start a Chat
C->>C: getAuthToken(chatId)
C->>I: Obtain opaque access token (directly, or via your backend)
I->>C: Opaque access token
C->>K: Authenticate the chat
K->>I: Introspection request (RFC 7662)
I->>K: claims
K->>C: avatar_url, full_name
opt Webhook dialogue
C->>K: Trigger a webhook dialogue
K->>B: Webhook request with forwarded token
B->>I: Introspect token
I->>B: claims
B->>K: Webhook response
K->>C: Text response
end
Authenticate the chat client
Whichever method you configured above, you tell the chat client how to obtain a token with a getAuthToken function. It's called with a single argument chatId and must return the token(s) for that chat.
getAuthToken can return either a plain string or a credentials object, depending on your setup:
- Return a string when a single token is enough. This is the case for Kindly JWT and single-token JWKS provider.
- Return a credentials object when you need to pass more than one token, or need to tell Kindly which provider to use. This is the case for dual-token JWKS provider and Introspection.
īģŋ | Required | Purpose |
|---|---|---|
idToken | Yes | The user's token: a Kindly JWT, a JWKS provider id token, a single JWT, or an opaque access token |
accessToken | Only for dual-token JWKS provider setups | Forwarded to your webhooks so you can identify the user server-side |
issuer | Yes for Introspection | Tells Kindly which Introspection provider to validate against. Must match the Provider ID (iss) you set above |
expiresAt | īģŋ | Unix seconds. Set this if you know the expiry of an opaque token that Kindly can't otherwise determine |
There are two ways to hand this function to the chat client: define it up front so the user is authenticated as soon as the chat starts, or call authenticate() later once you know who the user is.
Authenticate automatically when a chat starts
Define getAuthToken on window.kindlyOptions before the chat script loads. Kindly calls it as soon as a chat starts, and again whenever the token needs refreshing.
Returning a string:
Returning a credentials object:
Authenticate after a chat has started
If the user isn't known when the chat loads (for example they sign in partway through the conversation), call window.kindlyChat.authenticate() once they are. Pass it the same getAuthToken function; Kindly calls it immediately and re-authenticates the current chat.
If you already set getAuthToken on window.kindlyOptions, you can call window.kindlyChat.authenticate() with no argument to re-run it.
Deauthenticate
To clear the user's authenticated identity during an active chat (for example when they log out), call:
This removes the stored token and forgets the getAuthToken function, so the chat continues unauthenticated until you authenticate again. Kindly clears the identity on its side too: the user's details come off the chat, and the token stops being forwarded to your webhooks.
(Optional) Backchannel logout
If your identity provider supports OpenID Connect Back-Channel Logout, you can register Kindly as a backchannel-logout client so that ending a session at your IdP (e.g. on your own logout page) deauthenticates the matching Kindly chat, instead of waiting for the token to expire. It's the server-to-server counterpart of deauthenticate() above: the identity comes off the chat and the token stops reaching your webhooks, without your page having to be open.
Register this endpoint with your IdP as the backchannel logout URI, using your own workspace id in the path (the numeric id shown in your workspace's URL in the Kindly Platform):
Kindly verifies the logout token's signature, matches it to the chat session by sid or subject, and clears that session's identity so any later request needs to re-authenticate. Whichever method you use, the Provider ID (iss) has to be your provider's real HTTPS issuer URL: that's the value Kindly matches against the logout token's iss claim. A Provider ID that isn't a URL can't receive backchannel logout at all.
Where the signature is verified from depends on your method:
- JWKS provider: the JWKS you already configured. Include a sid matching the one in your tokens to clear a single session; without one, Kindly falls back to matching by subject.
- Kindly JWT: your own public key. Sign the logout token with the private key you already use for chat JWTs, with the iss you configured on the verifier, and include a sid matching the one in your chat JWTs to clear a single session (see the token format).
- Introspection provider: your introspection endpoint isn't used here. Kindly instead verifies the signature via the Discovery URL or JWKS URI you set on the Introspection provider, or from <iss>/.well-known/... by default if Provider ID (iss) is already a discoverable HTTPS URL.
Unlike the flows above, this one is initiated by your identity provider, not the chat client:
sequenceDiagram
participant U as User
participant I as Your identity provider
participant K as Kindly
participant C as Kindly Chat Client
U->>I: Log out
I->>K: POST /auth/backchannel-logout/{workspaceId} w/ logout_token
K->>I: Verify logout token via JWKS
K->>K: Match chat session and clear its identity
K-->>C: session identity cleared
Note over C: Not pushed to the client - the next request finds itself unauthenticatedYour logout token needs iss (matching the method it belongs to), exp, iat, an events claim containing http://schemas.openid.net/event/backchannel-logout, and sub, sid, or both, whichever your identity provider sends. A nonce is not allowed. It also needs an aud, which the spec requires anyway; on a JWKS provider method that aud has to be one of your Allowed id token audiences, when you've set that field. The Kindly JWT path is the exception: those tokens carry no audience, so aud is optional there. An Introspection provider's logout token isn't checked against an audience list either way; there's no equivalent field for it today.
What gets cleared depends on which of the two your identity provider sends:
- sid and sub: clears the chats bound with that session id. Chats bound under one of the user's other session ids are never touched. If no chat carries that sid at all (which happens when Kindly never recorded one, for instance because your introspection response doesn't return sid), the logout falls back to this user's chats that have no recorded session id, so it still takes effect.
- sid only: the same, except there's no sub to fall back to: if no chat carries that sid, nothing is cleared and you still get a 200.
- sub only: clears every chat bound to that user for this issuer. This is the all-sessions logout.
Kindly records a session id from the sid claim in your id token or Kindly JWT, or from a sid in your introspection response. Supplying one is worth doing, since it scopes the logout precisely instead of relying on the fallback above, but a logout still works without it as long as your logout token carries sub.
Kindly replies 200 when it accepted the token, including when no session matched: that's a normal no-op. A 400 means the request carried no usable logout_token. A 403 means the token couldn't be verified, or that its issuer matches no method configured on the workspace; the two are deliberately indistinguishable, so the response can't be used to probe which issuers a workspace has. Requests are rate limited per workspace, generously enough for a mass logout, and a throttled request gets 429 with Retry-After.
Session refresh
For long-lived sessions, the chat client re-runs getAuthToken on your behalf shortly before the token expires, so users don't get silently logged out mid-conversation. You don't need to do anything for this beyond making sure getAuthToken returns a fresh token each time it's called.
The refresh fires roughly 30 seconds before the earliest expiry Kindly knows about, which is the earlier of:
- the expiresAt you returned from getAuthToken, if any, and
- the expiry Kindly determined when it validated the token: the exp claim for a JWT, or the exp in your introspection response for an opaque token.
The second one is visible to you: Kindly returns it as expires_at (Unix seconds) in the response to the chat client's authentication request, and omits the field when it couldn't determine an expiry. So if a refresh isn't firing when you expect, expires_at tells you whether Kindly read an expiry off your token at all.
If neither is available (an opaque token, no expiresAt, and an introspection response without exp), there's no expiry to schedule against and no proactive refresh happens. In that case set expiresAt, or return exp from your introspection endpoint.
Identify users in webhooks
Once the authentication setup is done, a token from your authentication endpoint is passed to the Kindly API which can send the token to your webhook endpoints. When a webhook contains a token it can be used to identify a user and provide support for a wide range of integration use cases.
You'll find the token in the standard Authorization header, ie. Authorization: Bearer <token>. For dual-token JWKS provider setups, the forwarded token is your access token, not the id token. Remember, you can use the debug console to inspect webhook request and responses, including their headers.
Webhook domains
To prevent leaking authentication/tokens to unwanted third parties, the domains a token can be forwarded to must be known in advance:
- Kindly JWT: encode chat.webhook_domains in the token itself, as described above.
- JWKS provider / Introspection: configure Forward domains on the provider. If the token Kindly forwards is itself a JWT carrying chat.webhook_domains, the two lists are unioned, but note that's the access token in dual-token setups, and an opaque token can't carry a list at all, so Forward domains is the only allowlist there.
The token will not automatically be sent from Kindly to webhooks outside these allowlists.
Verify the token
Before trusting the authentication token we forward, you may want to validate in on your end:
- Kindly JWT: verify using the public key you generated earlier, the same public key you entered on the Kindly JWT verifier for your workspace, which Kindly also uses to verify the JWT.
- JWKS provider, JWT access token: verify against your own identity provider's JWKS (single JWT / dual-token access JWT).
- JWKS provider, opaque access token (dual-token setups): Kindly stored this token unverified alongside the verified id token, so it arrives at your webhook unchecked. Validate it the way you already do elsewhere in your stack: introspection, or your own session store.
- Introspection: treat the opaque access token as you already do elsewhere in your stack.
After you have verified the token, you can extract information from it and be sure of its authenticity.
Using more than one method
You can configure several authentication methods on the same workspace (e.g. a JWKS provider for your main app plus a Kindly JWT verifier for a legacy integration). How a token is matched to a method depends on whether it's a JWT:
- JWT tokens (Kindly JWT, JWKS provider): matched automatically by the iss claim in the token, which must equal the Issuer URL (iss) you set on the method. Nothing extra to select at runtime.
- Opaque tokens (Introspection): the token has no issuer claim, so there's nothing to match on. Always return an issuer from getAuthToken matching the method's Provider ID (iss).