feat(auth): introduce auth.providers for flexible authentication configuration

- Replaced legacy `api-keys` field with `auth.providers` in configuration, supporting multiple authentication providers including `config-api-key`.
- Added synchronization to maintain compatibility with legacy `api-keys`.
- Updated core components like request handling and middleware to use the new provider system.
- Enhanced management API endpoints for seamless integration with `auth.providers`.
This commit is contained in:
Luis Pater
2025-09-22 17:36:31 +08:00
parent c28a5d24f8
commit 4008be19f4
14 changed files with 587 additions and 90 deletions

12
sdk/access/errors.go Normal file
View File

@@ -0,0 +1,12 @@
package access
import "errors"
var (
// ErrNoCredentials indicates no recognizable credentials were supplied.
ErrNoCredentials = errors.New("access: no credentials provided")
// ErrInvalidCredential signals that supplied credentials were rejected by a provider.
ErrInvalidCredential = errors.New("access: invalid credential")
// ErrNotHandled tells the manager to continue trying other providers.
ErrNotHandled = errors.New("access: not handled")
)

89
sdk/access/manager.go Normal file
View File

@@ -0,0 +1,89 @@
package access
import (
"context"
"errors"
"net/http"
"sync"
)
// Manager coordinates authentication providers.
type Manager struct {
mu sync.RWMutex
providers []Provider
}
// NewManager constructs an empty manager.
func NewManager() *Manager {
return &Manager{}
}
// SetProviders replaces the active provider list.
func (m *Manager) SetProviders(providers []Provider) {
if m == nil {
return
}
cloned := make([]Provider, len(providers))
copy(cloned, providers)
m.mu.Lock()
m.providers = cloned
m.mu.Unlock()
}
// Providers returns a snapshot of the active providers.
func (m *Manager) Providers() []Provider {
if m == nil {
return nil
}
m.mu.RLock()
defer m.mu.RUnlock()
snapshot := make([]Provider, len(m.providers))
copy(snapshot, m.providers)
return snapshot
}
// Authenticate evaluates providers until one succeeds.
func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, error) {
if m == nil {
return nil, nil
}
providers := m.Providers()
if len(providers) == 0 {
return nil, nil
}
var (
missing bool
invalid bool
)
for _, provider := range providers {
if provider == nil {
continue
}
res, err := provider.Authenticate(ctx, r)
if err == nil {
return res, nil
}
if errors.Is(err, ErrNotHandled) {
continue
}
if errors.Is(err, ErrNoCredentials) {
missing = true
continue
}
if errors.Is(err, ErrInvalidCredential) {
invalid = true
continue
}
return nil, err
}
if invalid {
return nil, ErrInvalidCredential
}
if missing {
return nil, ErrNoCredentials
}
return nil, ErrNoCredentials
}

View File

@@ -0,0 +1,103 @@
package configapikey
import (
"context"
"net/http"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
)
type provider struct {
name string
keys map[string]struct{}
}
func init() {
sdkaccess.RegisterProvider(config.AccessProviderTypeConfigAPIKey, newProvider)
}
func newProvider(cfg *config.AccessProvider, _ *config.Config) (sdkaccess.Provider, error) {
name := cfg.Name
if name == "" {
name = config.DefaultAccessProviderName
}
keys := make(map[string]struct{}, len(cfg.APIKeys))
for _, key := range cfg.APIKeys {
if key == "" {
continue
}
keys[key] = struct{}{}
}
return &provider{name: name, keys: keys}, nil
}
func (p *provider) Identifier() string {
if p == nil || p.name == "" {
return config.DefaultAccessProviderName
}
return p.name
}
func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.Result, error) {
if p == nil {
return nil, sdkaccess.ErrNotHandled
}
if len(p.keys) == 0 {
return nil, sdkaccess.ErrNotHandled
}
authHeader := r.Header.Get("Authorization")
authHeaderGoogle := r.Header.Get("X-Goog-Api-Key")
authHeaderAnthropic := r.Header.Get("X-Api-Key")
queryKey := ""
if r.URL != nil {
queryKey = r.URL.Query().Get("key")
}
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" {
return nil, sdkaccess.ErrNoCredentials
}
apiKey := extractBearerToken(authHeader)
candidates := []struct {
value string
source string
}{
{apiKey, "authorization"},
{authHeaderGoogle, "x-goog-api-key"},
{authHeaderAnthropic, "x-api-key"},
{queryKey, "query-key"},
}
for _, candidate := range candidates {
if candidate.value == "" {
continue
}
if _, ok := p.keys[candidate.value]; ok {
return &sdkaccess.Result{
Provider: p.Identifier(),
Principal: candidate.value,
Metadata: map[string]string{
"source": candidate.source,
},
}, nil
}
}
return nil, sdkaccess.ErrInvalidCredential
}
func extractBearerToken(header string) string {
if header == "" {
return ""
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 {
return header
}
if strings.ToLower(parts[0]) != "bearer" {
return header
}
return strings.TrimSpace(parts[1])
}

88
sdk/access/registry.go Normal file
View File

@@ -0,0 +1,88 @@
package access
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
// Provider validates credentials for incoming requests.
type Provider interface {
Identifier() string
Authenticate(ctx context.Context, r *http.Request) (*Result, error)
}
// Result conveys authentication outcome.
type Result struct {
Provider string
Principal string
Metadata map[string]string
}
// ProviderFactory builds a provider from configuration data.
type ProviderFactory func(cfg *config.AccessProvider, root *config.Config) (Provider, error)
var (
registryMu sync.RWMutex
registry = make(map[string]ProviderFactory)
)
// RegisterProvider registers a provider factory for a given type identifier.
func RegisterProvider(typ string, factory ProviderFactory) {
if typ == "" || factory == nil {
return
}
registryMu.Lock()
registry[typ] = factory
registryMu.Unlock()
}
func buildProvider(cfg *config.AccessProvider, root *config.Config) (Provider, error) {
if cfg == nil {
return nil, fmt.Errorf("access: nil provider config")
}
registryMu.RLock()
factory, ok := registry[cfg.Type]
registryMu.RUnlock()
if !ok {
return nil, fmt.Errorf("access: provider type %q is not registered", cfg.Type)
}
provider, err := factory(cfg, root)
if err != nil {
return nil, fmt.Errorf("access: failed to build provider %q: %w", cfg.Name, err)
}
return provider, nil
}
// BuildProviders constructs providers declared in configuration.
func BuildProviders(root *config.Config) ([]Provider, error) {
if root == nil {
return nil, nil
}
providers := make([]Provider, 0, len(root.Access.Providers))
for i := range root.Access.Providers {
providerCfg := &root.Access.Providers[i]
if providerCfg.Type == "" {
continue
}
provider, err := buildProvider(providerCfg, root)
if err != nil {
return nil, err
}
providers = append(providers, provider)
}
if len(providers) == 0 && len(root.APIKeys) > 0 {
config.SyncInlineAPIKeys(root, root.APIKeys)
if providerCfg := root.ConfigAPIKeyProvider(); providerCfg != nil {
provider, err := buildProvider(providerCfg, root)
if err != nil {
return nil, err
}
providers = append(providers, provider)
}
}
return providers, nil
}