Overview
Authentication in ezz is a single interface plus a per-route rule. You register one provider on the app; each route carries a rule saying what it needs.
type AuthProvider interface {
Authenticate(r *http.Request) (any, error)
}
The provider turns a request into a user value of any type. The rule decides whether that user may proceed.
Turn authentication on
jwt := auth.NewHMACProvider(os.Getenv("JWT_SECRET"), "my-service")
app := ezz.New().Auth(auth.NewJWTAuthProvider(jwt))
From that point, every route requires a valid token, because the app's default rule is ezz.Authenticated(). Public endpoints are explicit:
app.GET("/health", health).Public()
app.POST("/login", login).Public()
app.GET("/me", profile) // needs a token
app.GET("/admin", panel).Auth(ezz.Role("admin")) // needs the admin role
app.Auth(...), no rule is enforced, including ezz.Role(...). The check is skipped entirely when there is no provider, so a service with route rules but no provider serves everything to everyone. Register the provider in the same place you build the app.To invert the default and opt into protection per route:
app := ezz.New().Auth(provider).DefaultAuth(nil)
Follow a request through
For a route that is not public:
Authenticate(r)runs. An error means401and the request stops.- The route's rule gets the returned user.
Checkreturning nil lets the request continue. - If
Checkfails, ezz looks at the user for a resolver error first. If there is one, the response is500; otherwise it is403, or the status carried by the returned*ezz.HTTPError. - The user is attached to the request context, so
ctx.User()andezz.UserFromContextcan reach it. - The middleware chain and your handler run.
Public routes skip all of it, so Authenticate is never called and ctx.User() is nil.
401 and 403 responses are not passed through logging, recovery, or CORS middleware. See the ordering notes in Middleware.What the JWT provider gives you
auth.NewJWTAuthProvider(jwtProvider) adapts any auth.JWTProvider to the app interface. It:
- pulls the token from
Authorization: Bearer <token>, then thetokenquery parameter, then anaccess_tokencookie; - validates it with the underlying provider;
- logs a failure through the audit logger, when one is configured;
- returns an
*auth.AuthorizedUser, which exposes the claims and answers role and permission checks.
provider := auth.NewJWTAuthProvider(jwt).
WithResolver(myResolver{db: db}). // roles and permissions from your store
WithAuditLogger(auth.NewSlogAuditLogger(nil))
app := ezz.New().Auth(provider)
Reach the user in a handler
app.GET("/me", ezz.HFunc(func(ctx *ezz.Context) (*Profile, error) {
user, ok := ctx.User().(auth.User)
if !ok {
return nil, ezz.Unauthorized("no user on request")
}
return loadProfile(ctx, user.ID())
}))
auth.User is the interface the built-in user values satisfy:
type User interface {
ID() string
GetClaims() *Claims
GetAttribute(key string) (any, bool)
}
For role checks inside a handler, assert the narrower interface instead of a concrete type:
if rc, ok := ctx.User().(interface{ HasRole(string) bool }); ok && rc.HasRole("admin") {
// admin-only branch
}
Pick your approach
| Situation | Use |
|---|---|
| You issue your own HS256 tokens | auth.NewHMACProvider |
| GoTrue or Supabase issues tokens | auth.NewGoTrueProvider or the GoTrue proxy |
| Roles and permissions live in your database | An authorization resolver |
| API keys, sessions, or mTLS | Implement ezz.AuthProvider yourself |
Next steps
- Auth rules for the rule set and how precedence works.
- Roles and permissions for authorization backed by your own data.
- Audit logging for a record of auth events.