slate-final-1770513664/pkg/auth/middleware.go
jordan f2ab90548d
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-08 01:21:05 +00:00

234 lines
6.4 KiB
Go

package auth
import (
"context"
"net/http"
"strings"
"git.threesix.ai/jordan/slate-final-1770513664/pkg/httperror"
"git.threesix.ai/jordan/slate-final-1770513664/pkg/httpresponse"
)
// MiddlewareConfig configures the authentication middleware.
type MiddlewareConfig struct {
// Validator is the token/key validator to use
Validator Validator
// TokenExtractor extracts the token from the request (optional)
// Default: BearerTokenExtractor or APIKeyExtractor
TokenExtractor func(*http.Request) string
// Optional returns 401 only when a token is provided but invalid.
// If no token is provided, the request continues without authentication.
Optional bool
// SkipPaths are paths that skip authentication entirely
SkipPaths []string
}
// Middleware creates an authentication middleware.
//
// Example:
//
// r.Use(auth.Middleware(auth.MiddlewareConfig{
// Validator: jwtValidator,
// }))
//
// // Or with optional auth (passes through if no token)
// r.Use(auth.Middleware(auth.MiddlewareConfig{
// Validator: jwtValidator,
// Optional: true,
// }))
func Middleware(cfg MiddlewareConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if path should be skipped
for _, path := range cfg.SkipPaths {
if strings.HasPrefix(r.URL.Path, path) {
next.ServeHTTP(w, r)
return
}
}
// Extract token
var token string
if cfg.TokenExtractor != nil {
token = cfg.TokenExtractor(r)
} else {
// Try Bearer token first, then API key
token = ExtractBearerToken(r)
if token == "" {
token = ExtractAPIKey(r)
}
}
// No token provided
if token == "" {
if cfg.Optional {
next.ServeHTTP(w, r)
return
}
httpresponse.Unauthorized(w, r, "authentication required")
return
}
// Validate token
user, err := cfg.Validator.Validate(r.Context(), token)
if err != nil {
httpresponse.Unauthorized(w, r, "invalid credentials")
return
}
// Store user and token in context
ctx := SetUser(r.Context(), user)
ctx = SetToken(ctx, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RequireAuth middleware requires authentication.
// Use after auth.Middleware to ensure a user is present.
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r.Context()) {
httpresponse.Unauthorized(w, r, "authentication required")
return
}
next.ServeHTTP(w, r)
})
}
// RequireRole middleware requires the user to have a specific role.
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUser(r.Context())
if user == nil {
httpresponse.Unauthorized(w, r, "authentication required")
return
}
if !user.HasRole(role) {
httpresponse.Forbidden(w, r, "insufficient permissions")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireAnyRole middleware requires the user to have any of the specified roles.
func RequireAnyRole(roles ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUser(r.Context())
if user == nil {
httpresponse.Unauthorized(w, r, "authentication required")
return
}
if !user.HasAnyRole(roles...) {
httpresponse.Forbidden(w, r, "insufficient permissions")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireScope middleware requires the user to have a specific scope.
func RequireScope(scope string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUser(r.Context())
if user == nil {
httpresponse.Unauthorized(w, r, "authentication required")
return
}
if !user.HasScope(scope) {
httpresponse.Forbidden(w, r, "insufficient scope")
return
}
next.ServeHTTP(w, r)
})
}
}
// -----------------------------------------------------------------------------
// Token Extractors
// -----------------------------------------------------------------------------
// ExtractBearerToken extracts a Bearer token from the Authorization header.
func ExtractBearerToken(r *http.Request) string {
auth := r.Header.Get("Authorization")
if auth == "" {
return ""
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return ""
}
return parts[1]
}
// ExtractAPIKey extracts an API key from the X-API-Key header.
func ExtractAPIKey(r *http.Request) string {
return r.Header.Get("X-API-Key")
}
// ExtractFromQuery extracts a token from a query parameter.
func ExtractFromQuery(paramName string) func(*http.Request) string {
return func(r *http.Request) string {
return r.URL.Query().Get(paramName)
}
}
// ExtractFromCookie extracts a token from a cookie.
func ExtractFromCookie(cookieName string) func(*http.Request) string {
return func(r *http.Request) string {
cookie, err := r.Cookie(cookieName)
if err != nil {
return ""
}
return cookie.Value
}
}
// -----------------------------------------------------------------------------
// Error-returning middleware helpers (for use with app.Wrap)
// -----------------------------------------------------------------------------
// RequireAuthErr returns an error if the user is not authenticated.
// Use with app.Wrap pattern.
func RequireAuthErr(ctx context.Context) error {
if !IsAuthenticated(ctx) {
return httperror.Unauthorized("authentication required")
}
return nil
}
// RequireRoleErr returns an error if the user doesn't have the role.
// Use with app.Wrap pattern.
func RequireRoleErr(ctx context.Context, role string) error {
user := GetUser(ctx)
if user == nil {
return httperror.Unauthorized("authentication required")
}
if !user.HasRole(role) {
return httperror.Forbidden("insufficient permissions")
}
return nil
}
// RequireScopeErr returns an error if the user doesn't have the scope.
// Use with app.Wrap pattern.
func RequireScopeErr(ctx context.Context, scope string) error {
user := GetUser(ctx)
if user == nil {
return httperror.Unauthorized("authentication required")
}
if !user.HasScope(scope) {
return httperror.Forbidden("insufficient scope")
}
return nil
}