---
title: Java
description: Verify sessions, manage users and organizations, receive webhooks and serve server-rendered pages from any JVM server, in Java or Kotlin.
---

```xml
<dependency>
  <groupId>com.zevauth</groupId>
  <artifactId>zevauth-java</artifactId>
  <version>0.1.0</version>
</dependency>
```

```kotlin
// Gradle
implementation("com.zevauth:zevauth-java:0.1.0")
```

Java 17 or later; works from Kotlin too. Using Spring Boot? Read
[Spring Boot](/sdk/spring/) instead. It builds on everything here and
wires it in for you.

## Who is signed in

```java
import com.zevauth.Auth;
import com.zevauth.ZevAuthVerifier;

ZevAuthVerifier verifier = ZevAuthVerifier.builder(System.getenv("ZEVAUTH_ENVIRONMENT_ID")).build();

Auth auth = verifier.getAuth(request.getHeader("Authorization"));

if (auth.isSignedIn()) {
  auth.getUserId();           // "user_…"
  auth.getSessionId();        // "sess_…"
  auth.getOrganizationId();   // null when not acting in an organization
  auth.getOrganizationRole(); // "org:admin"
  auth.hasPermission("org:members:manage");
  auth.getClaims().getEmail(); // every verified claim
}
```

`getAuth` reads the `Authorization: Bearer` header that the browser SDK
sends with `getToken()`. It never throws for a missing, expired or forged
token: those all mean nobody is signed in. Share one `ZevAuthVerifier` per
environment; it is thread-safe.

### Jakarta Servlet

```java
ZevAuthFilter filter = new ZevAuthFilter(verifier);              // every request passes, with its Auth
ZevAuthFilter guard = new ZevAuthFilter(verifier, null, RequireOptions.DEFAULT); // refuses the signed out

// In a servlet:
Auth auth = ServletSupport.getAuth(request);
```

### Refusing a request

```java
try {
  Auth auth = verifier.requireAuth(header, RequireOptions.mfaRequired());
} catch (AuthRefusal refusal) {
  response.setStatus(refusal.getStatus());
  response.getWriter().write(refusal.toJson());
}
```

| Status | `error.code` | When |
| --- | --- | --- |
| 401 | `unauthorized` | Not signed in |
| 403 | `session_task_pending` | The person still has to set up two-step verification. Use `RequireOptions.pendingTasksAllowed()` on the routes that help them do it |
| 403 | `mfa_required` | `mfaRequired()` and the session did not pass a second factor |

The body is `{ "error": { "code": "…", "message": "…" } }`, the same
envelope the ZevAuth API uses.

### Verifying a token yourself

```java
try {
  ZevAuthClaims claims = verifier.verify(token);
} catch (VerifyException e) {
  e.getCode(); // "token_expired", "invalid_audience", …
}
```

Only RS256 is accepted, whatever the token's header claims. The issuer
comes from your environment's discovery document, the audience must be
your environment id, and 5 seconds of clock skew is allowed. Keys are
cached for up to 10 minutes; a token signed by a new key triggers one
refetch, at most every 30 seconds. For tests and air-gapped servers, use
`.localKeys(issuer, jwksJson)` instead of fetching, and `.clock(...)` to
fix the time.

## Server-rendered pages

The session lives in the browser, so a page request carries no bearer
token. The [session bridge](/concepts/session-bridge/) keeps the browser's
access token in your own HttpOnly cookie:

```java
SessionBridge bridge = SessionBridge.builder(verifier).build();

context.addServlet("zevauthBridge", new SessionBridgeServlet(bridge)).addMapping("/zevauth/bridge");
context.addServlet("zevauthHandshake", new HandshakeServlet(
    new SessionBridge.HandshakeOptions(System.getenv("ZEVAUTH_PUBLISHABLE_KEY"), "/zevauth/bridge")))
    .addMapping(SessionBridge.DEFAULT_HANDSHAKE_PATH);
context.addFilter("zevauth", new ZevAuthFilter(verifier, bridge, null)).addMappingForUrlPatterns(null, false, "/*");
```

In the browser, pass `sessionBridge: { url: '/zevauth/bridge' }` to the
SDK. Your server never refreshes a session. When the cookie expires, the
filter sends the page through a short handshake that lets the browser
refresh it and come straight back. `SessionBridge` itself has no servlet
dependency, so other frameworks can call `handle`, `authenticate` and
`handshakePage` directly.

| Option | Default | |
| --- | --- | --- |
| `allowedOrigins` | the request's own origin | Origins that may post to the bridge |
| `cookieDomain` | none | Share the cookies with subdomains |
| `secure` | `true` | `false` for plain `http://` local development only |
| `handshakePath` | `/zevauth/handshake` | |

## Backend API

```java
ZevAuthClient zevauth = ZevAuthClient.builder(System.getenv("ZEVAUTH_SECRET_KEY")).build();

Models.User user = zevauth.users().get("user_…");
zevauth.users().setBlocked("user_…", true);
zevauth.users().resetMfa("user_…", true);

Models.Organization org = zevauth.organizations().create(Map.of("name", "Acme", "createdBy", "user_…"));
zevauth.organizations().update(org.id(), Map.of("requireMfa", true));
```

`iterate` fetches pages as you loop:

```java
for (Models.User u : zevauth.users().iterate(Map.of("metadata.plan", "pro"))) {
  System.out.println(u.id());
}
```

### Errors and retries

```java
try {
  zevauth.users().get("user_…");
} catch (ApiException e) {
  e.getCode();      // "not_found", "rate_limited", "organization_permission_required", …
  e.getStatus();    // 0 for a network failure
  e.getRequestId(); // quote this to ZevAuth support
}
```

429, 502, 503, 504 and network failures are retried twice, honouring
`Retry-After`. A 500 is never retried: the request may have run, and
retrying is how one new user becomes two. Each attempt times out after 15
seconds. The client uses `java.net.http`; pass `.transport(...)` to route
requests through your own HTTP client.

## Webhooks

```java
byte[] body = request.getInputStream().readAllBytes();
WebhookEvent event = Webhooks.constructEvent(body, request.getHeader("zevauth-signature"), System.getenv("ZEVAUTH_WEBHOOK_SECRET"));

switch (event.type()) {
  case "user.created" -> { /* event.data() */ }
}
```

Verify the **raw** body. The signature covers the exact bytes ZevAuth sent,
so a body that was decoded and re-encoded fails, correctly. Deliveries can
arrive more than once: use the `zevauth-delivery-id` header or the event
`id()` to handle each one once.

## The same in every language

This library implements the same contract as the ZevAuth libraries for
Node.js, PHP, Python, Ruby and Go, and passes the same conformance tests.
[Server SDKs](/concepts/server-sdks/) explains what that guarantees.

Source and issues: [github.com/zevop/zevauth-java](https://github.com/zevop/zevauth-java).
