Docs Authentication and JWT

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.