ZevAuth
Sign up

Your backend

Java

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

<dependency>
  <groupId>com.zevauth</groupId>
  <artifactId>zevauth-java</artifactId>
  <version>0.1.0</version>
</dependency>
// Gradle
implementation("com.zevauth:zevauth-java:0.1.0")

Java 17 or later; works from Kotlin too. Using Spring Boot? Read Spring Boot instead. It builds on everything here and wires it in for you.

Who is signed in

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

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

try {
  Auth auth = verifier.requireAuth(header, RequireOptions.mfaRequired());
} catch (AuthRefusal refusal) {
  response.setStatus(refusal.getStatus());
  response.getWriter().write(refusal.toJson());
}
Statuserror.codeWhen
401unauthorizedNot signed in
403session_task_pendingThe person still has to set up two-step verification. Use RequireOptions.pendingTasksAllowed() on the routes that help them do it
403mfa_requiredmfaRequired() 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

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 keeps the browser’s access token in your own HttpOnly cookie:

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.

OptionDefault
allowedOriginsthe request’s own originOrigins that may post to the bridge
cookieDomainnoneShare the cookies with subdomains
securetruefalse for plain http:// local development only
handshakePath/zevauth/handshake

Backend API

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:

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

Errors and retries

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

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 explains what that guarantees.

Source and issues: github.com/zevop/zevauth-java.

Updated at, Sunday, November 1, 2026