Authentication and JWT
Authentication is a cross-cutting concern.
If implemented carelessly, it quickly spreads through handlers, services, and domain code, creating tight coupling and long-term maintenance pain.
Plumego’s architecture makes a clear recommendation:
Authentication belongs in middleware. Identity belongs in context. Authorization belongs in application logic.
This guide explains how to implement JWT-based authentication following that principle.
Design Goals
Before touching code, define the goals clearly:
- Authentication must be centralized
- Identity must be request-scoped
- Core logic must remain framework-agnostic
- Authorization rules must be explicit
- Security behavior must be predictable and testable
JWT is a mechanism — not the architecture.
Authentication vs Authorization
A critical distinction:
- Authentication: Who is the caller?
- Authorization: What is the caller allowed to do?
In Plumego:
- Authentication → Middleware
- Authorization → Handlers / Usecases
- Domain → Enforces invariants, not access control
Blurring these roles leads to fragile systems.
Where JWT Fits
JWT is typically used to:
- Authenticate requests statelessly
- Convey identity and claims
- Avoid server-side session storage
JWT should not be used to:
- Encode complex business rules
- Replace authorization logic
- Carry large or sensitive payloads
JWT is an identity carrier, not a domain model.
JWT Authentication Flow (High Level)
A typical request flow looks like this:
Incoming Request
→ Trace ID Middleware
→ Logging Middleware
→ JWT Authentication Middleware
→ Handler
→ Usecase
→ Domain
If authentication fails, the request stops at middleware.
Step 1: Define the Identity Model
Define a simple identity model for your application.
Example:
type Identity struct {
UserID string
Roles []string
}
This type belongs to your application layer, not to Plumego.
It represents who the caller is, not what they can do.
Step 2: JWT Authentication Middleware
JWT validation belongs entirely in middleware.
Conceptual example:
func JWTAuthMiddleware(verifier JWTVerifier) plumego.Middleware {
return func(ctx *plumego.Context, next plumego.NextFunc) {
token := extractBearerToken(ctx.Request())
if token == "" {
ctx.JSON(http.StatusUnauthorized, errorResponse("missing token"))
return
}
claims, err := verifier.Verify(token)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse("invalid token"))
return
}
identity := Identity{
UserID: claims.Subject,
Roles: claims.Roles,
}
ctx.Set("identity", identity)
next()
}
}
Key points:
- JWT parsing and validation are centralized
- Identity is extracted once
- Identity is stored in context
- Handlers never parse tokens directly
Step 3: Accessing Identity in Handlers
Handlers can retrieve identity from context:
identity, ok := ctx.Get("identity").(Identity)
if !ok {
ctx.JSON(http.StatusUnauthorized, errorResponse("unauthenticated"))
return
}
Handlers should:
- Treat identity as read-only
- Pass identity data inward explicitly
- Avoid coupling usecases to JWT or HTTP
Step 4: Authorization at the Right Layer
Authorization decisions depend on what the system does, not on HTTP mechanics.
Two common patterns:
Handler-level authorization
Simple checks close to the boundary:
if !hasRole(identity, "admin") {
ctx.JSON(http.StatusForbidden, errorResponse("forbidden"))
return
}
Usecase-level authorization
More complex, rule-based checks:
err := usecase.Execute(identity.UserID, input)
if errors.Is(err, ErrForbidden) {
ctx.JSON(http.StatusForbidden, errorResponse(err))
return
}
Both are valid when used intentionally.
What Domain Code Must Not Know
Domain code must not know:
- JWT exists
- HTTP exists
- Context exists
- Roles exist as strings from tokens
Domain rules should operate on explicit inputs, not security artifacts.
Token Refresh and Expiration
JWT expiration handling belongs in authentication middleware.
Typical strategies:
- Reject expired tokens
- Allow refresh via a dedicated endpoint
- Issue short-lived access tokens
Do not scatter expiration logic across handlers.
Avoiding Common JWT Mistakes
Parsing JWT in Handlers
This duplicates logic and breaks boundaries.
JWT parsing must be centralized.
Putting Authorization Logic in Middleware Only
Middleware can decide whether a request proceeds.
It should not encode complex business authorization rules.
Overloading JWT Claims
JWTs should remain small and stable.
Do not encode dynamic permissions or business state.
Testing Authentication Logic
JWT authentication middleware should be tested separately:
- Valid token
- Invalid token
- Expired token
- Missing token
Handlers and usecases can be tested with mocked identity, without JWT.
This separation dramatically improves testability.
Summary
In Plumego:
- JWT authentication is explicit middleware
- Identity is request-scoped context data
- Authorization is explicit and layered
- Core logic remains framework-agnostic
- Security behavior is predictable
Authentication is infrastructure.
Authorization is application logic.
Business rules remain in the domain.
Next
With authentication in place, the next operational concern is:
→ Graceful Shutdown
This explains how to stop Plumego services safely during deploys and restarts.