> For the complete documentation index, see [llms.txt](https://docs.zerve.ai/self-managed/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zerve.ai/self-managed/configuration/authentication.md).

# Authentication

Zerve authenticates users against your own identity provider over [OIDC](https://openid.net/developers/how-connect-works/). The platform keeps no passwords and has no local accounts, every user signs in through your IdP, and Zerve creates the user record on their first sign-in.

Any provider that serves an OIDC discovery document and can issue JWT access tokens works: Auth0, Okta, Microsoft Entra ID, AWS Cognito, Keycloak, Logto. Step-by-step instructions for specific providers are under [IdP-Specific Setup](/self-managed/configuration/authentication/idp-setup.md).

Because accounts are created on first sign-in, anyone your IdP will issue a token for can get into Zerve. Restrict who can reach the application on the IdP side, or with a [claim rule](#restricting-access-with-a-claim-rule).

## Registering Zerve with your IdP

Create a single-page (public) application with:

* **Authorization code with PKCE.** No client secret is used anywhere — don't register Zerve as a confidential client.
* **Redirect URI** `https://zerve.acme.com/callback`, matching your ingress host exactly. The path is fixed.
* **Post-logout redirect URI** `https://zerve.acme.com`, or whatever you set `VITE_POST_LOGOUT_REDIRECT_URI` to.
* **Refresh tokens**, so `offline_access` returns one. Rotating refresh tokens are supported. Without a refresh token, users are bounced through the IdP again every time the access token expires.
* **JWT access tokens.** Zerve decodes the access token itself, so an opaque token cannot be used. Some providers only issue JWTs when the client asks for a specific API or resource. See `OIDC_AUDIENCE` below.
* **An `email` claim on the ID token.** If the ID token carries no email, the user is still created, but without one.

## Required values

The user service and the browser both need the same IdP coordinates:

```yaml
# my-values.yaml
user:
  workloads:
    common:
      env:
        OIDC_ISSUER: "https://auth.acme.com/oidc"
        OIDC_AUDIENCE: "https://zerve.acme.com/api/user"
        OIDC_CLIENT_ID: "<your-oidc-client-id>"

frontend:
  env:
    VITE_OIDC_AUTHORITY: "https://auth.acme.com/oidc"
    VITE_OIDC_CLIENT_ID: "<your-oidc-client-id>"
    VITE_OIDC_AUDIENCE: "https://zerve.acme.com/api/user"
```

`OIDC_ISSUER` and `VITE_OIDC_AUTHORITY` take the same value: the base URL of your provider, which must serve `{issuer}/.well-known/openid-configuration`. Discovery supplies the JWKS URI and the accepted signing algorithms (RS256 if the document doesn't advertise any). The issuer is also the expected `iss` claim on incoming tokens.

`OIDC_AUDIENCE` is the expected `aud` claim on access tokens, and is always required. `VITE_OIDC_AUDIENCE` is sent as the [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) `resource` parameter on both the authorization and the token request. If your provider always issues JWTs with a fixed audience, set `OIDC_AUDIENCE` to that audience and leave `VITE_OIDC_AUDIENCE` unset.

`OIDC_CLIENT_ID` is the client you registered above. The user service uses it as the expected `aud` of the ID token, because an ID token's audience is the client that requested it.

No other Zerve service needs OIDC configuration.

## Restricting access with a claim rule

`OIDC_REQUIRE_CLAIM` gates access on a claim in the access token. It is enforced at sign-in and on every request afterwards:

```yaml
# my-values.yaml
user:
  workloads:
    common:
      env:
        OIDC_REQUIRE_CLAIM: "groups contains zerve-users"
```

The format is `<claim> <operator> <value>`, where the operator is `eq`, `neq` or `contains`. `contains` matches a member of an array claim or a substring of a string claim; boolean claims compare against `true` and `false`. Only one rule is supported, there is no `and`/`or`.

The claim has to be on the **access** token, not the ID token, so configure your IdP's claim mappers accordingly. A token that doesn't carry the claim at all fails the rule, and requests are rejected with a 401.

Leaving `OIDC_REQUIRE_CLAIM` unset allows any user your IdP authenticates. Note that removing a user's claim blocks their requests but does not delete their Zerve data.

## Optional settings

```yaml
# my-values.yaml
user:
  workloads:
    common:
      env:
        OIDC_TOKEN_EXP_LEEWAY: "10"                                 # optional, seconds
        OIDC_CUSTOM_CLAIMS_PREFIX: "https://app.zerve.io/claims/"   # optional
        OIDC_VERIFY_TOKEN_ISS: "true"                               # optional
        OIDC_VERIFY_ISSUER_TLS: "true"                              # optional
        JWKS_CACHE_TTL: "inf"                                       # optional, seconds
        JWKS_TIMEOUT: "3"                                           # optional, seconds
```

* `OIDC_TOKEN_EXP_LEEWAY` — clock-skew tolerance applied to the `exp`, `nbf` and `iat` claims. Only raise it if you cannot fix the drift with NTP.
* `OIDC_CUSTOM_CLAIMS_PREFIX` — namespace Zerve prepends when reading its own custom claims (`{prefix}/email` and similar). Match it to the namespace your IdP's claim mappers emit.
* `OIDC_VERIFY_TOKEN_ISS` — whether a token's `iss` claim must match `OIDC_ISSUER`. Signature, audience and expiry verification are unaffected by this setting. See [Troubleshooting](#troubleshooting) for the one case where turning it off is the fix.
* `OIDC_VERIFY_ISSUER_TLS` — whether to verify the IdP's certificate when fetching discovery and JWKS. Prefer mounting your CA bundle over disabling this; see [Private CA](#private-ca).
* `JWKS_CACHE_TTL` and `JWKS_TIMEOUT` — lifetime of the in-process JWKS cache and the HTTP timeout when fetching it. The default cache never expires, which is safe: see [Signing keys](#signing-keys).

On the frontend:

```yaml
# my-values.yaml
frontend:
  env:
    VITE_OIDC_SCOPE: "openid email profile offline_access"   # optional
    VITE_POST_LOGOUT_REDIRECT_URI: "https://acme.com"        # optional
```

`VITE_OIDC_SCOPE` replaces the requested scopes wholesale, so keep `openid` and `offline_access` in the list if you override it. `VITE_POST_LOGOUT_REDIRECT_URI` defaults to the application's own origin; set it to send users somewhere else after signing out, and register that URL with your IdP.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.zerve.ai/self-managed/configuration/authentication.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
