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

  • 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.

[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 ServerJWT 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.

Mapping of groups to Squirro roles

Maps IDP group names to Squirro roles. See Mapping JWT Claims to Squirro below.

Default group

Squirro group assigned to every user authenticated through this configuration.

Enforce default group

When turned on, the user is assigned only the default group. Any groups from the JWT are ignored.

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:

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

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:

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:

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

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 page.

Refresh-Token Form (Stateful Session)#

POST /api/user/oauth2/token
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)#

POST /api/user/oauth2/token
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).

  • 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). For how Squirro roles and project permissions govern API access, see the Permissions Reference 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):

POST /api/user/oauth2/token
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:

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

{ ... }

Replace <project_id> with your project identifier (see Locate Your Project Identifier). For the full list of endpoints and the request and response schemas, see the GenAI Service 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 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 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 ServerJWT 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:

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

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

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