Files
RenewCMS/api/middleware/apiKey.go
T

119 lines
3.2 KiB
Go

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)
}