ZevAuth
Sign up

Your backend

Go

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

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

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

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

auth, refusal := verifier.RequireAuth(r, zevauth.RequireOptions{RequireMFA: true})
if refusal != nil {
	refusal.ServeHTTP(w, r)
	return
}
Statuserror.codeWhen
401unauthorizedNot signed in
403session_task_pendingThe person still has to set up two-step verification. Pass AllowPendingTasks: true on the routes that help them do it
403mfa_requiredRequireMFA 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

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

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.

OptionDefault
AllowedOriginsthe request’s own originOrigins that may post to the bridge
CookieDomainnoneShare the cookies with subdomains
InsecurefalsePlain http:// local development only
HandshakePath/zevauth/handshake

Backend API

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:

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

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

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

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

Updated at, Sunday, November 1, 2026