<!-- Source: https://docs.squirro.com/en/latest/technical/admin/external-authentication/jwt.html -->
# JWT Authentication

Squirro supports authenticating API and frontend requests with JSON Web Tokens (JWTs) issued by an external OpenID Connect (OIDC) Identity Provider (IDP), such as Keycloak, Auth0, Microsoft Entra ID, or Okta.

When JWT authentication is turned on, Squirro accepts a JWT in place of a Squirro-issued `access_token` or `refresh_token` on the standard session login flow. The token is validated against the IDP public keys, and the JWT claims are mapped to a Squirro user, tenant, role, and groups.

That feature is available from Squirro 3.15.4 onwards.

## Overview

JWT authentication in Squirro consists of two configurable pieces:

- The **JWT Validator** Studio plugin, where administrators describe how JWTs from a given IDP should be validated and how claims are mapped to Squirro users, roles, and groups.
- A small **server-side switch** in the user service configuration that turns the feature on and selects the supported login flows.

Once turned on, Squirro discovers the IDP signing keys automatically via OIDC, verifies each JWT signature and standard claims, and maps the token contents to a Squirro identity. Validation results and signing keys are cached for performance.

## Token Modes

Squirro supports JWTs in two login flows, controlled by the `token_role` server setting (see [Server-Side Configuration](#jwt-server-configuration)):

- `refresh`

  JWTs are accepted as the `refresh_token` on the session endpoint. A Squirro user session is created, and the session expiry is set from the JWT `exp` claim. Subsequent requests within the session reuse the existing JWT without re-validating it from scratch.
- `access`

  JWTs are accepted as the `access_token` on the session endpoint. No long-lived Squirro session is created. The JWT itself is the access token. Validation results are cached briefly so repeated requests with the same token are inexpensive.
- `*` (default)

  Both modes are accepted. The mode is chosen per request based on which field the JWT was supplied in.

In all modes, a Squirro user is automatically provisioned the first time the JWT is seen, using the JWT subject claim and the tenant configured in the plugin.

## Server-Side Configuration

The server-side switch lives in the user service INI file (typically `/etc/squirro/user.ini`) under the `[jwt]` section. All values are optional and have the defaults shown below.

```ini
[jwt]
# Master switch. JWT validation is OFF by default.
enabled = false

# Where JWTs are accepted: 'access', 'refresh', or '*' (both).
token_role = *

# Caching of validation results.
cache_enabled            = true
access_token_cache_ttl   = 3600     # seconds; max TTL for access-token mode
refresh_token_cache_ttl  = 86400    # seconds; max TTL for refresh-token mode
jwks_cache_ttl           = 3600     # seconds; signing-key cache TTL
```

For most deployments, only `enabled = true` needs to be set. Restart the user service after changing INI settings.

## Studio Plugin Configuration (UI)

JWT validation rules are configured per **domain** in the _JWT Validator_ Studio plugin. The plugin is active by default. Navigate to Server → JWT Validator to manage configurations.

Each configuration is keyed by a domain string. Use `*` for a single tenant-wide rule, or specify an exact domain to scope the rule.

### Required Fields

| Field | Description |
| --- | --- |
| Domain | Domain on which this configuration is active. `*` matches any domain. |
| OAuth Client ID | The client ID issued by the IDP. Used as the expected JWT audience. Required unless audience checking is skipped. |
| Authentication Server URL | Base URL of the IDP / realm, for example `https://keycloak.example.com/auth/realms/MyRealm`. Used for OIDC discovery and, by default, as the expected issuer. |

### Optional Fields

| Field | Description |
| --- | --- |
| Additional allowed clients | Comma-separated list of additional client IDs that are accepted in the JWT audience. Use this when the same JWT is shared between multiple applications. |
| JWT Issuer | Explicit value for the expected issuer. Defaults to the _Authentication Server URL_ (trailing slash stripped) when empty. |
| Well-known OIDC endpoint | Path (or full URL) used for OIDC discovery. Defaults to `/.well-known/openid-configuration`. The discovered `jwks_uri` is used to fetch signing keys. |
| Group names field | Dot-path inside the JWT claims that contains the user groups. Defaults to `realm_access.roles` (Keycloak). List values and string values (comma- or semicolon-separated) are both supported. These groups are used for role mapping only; they do not grant Squirro group membership. |
| Mapping of groups to Squirro roles | Maps IDP group names to Squirro roles. See [Mapping JWT Claims to Squirro](#jwt-role-mapping) below. |
| Default group | Squirro group assigned to every user authenticated through this configuration, when _Enforce default group_ is turned on. This is the only setting that grants Squirro group membership. Project access follows from that group’s project permissions, so share the group with the projects its users need. |
| Enforce default group | Turn on to assign the _Default group_ above. Membership is added alongside any group the user already has, so a group assigned by other means (for example manually by an administrator) is left in place. Groups from the JWT are never assigned as Squirro group memberships. |
| Fields to map in as user values | Comma- or newline-separated list of JWT claim → Squirro user-value mappings, e.g. `email, department=dept, full_name=name`. |
| Custom User-Agent | User-Agent header sent on outbound requests to the IDP. Useful when the IDP enforces a User-Agent allowlist. |
| Default tenant | Tenant assigned to every user authenticated through this configuration. Automatically captured from the saving admin context if left empty. |

### Development-Only Flags

> **Warning**
>
> The following flags are intended for development and testing only. They emit warnings to the server log on every validation. Do **not** turn them on in production.

| Field | Effect when turned on |
| --- | --- |
| Accept expired JWT tokens | Skips the JWT expiry check. |
| Skip JWT issuer validation | Skips the issuer claim check. |
| Skip JWT audience validation | Skips the audience claim check. When set, the _OAuth Client ID_ becomes optional. |

## Mapping JWT Claims to Squirro

### Role Mapping

Squirro extracts the user groups from the JWT using the path in _Group names field_ and maps them to a Squirro role via _Mapping of groups to Squirro roles_.

The format is a semicolon-separated list of expressions. Each expression has the form:

```text
<group from JWT> = <Squirro role>
```

> **Note**
>
> The **left** side of `=` is the **group name from the JWT** (one of the values found in the _Group names field_ claim).
>
>
>
> The **right** side of `=` is the **Squirro role** to assign. Valid Squirro roles are `reject`, `demo`, `reader`, `user`, and `admin`.

Additional rules:

- An expression without `=` (just a role on its own) applies to **all users** as a fallback.
- JWT group matching is case-insensitive.
- If multiple expressions match the same user, the **highest-permission** role wins.
- If nothing matches, the user is rejected.

Example:

```text
keycloak-admins=admin; keycloak-employees=user; reader
```

That grants `admin` to anyone whose JWT groups contain `keycloak-admins`, `user` to anyone whose JWT groups contain `keycloak-employees`, and `reader` to everyone else (the trailing fallback).

Use `reject` to deny logins explicitly:

```text
keycloak-admins=admin; reject
```

Here only members of `keycloak-admins` are admitted (as admins). All other users are rejected.

### User Identifier, Email, and Full Name

The Squirro user is keyed by the JWT `sub` claim. The email address is taken from the `email` claim (falling back to `preferred_username`), and the full name from the `name` claim. If no email claim is present, Squirro synthesizes one from the subject.

### Custom User Values

Use _Fields to map in as user values_ to pull additional claim values into Squirro per-user value store. Entries are separated by commas or newlines, and each entry has the form:

```text
<claim from JWT> = <Squirro user-value key>
```

> **Note**
>
> The **left** side of `=` is the **JWT claim name** (looked up at the top level of the JWT claims).
>
>
>
> The **right** side of `=` is the **Squirro key** under which the value is stored on the user record.
>
>
>
> If an entry has no `=`, the JWT claim name is used as both the source and the Squirro key.

Example:

```text
email
department=dept
full_name=name
```

That stores the JWT `email` claim under the Squirro key `email`, the JWT `department` claim under `dept`, and the JWT `full_name` claim under `name`.

### Tenant

The tenant is **always** taken from the plugin configuration (_Default tenant_ field), never from the JWT. If the client supplies a tenant filter when calling the session endpoint and it does not match the configured tenant, the JWT is rejected.

## Caching

Two caches affect JWT authentication:

- **Signing key (JWKS) cache**

  IDP public keys are cached for `jwks_cache_ttl` seconds (default 1 hour). After an IDP key rotation, validation can fail until the cache expires. Shorten the TTL or restart the service to force a refresh.
- **Validation result cache**

  Successful validations are cached for `min(JWT expiry - now, *_cache_ttl)`. Set `cache_enabled = false` in `user.ini` to turn off caching. Turning off caching has a noticeable performance impact: each request triggers a fresh signature verification.

## Logging In

Once turned on, clients log in by posting their IDP-issued JWT to the standard token endpoint (`/api/user/oauth2/token`). Both forms below are supported (controlled by `token_role`). For details on the token endpoint, access token lifetime, and refreshing tokens, see the [Authentication](../../api/authentication.md#api-authentication) page.

### Refresh-Token Form (Stateful Session)

```http
POST /api/user/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<jwt>
```

A Squirro session is created (or reused), and the response contains a Squirro `access_token`. The session remains valid until the JWT `exp` claim, after which a new JWT is required.

### Access-Token Form (Stateless)

```http
POST /api/user/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=access_token&access_token=<jwt>
```

The response includes the JWT itself as the `access_token` along with the resolved tenant, role, and user information. No long-lived Squirro session is created.

## Calling the GenAI REST API with an IDP Token

The GenAI Service REST API authenticates each request with a Squirro access token supplied as a bearer token in the `Authorization` header. To call the GenAI REST API with an identity from your IDP, exchange your IDP-issued JWT for a Squirro access token, then send that access token on your GenAI requests.

### Prerequisites

- Squirro 3.15.4 or later.
- JWT authentication turned on, with `token_role` set to `refresh` or `*` (see [Server-Side Configuration](#jwt-server-configuration)).
- A JWT Validator configuration for your IDP whose role mapping grants the authenticated user at least read access to the target project (see [Mapping JWT Claims to Squirro](#jwt-role-mapping)). For how Squirro roles and project permissions govern API access, see the [Permissions Reference](../../api/squirro_client/sc-api-permissions.md#sc-api-permissions) page.

### Exchanging the JWT for a Squirro Access Token

First, obtain an access or ID token (JWT) from your OIDC Identity Provider using your standard IDP client flow. Then post the JWT to the token endpoint using the refresh-token form (see [Logging In](#jwt-logging-in)):

```http
POST /api/user/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<jwt>
```

The response contains a Squirro `access_token`. Browser-based clients must obtain the access token through a same-origin or CORS-permitted request to the token endpoint.

### Calling the GenAI REST API

Send the Squirro access token as a bearer token. For example, to call the streaming chat endpoint:

```http
POST /service/genai/v0/projects/<project_id>/streaming_chat HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json

{"instruction": "Who was Albert Einstein?"}
```

Replace `<project_id>` with your project identifier (see [Locate Your Project Identifier](../../ui/connecting.md#connecting-project-id)). For the full list of endpoints and the request and response schemas, see the [GenAI Service API](../../api/services/genai/index.md#genai-api) page.

### Token Expiry and Renewal

The Squirro `access_token` returned by the exchange is short-lived. When it expires, exchange your IDP JWT again to obtain a new access token. Once the IDP JWT itself expires, obtain a fresh JWT from your IDP and repeat the exchange. For the access token lifetime and the refresh mechanism, see the [Authentication](../../api/authentication.md#api-authentication) page.

### Troubleshooting

| Status | Cause and remedy |
| --- | --- |
| `403` on the token exchange | The JWT was rejected. Common reasons are an expired token, an issuer or audience that does not match the JWT Validator configuration, a mapped role of `reject`, or a requested tenant that does not match the configured tenant. Review the JWT Validator configuration and the IDP token contents. |
| `401` on a GenAI request | The Squirro access token is missing, invalid, or expired. Repeat the exchange to obtain a new access token. |
| `403` on a GenAI request | The authenticated user does not have access to the requested project. Review the role mapping and the project permissions. |
| `422` on a GenAI request | The access token payload is invalid. Obtain a new access token. |

For the full list of API status codes, see the [Common Status Codes](../../api/common-status.md#api-common-status) page.

> **Note**
>
> Use the refresh-token mode for GenAI REST API calls. The access-token mode returns the JWT itself rather than a Squirro access token, which the GenAI REST API does not accept directly.

## Example: Keycloak Quick Setup

1. In Keycloak, create a realm (for example, `my-realm`) and a client (for example, `squirro-app`) that issues `RS256`-signed access tokens. Make sure the realm exposes the standard `/.well-known/openid-configuration` endpoint.
2. In Squirro, navigate to Server → JWT Validator and create a configuration with:

   - Domain: `*`
   - OAuth Client ID: `squirro-app`
   - Authentication Server URL: `https://keycloak.example.com/realms/my-realm`
   - Group names field: `realm_access.roles`
   - Mapping of groups to Squirro roles: `squirro-admins=admin; squirro-users=user; reject`
   - Default tenant: leave empty (captured from the admin’s context).
3. On the server, edit `/etc/squirro/user.ini`:

   ```ini
   [jwt]
   enabled = true
   ```
4. Restart the user service:

   ```bash
   systemctl restart squser
   ```
5. Test with a token obtained from Keycloak:

   ```bash
   curl -X POST https://squirro.example.com/api/user/oauth2/token \
        -d "grant_type=refresh_token&refresh_token=$JWT"
   ```
