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  }