Repository navigation
Expand file tree
/
Copy pathjwks.go
More file actions
117 lines (102 loc) · 2.74 KB
/
Copy pathjwks.go
File metadata and controls
117 lines (102 loc) · 2.74 KB
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
package aoa
import (
"context"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
)
// keySource returns the JWK set for a given key ID.
type keySource interface {
setForKID(ctx context.Context, kid string) (jwk.Set, error)
}
type staticKeySource struct{ set jwk.Set }
func (s *staticKeySource) setForKID(context.Context, string) (jwk.Set, error) {
return s.set, nil
}
// remoteKeySource fetches and caches a JWKS from a URI, refreshing on TTL
// expiry or unknown kid. Concurrent refreshes are single-flighted.
type remoteKeySource struct {
uri string
ttl time.Duration
client *http.Client
mu sync.Mutex
set jwk.Set
fetchedAt time.Time
fetching chan struct{} // non-nil while a fetch is in flight
lastErr error // error from the most recent fetch; nil on success
}
func newRemoteKeySource(uri string, ttl time.Duration) *remoteKeySource {
return &remoteKeySource{uri: uri, ttl: ttl, client: &http.Client{Timeout: 10 * time.Second}}
}
func (r *remoteKeySource) setForKID(ctx context.Context, kid string) (jwk.Set, error) {
r.mu.Lock()
cur := r.set
stale := cur == nil || time.Since(r.fetchedAt) > r.ttl
present := false
if cur != nil {
_, present = cur.LookupKeyID(kid)
}
r.mu.Unlock()
if cur != nil && present && !stale {
return cur, nil
}
return r.refresh(ctx)
}
// refresh fetches the JWKS once, sharing an in-flight fetch across callers
func (r *remoteKeySource) refresh(ctx context.Context) (jwk.Set, error) {
r.mu.Lock()
if r.fetching != nil {
done := r.fetching
r.mu.Unlock()
<-done
r.mu.Lock()
set, err := r.set, r.lastErr
r.mu.Unlock()
// mirror the leader's outcome: if the in-flight fetch failed, every
// joiner fails too. Otherwise some callers would pass on a stale set
// while the leader returns 401, giving split outcomes for the same keys.
if err != nil {
return nil, fmt.Errorf("aoa: jwks refresh failed: %w", err)
}
return set, nil
}
done := make(chan struct{})
r.fetching = done
r.mu.Unlock()
set, err := r.fetch(ctx)
r.mu.Lock()
if err == nil {
r.set = set
r.fetchedAt = time.Now()
}
r.lastErr = err
r.fetching = nil
r.mu.Unlock()
close(done)
if err != nil {
return nil, err
}
return set, nil
}
func (r *remoteKeySource) fetch(ctx context.Context) (jwk.Set, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.uri, nil)
if err != nil {
return nil, err
}
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("aoa: fetch jwks: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("aoa: jwks status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
return jwk.Parse(body)
}