---
title: Spring Boot
description: Auto-configured ZevAuth for Spring Boot. Protect controllers with @RequireAuth, take the Auth as an argument, use Spring Security, and serve server-rendered pages.
---

```xml
<dependency>
  <groupId>com.zevauth</groupId>
  <artifactId>zevauth-spring-boot-starter</artifactId>
  <version>0.1.0</version>
</dependency>
```

Spring Boot 4 on Java 17 or later, in Java or Kotlin. The starter builds
on [Java](/sdk/java/); everything on that page is available here too.

```properties
zevauth.environment-id=env_live_…
zevauth.secret-key=${ZEVAUTH_SECRET_KEY}
zevauth.webhook-secret=${ZEVAUTH_WEBHOOK_SECRET}
```

That is the whole setup. Each property switches on what it needs:

| Property | Gives you |
| --- | --- |
| `zevauth.environment-id` | A `ZevAuthVerifier` bean and a filter that reads who every request is from |
| `zevauth.secret-key` | A `ZevAuthClient` bean for the backend API |
| `zevauth.webhook-secret` | A `ZevAuthWebhooks` bean |
| `zevauth.bridge.enabled=true` | The session bridge, for server-rendered pages |

Define any of these beans yourself and the starter uses yours instead.

## Protecting controllers

```java
@RestController
class AccountController {

  @RequireAuth
  @GetMapping("/api/me")
  Map<String, Object> me(Auth auth) {
    return Map.of("userId", auth.getUserId());
  }

  @RequireAuth(requireMfa = true)
  @PostMapping("/api/payouts")
  void payout(Auth auth) { … }

  @GetMapping("/api/public")
  String open(Auth auth) {
    return auth.isSignedIn() ? "hello " + auth.getUserId() : "hello";
  }
}
```

`@RequireAuth` goes on a controller or a method; the method's wins. It
answers with the shared envelope:

| Status | `error.code` | When |
| --- | --- | --- |
| 401 | `unauthorized` | Not signed in |
| 403 | `session_task_pending` | Two-step setup is still pending. Use `@RequireAuth(allowPendingTasks = true)` on the routes that help finish it |
| 403 | `mfa_required` | `requireMfa = true` and the session did not pass a second factor |

Any controller method can take an `Auth` parameter. It is signed out when
the request carried no valid session.

## Spring Security

With Spring Security on the classpath, every signed-in request also gets a
`ZevAuthAuthentication`. Its principal is the user id, and its authorities
are the active organization's role and permissions:

```java
@PreAuthorize("hasAuthority('org:members:manage')")
@PostMapping("/api/members")
void invite(Auth auth) { … }
```

To use ZevAuth in URL rules, add the starter's filter to your chain, and
keep the chain stateless so a session token is never stored in an HTTP
session:

```java
@Bean
SecurityFilterChain security(HttpSecurity http, ZevAuthRequestFilter zevauth) throws Exception {
  return http
      .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .addFilterBefore(zevauth, AuthorizationFilter.class)
      .authorizeHttpRequests(a -> a.requestMatchers("/api/**").authenticated().anyRequest().permitAll())
      .build();
}
```

The filter runs once per request, so it is safe to have it both in the
chain and registered by the starter.

## Server-rendered pages

Thymeleaf, JTE and other server-rendered apps get the
[session bridge](/concepts/session-bridge/) from two properties:

```properties
zevauth.publishable-key=pk_live_…
zevauth.bridge.enabled=true
```

The starter then serves the bridge endpoint at `/zevauth/bridge` and the
handshake page at `/zevauth/handshake`, reads the session cookie on every
request, and sends a page through the handshake when the cookie has
expired. In the browser, pass `sessionBridge: { url: '/zevauth/bridge' }`
to the SDK.

| Property | Default | |
| --- | --- | --- |
| `zevauth.bridge.path` | `/zevauth/bridge` | |
| `zevauth.bridge.handshake-path` | `/zevauth/handshake` | |
| `zevauth.bridge.allowed-origins` | the request's own origin | Origins that may post to the bridge |
| `zevauth.bridge.cookie-domain` | none | Share the cookies with subdomains |
| `zevauth.bridge.secure` | `true` | `false` for plain `http://` local development only |
| `zevauth.bridge.ui-script-url` | jsDelivr | Serve your own copy of `@zevauth/ui` under a strict CSP |

## Webhooks

```java
@PostMapping("/webhooks/zevauth")
ResponseEntity<Void> hook(@RequestBody byte[] body, @RequestHeader("zevauth-signature") String signature)
    throws WebhookException {
  WebhookEvent event = webhooks.constructEvent(body, signature);
  return ResponseEntity.noContent().build();
}
```

Take the body as `byte[]`. A `@RequestBody` object would be parsed and
re-serialised, and the signature, which covers the exact bytes ZevAuth
sent, would correctly fail.

## Backend API

```java
@Service
class Members {
  private final ZevAuthClient zevauth;

  Members(ZevAuthClient zevauth) {
    this.zevauth = zevauth;
  }

  void block(String userId) {
    zevauth.users().setBlocked(userId, true);
  }
}
```

See [Java](/sdk/java/#backend-api) for every resource, iteration, errors
and retries.

## Testing

Replace the verifier with one that uses a local key set and a fixed clock:

```java
@TestConfiguration
class ZevAuthTestKeys {
  @Bean
  ZevAuthVerifier zevAuthVerifier() {
    return ZevAuthVerifier.builder("env_test_…")
        .localKeys("https://api.zevauth.net/v1/env_test_…", jwksJson)
        .clock(Clock.fixed(now, ZoneOffset.UTC))
        .build();
  }
}
```

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