# JWT Header Auth

Authenticate API requests with JWTs minted by your own identity provider

Quor can accept a JWT that your identity provider or API gateway mints, and use it to authenticate a request.
This is useful when another system already holds the user's identity and calls Quor on their behalf — for example an API
gateway, a portal, or a service that fronts Quor for its own users.

Set `JWT_PUBLIC_KEY_URL` to a URL that serves the public key material for your token issuer. When the variable is set,
Quor accepts a signed JWT in the `Authorization` header:

```
Authorization: Bearer <JWT>
```

<Note>
  This option is part of Quor Community Edition. It needs no Enterprise license,
  and it works the same way in both editions. See [Support and stability](#support-and-stability) below.
</Note>

## Setup

<Steps>
  <Step title="Publish your public key">
    Your identity provider must serve the verification key at a URL that the Quor API server can reach.
    Quor accepts two formats:

    - **JWKS** — a JSON document with a `keys` array. This is what Microsoft Entra ID, Okta, Auth0,
    and most other providers publish at their `jwks_uri`.
    - **PEM** — a single PEM-encoded RSA public key.

    Quor picks the format from the response. A response with `Content-Type: application/json`,
    or a body that starts with `{`, is read as JWKS. Any other body is read as PEM.

    <Tip>
      Most identity providers list the JWKS URL in their OIDC discovery document at
      `https://<YOUR_IDP>/.well-known/openid-configuration`, under the `jwks_uri` field.
    </Tip>
  </Step>

  <Step title="Configure Quor">
    Set the variable in your `.env` or `values.yaml` file (Docker and Kubernetes, respectively).

      ```bash .env
      JWT_PUBLIC_KEY_URL=https://<YOUR_IDP>/.well-known/jwks.json
      ```

      ```yaml values.yaml
      configMap:
        JWT_PUBLIC_KEY_URL: https://<YOUR_IDP>/.well-known/jwks.json
      ```

    Restart the API server to apply the change.
  </Step>

  <Step title="Send a request">
    Put the token in the `Authorization` header of any Quor API call. `GET /api/me` returns the user that Quor resolved,
    so it is a good first check.

      ```bash
      curl https://<YOUR_ONYX_DOMAIN>/api/me \
        -H "Authorization: Bearer <JWT>"
      ```
  </Step>
</Steps>

## Token requirements

| Requirement    | Value                                                                        |
| -------------- | ---------------------------------------------------------------------------- |
| Signature      | RS256                                                                        |
| Identity claim | The first valid email address in `email`, `preferred_username`, then `upn`   |
| Expiry         | `exp` is enforced. Expired tokens are rejected                                |
| Audience       | `aud` and `iss` are **not** checked                                          |

The address in the identity claim is normalized and lowercased. It becomes the Quor user's email.

<Warning>
  Quor does not check the `aud` or `iss` claims. Any token that your configured key signs is accepted,
  even if it was minted for a different application. Publish a key that only signs tokens for Quor,
  or make sure every consumer of that key is equally trusted.
</Warning>

### Key selection and rotation

When the URL serves JWKS, Quor selects the key by the token header `kid`, then by `x5t`.
If neither matches and the document holds exactly one key, Quor uses that key.

Quor caches the fetched key material. If verification fails, Quor clears the cache, fetches the URL again,
and retries once. Routine key rotation therefore needs no restart.

## User handling

A request authenticated by JWT is treated like any other logged-in user.

- **Just-in-time provisioning.** If no Quor user has that email, Quor creates one. The account is
  marked verified and gets a random password it never uses.
- **Existing users.** If the email belongs to an existing user, that user is returned. Deactivated
  users are rejected. Accounts that are not web-login accounts are rejected.
- **Access policies still apply.** The email must satisfy the same invite allowlist and
  `VALID_EMAIL_DOMAINS` rules as every other login path.
- **Session expiry.** With `TRACK_EXTERNAL_IDP_EXPIRY=true`, Quor stores the token's `exp` as the
  user's external IdP expiry.

A user provisioned this way gets no elevated permissions, with one exception:
the **first** user on an empty instance becomes an admin, whichever login path creates them.
Grant admin or curator access to anyone else from the **Admin Panel → Users** page.

<Info>
  Enterprise Edition adds two behaviors on this path. Emails in the default-admin list are made admins at creation.
  Provisioning a new user also consumes a license seat,
  so a login for an unknown user fails once the workspace reaches its seat limit.
  Community Edition applies no seat limit.
  Enterprise Edition can also manage access through [SCIM](/deployment/authentication/scim).
</Info>

## Precedence

A valid Quor session cookie takes priority. Quor reads the `Authorization` header only when the request has no session.

API keys and personal access tokens use the same header. Quor tries the JWT path first,
and a value that is not a valid RS256 JWT falls through to the API key and personal access token paths.
Those credentials keep working unchanged when `JWT_PUBLIC_KEY_URL` is set.

## Support and stability

`JWT_PUBLIC_KEY_URL` is a documented and supported configuration option.
Quor treats it like every other published setting on this page:

- The variable name, the `Authorization: Bearer` header, the RS256 requirement, the accepted key
  formats, and the identity claim order are part of the documented interface.
- Automated tests cover the behavior described above — existing-user login, just-in-time
  provisioning, rejection of tokens signed by an unknown key, rejection of expired tokens,
  and key rotation without a restart.
- If a future release changes or replaces this option, the change is called out in the release
  notes for that release, together with the mechanism that replaces it.

<Note>
  If you plan to build an integration on this option, [contact us](/deployment/miscellaneous/contact_us).
  We are happy to review your design and tell you about anything on the roadmap that touches it.
</Note>

## Troubleshooting

Quor logs every verification failure on the API server. Check `api_server` logs for these messages:

| Log message                                        | Cause                                                              |
| -------------------------------------------------- | ------------------------------------------------------------------ |
| `JWT_PUBLIC_KEY_URL is not set`                    | The variable is missing or empty                                   |
| `Failed to fetch JWT public key`                   | Quor cannot reach the URL. Check egress rules and TLS              |
| `JWT public key URL returned invalid JSON`         | The response looks like JSON but does not parse                    |
| `no JWKS 'keys' field was found`                   | The JSON response is not a JWKS document                           |
| `No matching JWK found for token header`           | The token `kid` is absent from the JWKS document                   |
| `Invalid JWT token`                                | Bad signature, expired token, or an algorithm other than RS256     |
| `no email claim found`                             | None of `email`, `preferred_username`, or `upn` holds a valid email |

A request that fails JWT verification is not rejected outright. It continues as an unauthenticated request,
and the endpoint returns `401` if it needs a user.
