234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.threesix.ai/jordan/css-verify-1770193392/pkg/httperror"
|
|
"git.threesix.ai/jordan/css-verify-1770193392/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
|
|
}
|