package middleware import ( "RenewCMS/api/pkg/apperr" "RenewCMS/internal/domain/apikey" "RenewCMS/internal/infrastructure/useCases" "context" "errors" "net/http" "strconv" "strings" "time" "github.com/go-chi/jwtauth/v5" "github.com/lestrrat-go/jwx/v3/jwt" ) const bearerScheme = "bearer " type apiKeyIdContextKey struct{} type apiKeyNameContextKey struct{} func bearerAPIKey(r *http.Request) (string, bool) { header := r.Header.Get("Authorization") if len(header) < len(bearerScheme) || !strings.EqualFold(header[:len(bearerScheme)], bearerScheme) { return "", false } credential := strings.TrimSpace(header[len(bearerScheme):]) return credential, strings.HasPrefix(credential, apikey.RawKeyPrefix) } func apiKeyIdFrom(r *http.Request) (uint32, bool) { id, ok := r.Context().Value(apiKeyIdContextKey{}).(uint32) return id, ok } func apiKeyNameFrom(r *http.Request) string { name, _ := r.Context().Value(apiKeyNameContextKey{}).(string) return name } func RequireSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, isKey := bearerAPIKey(r); isKey { http.Error(w, apperr.SessionRequiredErrorMessage, http.StatusForbidden) return } next.ServeHTTP(w, r) }) } func RejectAPIKey(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, authenticatedByKey := apiKeyIdFrom(r); authenticatedByKey { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } func APIKeyAuth(authenticate *useCases.AuthenticateAPIKeyUseCase, maxRequests int, window time.Duration) func(http.Handler) http.Handler { limiter := newKeyedRateLimiter(maxRequests, window, apiKeyRateLimitKey) return func(next http.Handler) http.Handler { limited := limiter(next) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawKey, isKey := bearerAPIKey(r) if !isKey { next.ServeHTTP(w, r) return } authenticated, err := authenticate.AuthenticateAPIKey(rawKey) if err != nil { writeAPIKeyError(w, err) return } token, err := sessionTokenFor(authenticated) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } ctx := jwtauth.NewContext(r.Context(), token, nil) ctx = context.WithValue(ctx, apiKeyIdContextKey{}, authenticated.Key.ID) ctx = context.WithValue(ctx, apiKeyNameContextKey{}, authenticated.Key.Name) limited.ServeHTTP(w, r.WithContext(ctx)) }) } } func writeAPIKeyError(w http.ResponseWriter, err error) { if errors.Is(err, apikey.ErrAPIKeyInvalid) { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } http.Error(w, "Internal server error", http.StatusInternalServerError) } func sessionTokenFor(authenticated useCases.AuthenticatedAPIKey) (jwt.Token, error) { token := jwt.New() if err := token.Set("user_id", float64(authenticated.User.ID)); err != nil { return nil, err } if err := token.Set("token_version", float64(authenticated.User.TokenVersion)); err != nil { return nil, err } return token, nil } func apiKeyRateLimitKey(r *http.Request) string { id, _ := apiKeyIdFrom(r) return strconv.FormatUint(uint64(id), 10) }