github.com/lestrrat-go/jwx/v2@v2.0.21/jwk/cache.go (about) 1 package jwk 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "time" 9 10 "github.com/lestrrat-go/httprc" 11 "github.com/lestrrat-go/iter/arrayiter" 12 "github.com/lestrrat-go/iter/mapiter" 13 ) 14 15 type Transformer = httprc.Transformer 16 type HTTPClient = httprc.HTTPClient 17 type ErrSink = httprc.ErrSink 18 19 // Whitelist describes a set of rules that allows users to access 20 // a particular URL. By default all URLs are blocked for security 21 // reasons. You will HAVE to provide some sort of whitelist. See 22 // the documentation for github.com/lestrrat-go/httprc for more details. 23 type Whitelist = httprc.Whitelist 24 25 // Cache is a container that keeps track of Set object by their source URLs. 26 // The Set objects are stored in memory, and are refreshed automatically 27 // behind the scenes. 28 // 29 // Before retrieving the Set objects, the user must pre-register the 30 // URLs they intend to use by calling `Register()` 31 // 32 // c := jwk.NewCache(ctx) 33 // c.Register(url, options...) 34 // 35 // Once registered, you can call `Get()` to retrieve the Set object. 36 // 37 // All JWKS objects that are retrieved via this mechanism should be 38 // treated read-only, as they are shared among all consumers, as well 39 // as the `jwk.Cache` object. 40 // 41 // There are cases where `jwk.Cache` and `jwk.CachedSet` should and 42 // should not be used. 43 // 44 // First and foremost, do NOT use a cache for those JWKS objects that 45 // need constant checking. For example, unreliable or user-provided JWKS (i.e. those 46 // JWKS that are not from a well-known provider) should not be fetched 47 // through a `jwk.Cache` or `jwk.CachedSet`. 48 // 49 // For example, if you have a flaky JWKS server for development 50 // that can go down often, you should consider alternatives such as 51 // providing `http.Client` with a caching `http.RoundTripper` configured 52 // (see `jwk.WithHTTPClient`), setting up a reverse proxy, etc. 53 // These techniques allow you to setup a more robust way to both cache 54 // and report precise causes of the problems than using `jwk.Cache` or 55 // `jwk.CachedSet`. If you handle the caching at the HTTP level like this, 56 // you will be able to use a simple `jwk.Fetch` call and not worry about the cache. 57 // 58 // User-provided JWKS objects may also be problematic, as it may go down 59 // unexpectedly (and frequently!), and it will be hard to detect when 60 // the URLs or its contents are swapped. 61 // 62 // A good use-case for `jwk.Cache` and `jwk.CachedSet` are for "stable" 63 // JWKS objects. 64 // 65 // When we say "stable", we are thinking of JWKS that should mostly be 66 // ALWAYS available. A good example are those JWKS objects provided by 67 // major cloud providers such as Google Cloud, AWS, or Azure. 68 // Stable JWKS may still experience intermittent network connectivity problems, 69 // but you can expect that they will eventually recover in relatively 70 // short period of time. They rarely change URLs, and the contents are 71 // expected to be valid or otherwise it would cause havoc to those providers 72 // 73 // We also know that these stable JWKS objects are rotated periodically, 74 // which is a perfect use for `jwk.Cache` and `jwk.CachedSet`. The caches 75 // can be configured to perodically refresh the JWKS thereby keeping them 76 // fresh without extra intervention from the developer. 77 // 78 // Notice that for these recommended use-cases the requirement to check 79 // the validity or the availability of the JWKS objects are non-existent, 80 // as it is expected that they will be available and will be valid. The 81 // caching mechanism can hide intermittent connectivity problems as well 82 // as keep the objects mostly fresh. 83 type Cache struct { 84 cache *httprc.Cache 85 } 86 87 // PostFetcher is an interface for objects that want to perform 88 // operations on the `Set` that was fetched. 89 type PostFetcher interface { 90 // PostFetch revceives the URL and the JWKS, after a successful 91 // fetch and parse. 92 // 93 // It should return a `Set`, optionally modified, to be stored 94 // in the cache for subsequent use 95 PostFetch(string, Set) (Set, error) 96 } 97 98 // PostFetchFunc is a PostFetcher based on a functon. 99 type PostFetchFunc func(string, Set) (Set, error) 100 101 func (f PostFetchFunc) PostFetch(u string, set Set) (Set, error) { 102 return f(u, set) 103 } 104 105 // httprc.Transofmer that transforms the response into a JWKS 106 type jwksTransform struct { 107 postFetch PostFetcher 108 parseOptions []ParseOption 109 } 110 111 // Default transform has no postFetch. This can be shared 112 // by multiple fetchers 113 var defaultTransform = &jwksTransform{} 114 115 func (t *jwksTransform) Transform(u string, res *http.Response) (interface{}, error) { 116 if res.StatusCode != http.StatusOK { 117 return nil, fmt.Errorf(`failed to process response: non-200 response code %q`, res.Status) 118 } 119 buf, err := io.ReadAll(res.Body) 120 if err != nil { 121 return nil, fmt.Errorf(`failed to read response body status: %w`, err) 122 } 123 124 set, err := Parse(buf, t.parseOptions...) 125 if err != nil { 126 return nil, fmt.Errorf(`failed to parse JWK set at %q: %w`, u, err) 127 } 128 129 if pf := t.postFetch; pf != nil { 130 v, err := pf.PostFetch(u, set) 131 if err != nil { 132 return nil, fmt.Errorf(`failed to execute PostFetch: %w`, err) 133 } 134 set = v 135 } 136 137 return set, nil 138 } 139 140 // NewCache creates a new `jwk.Cache` object. 141 // 142 // Please refer to the documentation for `httprc.New` for more 143 // details. 144 func NewCache(ctx context.Context, options ...CacheOption) *Cache { 145 var hrcopts []httprc.CacheOption 146 for _, option := range options { 147 //nolint:forcetypeassert 148 switch option.Ident() { 149 case identRefreshWindow{}: 150 hrcopts = append(hrcopts, httprc.WithRefreshWindow(option.Value().(time.Duration))) 151 case identErrSink{}: 152 hrcopts = append(hrcopts, httprc.WithErrSink(option.Value().(ErrSink))) 153 } 154 } 155 156 return &Cache{ 157 cache: httprc.NewCache(ctx, hrcopts...), 158 } 159 } 160 161 // Register registers a URL to be managed by the cache. URLs must 162 // be registered before issuing `Get` 163 // 164 // This method is almost identical to `(httprc.Cache).Register`, except 165 // it accepts some extra options. 166 // 167 // Use `jwk.WithParser` to configure how the JWKS should be parsed, 168 // such as passing it extra options. 169 // 170 // Please refer to the documentation for `(httprc.Cache).Register` for more 171 // details. 172 // 173 // Register does not check for the validity of the url being registered. 174 // If you need to make sure that a url is valid before entering your main 175 // loop, call `Refresh` once to make sure the JWKS is available. 176 // 177 // _ = cache.Register(url) 178 // if _, err := cache.Refresh(ctx, url); err != nil { 179 // // url is not a valid JWKS 180 // panic(err) 181 // } 182 func (c *Cache) Register(u string, options ...RegisterOption) error { 183 var hrropts []httprc.RegisterOption 184 var pf PostFetcher 185 var parseOptions []ParseOption 186 187 // Note: we do NOT accept Transform option 188 for _, option := range options { 189 if parseOpt, ok := option.(ParseOption); ok { 190 parseOptions = append(parseOptions, parseOpt) 191 continue 192 } 193 194 //nolint:forcetypeassert 195 switch option.Ident() { 196 case identHTTPClient{}: 197 hrropts = append(hrropts, httprc.WithHTTPClient(option.Value().(HTTPClient))) 198 case identRefreshInterval{}: 199 hrropts = append(hrropts, httprc.WithRefreshInterval(option.Value().(time.Duration))) 200 case identMinRefreshInterval{}: 201 hrropts = append(hrropts, httprc.WithMinRefreshInterval(option.Value().(time.Duration))) 202 case identFetchWhitelist{}: 203 hrropts = append(hrropts, httprc.WithWhitelist(option.Value().(httprc.Whitelist))) 204 case identPostFetcher{}: 205 pf = option.Value().(PostFetcher) 206 } 207 } 208 209 var t *jwksTransform 210 if pf == nil && len(parseOptions) == 0 { 211 t = defaultTransform 212 } else { 213 // User-supplied PostFetcher is attached to the transformer 214 t = &jwksTransform{ 215 postFetch: pf, 216 parseOptions: parseOptions, 217 } 218 } 219 220 // Set the transfomer at the end so that nobody can override it 221 hrropts = append(hrropts, httprc.WithTransformer(t)) 222 return c.cache.Register(u, hrropts...) 223 } 224 225 // Get returns the stored JWK set (`Set`) from the cache. 226 // 227 // Please refer to the documentation for `(httprc.Cache).Get` for more 228 // details. 229 func (c *Cache) Get(ctx context.Context, u string) (Set, error) { 230 v, err := c.cache.Get(ctx, u) 231 if err != nil { 232 return nil, err 233 } 234 235 set, ok := v.(Set) 236 if !ok { 237 return nil, fmt.Errorf(`cached object is not a Set (was %T)`, v) 238 } 239 return set, nil 240 } 241 242 // Refresh is identical to Get(), except it always fetches the 243 // specified resource anew, and updates the cached content 244 // 245 // Please refer to the documentation for `(httprc.Cache).Refresh` for 246 // more details 247 func (c *Cache) Refresh(ctx context.Context, u string) (Set, error) { 248 v, err := c.cache.Refresh(ctx, u) 249 if err != nil { 250 return nil, err 251 } 252 253 set, ok := v.(Set) 254 if !ok { 255 return nil, fmt.Errorf(`cached object is not a Set (was %T)`, v) 256 } 257 return set, nil 258 } 259 260 // IsRegistered returns true if the given URL `u` has already been registered 261 // in the cache. 262 // 263 // Please refer to the documentation for `(httprc.Cache).IsRegistered` for more 264 // details. 265 func (c *Cache) IsRegistered(u string) bool { 266 return c.cache.IsRegistered(u) 267 } 268 269 // Unregister removes the given URL `u` from the cache. 270 // 271 // Please refer to the documentation for `(httprc.Cache).Unregister` for more 272 // details. 273 func (c *Cache) Unregister(u string) error { 274 return c.cache.Unregister(u) 275 } 276 277 func (c *Cache) Snapshot() *httprc.Snapshot { 278 return c.cache.Snapshot() 279 } 280 281 // CachedSet is a thin shim over jwk.Cache that allows the user to cloack 282 // jwk.Cache as if it's a `jwk.Set`. Behind the scenes, the `jwk.Set` is 283 // retrieved from the `jwk.Cache` for every operation. 284 // 285 // Since `jwk.CachedSet` always deals with a cached version of the `jwk.Set`, 286 // all operations that mutate the object (such as AddKey(), RemoveKey(), et. al) 287 // are no-ops and return an error. 288 // 289 // Note that since this is a utility shim over `jwk.Cache`, you _will_ lose 290 // the ability to control the finer details (such as controlling how long to 291 // wait for in case of a fetch failure using `context.Context`) 292 // 293 // Make sure that you read the documentation for `jwk.Cache` as well. 294 type CachedSet struct { 295 cache *Cache 296 url string 297 } 298 299 var _ Set = &CachedSet{} 300 301 func NewCachedSet(cache *Cache, url string) Set { 302 return &CachedSet{ 303 cache: cache, 304 url: url, 305 } 306 } 307 308 func (cs *CachedSet) cached() (Set, error) { 309 return cs.cache.Get(context.Background(), cs.url) 310 } 311 312 // Add is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only 313 func (*CachedSet) AddKey(_ Key) error { 314 return fmt.Errorf(`(jwk.Cachedset).AddKey: jwk.CachedSet is immutable`) 315 } 316 317 // Clear is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only 318 func (*CachedSet) Clear() error { 319 return fmt.Errorf(`(jwk.CachedSet).Clear: jwk.CachedSet is immutable`) 320 } 321 322 // Set is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only 323 func (*CachedSet) Set(_ string, _ interface{}) error { 324 return fmt.Errorf(`(jwk.CachedSet).Set: jwk.CachedSet is immutable`) 325 } 326 327 // Remove is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only 328 func (*CachedSet) Remove(_ string) error { 329 // TODO: Remove() should be renamed to Remove(string) error 330 return fmt.Errorf(`(jwk.CachedSet).Remove: jwk.CachedSet is immutable`) 331 } 332 333 // RemoveKey is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only 334 func (*CachedSet) RemoveKey(_ Key) error { 335 return fmt.Errorf(`(jwk.CachedSet).RemoveKey: jwk.CachedSet is immutable`) 336 } 337 338 func (cs *CachedSet) Clone() (Set, error) { 339 set, err := cs.cached() 340 if err != nil { 341 return nil, fmt.Errorf(`failed to get cached jwk.Set: %w`, err) 342 } 343 344 return set.Clone() 345 } 346 347 // Get returns the value of non-Key field stored in the jwk.Set 348 func (cs *CachedSet) Get(name string) (interface{}, bool) { 349 set, err := cs.cached() 350 if err != nil { 351 return nil, false 352 } 353 354 return set.Get(name) 355 } 356 357 // Key returns the Key at the specified index 358 func (cs *CachedSet) Key(idx int) (Key, bool) { 359 set, err := cs.cached() 360 if err != nil { 361 return nil, false 362 } 363 364 return set.Key(idx) 365 } 366 367 func (cs *CachedSet) Index(key Key) int { 368 set, err := cs.cached() 369 if err != nil { 370 return -1 371 } 372 373 return set.Index(key) 374 } 375 376 func (cs *CachedSet) Keys(ctx context.Context) KeyIterator { 377 set, err := cs.cached() 378 if err != nil { 379 return arrayiter.New(nil) 380 } 381 382 return set.Keys(ctx) 383 } 384 385 func (cs *CachedSet) Iterate(ctx context.Context) HeaderIterator { 386 set, err := cs.cached() 387 if err != nil { 388 return mapiter.New(nil) 389 } 390 391 return set.Iterate(ctx) 392 } 393 394 func (cs *CachedSet) Len() int { 395 set, err := cs.cached() 396 if err != nil { 397 return -1 398 } 399 400 return set.Len() 401 } 402 403 func (cs *CachedSet) LookupKeyID(kid string) (Key, bool) { 404 set, err := cs.cached() 405 if err != nil { 406 return nil, false 407 } 408 409 return set.LookupKeyID(kid) 410 }