package auth import ( "context" "net/http" "strings" "git.threesix.ai/jordan/slack5-1770574304/pkg/httperror" "git.threesix.ai/jordan/slack5-1770574304/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 }