Harden API-key model: per-key scoping + revocable keys, and configurable/whitelist-aware rate limiting (#16)
* API scope and rate limiting * improvements from review
This commit is contained in:
1 parent
a400e07ae2
commit
495edffde3
11 files changed
+843
-42
No files matched your search
@@ -5,12 +5,69 @@ import (
|
||||
"crypto/subtle"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/Turbootzz/vaultwarden-api/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Middleware creates an authentication middleware for API key validation
|
||||
func Middleware(apiKey string) fiber.Handler {
|
||||
// Scope limits which secrets a key may read, enforced server-side.
|
||||
// Entries may be organization/collection names or UUIDs (resolved per-request).
|
||||
// An empty scope (no organizations and no collections) grants full access.
|
||||
type Scope struct {
|
||||
Organizations []string
|
||||
Collections []string
|
||||
}
|
||||
|
||||
// IsEmpty reports whether the scope imposes no constraint (full access).
|
||||
func (s Scope) IsEmpty() bool {
|
||||
return len(s.Organizations) == 0 && len(s.Collections) == 0
|
||||
}
|
||||
|
||||
// APIKey is a single configured key with its server-side scope.
|
||||
type APIKey struct {
|
||||
Name string
|
||||
Key string
|
||||
Scope Scope
|
||||
}
|
||||
|
||||
// Store holds the configured API keys and resolves a presented key to its scope.
|
||||
type Store struct {
|
||||
keys []APIKey
|
||||
}
|
||||
|
||||
// NewStore builds a key store from the configured keys.
|
||||
func NewStore(keys []APIKey) *Store {
|
||||
return &Store{keys: keys}
|
||||
}
|
||||
|
||||
// Match returns the configured key matching the presented secret, if any.
|
||||
// It compares against every key without short-circuiting so that timing does
|
||||
// not reveal a key's position in the list.
|
||||
func (s *Store) Match(provided string) (APIKey, bool) {
|
||||
var matched APIKey
|
||||
found := false
|
||||
for _, k := range s.keys {
|
||||
if secureCompare(provided, k.Key) {
|
||||
matched = k
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return matched, found
|
||||
}
|
||||
|
||||
// ctxKey is the unexported type for the scope stored in the request context.
|
||||
type ctxKey struct{}
|
||||
|
||||
var scopeKey ctxKey
|
||||
|
||||
// ScopeFromCtx returns the authenticated key's scope from the request context.
|
||||
func ScopeFromCtx(c *fiber.Ctx) (Scope, bool) {
|
||||
scope, ok := c.Locals(scopeKey).(Scope)
|
||||
return scope, ok
|
||||
}
|
||||
|
||||
// Middleware creates an authentication middleware that validates the bearer
|
||||
// API key against the store and attaches the matched key's scope to the context.
|
||||
func Middleware(store *Store) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Get the Authorization header
|
||||
authHeader := c.Get("Authorization")
|
||||
@@ -33,14 +90,16 @@ func Middleware(apiKey string) fiber.Handler {
|
||||
|
||||
providedKey := parts[1]
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
if !secureCompare(providedKey, apiKey) {
|
||||
key, ok := store.Match(providedKey)
|
||||
if !ok {
|
||||
logger.Warn.Printf("Invalid API key from IP: %s", c.IP())
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid api key",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals(scopeKey, key.Scope)
|
||||
|
||||
// Authentication successful
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user