1 package keyfunc 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "time" 11 12 "github.com/golang-jwt/jwt/v5" 13 ) 14 15 // ErrInvalidHTTPStatusCode indicates that the HTTP status code is invalid. 16 var ErrInvalidHTTPStatusCode = errors.New("invalid HTTP status code") 17 18 // Options represents the configuration options for a JWKS. 19 // 20 // If either RefreshInterval is non-zero or RefreshUnknownKID is true, then a background goroutine will be launched to refresh the 21 // remote JWKS under the specified circumstances. 22 // 23 // When using a background refresh goroutine, make sure to use RefreshRateLimit if paired with RefreshUnknownKID. Also 24 // make sure to end the background refresh goroutine with the JWKS.EndBackground method when it's no longer needed. 25 type Options struct { 26 // Client is the HTTP client used to get the JWKS via HTTP. 27 Client *http.Client 28 29 // Ctx is the context for the keyfunc's background refresh. When the context expires or is canceled, the background 30 // goroutine will end. 31 Ctx context.Context 32 33 // GivenKeys is a map of JWT key IDs, `kid`, to their given keys. If the JWKS has a background refresh goroutine, 34 // these values persist across JWKS refreshes. By default, if the remote JWKS resource contains a key with the same 35 // `kid` any given keys with the same `kid` will be overwritten by the keys from the remote JWKS. Use the 36 // GivenKIDOverride option to flip this behavior. 37 GivenKeys map[string]GivenKey 38 39 // GivenKIDOverride will make a GivenKey override any keys with the same ID (`kid`) in the remote JWKS. The is only 40 // effectual if GivenKeys is provided. 41 GivenKIDOverride bool 42 43 // JWKUseWhitelist is a whitelist of JWK `use` parameter values that will restrict what keys can be returned for 44 // jwt.Keyfunc. The assumption is that jwt.Keyfunc is only used for JWT signature verification. 45 // The default behavior is to only return a JWK if its `use` parameter has the value `"sig"`, an empty string, or if 46 // the parameter was omitted entirely. 47 JWKUseWhitelist []JWKUse 48 49 // JWKUseNoWhitelist overrides the JWKUseWhitelist field and its default behavior. If set to true, all JWKs will be 50 // returned regardless of their `use` parameter value. 51 JWKUseNoWhitelist bool 52 53 // RefreshErrorHandler is a function that consumes errors that happen during a JWKS refresh. This is only effectual 54 // if a background refresh goroutine is active. 55 RefreshErrorHandler ErrorHandler 56 57 // RefreshInterval is the duration to refresh the JWKS in the background via a new HTTP request. If this is not zero, 58 // then a background goroutine will be used to refresh the JWKS once per the given interval. Make sure to call the 59 // JWKS.EndBackground method to end this goroutine when it's no longer needed. 60 RefreshInterval time.Duration 61 62 // RefreshRateLimit limits the rate at which refresh requests are granted. Only one refresh request can be queued 63 // at a time any refresh requests received while there is already a queue are ignored. It does not make sense to 64 // have RefreshInterval's value shorter than this. 65 RefreshRateLimit time.Duration 66 67 // RefreshTimeout is the duration for the context timeout used to create the HTTP request for a refresh of the JWKS. 68 // This defaults to one minute. This is used for the HTTP request and any background goroutine refreshes. 69 RefreshTimeout time.Duration 70 71 // RefreshUnknownKID indicates that the JWKS refresh request will occur every time a kid that isn't cached is seen. 72 // This is done through a background goroutine. Without specifying a RefreshInterval a malicious client could 73 // self-sign X JWTs, send them to this service, then cause potentially high network usage proportional to X. Make 74 // sure to call the JWKS.EndBackground method to end this goroutine when it's no longer needed. 75 // 76 // It is recommended this option is not used when in MultipleJWKS. This is because KID collisions SHOULD be uncommon 77 // meaning nearly any JWT SHOULD trigger a refresh for the number of JWKS in the MultipleJWKS minus one. 78 RefreshUnknownKID bool 79 80 // RequestFactory creates HTTP requests for the remote JWKS resource located at the given url. For example, an 81 // HTTP header could be added to indicate a User-Agent. 82 RequestFactory func(ctx context.Context, url string) (*http.Request, error) 83 84 // ResponseExtractor consumes a *http.Response and produces the raw JSON for the JWKS. By default, the 85 // ResponseExtractorStatusOK function is used. The default behavior changed in v1.4.0. 86 ResponseExtractor func(ctx context.Context, resp *http.Response) (json.RawMessage, error) 87 88 // TolerateInitialJWKHTTPError will tolerate any error from the initial HTTP JWKS request. If an error occurs, 89 // the RefreshErrorHandler will be given the error. The program will continue to run as if the error did not occur 90 // and a valid JWK Set with no keys was received in the response. This allows for the background goroutine to 91 // request the JWKS at a later time. 92 // 93 // It does not make sense to mark this field as true unless the background refresh goroutine is active. 94 TolerateInitialJWKHTTPError bool 95 } 96 97 // MultipleOptions is used to configure the behavior when multiple JWKS are used by MultipleJWKS. 98 type MultipleOptions struct { 99 // KeySelector is a function that selects the key to use for a given token. It will be used in the implementation 100 // for jwt.Keyfunc. If implementing this custom selector extract the key ID and algorithm from the token's header. 101 // Use the key ID to select a token and confirm the key's algorithm before returning it. 102 // 103 // This value defaults to KeySelectorFirst. 104 KeySelector func(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error) 105 } 106 107 // RefreshOptions are used to specify manual refresh behavior. 108 type RefreshOptions struct { 109 IgnoreRateLimit bool 110 } 111 112 type refreshRequest struct { 113 cancel context.CancelFunc 114 ignoreRateLimit bool 115 } 116 117 // ResponseExtractorStatusOK is meant to be used as the ResponseExtractor field for Options. It confirms that response 118 // status code is 200 OK and returns the raw JSON from the response body. 119 func ResponseExtractorStatusOK(ctx context.Context, resp *http.Response) (json.RawMessage, error) { 120 //goland:noinspection GoUnhandledErrorResult 121 defer resp.Body.Close() 122 if resp.StatusCode != http.StatusOK { 123 return nil, fmt.Errorf("%w: %d", ErrInvalidHTTPStatusCode, resp.StatusCode) 124 } 125 return io.ReadAll(resp.Body) 126 } 127 128 // ResponseExtractorStatusAny is meant to be used as the ResponseExtractor field for Options. It returns the raw JSON 129 // from the response body regardless of the response status code. 130 func ResponseExtractorStatusAny(ctx context.Context, resp *http.Response) (json.RawMessage, error) { 131 //goland:noinspection GoUnhandledErrorResult 132 defer resp.Body.Close() 133 return io.ReadAll(resp.Body) 134 } 135 136 // applyOptions applies the given options to the given JWKS. 137 func applyOptions(jwks *JWKS, options Options) { 138 if options.Ctx != nil { 139 jwks.ctx, jwks.cancel = context.WithCancel(options.Ctx) 140 } 141 142 if options.GivenKeys != nil { 143 jwks.givenKeys = make(map[string]GivenKey) 144 for kid, key := range options.GivenKeys { 145 jwks.givenKeys[kid] = key 146 } 147 } 148 149 if !options.JWKUseNoWhitelist { 150 jwks.jwkUseWhitelist = make(map[JWKUse]struct{}) 151 for _, use := range options.JWKUseWhitelist { 152 jwks.jwkUseWhitelist[use] = struct{}{} 153 } 154 } 155 156 jwks.client = options.Client 157 jwks.givenKIDOverride = options.GivenKIDOverride 158 jwks.refreshErrorHandler = options.RefreshErrorHandler 159 jwks.refreshInterval = options.RefreshInterval 160 jwks.refreshRateLimit = options.RefreshRateLimit 161 jwks.refreshTimeout = options.RefreshTimeout 162 jwks.refreshUnknownKID = options.RefreshUnknownKID 163 jwks.requestFactory = options.RequestFactory 164 jwks.responseExtractor = options.ResponseExtractor 165 } 166