Skip to main content
Version: v0.11.0

Middleware

Middleware in Okapi allows you to intercept and process HTTP requests before they reach your route handlers. This is useful for authentication, logging, request validation, and more.

Built-in Middleware​

Basic Authentication​

auth := okapi.BasicAuth{
Username: "admin",
Password: "password",
Realm: "Restricted",
ContextKey: "user", // where to store the username (default: "username")
}

// Global middleware
o.Use(auth.Middleware)

// Route-specific middleware
o.Get("/admin", adminHandler).Use(auth.Middleware)

CORS Middleware​

cors := okapi.Cors{
AllowedOrigins: []string{"http://localhost:8080", "https://example.com"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
}

o := okapi.New(okapi.WithCors(cors))

JWT Middleware​

Okapi includes powerful JWT middleware to secure your routes with JSON Web Tokens.

Features​

  • HS256 symmetric signing via SigningSecret
  • RS256 and other asymmetric algorithms via RSAKey
  • Remote JWKS discovery via JwksUrl (e.g., OIDC or Auth0)
  • Local JWKS via JwksFile
  • Claims validation with ClaimsExpression or ValidateClaims
  • OpenAPI integration with .WithBearerAuth()
  • Selective claim forwarding using ForwardClaims

Basic HS256 Authentication​

jwtAuth := okapi.JWTAuth{
SigningSecret: []byte("supersecret"), // Shared secret for HS256
TokenLookup: "header:Authorization", // Token source
ContextKey: "user", // Key for storing claims in context
}

Claim Validation Defaults​

Audience and Issuer are optional: when either is left empty the corresponding claim is not checked, and a token is accepted whether or not it carries one. Set them to require a specific value.

An exp (expiry) claim is required by default. A signed token without one never expires and the middleware has no revocation path, so any such token that leaks stays valid until the signing key is rotated. If your issuer deliberately mints non-expiring tokens, opt in explicitly:

jwtAuth := okapi.JWTAuth{
SigningSecret: []byte("supersecret"),
AllowMissingExpiry: true, // accept tokens with no "exp" claim
}

Remote JWKS (OIDC, Auth0)​

jwtAuth := okapi.JWTAuth{
JwksUrl: "https://example.com/.well-known/jwks.json",
TokenLookup: "header:Authorization",
ContextKey: "user",
}

Claims Expression​

Use ClaimsExpression to validate claims using simple expressions:

Supported Functions​

  • Equals(field, value)
  • Prefix(field, prefix)
  • Contains(field, val1, val2, ...) — the claim equals one of the values, or, for an array claim, has an element equal to one of them
  • OneOf(field, val1, val2, ...)
  • Substring(field, val1, val2, ...) — the claim contains one of the values as a substring

Substring is a deliberately weak test and is not suitable for authorization decisions: wherever any part of a role, scope or tenant string is user-influenced, an attacker can make it contain the value you are checking for. Prefer Equals, OneOf or Contains.

Logical Operators​

  • ! — NOT
  • && — AND
  • || — OR
jwtAuth := okapi.JWTAuth{
SigningSecret: []byte("supersecret"),
ClaimsExpression: "Equals(`email_verified`, `true`) && Equals(`user.role`, `admin`)",
TokenLookup: "header:Authorization",
ContextKey: "user",
}

Forwarding Claims to Context​

jwtAuth.ForwardClaims = map[string]string{
"email": "user.email",
"role": "user.role",
"name": "user.name",
}

Access claims in your handler:

func whoAmIHandler(c *okapi.Context) error {
email := c.GetString("email")
if email == "" {
return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated"))
}

return c.JSON(http.StatusOK, okapi.M{
"email": email,
"role": c.GetString("role"),
"name": c.GetString("name"),
})
}

Custom Claim Validation​

jwtAuth.ValidateClaims = func(c *Context, claims jwt.Claims) error {
mapClaims, ok := claims.(jwt.MapClaims)
if !ok {
return errors.New("invalid claims type")
}

if emailVerified, _ := mapClaims["email_verified"].(bool); !emailVerified {
return errors.New("email not verified")
}

if role, _ := mapClaims["role"].(string); role != "admin" {
return errors.New("unauthorized role")
}

return nil
}

Custom Error Handling​

auth := okapi.JWTAuth{
Audience: "okapi.example.com",
SigningSecret: SigningSecret,
OnUnauthorized: func(c *okapi.Context) error {
return c.ErrorUnauthorized("Unauthorized")
},
}

Protecting Routes​

// Apply middleware globally
o.Use(jwtAuth.Middleware)

// Protect specific group
admin := o.Group("/admin", jwtAuth.Middleware).
WithBearerAuth() // Adds Bearer auth to OpenAPI docs

admin.Get("/users", adminGetUsersHandler)

// Route-specific middleware
o.Get("/protected", protectedHandler).Use(jwtAuth.Middleware)

Custom Middleware​

Create your own middleware functions. Call c.Next() to pass control to the next middleware or handler:

func customMiddleware(c *okapi.Context) error {
start := time.Now()
err := c.Next()
log.Printf("Request took %v", time.Since(start))
return err
}

o.Use(customMiddleware)

Standard Library Middleware​

You can also use standard http.Handler middleware:

o.UseMiddleware(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.Info("Hello from standard HTTP middleware")
handler.ServeHTTP(w, r)
})
})

Middleware Chaining​

Apply multiple middleware to a route or group:

o.Get("/admin",
adminHandler,
).Use(
authMiddleware,
loggingMiddleware,
rateLimitMiddleware,
)