modkit uses standard Go middleware for authentication—no framework-specific guards or decorators. This guide covers common authentication patterns.
In NestJS, guards are decorators that protect routes. In modkit (and idiomatic Go), authentication is middleware that:
- Extracts credentials from the request
- Validates the credentials
- Attaches user info to the request context
- Allows or denies access
type contextKey string
const UserContextKey contextKey = "user"
func AuthMiddleware(validateToken func(string) (*User, error)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
user, err := validateToken(token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func extractBearerToken(r *http.Request) string {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return ""
}
return strings.TrimPrefix(auth, "Bearer ")
}func (c *UsersController) GetProfile(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserContextKey).(*User)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Type-safe helper
func UserFromContext(ctx context.Context) (*User, bool) {
user, ok := ctx.Value(UserContextKey).(*User)
return user, ok
}import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID int `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func JWTMiddleware(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := extractBearerToken(r)
if tokenStr == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return secret, nil
})
if err != nil || !token.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
claims := token.Claims.(*Claims)
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}func (s *AuthService) GenerateToken(user *User) (string, error) {
claims := Claims{
UserID: user.ID,
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secret)
}func APIKeyMiddleware(validKeys map[string]bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key == "" {
key = r.URL.Query().Get("api_key")
}
if !validKeys[key] {
http.Error(w, "invalid API key", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}func RequireRole(roles ...string) func(http.Handler) http.Handler {
allowed := make(map[string]bool)
for _, r := range roles {
allowed[r] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := UserFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !allowed[user.Role] {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}func (c *AdminController) RegisterRoutes(r mkhttp.Router) {
r.Group("/admin", func(r mkhttp.Router) {
r.Use(RequireRole("admin", "superadmin"))
r.Handle(http.MethodGet, "/users", http.HandlerFunc(c.ListUsers))
r.Handle(http.MethodDelete, "/users/{id}", http.HandlerFunc(c.DeleteUser))
})
}For middleware that needs dependencies, register as a provider:
func (m *AuthModule) Definition() module.ModuleDef {
return module.ModuleDef{
Name: "auth",
Providers: []module.ProviderDef{
{
Token: "auth.middleware",
Build: func(r module.Resolver) (any, error) {
svc, err := module.Get[*AuthService](r, TokenAuthService)
if err != nil {
return nil, err
}
return JWTMiddleware(svc.Secret()), nil
},
},
},
Exports: []module.Token{"auth.middleware"},
}
}Usage at startup:
app, err := kernel.Bootstrap(&AppModule{})
if err != nil {
log.Fatal(err)
}
authMW, err := module.Get[func(http.Handler) http.Handler](app, "auth.middleware")
if err != nil {
log.Fatal(err)
}
router.Use(authMW)For routes where auth is optional:
func OptionalAuth(validateToken func(string) (*User, error)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token != "" {
if user, err := validateToken(token); err == nil {
ctx := context.WithValue(r.Context(), UserContextKey, user)
r = r.WithContext(ctx)
}
}
next.ServeHTTP(w, r)
})
}
}Mix public and protected routes in a controller:
func (c *UsersController) RegisterRoutes(r mkhttp.Router) {
// Public routes
r.Handle(http.MethodPost, "/login", http.HandlerFunc(c.Login))
r.Handle(http.MethodPost, "/register", http.HandlerFunc(c.Register))
// Protected routes
r.Group("/users", func(r mkhttp.Router) {
r.Use(c.authMiddleware)
r.Handle(http.MethodGet, "/me", http.HandlerFunc(c.GetProfile))
r.Handle(http.MethodPut, "/me", http.HandlerFunc(c.UpdateProfile))
})
}func TestGetProfile_Authenticated(t *testing.T) {
user := &User{ID: 1, Email: "test@example.com"}
req := httptest.NewRequest(http.MethodGet, "/users/me", nil)
ctx := context.WithValue(req.Context(), UserContextKey, user)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
controller.GetProfile(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}
func TestGetProfile_Unauthenticated(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/me", nil)
rec := httptest.NewRecorder()
// Apply middleware + handler
handler := authMiddleware(http.HandlerFunc(controller.GetProfile))
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}| NestJS | modkit |
|---|---|
@UseGuards(AuthGuard) |
r.Use(authMiddleware) |
@Roles('admin') |
r.Use(RequireRole("admin")) |
@Request() req |
r.Context().Value(UserContextKey) |
| Passport.js strategies | Direct implementation or libraries |
JwtModule |
JWT library + middleware |
- Use context for passing user info—it's the Go-idiomatic approach
- Create type-safe context helpers (
UserFromContext) - Keep auth middleware thin—delegate to services
- Use different middleware for different auth strategies
- Return 401 for "who are you?" and 403 for "you can't do that"
- Test both authenticated and unauthenticated scenarios