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());
}
| 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
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.
| 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
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