---
title: Go
description: Verify sessions, manage users and organizations, receive webhooks and serve server-rendered pages from any Go server, with the standard library only.
---

```bash
go get github.com/zevop/zevauth-go
```

Go 1.22 or later. No dependencies beyond the standard library: it works
with `net/http` and with any router built on it (chi, gorilla/mux, the
standard `ServeMux`).

## Who is signed in

```go
import zevauth "github.com/zevop/zevauth-go"

verifier, err := zevauth.NewVerifier(zevauth.VerifierConfig{
	EnvironmentID: os.Getenv("ZEVAUTH_ENVIRONMENT_ID"), // env_live_…
})

auth := verifier.GetAuth(r)

if auth.IsSignedIn {
	auth.UserID           // "user_…"
	auth.SessionID        // "sess_…"
	auth.OrganizationID   // "" when not acting in an organization
	auth.OrganizationRole // "org:admin"
	auth.Has(zevauth.HasCheck{Permission: "org:members:manage"})
	auth.Claims.Email     // every verified claim
}
```

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

### Middleware

```go
mux := http.NewServeMux()
mux.HandleFunc("GET /api/me", func(w http.ResponseWriter, r *http.Request) {
	auth := zevauth.AuthFromContext(r.Context())
	fmt.Fprintf(w, "hello %s", auth.UserID)
})

// Require: refuse requests that are not signed in.
http.ListenAndServe(":8080", zevauth.Middleware(verifier, zevauth.MiddlewareOptions{Require: true})(mux))
```

Without `Require`, every request passes through with its `Auth` in the
context, and each handler decides.

### Refusing a request

```go
auth, refusal := verifier.RequireAuth(r, zevauth.RequireOptions{RequireMFA: true})
if refusal != nil {
	refusal.ServeHTTP(w, r)
	return
}
```

| Status | `error.code` | When |
| --- | --- | --- |
| 401 | `unauthorized` | Not signed in |
| 403 | `session_task_pending` | The person still has to set up two-step verification. Pass `AllowPendingTasks: true` on the routes that help them do it |
| 403 | `mfa_required` | `RequireMFA` 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

```go
claims, err := verifier.Verify(ctx, token)

var verr *zevauth.VerifyError
if errors.As(err, &verr) {
	verr.Code // "token_expired", "invalid_audience", …
}
errors.Is(err, zevauth.ErrTokenExpired) // also works
```

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, set
`Issuer` and `JWKS` to use a local key set instead of fetching.

## 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:

```go
bridge := &zevauth.Bridge{Verifier: verifier}

mux.Handle("POST /zevauth/bridge", bridge)
mux.Handle("GET "+zevauth.DefaultHandshakePath, zevauth.HandshakeHandler(zevauth.HandshakeOptions{
	PublishableKey: os.Getenv("ZEVAUTH_PUBLISHABLE_KEY"),
	BridgePath:     "/zevauth/bridge",
}))

// Reads the header, then the cookie, and runs the handshake when needed.
http.ListenAndServe(":8080", bridge.Middleware(zevauth.MiddlewareOptions{})(mux))
```

In the browser, pass `sessionBridge: { url: '/zevauth/bridge' }` to the
SDK. Your server never refreshes a session. When the cookie expires, the
middleware sends the page through a short handshake that lets the browser
refresh it and come straight back.

| Option | Default | |
| --- | --- | --- |
| `AllowedOrigins` | the request's own origin | Origins that may post to the bridge |
| `CookieDomain` | none | Share the cookies with subdomains |
| `Insecure` | `false` | Plain `http://` local development only |
| `HandshakePath` | `/zevauth/handshake` | |

## Backend API

```go
client, err := zevauth.NewClient(os.Getenv("ZEVAUTH_SECRET_KEY"), zevauth.ClientOptions{})

user, err := client.Users.Get(ctx, "user_…")
_, err = client.Users.SetBlocked(ctx, "user_…", true)
err = client.Users.ResetMFA(ctx, "user_…", zevauth.ResetMFAOptions{RevokeSessions: true})

org, err := client.Organizations.Create(ctx, zevauth.CreateOrganizationParams{Name: "Acme", CreatedBy: "user_…"})
_, err = client.Organizations.Update(ctx, org.ID, map[string]any{"requireMfa": true})
```

Every list has an iterator that fetches pages as you go:

```go
it := client.Users.Iter(ctx, zevauth.ListUsersParams{Metadata: map[string]string{"plan": "pro"}})
for it.Next() {
	fmt.Println(it.User().ID)
}
if err := it.Err(); err != nil { … }
```

### Errors and retries

```go
var apiErr *zevauth.APIError
if errors.As(err, &apiErr) {
	apiErr.Code      // "not_found", "rate_limited", "organization_permission_required", …
	apiErr.Status    // 0 for a network failure
	apiErr.RequestID // 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. Pass a `context.Context` to cancel.

## Webhooks

```go
mux.HandleFunc("POST /webhooks/zevauth", func(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	event, err := zevauth.ConstructWebhookEvent(body, r.Header.Get("zevauth-signature"), os.Getenv("ZEVAUTH_WEBHOOK_SECRET"), zevauth.WebhookOptions{})
	if err != nil {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}
	switch event.Type {
	case "user.created":
		// json.Unmarshal(event.Data, &user)
	}
	w.WriteHeader(http.StatusNoContent)
})
```

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 Java, and passes the same conformance tests.
[Server SDKs](/concepts/server-sdks/) explains what that guarantees.

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