1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
package auth
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync/atomic"
"aigw/internal/domain"
)
var ErrUnauthorized = errors.New("invalid or missing API key")
type Authenticator interface {
Authenticate(*http.Request) (domain.Principal, error)
}
type KeyRecord struct {
Key string `json:"key"`
KeyID string `json:"key_id"`
TenantID string `json:"tenant_id"`
ProjectID string `json:"project_id"`
Scopes []string `json:"scopes"`
}
type StaticAuthenticator struct {
state atomic.Pointer[keySnapshot]
allowAnonymous bool
}
type keySnapshot struct {
keys map[[sha256.Size]byte]domain.Principal
}
type HashedKeyRecord struct {
Hash [sha256.Size]byte
Principal domain.Principal
}
func NewStatic(raw string, allowAnonymous bool) (*StaticAuthenticator, error) {
result := &StaticAuthenticator{allowAnonymous: allowAnonymous}
if strings.TrimSpace(raw) == "" {
if allowAnonymous {
result.ReplaceHashed(nil)
return result, nil
}
return nil, errors.New("client API key environment variable is empty")
}
var records []KeyRecord
if err := json.Unmarshal([]byte(raw), &records); err != nil {
return nil, fmt.Errorf("parse client API keys JSON: %w", err)
}
hashed := make([]HashedKeyRecord, 0, len(records))
seen := make(map[[sha256.Size]byte]struct{}, len(records))
for i, record := range records {
if record.Key == "" || record.KeyID == "" || record.TenantID == "" || record.ProjectID == "" {
return nil, fmt.Errorf("client API key record %d requires key, key_id, tenant_id, and project_id", i)
}
hash := sha256.Sum256([]byte(record.Key))
if _, exists := seen[hash]; exists {
return nil, fmt.Errorf("duplicate client API key at record %d", i)
}
seen[hash] = struct{}{}
hashed = append(hashed, HashedKeyRecord{Hash: hash, Principal: domain.Principal{
KeyID: record.KeyID, TenantID: record.TenantID, ProjectID: record.ProjectID,
Scopes: append([]string(nil), record.Scopes...),
}})
}
if len(hashed) == 0 && !allowAnonymous {
return nil, errors.New("at least one client API key is required")
}
result.ReplaceHashed(hashed)
return result, nil
}
func NewDynamic(records []HashedKeyRecord, allowAnonymous bool) *StaticAuthenticator {
result := &StaticAuthenticator{allowAnonymous: allowAnonymous}
result.ReplaceHashed(records)
return result
}
func (a *StaticAuthenticator) ReplaceHashed(records []HashedKeyRecord) {
keys := make(map[[sha256.Size]byte]domain.Principal, len(records))
for _, record := range records {
principal := record.Principal
principal.Scopes = append([]string(nil), principal.Scopes...)
keys[record.Hash] = principal
}
a.state.Store(&keySnapshot{keys: keys})
}
func (a *StaticAuthenticator) Authenticate(r *http.Request) (domain.Principal, error) {
key := bearerToken(r.Header.Get("Authorization"))
if key == "" {
key = strings.TrimSpace(r.Header.Get("x-api-key"))
}
if key == "" && a.allowAnonymous {
return domain.Principal{KeyID: "anonymous", TenantID: "anonymous", ProjectID: "anonymous"}, nil
}
if key == "" {
return domain.Principal{}, ErrUnauthorized
}
snapshot := a.state.Load()
if snapshot == nil {
return domain.Principal{}, ErrUnauthorized
}
principal, ok := snapshot.keys[sha256.Sum256([]byte(key))]
if !ok {
return domain.Principal{}, ErrUnauthorized
}
return principal, nil
}
func bearerToken(header string) string {
parts := strings.Fields(header)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return ""
}
return parts[1]
}
|