Skip to content

auth ​

JWT authentication module for MinStack. Provides RSA or HMAC token signing and validation, plus composable gin middleware for route protection with role-based access control.

Installation ​

sh
go get github.com/go-minstack/go-minstack/auth

Usage ​

Bootstrap (cmd/main.go) ​

go
package main

import (
    "github.com/go-minstack/go-minstack/auth"
    "github.com/go-minstack/go-minstack/core"
    mgin "github.com/go-minstack/go-minstack/gin"
    "github.com/example/app/internal/users"
)

func main() {
    app := core.New(mgin.Module(), auth.Module())

    app.Provide(users.NewUserController)
    app.Invoke(users.RegisterRoutes)

    app.Run()
}

DTOs (dto/) ​

go
// dto/login_request.dto.go
package dto

type LoginRequestDto struct {
    Email    string `json:"email"    binding:"required,email"`
    Password string `json:"password" binding:"required"`
}
go
// dto/error.dto.go
package dto

type ErrorDto struct {
    Error string `json:"error"`
}

func NewErrorDto(err error) ErrorDto {
    return ErrorDto{Error: err.Error()}
}
go
// dto/message.dto.go
package dto

type MessageDto struct {
    Message string `json:"message"`
}

func NewMessageDto(message string) MessageDto {
    return MessageDto{Message: message}
}
go
// dto/login_response.dto.go
package dto

type LoginResponseDto struct {
    Token     string `json:"token"`
    ExpiresIn int64  `json:"expires_in"`
}

func NewLoginResponseDto(token string, expiresInSeconds int64) LoginResponseDto {
    return LoginResponseDto{Token: token, ExpiresIn: expiresInSeconds}
}
go
// dto/profile.dto.go
package dto

import "github.com/go-minstack/go-minstack/auth"

type ProfileDto struct {
    Subject string   `json:"subject"`
    Name    string   `json:"name"`
    Roles   []string `json:"roles"`
}

func NewProfileDto(claims *auth.Claims) ProfileDto {
    return ProfileDto{Subject: claims.Subject, Name: claims.Name, Roles: claims.Roles}
}

Routes (user.routes.go) ​

Apply Authenticate at the group level once. Use RequireRole per route for fine-grained access control.

go
package users

import (
    "github.com/gin-gonic/gin"
    "github.com/go-minstack/go-minstack/auth"
)

func RegisterRoutes(r *gin.Engine, c *UserController, svc *auth.JwtService) {
    r.POST("/api/users/login", c.login)

    protected := r.Group("/api/users", auth.Authenticate(svc))
    protected.GET("/profile", c.profile)
    protected.GET("/admin", auth.RequireRole("admin"), c.adminOnly)
}

Controller (user.controller.go) ​

go
package users

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/go-minstack/go-minstack/auth"
    "github.com/example/app/internal/users/dto"
)

const tokenExpiry = time.Hour

type UserController struct {
    svc *auth.JwtService
}

func NewUserController(svc *auth.JwtService) *UserController {
    return &UserController{svc: svc}
}

func (c *UserController) login(ctx *gin.Context) {
    var input dto.LoginRequestDto
    if err := ctx.ShouldBindJSON(&input); err != nil {
        ctx.JSON(http.StatusBadRequest, dto.NewErrorDto(err))
        return
    }

    // In a real app: look up the user and verify the password.
    token, err := c.svc.Sign(auth.Claims{
        Subject: "user-123",
        Name:    "Alice",
        Roles:   []string{"user"},
    }, tokenExpiry)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, dto.NewErrorDto(err))
        return
    }

    ctx.JSON(http.StatusOK, dto.NewLoginResponseDto(token, int64(tokenExpiry.Seconds())))
}

func (c *UserController) profile(ctx *gin.Context) {
    claims, _ := auth.ClaimsFromContext(ctx)
    ctx.JSON(http.StatusOK, dto.NewProfileDto(claims))
}

func (c *UserController) adminOnly(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, dto.NewMessageDto("welcome, admin"))
}

API ​

auth.Module() fx.Option ​

Registers *JwtService into the DI container. Keys are loaded from environment variables during the FX OnStart lifecycle hook.

auth.Authenticate(svc *JwtService) gin.HandlerFunc ​

Validates the Authorization: Bearer <token> header and stores the claims in the gin context. Returns 401 Unauthorized on failure.

auth.RequireRole(roles ...string) gin.HandlerFunc ​

Checks that the authenticated user has at least one of the given roles. Must run after Authenticate. Returns 403 Forbidden on failure.

auth.ClaimsFromContext(c *gin.Context) (*Claims, bool) ​

Returns the claims set by Authenticate, or false if not present.

(*JwtService).Sign(claims Claims, expiry time.Duration) (string, error) ​

Creates a signed JWT. Requires MINSTACK_JWT_PRIVATE_KEY (RSA) or MINSTACK_JWT_SECRET (HMAC).

(*JwtService).Validate(token string) (*Claims, error) ​

Parses and validates a JWT. Requires any verification key.

Environment variables ​

VariablePurpose
MINSTACK_JWT_PRIVATE_KEYPath to RSA private key PEM — enables Sign + Validate
MINSTACK_JWKS_URLJWKS endpoint — fetches RSA public key on start (verify-only)
MINSTACK_JWT_PUBLIC_KEYPath to RSA public key PEM — fallback after JWKS, also used as cache
MINSTACK_JWT_SECRETHMAC secret string — ⚠ not recommended for production

Key loading priority:

  1. MINSTACK_JWT_PRIVATE_KEY — RSA private key (public key derived automatically)
  2. MINSTACK_JWKS_URL — fetch RSA public key from JWKS endpoint
  3. MINSTACK_JWT_PUBLIC_KEY — RSA public key from file
  4. MINSTACK_JWT_SECRET — HMAC HS256

Notes ​

  • Use RSA keys whenever you sign tokens — HMAC secrets cannot be rotated safely across services
  • Apply Authenticate once on a route group, RequireRole per route as needed
  • When MINSTACK_JWKS_URL is set and MINSTACK_JWT_PUBLIC_KEY is configured, the fetched key is cached to the file for faster restarts