> For the complete documentation index, see [llms.txt](https://docs.empe.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.empe.io/develop/verifier/authentication.md).

# Authentication

This page explains how to authenticate calls to the Verifier API. Every non-public endpoint is protected by an **OIDC JWT Bearer token**: you obtain a token from your identity provider, send it in the `Authorization` header, and the Verifier validates it against a JWKS endpoint before serving the request. The only credential the API accepts is a signed, unexpired JWT — there is no client secret or per-verifier shared secret to manage.

***

## Overview

* The Verifier validates an incoming token's signature against the JSON Web Key Set (JWKS) published by your OIDC provider (for example Keycloak, Auth0, or any compliant issuer). The JWKS location is configured on the service through the `OIDC_JWKS_URL` environment variable.
* Authentication is **stateless**: the service does not maintain login sessions for the API. Each request must carry its own token, and the token's claims identify the caller.
* Most Verifier endpoints — creating authorization requests, reading verification sessions, and verifying a single credential — require **only a valid token, with no specific role**. The `admin` role is enforced separately on the agent-management routes (`/agent/*`) used to register a verifier and on cloud-wallet routes (`/holders/*`).
* For local development, see [Development mode](#development-mode).

***

## Sending the Authorization header

Attach the token to every protected request as a Bearer credential:

```
Authorization: Bearer <your-jwt>
```

A typical call from a backend looks like this:

```typescript
const VERIFIER_BASE_URL = 'https://your-verifier.evdi.app';

async function verifyCredential(token: string, credential: string) {
  const res = await fetch(`${VERIFIER_BASE_URL}/credentials/verify`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ credential }),
  });

  return res.json();
}
```

The same header applies to the authorization-request and verification-session endpoints. If the header is missing, malformed, or carries an expired token, the request is rejected with `401 Unauthorized` before any business logic runs.

***

## Token validation

When a request arrives with a Bearer token, the Verifier performs the following checks:

* **Signature** — the token is verified with the `RS256` algorithm. The service reads the `kid` from the JWT header and selects the matching public key from the JWKS published at `OIDC_JWKS_URL`. A token whose header has no `kid`, or whose `kid` does not match any published key, is rejected.
* **Expiration** — the `exp` claim is enforced; expired tokens are rejected.
* **Key caching** — fetched signing keys are cached for roughly 10 minutes, and JWKS requests are rate-limited to 10 per minute, so the service does not hit your identity provider on every call.

A request that fails any of these checks receives a `401 Unauthorized` response in the standard error shape:

```json
{
  "statusCode": 401,
  "message": "Unauthorized",
  "path": "/credentials/verify",
  "timestamp": "2026-06-22T12:34:56.789Z"
}
```

Because validation relies on the JWKS, your OIDC provider can rotate its signing keys without any change to the Verifier — the new key is picked up automatically once the cache expires.

***

## Roles and access control

The Verifier reads roles from the token's claims and applies them per route group:

* **Verifier endpoints** require **only a valid token**. The authorization-request routes (`/verifiers/{verifierId}/authorization-requests`), the verification-session routes (`/verification-sessions/{sessionId}`), and the direct verification route (`/credentials/verify`) carry no role requirement — any caller with a valid JWT may use them.
* **Agent-management endpoints** under `/agent/*` — including `POST /agent/verifier`, which you call once to register a verifier — additionally require the **`admin`** role. The role is read from the token's `realm_access.roles` array (client roles in `resource_access.<client>.roles` are also accepted). A valid token without the `admin` role is rejected with `403 Forbidden`.

In practice this means you need an `admin`-roled token to set up a verifier, but the day-to-day verification calls only need an authenticated token. Restrict who can obtain `admin` tokens in your identity provider, and scope the tokens your application uses to the minimum it needs.

A typical token payload that satisfies the `admin` requirement looks like this:

```json
{
  "sub": "service-account-verifier",
  "exp": 1782345296,
  "realm_access": {
    "roles": ["admin"]
  }
}
```

***

## Obtaining a token

The Verifier does not issue tokens; you request them from your OIDC provider. For server-to-server integrations the **client-credentials grant** is the usual choice — it yields a token tied to a service account rather than an interactive user:

```typescript
async function getAccessToken(): Promise<string> {
  const res = await fetch(
    'https://auth.example.com/realms/empe/protocol/openid-connect/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: 'your-client-id',
        client_secret: 'your-oidc-client-credentials-secret',
      }),
    },
  );

  const data = await res.json();
  return data.access_token;
}
```

The `client_secret` above is the credential **your application** presents to the **OIDC provider** to obtain a token — it is never sent to the Verifier, which only ever sees the resulting JWT in the `Authorization` header.

> For a **One-Click deployment**, obtain the token from your realm's Keycloak token endpoint — `https://<keycloak-host>/realms/<realm>/protocol/openid-connect/token` — using the credentials shown when you created the deployment; see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment.md). If you run your **own identity provider**, use its token endpoint and a client you control (as shown above).

***

## Development mode

To make local testing easier, the service supports a development bypass. Setting `AUTH_DISABLED=true` skips token validation entirely and treats every request as an authenticated `admin` user:

```bash
AUTH_DISABLED=true
```

With this flag set, you can omit the `Authorization` header completely and call any endpoint, including the `/agent/*` routes. When authentication is disabled, `OIDC_JWKS_URL` is not required. Because `AUTH_DISABLED=true` removes all access control, use it only for local development and never in any deployed or shared environment.

***

## Best practices

* **Always use HTTPS.** Tokens are bearer credentials — anyone who captures one can act as you until it expires — and OpenID4VP requires HTTPS in production regardless.
* **Keep tokens server-side, short-lived, and cached.** Obtain and store tokens in your backend (never in browser or mobile clients), and refresh them automatically shortly before `exp` rather than on every call, so a leaked token has a limited window of usefulness.
* **Scope roles tightly.** Reserve the `admin` role for the one-time verifier setup, and use lower-privilege tokens for ongoing verification traffic.
* **Account for rate limiting.** The service applies a global limit of 60 requests per 60 seconds; design your retry and refresh logic with this in mind.

***

## Related pages

* [Verifier Client Configuration](/develop/verifier/client-configuration.md) — end-to-end server-side integration, including registering a verifier and creating authorization requests with the Bearer token.
* [Security Considerations](/develop/verifier/security-considerations.md) — the full security model around transport, request binding, and credential integrity.


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.empe.io/develop/verifier/authentication.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
