From ebd58fcfdb02ec3d079122810cd7ea5c9e7136ba Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Mon, 12 May 2025 11:14:21 +0200
Subject: [PATCH] Introduce a the auth-api service
* primitive implementation to demonstrate how it could work, still to
be considered WIP at best
* add new dependency: MicahParks/jwkset and MicahParks/keyfunc to
retrieve the JWK set from KeyCloak to verify the signature of the
JWTs sent as part of Bearer authentication in the /auth API
* (minor) opencloud/.../service.go: clean up a logging statement that
was introduced earlier to hunt down why the auth-api service was not
being started
---
.../opencloud_full/debug-opencloud-paused.yml | 7 +
go.mod | 2 +
go.sum | 4 +
services/auth-api/pkg/config/config.go | 1 +
.../pkg/config/defaults/defaultconfig.go | 3 +
services/auth-api/pkg/metrics/metrics.go | 52 +-
services/auth-api/pkg/metrics/options.go | 31 ++
services/auth-api/pkg/server/http/server.go | 4 +-
.../auth-api/pkg/service/http/v0/option.go | 14 +
.../auth-api/pkg/service/http/v0/service.go | 233 ++++++--
.../pkg/service/http/v0/service_test.go | 17 +
.../github.com/MicahParks/jwkset/.gitignore | 2 +
vendor/github.com/MicahParks/jwkset/LICENSE | 201 +++++++
vendor/github.com/MicahParks/jwkset/README.md | 133 +++++
.../github.com/MicahParks/jwkset/constants.go | 167 ++++++
vendor/github.com/MicahParks/jwkset/http.go | 276 ++++++++++
vendor/github.com/MicahParks/jwkset/jwk.go | 494 +++++++++++++++++
.../github.com/MicahParks/jwkset/marshal.go | 511 ++++++++++++++++++
.../github.com/MicahParks/jwkset/storage.go | 314 +++++++++++
vendor/github.com/MicahParks/jwkset/x509.go | 125 +++++
.../github.com/MicahParks/jwkset/x509_gen.sh | 15 +
.../github.com/MicahParks/keyfunc/v3/LICENSE | 201 +++++++
.../MicahParks/keyfunc/v3/README.md | 81 +++
.../MicahParks/keyfunc/v3/keyfunc.go | 177 ++++++
vendor/modules.txt | 6 +
25 files changed, 3012 insertions(+), 59 deletions(-)
create mode 100644 devtools/deployments/opencloud_full/debug-opencloud-paused.yml
create mode 100644 services/auth-api/pkg/metrics/options.go
create mode 100644 services/auth-api/pkg/service/http/v0/service_test.go
create mode 100644 vendor/github.com/MicahParks/jwkset/.gitignore
create mode 100644 vendor/github.com/MicahParks/jwkset/LICENSE
create mode 100644 vendor/github.com/MicahParks/jwkset/README.md
create mode 100644 vendor/github.com/MicahParks/jwkset/constants.go
create mode 100644 vendor/github.com/MicahParks/jwkset/http.go
create mode 100644 vendor/github.com/MicahParks/jwkset/jwk.go
create mode 100644 vendor/github.com/MicahParks/jwkset/marshal.go
create mode 100644 vendor/github.com/MicahParks/jwkset/storage.go
create mode 100644 vendor/github.com/MicahParks/jwkset/x509.go
create mode 100644 vendor/github.com/MicahParks/jwkset/x509_gen.sh
create mode 100644 vendor/github.com/MicahParks/keyfunc/v3/LICENSE
create mode 100644 vendor/github.com/MicahParks/keyfunc/v3/README.md
create mode 100644 vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
diff --git a/devtools/deployments/opencloud_full/debug-opencloud-paused.yml b/devtools/deployments/opencloud_full/debug-opencloud-paused.yml
new file mode 100644
index 000000000..acd010de7
--- /dev/null
+++ b/devtools/deployments/opencloud_full/debug-opencloud-paused.yml
@@ -0,0 +1,7 @@
+---
+services:
+
+ opencloud:
+ command: [ "-c", "opencloud init || true; dlv --listen=:40000 --headless=true --check-go-version=false --api-version=2 --accept-multiclient exec /usr/bin/opencloud server" ]
+ ports:
+ - 40000:40000
diff --git a/go.mod b/go.mod
index f4dd437f0..c6a3eefd6 100644
--- a/go.mod
+++ b/go.mod
@@ -127,6 +127,8 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
+ github.com/MicahParks/jwkset v0.8.0 // indirect
+ github.com/MicahParks/keyfunc/v3 v3.3.11 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
diff --git a/go.sum b/go.sum
index 75e411b26..e3db88ac6 100644
--- a/go.sum
+++ b/go.sum
@@ -82,8 +82,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/MicahParks/jwkset v0.8.0 h1:jHtclI38Gibmu17XMI6+6/UB59srp58pQVxePHRK5o8=
+github.com/MicahParks/jwkset v0.8.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
+github.com/MicahParks/keyfunc/v3 v3.3.11 h1:eA6wNltwdSRX2gtpTwZseBCC9nGeBkI9KxHtTyZbDbo=
+github.com/MicahParks/keyfunc/v3 v3.3.11/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
diff --git a/services/auth-api/pkg/config/config.go b/services/auth-api/pkg/config/config.go
index 68519b9ce..b371c200c 100644
--- a/services/auth-api/pkg/config/config.go
+++ b/services/auth-api/pkg/config/config.go
@@ -24,4 +24,5 @@ type Config struct {
}
type AuthenticationAPI struct {
+ JwkEndpoint string `yaml:"jwk_endpoint"`
}
diff --git a/services/auth-api/pkg/config/defaults/defaultconfig.go b/services/auth-api/pkg/config/defaults/defaultconfig.go
index 5ea474813..6be01b217 100644
--- a/services/auth-api/pkg/config/defaults/defaultconfig.go
+++ b/services/auth-api/pkg/config/defaults/defaultconfig.go
@@ -31,6 +31,9 @@ func DefaultConfig() *config.Config {
Service: config.Service{
Name: "auth-api",
},
+ Authentication: config.AuthenticationAPI{
+ JwkEndpoint: "https://keycloak.opencloud.test/realms/openCloud/protocol/openid-connect/certs",
+ },
}
}
diff --git a/services/auth-api/pkg/metrics/metrics.go b/services/auth-api/pkg/metrics/metrics.go
index 7f2c82866..c684316cd 100644
--- a/services/auth-api/pkg/metrics/metrics.go
+++ b/services/auth-api/pkg/metrics/metrics.go
@@ -7,16 +7,30 @@ var (
Namespace = "opencloud"
// Subsystem defines the subsystem for the defines metrics.
- Subsystem = "authentication-api"
+ Subsystem = "authapi"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
BuildInfo *prometheus.GaugeVec
+ Duration *prometheus.HistogramVec
+ Attempts *prometheus.CounterVec
}
+const (
+ TypeLabel = "type"
+ BasicType = "basic"
+ BearerType = "bearer"
+ UnsupportedType = "unsupported"
+ OutcomeLabel = "outcome"
+ AttemptSuccessOutcome = "success"
+ AttemptFailureOutcome = "failure"
+)
+
// New initializes the available metrics.
-func New() *Metrics {
+func New(opts ...Option) *Metrics {
+ options := newOptions(opts...)
+
m := &Metrics{
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
@@ -24,11 +38,37 @@ func New() *Metrics {
Name: "build_info",
Help: "Build information",
}, []string{"version"}),
+ Duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: Namespace,
+ Subsystem: Subsystem,
+ Name: "authentication_duration_seconds",
+ Help: "Authentication processing time in seconds",
+ }, []string{"type"}),
+ Attempts: prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: Namespace,
+ Subsystem: Subsystem,
+ Name: "athentication_attempts_total",
+ Help: "How many authentication attempts were processed",
+ }, []string{"outcome"}),
}
- _ = prometheus.Register(
- m.BuildInfo,
- )
-
+ if err := prometheus.Register(m.BuildInfo); err != nil {
+ options.Logger.Error().
+ Err(err).
+ Str("metric", "BuildInfo").
+ Msg("Failed to register prometheus metric")
+ }
+ if err := prometheus.Register(m.Duration); err != nil {
+ options.Logger.Error().
+ Err(err).
+ Str("metric", "Duration").
+ Msg("Failed to register prometheus metric")
+ }
+ if err := prometheus.Register(m.Attempts); err != nil {
+ options.Logger.Error().
+ Err(err).
+ Str("metric", "Attempts").
+ Msg("Failed to register prometheus metric")
+ }
return m
}
diff --git a/services/auth-api/pkg/metrics/options.go b/services/auth-api/pkg/metrics/options.go
new file mode 100644
index 000000000..304456e7d
--- /dev/null
+++ b/services/auth-api/pkg/metrics/options.go
@@ -0,0 +1,31 @@
+package metrics
+
+import (
+ "github.com/opencloud-eu/opencloud/pkg/log"
+)
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// Options defines the available options for this package.
+type Options struct {
+ Logger log.Logger
+}
+
+// newOptions initializes the available default options.
+func newOptions(opts ...Option) Options {
+ opt := Options{}
+
+ for _, o := range opts {
+ o(&opt)
+ }
+
+ return opt
+}
+
+// Logger provides a function to set the logger option.
+func Logger(val log.Logger) Option {
+ return func(o *Options) {
+ o.Logger = val
+ }
+}
diff --git a/services/auth-api/pkg/server/http/server.go b/services/auth-api/pkg/server/http/server.go
index 0237b55f5..33141d1fd 100644
--- a/services/auth-api/pkg/server/http/server.go
+++ b/services/auth-api/pkg/server/http/server.go
@@ -15,8 +15,6 @@ import (
func Server(opts ...Option) (http.Service, error) {
options := newOptions(opts...)
- fmt.Printf("===== HTTP addr: %v\n", options.Config.HTTP.Addr)
-
service, err := http.NewService(
http.TLSConfig(options.Config.HTTP.TLS),
http.Logger(options.Logger),
@@ -37,6 +35,8 @@ func Server(opts ...Option) (http.Service, error) {
handle := svc.NewService(
svc.Logger(options.Logger),
svc.Config(options.Config),
+ svc.Metrics(options.Metrics),
+ svc.TraceProvider(options.TraceProvider),
svc.Middleware(
middleware.RealIP,
middleware.RequestID,
diff --git a/services/auth-api/pkg/service/http/v0/option.go b/services/auth-api/pkg/service/http/v0/option.go
index 581a7b663..cb0e6615f 100644
--- a/services/auth-api/pkg/service/http/v0/option.go
+++ b/services/auth-api/pkg/service/http/v0/option.go
@@ -5,6 +5,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
"go.opentelemetry.io/otel/trace"
)
@@ -16,6 +17,7 @@ type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
+ Metrics *metrics.Metrics
TraceProvider trace.TracerProvider
}
@@ -50,3 +52,15 @@ func Middleware(val ...func(http.Handler) http.Handler) Option {
o.Middleware = val
}
}
+
+func TraceProvider(tp trace.TracerProvider) Option {
+ return func(o *Options) {
+ o.TraceProvider = tp
+ }
+}
+
+func Metrics(m *metrics.Metrics) Option {
+ return func(o *Options) {
+ o.Metrics = m
+ }
+}
diff --git a/services/auth-api/pkg/service/http/v0/service.go b/services/auth-api/pkg/service/http/v0/service.go
index a779d33a2..cd3b90d98 100644
--- a/services/auth-api/pkg/service/http/v0/service.go
+++ b/services/auth-api/pkg/service/http/v0/service.go
@@ -1,18 +1,26 @@
package svc
import (
+ "context"
+ "crypto/tls"
"net/http"
"regexp"
"time"
+ "github.com/MicahParks/jwkset"
+ "github.com/MicahParks/keyfunc/v3"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/golang-jwt/jwt/v5"
+
"github.com/riandyrn/otelchi"
+ "go.opentelemetry.io/otel/attribute"
+ oteltrace "go.opentelemetry.io/otel/trace"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/tracing"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
)
// Service defines the service handlers.
@@ -33,13 +41,16 @@ func NewService(opts ...Option) Service {
otelchi.WithChiRoutes(m),
otelchi.WithTracerProvider(options.TraceProvider),
otelchi.WithPropagators(tracing.GetPropagator()),
+ otelchi.WithTraceResponseHeaders(otelchi.TraceHeaderConfig{}),
),
)
- svc := NewAuthenticationApi(options.Config, &options.Logger, m)
+ svc, err := NewAuthenticationApi(options.Config, &options.Logger, options.TraceProvider, m)
+ if err != nil {
+ panic(err) // TODO p.bleser what to do when we encounter an error in a NewService() ?
+ }
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
- r.Get("/", svc.Authenticate)
r.Post("/", svc.Authenticate)
})
@@ -52,84 +63,204 @@ func NewService(opts ...Option) Service {
}
type AuthenticationApi struct {
- config *config.Config
- logger *log.Logger
- mux *chi.Mux
+ config *config.Config
+ logger *log.Logger
+ metrics *metrics.Metrics
+ tracer oteltrace.Tracer
+ mux *chi.Mux
+ refreshCtx context.Context
+ jwksFunc keyfunc.Keyfunc
}
-func NewAuthenticationApi(config *config.Config, logger *log.Logger, mux *chi.Mux) *AuthenticationApi {
- return &AuthenticationApi{
- config: config,
- mux: mux,
- logger: logger,
+func NewAuthenticationApi(
+ config *config.Config,
+ logger *log.Logger,
+ metrics *metrics.Metrics,
+ tracerProvider oteltrace.TracerProvider,
+ mux *chi.Mux,
+) (*AuthenticationApi, error) {
+
+ tracer := tracerProvider.Tracer("instrumentation/" + config.HTTP.Namespace + "/" + config.Service.Name)
+
+ var httpClient *http.Client
+ {
+ tr := http.DefaultTransport.(*http.Transport).Clone()
+ tr.ResponseHeaderTimeout = time.Duration(10) * time.Second
+ tlsConfig := &tls.Config{InsecureSkipVerify: true}
+ tr.TLSClientConfig = tlsConfig
+ h := http.DefaultClient
+ h.Transport = tr
+ httpClient = h
}
+
+ refreshCtx := context.Background()
+
+ storage, err := jwkset.NewStorageFromHTTP(config.Authentication.JwkEndpoint, jwkset.HTTPClientStorageOptions{
+ Client: httpClient,
+ Ctx: refreshCtx,
+ HTTPExpectedStatus: http.StatusOK,
+ HTTPMethod: http.MethodGet,
+ HTTPTimeout: time.Duration(10) * time.Second,
+ NoErrorReturnFirstHTTPReq: true,
+ RefreshInterval: time.Duration(10) * time.Minute,
+ RefreshErrorHandler: func(ctx context.Context, err error) {
+ logger.Error().Err(err).Ctx(ctx).Str("url", config.Authentication.JwkEndpoint).Msg("failed to refresh JWK Set from IDP")
+ },
+ //ValidateOptions: jwkset.JWKValidateOptions{},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ jwksFunc, err := keyfunc.New(keyfunc.Options{
+ Ctx: refreshCtx,
+ UseWhitelist: []jwkset.USE{jwkset.UseSig},
+ Storage: storage,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return &AuthenticationApi{
+ config: config,
+ mux: mux,
+ logger: logger,
+ metrics: metrics,
+ tracer: tracer,
+ refreshCtx: refreshCtx,
+ jwksFunc: jwksFunc,
+ }, nil
}
func (a AuthenticationApi) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}
-type AuthResponse struct {
- Subject string
+type SuccessfulAuthResponse struct {
+ Subject string `json:"subject"`
+ Roles []string `json:"roles,omitempty"`
}
-func (AuthResponse) Render(w http.ResponseWriter, r *http.Request) error {
+func (SuccessfulAuthResponse) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
-var authRegex = regexp.MustCompile("^(i:Basic|Bearer)\\s+(.+)$")
+type FailedAuthResponse struct {
+ Reason string `json:"reason,omitempty"`
+}
+
+func (FailedAuthResponse) Render(w http.ResponseWriter, r *http.Request) error {
+ return nil
+}
+
+type CustomClaims struct {
+ Roles []string `json:"roles,omitempty"`
+ AuthorizedParties jwt.ClaimStrings `json:"azp,omitempty"`
+ SessionId string `json:"sid,omitempty"`
+ AuthenticationContextClassReference string `json:"acr,omitempty"`
+ Scope jwt.ClaimStrings `json:"scope,omitempty"`
+ EmailVerified bool `json:"email_verified,omitempty"`
+ Name string `json:"name,omitempty"`
+ Groups jwt.ClaimStrings `json:"groups,omitempty"`
+ PreferredUsername string `json:"preferred_username,omitempty"`
+ GivenName string `json:"given_name,omitempty"`
+ FamilyName string `json:"family_name,omitempty"`
+ Uuid string `json:"uuid,omitempty"`
+ Email string `json:"email,omitempty"`
+
+ jwt.RegisteredClaims
+}
+
+var authRegex = regexp.MustCompile(`(?i)^(Basic|Bearer)\s+(.+)$`)
+
+func (a AuthenticationApi) failedAuth() {
+ a.metrics.Attempts.WithLabelValues(metrics.OutcomeLabel, metrics.AttemptFailureOutcome).Inc()
+}
+func (a AuthenticationApi) succeededAuth() {
+ a.metrics.Attempts.WithLabelValues(metrics.OutcomeLabel, metrics.AttemptSuccessOutcome).Inc()
+}
func (a AuthenticationApi) Authenticate(w http.ResponseWriter, r *http.Request) {
+ _, span := a.tracer.Start(r.Context(), "authenticate")
+ defer span.End()
+
auth := r.Header.Get("Authorization")
if auth == "" {
+ a.logger.Warn().Msg("missing Authorization header")
w.WriteHeader(http.StatusBadRequest) // authentication header is missing altogether
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Missing Authorization header"})
+ a.failedAuth()
return
}
- matches := authRegex.FindAllString(auth, 2)
- if matches == nil {
+ matches := authRegex.FindStringSubmatch(auth)
+ if matches == nil || len(matches) != 3 {
+ a.logger.Warn().Msg("unsupported Authorization header")
w.WriteHeader(http.StatusBadRequest) // authentication header is unsupported
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Unsupported Authorization header"})
+ a.failedAuth()
return
}
- if matches[0] == "Basic" {
+ if matches[1] == "Basic" {
+ span.SetAttributes(attribute.String("authenticate.scheme", "basic"))
+ a.metrics.Attempts.WithLabelValues(metrics.TypeLabel, metrics.BasicType).Inc()
+
username, password, ok := r.BasicAuth()
if !ok {
+ a.logger.Warn().Msg("failed to decode basic credentials")
w.WriteHeader(http.StatusBadRequest) // failed to decode the basic credentials
- }
- if password == "secret" {
- _ = render.Render(w, r, AuthResponse{Subject: username})
- } else {
- w.WriteHeader(http.StatusUnauthorized)
- }
- } else if matches[0] == "Bearer" {
- claims := jwt.MapClaims{}
- publicKey := nil
- tokenString := matches[1]
- token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
- token.Header["kid"]
- return publicKey, nil
- }, jwt.WithExpirationRequired(), jwt.WithLeeway(5*time.Second))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest) // failed to parse bearer token
- }
- sub, err := token.Claims.GetSubject()
- if err != nil {
- w.WriteHeader(http.StatusBadRequest) // failed to extract sub claim from bearer token
- }
- _ = render.Render(w, r, AuthResponse{Subject: sub})
- } else {
- w.WriteHeader(http.StatusBadRequest) // authentication header is unsupported
- return
- }
-
- // TODO
-
- /*
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Failed to decode basic credentials"})
+ a.failedAuth()
return
}
- */
+ if password == "secret" {
+ _ = render.Render(w, r, SuccessfulAuthResponse{Subject: username})
+ a.succeededAuth()
+ } else {
+ a.logger.Info().Str("username", username).Msg("authentication failed")
+ w.WriteHeader(http.StatusUnauthorized)
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Unauthorized credentials"})
+ a.failedAuth()
+ return
+ }
+ } else if matches[1] == "Bearer" {
+ span.SetAttributes(attribute.String("authenticate.scheme", "bearer"))
+ a.metrics.Attempts.WithLabelValues(metrics.TypeLabel, metrics.BearerType).Inc()
- _ = render.Render(w, r, AuthResponse{Subject: "todo"})
+ claims := &CustomClaims{}
+ tokenString := matches[2]
+ token, err := jwt.ParseWithClaims(tokenString, claims, a.jwksFunc.Keyfunc, jwt.WithExpirationRequired(), jwt.WithLeeway(5*time.Second))
+ if err != nil {
+ a.logger.Warn().Err(err).Msg("failed to parse bearer token")
+ w.WriteHeader(http.StatusBadRequest) // failed to parse bearer token
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Failed to parse bearer token"})
+ return
+ }
+
+ a.logger.Info().Str("type", matches[1]).Interface("header", token.Header).Interface("claims", token.Claims).Bool("valid", token.Valid).Msgf("successfully parsed token")
+
+ if typedClaims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
+ sub := typedClaims.PreferredUsername
+ if sub == "" {
+ sub, err = typedClaims.GetSubject()
+ if err != nil {
+ a.logger.Warn().Err(err).Msg("failed to retrieve sub claim from token")
+ w.WriteHeader(http.StatusBadRequest) // failed to extract sub claim from bearer token
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Failed to extract sub claim from bearer token"})
+ return
+ }
+ }
+ _ = render.Render(w, r, SuccessfulAuthResponse{Subject: sub, Roles: claims.Roles})
+ } else {
+ w.WriteHeader(http.StatusBadRequest) // failed to extract sub claim from bearer token
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Failed to parse bearer token"})
+ return
+ }
+ } else {
+ a.metrics.Attempts.WithLabelValues(metrics.TypeLabel, metrics.UnsupportedType).Inc()
+
+ w.WriteHeader(http.StatusBadRequest) // authentication header is unsupported
+ _ = render.Render(w, r, FailedAuthResponse{Reason: "Unsupported Authorization type"})
+ return
+ }
}
diff --git a/services/auth-api/pkg/service/http/v0/service_test.go b/services/auth-api/pkg/service/http/v0/service_test.go
new file mode 100644
index 000000000..23f3520ce
--- /dev/null
+++ b/services/auth-api/pkg/service/http/v0/service_test.go
@@ -0,0 +1,17 @@
+package svc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRegex(t *testing.T) {
+ require := require.New(t)
+
+ matches := authRegex.FindStringSubmatch("Basic abc")
+ require.NotNil(matches)
+ require.Len(matches, 3)
+ require.Equal("Basic", matches[1])
+ require.Equal("abc", matches[2])
+}
diff --git a/vendor/github.com/MicahParks/jwkset/.gitignore b/vendor/github.com/MicahParks/jwkset/.gitignore
new file mode 100644
index 000000000..040ac50a4
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/.gitignore
@@ -0,0 +1,2 @@
+config.*json
+node_modules
diff --git a/vendor/github.com/MicahParks/jwkset/LICENSE b/vendor/github.com/MicahParks/jwkset/LICENSE
new file mode 100644
index 000000000..05f2ccbd2
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2022 Micah Parks
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/MicahParks/jwkset/README.md b/vendor/github.com/MicahParks/jwkset/README.md
new file mode 100644
index 000000000..e9d511eb3
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/README.md
@@ -0,0 +1,133 @@
+[](https://pkg.go.dev/github.com/MicahParks/jwkset)
+
+# JWK Set (JSON Web Key Set)
+
+This is a JWK Set (JSON Web Key Set) implementation written in Golang.
+
+The goal of this project is to provide a complete implementation of JWK and JWK Sets within the constraints of the
+Golang standard library, without implementing any cryptographic algorithms. For example, `Ed25519` is supported, but
+`Ed448` is not, because the Go standard library does not have a high level implementation of `Ed448`.
+
+If you would like to generate or validate a JWK without writing any Golang code, please visit
+the [Generate a JWK Set](#generate-a-jwk-set) section.
+
+If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the
+[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the
+language of your choice using an OpenAPI interface.
+
+# Generate a JWK Set
+
+If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK
+Set from:
+
+* PEM encoded X.509 Certificates
+* PEM encoded public keys
+* PEM encoded private keys
+
+The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section
+for a list of supported cryptographic key types.
+
+## Website
+
+Visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this website
+by following the instructions in the `README.md` in
+the [website](https://github.com/MicahParks/jwkset/tree/master/website) directory.
+
+## Command line
+
+Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set.
+
+**Install**
+
+```
+go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest
+```
+
+**Usage**
+
+```
+jwksetinfer mykey.pem mycert.crt
+```
+
+## Custom server
+
+This project can be used in creating a custom JWK Set server. A good place to start is `examples/http_server/main.go`.
+
+# Golang JWK Set client
+
+If you are using [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) take a look
+at [`github.com/MicahParks/keyfunc/v3`](https://github.com/MicahParks/keyfunc).
+
+This project can be used to create JWK Set clients. An HTTP client is provided. See a snippet of the usage
+from `examples/default_http_client/main.go` below.
+
+## Create a JWK Set client from the server's HTTP URL.
+
+```go
+jwks, err := jwkset.NewDefaultHTTPClient([]string{server.URL})
+if err != nil {
+ log.Fatalf("Failed to create client JWK set. Error: %s", err)
+}
+```
+
+## Read a key from the client.
+
+```go
+jwk, err = jwks.KeyRead(ctx, myKeyID)
+if err != nil {
+ log.Fatalf("Failed to read key from client JWK set. Error: %s", err)
+}
+```
+
+# Supported keys
+
+This project supports the following key types:
+
+* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only)
+ * Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey`
+* [Elliptic-curve Diffie–Hellman (ECDH)](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) (X25519
+ only)
+ * Go Types: `*ecdh.PrivateKey` and `*ecdh.PublicKey`
+* [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
+ * Go Types: `*ecdsa.PrivateKey` and `*ecdsa.PublicKey`
+* [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem))
+ * Go Types: `*rsa.PrivateKey` and `*rsa.PublicKey`
+* [HMAC](https://en.wikipedia.org/wiki/HMAC), [AES Key Wrap](https://en.wikipedia.org/wiki/Key_Wrap), and other
+ symmetric keys
+ * Go Type: `[]byte`
+
+Cryptographic keys can be added, deleted, and read from the JWK Set. A JSON representation of the JWK Set can be created
+for hosting via HTTPS. This project includes an in-memory storage implementation, but an interface is provided for more
+advanced use cases.
+
+# Notes
+
+This project aims to implement the relevant RFCs to the fullest extent possible using the Go standard library, but does
+not implement any cryptographic algorithms itself.
+
+* RFC 8037 adds support for `Ed448`, `X448`, and `secp256k1`, but there is no Golang standard library support for these
+ key types.
+* In order to be compatible with non-RFC compliant JWK Set providers, this project does not strictly enforce JWK
+ parameters that are integers and have extra or missing leading padding. See the release notes
+ of [`v0.5.15`](https://github.com/MicahParks/jwkset/releases/tag/v0.5.15) for details.
+* `Base64url Encoding` requires that all trailing `=` characters be removed. This project automatically strips any
+ trailing `=` characters in an attempt to be compatible with improper implementations of JWK.
+* This project does not currently support JWK Set encryption using JWE. This would involve implementing the relevant JWE
+ specifications. It may be implemented in the future if there is interest. Open a GitHub issue to express interest.
+
+# Related projects
+
+## [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc)
+
+A JWK Set client for the [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) project.
+
+## [`github.com/MicahParks/jcp`](https://github.com/MicahParks/jcp)
+
+A JWK Set client proxy. JCP for short. This project is a standalone service that uses keyfunc under the hood. It
+primarily exists for these use cases:
+
+The language or shell a program is written in does not have an adequate JWK Set client. Validate JWTs with curl? Why
+not?
+Restrictive networking policies prevent a program from accessing the remote JWK Set directly.
+Many co-located services need to validate JWTs that were signed by a key that lives in a remote JWK Set.
+If you can integrate keyfunc directly into your program, you likely don't need JCP.
diff --git a/vendor/github.com/MicahParks/jwkset/constants.go b/vendor/github.com/MicahParks/jwkset/constants.go
new file mode 100644
index 000000000..15e219e0d
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/constants.go
@@ -0,0 +1,167 @@
+package jwkset
+
+const (
+ // HeaderKID is a JWT header for the key ID.
+ HeaderKID = "kid"
+)
+
+// These are string constants set in https://www.iana.org/assignments/jose/jose.xhtml
+// See their respective types for more information.
+const (
+ AlgHS256 ALG = "HS256"
+ AlgHS384 ALG = "HS384"
+ AlgHS512 ALG = "HS512"
+ AlgRS256 ALG = "RS256"
+ AlgRS384 ALG = "RS384"
+ AlgRS512 ALG = "RS512"
+ AlgES256 ALG = "ES256"
+ AlgES384 ALG = "ES384"
+ AlgES512 ALG = "ES512"
+ AlgPS256 ALG = "PS256"
+ AlgPS384 ALG = "PS384"
+ AlgPS512 ALG = "PS512"
+ AlgNone ALG = "none"
+ AlgRSA1_5 ALG = "RSA1_5"
+ AlgRSAOAEP ALG = "RSA-OAEP"
+ AlgRSAOAEP256 ALG = "RSA-OAEP-256"
+ AlgA128KW ALG = "A128KW"
+ AlgA192KW ALG = "A192KW"
+ AlgA256KW ALG = "A256KW"
+ AlgDir ALG = "dir"
+ AlgECDHES ALG = "ECDH-ES"
+ AlgECDHESA128KW ALG = "ECDH-ES+A128KW"
+ AlgECDHESA192KW ALG = "ECDH-ES+A192KW"
+ AlgECDHESA256KW ALG = "ECDH-ES+A256KW"
+ AlgA128GCMKW ALG = "A128GCMKW"
+ AlgA192GCMKW ALG = "A192GCMKW"
+ AlgA256GCMKW ALG = "A256GCMKW"
+ AlgPBES2HS256A128KW ALG = "PBES2-HS256+A128KW"
+ AlgPBES2HS384A192KW ALG = "PBES2-HS384+A192KW"
+ AlgPBES2HS512A256KW ALG = "PBES2-HS512+A256KW"
+ AlgA128CBCHS256 ALG = "A128CBC-HS256"
+ AlgA192CBCHS384 ALG = "A192CBC-HS384"
+ AlgA256CBCHS512 ALG = "A256CBC-HS512"
+ AlgA128GCM ALG = "A128GCM"
+ AlgA192GCM ALG = "A192GCM"
+ AlgA256GCM ALG = "A256GCM"
+ AlgEdDSA ALG = "EdDSA"
+ AlgRS1 ALG = "RS1" // Prohibited.
+ AlgRSAOAEP384 ALG = "RSA-OAEP-384"
+ AlgRSAOAEP512 ALG = "RSA-OAEP-512"
+ AlgA128CBC ALG = "A128CBC" // Prohibited.
+ AlgA192CBC ALG = "A192CBC" // Prohibited.
+ AlgA256CBC ALG = "A256CBC" // Prohibited.
+ AlgA128CTR ALG = "A128CTR" // Prohibited.
+ AlgA192CTR ALG = "A192CTR" // Prohibited.
+ AlgA256CTR ALG = "A256CTR" // Prohibited.
+ AlgHS1 ALG = "HS1" // Prohibited.
+ AlgES256K ALG = "ES256K"
+
+ CrvP256 CRV = "P-256"
+ CrvP384 CRV = "P-384"
+ CrvP521 CRV = "P-521"
+ CrvEd25519 CRV = "Ed25519"
+ CrvEd448 CRV = "Ed448"
+ CrvX25519 CRV = "X25519"
+ CrvX448 CRV = "X448"
+ CrvSECP256K1 CRV = "secp256k1"
+
+ KeyOpsSign KEYOPS = "sign"
+ KeyOpsVerify KEYOPS = "verify"
+ KeyOpsEncrypt KEYOPS = "encrypt"
+ KeyOpsDecrypt KEYOPS = "decrypt"
+ KeyOpsWrapKey KEYOPS = "wrapKey"
+ KeyOpsUnwrapKey KEYOPS = "unwrapKey"
+ KeyOpsDeriveKey KEYOPS = "deriveKey"
+ KeyOpsDeriveBits KEYOPS = "deriveBits"
+
+ KtyEC KTY = "EC"
+ KtyOKP KTY = "OKP"
+ KtyRSA KTY = "RSA"
+ KtyOct KTY = "oct"
+
+ UseEnc USE = "enc"
+ UseSig USE = "sig"
+)
+
+// ALG is a set of "JSON Web Signature and Encryption Algorithms" types from
+// https://www.iana.org/assignments/jose/jose.xhtml as defined in
+// https://www.rfc-editor.org/rfc/rfc7518#section-7.1
+type ALG string
+
+func (alg ALG) IANARegistered() bool {
+ switch alg {
+ case AlgHS256, AlgHS384, AlgHS512, AlgRS256, AlgRS384, AlgRS512, AlgES256, AlgES384, AlgES512, AlgPS256, AlgPS384,
+ AlgPS512, AlgNone, AlgRSA1_5, AlgRSAOAEP, AlgRSAOAEP256, AlgA128KW, AlgA192KW, AlgA256KW, AlgDir, AlgECDHES,
+ AlgECDHESA128KW, AlgECDHESA192KW, AlgECDHESA256KW, AlgA128GCMKW, AlgA192GCMKW, AlgA256GCMKW,
+ AlgPBES2HS256A128KW, AlgPBES2HS384A192KW, AlgPBES2HS512A256KW, AlgA128CBCHS256, AlgA192CBCHS384,
+ AlgA256CBCHS512, AlgA128GCM, AlgA192GCM, AlgA256GCM, AlgEdDSA, AlgRS1, AlgRSAOAEP384, AlgRSAOAEP512, AlgA128CBC,
+ AlgA192CBC, AlgA256CBC, AlgA128CTR, AlgA192CTR, AlgA256CTR, AlgHS1, AlgES256K, "":
+ return true
+ }
+ return false
+}
+func (alg ALG) String() string {
+ return string(alg)
+}
+
+// CRV is a set of "JSON Web Key Elliptic Curve" types from https://www.iana.org/assignments/jose/jose.xhtml as
+// mentioned in https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1
+type CRV string
+
+func (crv CRV) IANARegistered() bool {
+ switch crv {
+ case CrvP256, CrvP384, CrvP521, CrvEd25519, CrvEd448, CrvX25519, CrvX448, CrvSECP256K1, "":
+ return true
+ }
+ return false
+}
+func (crv CRV) String() string {
+ return string(crv)
+}
+
+// KEYOPS is a set of "JSON Web Key Operations" from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
+// https://www.rfc-editor.org/rfc/rfc7517#section-4.3
+type KEYOPS string
+
+func (keyopts KEYOPS) IANARegistered() bool {
+ switch keyopts {
+ case KeyOpsSign, KeyOpsVerify, KeyOpsEncrypt, KeyOpsDecrypt, KeyOpsWrapKey, KeyOpsUnwrapKey, KeyOpsDeriveKey,
+ KeyOpsDeriveBits:
+ return true
+ }
+ return false
+}
+func (keyopts KEYOPS) String() string {
+ return string(keyopts)
+}
+
+// KTY is a set of "JSON Web Key Types" from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
+// https://www.rfc-editor.org/rfc/rfc7517#section-4.1
+type KTY string
+
+func (kty KTY) IANARegistered() bool {
+ switch kty {
+ case KtyEC, KtyOKP, KtyRSA, KtyOct:
+ return true
+ }
+ return false
+}
+func (kty KTY) String() string {
+ return string(kty)
+}
+
+// USE is a set of "JSON Web Key Use" types from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
+// https://www.rfc-editor.org/rfc/rfc7517#section-4.2
+type USE string
+
+func (use USE) IANARegistered() bool {
+ switch use {
+ case UseEnc, UseSig, "":
+ return true
+ }
+ return false
+}
+func (use USE) String() string {
+ return string(use)
+}
diff --git a/vendor/github.com/MicahParks/jwkset/http.go b/vendor/github.com/MicahParks/jwkset/http.go
new file mode 100644
index 000000000..36f151c24
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/http.go
@@ -0,0 +1,276 @@
+package jwkset
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log/slog"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+var (
+ // ErrNewClient fails to create a new JWK Set client.
+ ErrNewClient = errors.New("failed to create new JWK Set client")
+)
+
+// HTTPClientOptions are options for creating a new JWK Set client.
+type HTTPClientOptions struct {
+ // Given contains keys known from outside HTTP URLs.
+ Given Storage
+ // HTTPURLs are a mapping of HTTP URLs to JWK Set endpoints to storage implementations for the keys located at the
+ // URL. If empty, HTTP will not be used.
+ HTTPURLs map[string]Storage
+ // PrioritizeHTTP is a flag that indicates whether keys from the HTTP URL should be prioritized over keys from the
+ // given storage.
+ PrioritizeHTTP bool
+ // RateLimitWaitMax is the timeout for waiting for rate limiting to end.
+ RateLimitWaitMax time.Duration
+ // RefreshUnknownKID is non-nil to indicate that remote HTTP resources should be refreshed if a key with an unknown
+ // key ID is trying to be read. This makes reading methods block until the context is over, a key with the matching
+ // key ID is found in a refreshed remote resource, or all refreshes complete.
+ RefreshUnknownKID *rate.Limiter
+}
+
+// Client is a JWK Set client.
+type httpClient struct {
+ given Storage
+ httpURLs map[string]Storage
+ prioritizeHTTP bool
+ rateLimitWaitMax time.Duration
+ refreshUnknownKID *rate.Limiter
+}
+
+// NewHTTPClient creates a new JWK Set client from remote HTTP resources.
+func NewHTTPClient(options HTTPClientOptions) (Storage, error) {
+ if options.Given == nil && len(options.HTTPURLs) == 0 {
+ return nil, fmt.Errorf("%w: no given keys or HTTP URLs", ErrNewClient)
+ }
+ for u, store := range options.HTTPURLs {
+ if store == nil {
+ var err error
+ options.HTTPURLs[u], err = NewStorageFromHTTP(u, HTTPClientStorageOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create HTTP client storage for %q: %w", u, errors.Join(err, ErrNewClient))
+ }
+ }
+ }
+ given := options.Given
+ if given == nil {
+ given = NewMemoryStorage()
+ }
+ c := httpClient{
+ given: given,
+ httpURLs: options.HTTPURLs,
+ prioritizeHTTP: options.PrioritizeHTTP,
+ rateLimitWaitMax: options.RateLimitWaitMax,
+ refreshUnknownKID: options.RefreshUnknownKID,
+ }
+ return c, nil
+}
+
+// NewDefaultHTTPClient creates a new JWK Set client with default options from remote HTTP resources.
+//
+// The default behavior is to:
+// 1. Refresh remote HTTP resources every hour.
+// 2. Prioritize keys from remote HTTP resources over keys from the given storage.
+// 3. Refresh remote HTTP resources if a key with an unknown key ID is trying to be read, with a rate limit of 5 minutes.
+// 4. Log to slog.Default() if a refresh fails.
+func NewDefaultHTTPClient(urls []string) (Storage, error) {
+ return NewDefaultHTTPClientCtx(context.Background(), urls)
+}
+
+// NewDefaultHTTPClientCtx is the same as NewDefaultHTTPClient, but with a context that can end the refresh goroutine.
+func NewDefaultHTTPClientCtx(ctx context.Context, urls []string) (Storage, error) {
+ clientOptions := HTTPClientOptions{
+ HTTPURLs: make(map[string]Storage),
+ RateLimitWaitMax: time.Minute,
+ RefreshUnknownKID: rate.NewLimiter(rate.Every(5*time.Minute), 1),
+ }
+ for _, u := range urls {
+ refreshErrorHandler := func(ctx context.Context, err error) {
+ slog.Default().ErrorContext(ctx, "Failed to refresh HTTP JWK Set from remote HTTP resource.",
+ "error", err,
+ "url", u,
+ )
+ }
+ options := HTTPClientStorageOptions{
+ Ctx: ctx,
+ NoErrorReturnFirstHTTPReq: true,
+ RefreshErrorHandler: refreshErrorHandler,
+ RefreshInterval: time.Hour,
+ }
+ c, err := NewStorageFromHTTP(u, options)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create HTTP client storage for %q: %w", u, errors.Join(err, ErrNewClient))
+ }
+ clientOptions.HTTPURLs[u] = c
+ }
+ return NewHTTPClient(clientOptions)
+}
+
+func (c httpClient) KeyDelete(ctx context.Context, keyID string) (ok bool, err error) {
+ ok, err = c.given.KeyDelete(ctx, keyID)
+ if err != nil && !errors.Is(err, ErrKeyNotFound) {
+ return false, fmt.Errorf("failed to delete key with ID %q from given storage due to error: %w", keyID, err)
+ }
+ if ok {
+ return true, nil
+ }
+ for _, store := range c.httpURLs {
+ ok, err = store.KeyDelete(ctx, keyID)
+ if err != nil && !errors.Is(err, ErrKeyNotFound) {
+ return false, fmt.Errorf("failed to delete key with ID %q from HTTP storage due to error: %w", keyID, err)
+ }
+ if ok {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+func (c httpClient) KeyRead(ctx context.Context, keyID string) (jwk JWK, err error) {
+ if !c.prioritizeHTTP {
+ jwk, err = c.given.KeyRead(ctx, keyID)
+ switch {
+ case errors.Is(err, ErrKeyNotFound):
+ // Do nothing.
+ case err != nil:
+ return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in given storage due to error: %w", keyID, err)
+ default:
+ return jwk, nil
+ }
+ }
+ for _, store := range c.httpURLs {
+ jwk, err = store.KeyRead(ctx, keyID)
+ switch {
+ case errors.Is(err, ErrKeyNotFound):
+ continue
+ case err != nil:
+ return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in HTTP storage due to error: %w", keyID, err)
+ default:
+ return jwk, nil
+ }
+ }
+ if c.prioritizeHTTP {
+ jwk, err = c.given.KeyRead(ctx, keyID)
+ switch {
+ case errors.Is(err, ErrKeyNotFound):
+ // Do nothing.
+ case err != nil:
+ return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in given storage due to error: %w", keyID, err)
+ default:
+ return jwk, nil
+ }
+ }
+ if c.refreshUnknownKID != nil {
+ var cancel context.CancelFunc = func() {}
+ if c.rateLimitWaitMax > 0 {
+ ctx, cancel = context.WithTimeout(ctx, c.rateLimitWaitMax)
+ }
+ defer cancel()
+ err = c.refreshUnknownKID.Wait(ctx)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to wait for JWK Set refresh rate limiter due to error: %w", err)
+ }
+ for _, store := range c.httpURLs {
+ s, ok := store.(httpStorage)
+ if !ok {
+ continue
+ }
+ err = s.refresh(ctx)
+ if err != nil {
+ if s.options.RefreshErrorHandler != nil {
+ s.options.RefreshErrorHandler(ctx, err)
+ }
+ continue
+ }
+ jwk, err = store.KeyRead(ctx, keyID)
+ switch {
+ case errors.Is(err, ErrKeyNotFound):
+ // Do nothing.
+ case err != nil:
+ return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in HTTP storage due to error: %w", keyID, err)
+ default:
+ return jwk, nil
+ }
+ }
+ }
+ return JWK{}, fmt.Errorf("%w %q", ErrKeyNotFound, keyID)
+}
+func (c httpClient) KeyReadAll(ctx context.Context) ([]JWK, error) {
+ jwks, err := c.given.KeyReadAll(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to snapshot given keys due to error: %w", err)
+ }
+ for u, store := range c.httpURLs {
+ j, err := store.KeyReadAll(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to snapshot HTTP keys from %q due to error: %w", u, err)
+ }
+ jwks = append(jwks, j...)
+ }
+ return jwks, nil
+}
+func (c httpClient) KeyWrite(ctx context.Context, jwk JWK) error {
+ return c.given.KeyWrite(ctx, jwk)
+}
+
+func (c httpClient) JSON(ctx context.Context) (json.RawMessage, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.JSON(ctx)
+}
+func (c httpClient) JSONPublic(ctx context.Context) (json.RawMessage, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.JSONPublic(ctx)
+}
+func (c httpClient) JSONPrivate(ctx context.Context) (json.RawMessage, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.JSONPrivate(ctx)
+}
+func (c httpClient) JSONWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (json.RawMessage, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.JSONWithOptions(ctx, marshalOptions, validationOptions)
+}
+func (c httpClient) Marshal(ctx context.Context) (JWKSMarshal, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return JWKSMarshal{}, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.Marshal(ctx)
+}
+func (c httpClient) MarshalWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (JWKSMarshal, error) {
+ m, err := c.combineStorage(ctx)
+ if err != nil {
+ return JWKSMarshal{}, fmt.Errorf("failed to combine storage due to error: %w", err)
+ }
+ return m.MarshalWithOptions(ctx, marshalOptions, validationOptions)
+}
+
+func (c httpClient) combineStorage(ctx context.Context) (Storage, error) {
+ jwks, err := c.KeyReadAll(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to snapshot keys due to error: %w", err)
+ }
+ m := NewMemoryStorage()
+ for _, jwk := range jwks {
+ err = m.KeyWrite(ctx, jwk)
+ if err != nil {
+ return nil, fmt.Errorf("failed to write key to memory storage due to error: %w", err)
+ }
+ }
+ return m, nil
+}
diff --git a/vendor/github.com/MicahParks/jwkset/jwk.go b/vendor/github.com/MicahParks/jwkset/jwk.go
new file mode 100644
index 000000000..9fd55580d
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/jwk.go
@@ -0,0 +1,494 @@
+package jwkset
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "net/url"
+ "slices"
+ "time"
+)
+
+var (
+ // ErrPadding indicates that there is invalid padding.
+ ErrPadding = errors.New("padding error")
+)
+
+// JWK represents a JSON Web Key.
+type JWK struct {
+ key any
+ marshal JWKMarshal
+ options JWKOptions
+}
+
+// JWKMarshalOptions are used to specify options for JSON marshaling a JWK.
+type JWKMarshalOptions struct {
+ // Private is used to indicate that the JWK's private key material should be JSON marshaled and unmarshalled. This
+ // includes symmetric and asymmetric keys. Setting this to true is the only way to marshal and unmarshal symmetric
+ // keys.
+ Private bool
+}
+
+// JWKX509Options holds the X.509 certificate information for a JWK. This data structure is not used for JSON marshaling.
+type JWKX509Options struct {
+ // X5C contains a chain of one or more PKIX certificates. The PKIX certificate containing the key value MUST be the
+ // first certificate.
+ X5C []*x509.Certificate // The PKIX certificate containing the key value MUST be the first certificate.
+
+ // X5T is calculated automatically.
+ // X5TS256 is calculated automatically.
+
+ // X5U Is a URI that refers to a resource for an X.509 public key certificate or certificate chain.
+ X5U string // https://www.rfc-editor.org/rfc/rfc7517#section-4.6
+}
+
+// JWKValidateOptions are used to specify options for validating a JWK.
+type JWKValidateOptions struct {
+ /*
+ This package intentionally does not confirm if certificate's usage or compare that to the JWK's use parameter.
+ Please open a GitHub issue if you think this should be an option.
+ */
+ // CheckX509ValidTime is used to indicate that the X.509 certificate's valid time should be checked.
+ CheckX509ValidTime bool
+ // GetX5U is used to get and validate the X.509 certificate from the X5U URI. Use DefaultGetX5U for the default
+ // behavior.
+ GetX5U func(x5u *url.URL) ([]*x509.Certificate, error)
+ // SkipAll is used to skip all validation.
+ SkipAll bool
+ // SkipKeyOps is used to skip validation of the key operations (key_ops).
+ SkipKeyOps bool
+ // SkipMetadata skips checking if the JWKMetadataOptions match the JWKMarshal.
+ SkipMetadata bool
+ // SkipUse is used to skip validation of the key use (use).
+ SkipUse bool
+ // SkipX5UScheme is used to skip checking if the X5U URI scheme is https.
+ SkipX5UScheme bool
+ // StrictPadding is used to indicate that the JWK should be validated with strict padding.
+ StrictPadding bool
+}
+
+// JWKMetadataOptions are direct passthroughs into the JWKMarshal.
+type JWKMetadataOptions struct {
+ // ALG is the algorithm (alg).
+ ALG ALG
+ // KID is the key ID (kid).
+ KID string
+ // KEYOPS is the key operations (key_ops).
+ KEYOPS []KEYOPS
+ // USE is the key use (use).
+ USE USE
+}
+
+// JWKOptions are used to specify options for marshaling a JSON Web Key.
+type JWKOptions struct {
+ Marshal JWKMarshalOptions
+ Metadata JWKMetadataOptions
+ Validate JWKValidateOptions
+ X509 JWKX509Options
+}
+
+// NewJWKFromKey uses the given key and options to create a JWK. It is possible to provide a private key with an X.509
+// certificate, which will be validated to contain the correct public key.
+func NewJWKFromKey(key any, options JWKOptions) (JWK, error) {
+ marshal, err := keyMarshal(key, options)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to marshal JSON Web Key: %w", err)
+ }
+ switch key.(type) {
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ if options.Metadata.ALG == "" {
+ options.Metadata.ALG = AlgEdDSA
+ } else if options.Metadata.ALG != AlgEdDSA {
+ return JWK{}, fmt.Errorf("%w: invalid ALG for Ed25519 key: %q", ErrOptions, options.Metadata.ALG)
+ }
+ }
+ j := JWK{
+ key: key,
+ marshal: marshal,
+ options: options,
+ }
+ err = j.Validate()
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
+ }
+ return j, nil
+}
+
+// NewJWKFromRawJSON uses the given raw JSON to create a JWK.
+func NewJWKFromRawJSON(j json.RawMessage, marshalOptions JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
+ marshal := JWKMarshal{}
+ err := json.Unmarshal(j, &marshal)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to unmarshal JSON Web Key: %w", err)
+ }
+ return NewJWKFromMarshal(marshal, marshalOptions, validateOptions)
+}
+
+// NewJWKFromMarshal transforms a JWKMarshal into a JWK.
+func NewJWKFromMarshal(marshal JWKMarshal, marshalOptions JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
+ j, err := keyUnmarshal(marshal, marshalOptions, validateOptions)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to unmarshal JSON Web Key: %w", err)
+ }
+ err = j.Validate()
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
+ }
+ return j, nil
+}
+
+// NewJWKFromX5C uses the X.509 X5C information in the options to create a JWK.
+func NewJWKFromX5C(options JWKOptions) (JWK, error) {
+ if len(options.X509.X5C) == 0 {
+ return JWK{}, fmt.Errorf("%w: no X.509 certificates provided", ErrOptions)
+ }
+ cert := options.X509.X5C[0]
+ marshal, err := keyMarshal(cert.PublicKey, options)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to marshal JSON Web Key: %w", err)
+ }
+
+ if cert.PublicKeyAlgorithm == x509.Ed25519 {
+ if options.Metadata.ALG != "" && options.Metadata.ALG != AlgEdDSA {
+ return JWK{}, fmt.Errorf("%w: ALG in metadata does not match ALG in X.509 certificate", errors.Join(ErrOptions, ErrX509Mismatch))
+ }
+ options.Metadata.ALG = AlgEdDSA
+ }
+
+ j := JWK{
+ key: options.X509.X5C[0].PublicKey,
+ marshal: marshal,
+ options: options,
+ }
+ err = j.Validate()
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
+ }
+ return j, nil
+}
+
+// NewJWKFromX5U uses the X.509 X5U information in the options to create a JWK.
+func NewJWKFromX5U(options JWKOptions) (JWK, error) {
+ if options.X509.X5U == "" {
+ return JWK{}, fmt.Errorf("%w: no X.509 URI provided", ErrOptions)
+ }
+ u, err := url.ParseRequestURI(options.X509.X5U)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to parse X5U URI: %w", errors.Join(ErrOptions, err))
+ }
+ if !options.Validate.SkipX5UScheme && u.Scheme != "https" {
+ return JWK{}, fmt.Errorf("%w: X5U URI scheme must be https", errors.Join(ErrOptions))
+ }
+ get := options.Validate.GetX5U
+ if get == nil {
+ get = DefaultGetX5U
+ }
+ certs, err := get(u)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to get X5U URI: %w", err)
+ }
+ options.X509.X5C = certs
+ jwk, err := NewJWKFromX5C(options)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to create JWK from fetched X5U assets: %w", err)
+ }
+ return jwk, nil
+}
+
+// Key returns the public or private cryptographic key associated with the JWK.
+func (j JWK) Key() any {
+ return j.key
+}
+
+// Marshal returns Go type that can be marshalled into JSON.
+func (j JWK) Marshal() JWKMarshal {
+ return j.marshal
+}
+
+// X509 returns the X.509 certificate information for the JWK.
+func (j JWK) X509() JWKX509Options {
+ return j.options.X509
+}
+
+// Validate validates the JWK. The JWK is automatically validated when created from a function in this package.
+func (j JWK) Validate() error {
+ if j.options.Validate.SkipAll {
+ return nil
+ }
+ if !j.marshal.KTY.IANARegistered() {
+ return fmt.Errorf("%w: invalid or unsupported key type %q", ErrJWKValidation, j.marshal.KTY)
+ }
+
+ if !j.options.Validate.SkipUse && !j.marshal.USE.IANARegistered() {
+ return fmt.Errorf("%w: invalid or unsupported key use %q", ErrJWKValidation, j.marshal.USE)
+ }
+
+ if !j.options.Validate.SkipKeyOps {
+ for _, o := range j.marshal.KEYOPS {
+ if !o.IANARegistered() {
+ return fmt.Errorf("%w: invalid or unsupported key_opt %q", ErrJWKValidation, o)
+ }
+ }
+ }
+
+ if !j.options.Validate.SkipMetadata {
+ if j.marshal.ALG != j.options.Metadata.ALG {
+ return fmt.Errorf("%w: ALG in marshal does not match ALG in options", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ if j.marshal.KID != j.options.Metadata.KID {
+ return fmt.Errorf("%w: KID in marshal does not match KID in options", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ if !slices.Equal(j.marshal.KEYOPS, j.options.Metadata.KEYOPS) {
+ return fmt.Errorf("%w: KEYOPS in marshal does not match KEYOPS in options", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ if j.marshal.USE != j.options.Metadata.USE {
+ return fmt.Errorf("%w: USE in marshal does not match USE in options", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ }
+
+ if len(j.options.X509.X5C) > 0 {
+ cert := j.options.X509.X5C[0]
+ i := cert.PublicKey
+ switch k := j.key.(type) {
+ // ECDH keys are not used to sign certificates.
+ case *ecdsa.PublicKey:
+ pub, ok := i.(*ecdsa.PublicKey)
+ if !ok {
+ return fmt.Errorf("%w: Golang key is type *ecdsa.Public but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
+ }
+ if !k.Equal(pub) {
+ return fmt.Errorf("%w: Golang *ecdsa.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
+ }
+ case ed25519.PublicKey:
+ pub, ok := i.(ed25519.PublicKey)
+ if !ok {
+ return fmt.Errorf("%w: Golang key is type ed25519.PublicKey but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
+ }
+ if !bytes.Equal(k, pub) {
+ return fmt.Errorf("%w: Golang ed25519.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
+ }
+ case *rsa.PublicKey:
+ pub, ok := i.(*rsa.PublicKey)
+ if !ok {
+ return fmt.Errorf("%w: Golang key is type *rsa.PublicKey but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
+ }
+ if !k.Equal(pub) {
+ return fmt.Errorf("%w: Golang *rsa.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
+ }
+ default:
+ return fmt.Errorf("%w: Golang key is type %T, which is not supported, so it cannot be compared to given X.509 certificates", errors.Join(ErrJWKValidation, ErrUnsupportedKey, ErrX509Mismatch), j.key)
+ }
+ if cert.PublicKeyAlgorithm == x509.Ed25519 {
+ if j.marshal.ALG != AlgEdDSA {
+ return fmt.Errorf("%w: ALG in marshal does not match ALG in X.509 certificate", errors.Join(ErrJWKValidation, ErrX509Mismatch))
+ }
+ }
+ if j.options.Validate.CheckX509ValidTime {
+ now := time.Now()
+ if now.Before(cert.NotBefore) {
+ return fmt.Errorf("%w: X.509 certificate is not yet valid", ErrJWKValidation)
+ }
+ if now.After(cert.NotAfter) {
+ return fmt.Errorf("%w: X.509 certificate is expired", ErrJWKValidation)
+ }
+ }
+ }
+
+ marshalled, err := keyMarshal(j.key, j.options)
+ if err != nil {
+ return fmt.Errorf("failed to marshal JSON Web Key: %w", errors.Join(ErrJWKValidation, err))
+ }
+
+ // Remove automatically computed thumbprints if not set in given JWK.
+ if j.marshal.X5T == "" {
+ marshalled.X5T = ""
+ }
+ if j.marshal.X5TS256 == "" {
+ marshalled.X5TS256 = ""
+ }
+
+ canComputeThumbprint := len(j.marshal.X5C) > 0
+ if j.marshal.X5T != marshalled.X5T && canComputeThumbprint {
+ return fmt.Errorf("%w: X5T in marshal does not match X5T in marshalled", ErrJWKValidation)
+ }
+ if j.marshal.X5TS256 != marshalled.X5TS256 && canComputeThumbprint {
+ return fmt.Errorf("%w: X5TS256 in marshal does not match X5TS256 in marshalled", ErrJWKValidation)
+ }
+ if j.marshal.CRV != marshalled.CRV {
+ return fmt.Errorf("%w: CRV in marshal does not match CRV in marshalled", ErrJWKValidation)
+ }
+ switch j.marshal.KTY {
+ case KtyEC:
+ err = cmpBase64Int(j.marshal.X, marshalled.X, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: X in marshal does not match X in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.Y, marshalled.Y, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: Y in marshal does not match Y in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.D, marshalled.D, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: D in marshal does not match D in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ case KtyOKP:
+ if j.marshal.X != marshalled.X {
+ return fmt.Errorf("%w: X in marshal does not match X in marshalled", ErrJWKValidation)
+ }
+ if j.marshal.D != marshalled.D {
+ return fmt.Errorf("%w: D in marshal does not match D in marshalled", ErrJWKValidation)
+ }
+ case KtyRSA:
+ err = cmpBase64Int(j.marshal.D, marshalled.D, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: D in marshal does not match D in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.N, marshalled.N, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: N in marshal does not match N in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.E, marshalled.E, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: E in marshal does not match E in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.P, marshalled.P, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: P in marshal does not match P in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.Q, marshalled.Q, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: Q in marshal does not match Q in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.DP, marshalled.DP, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: DP in marshal does not match DP in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ err = cmpBase64Int(j.marshal.DQ, marshalled.DQ, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: DQ in marshal does not match DQ in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ if len(j.marshal.OTH) != len(marshalled.OTH) {
+ return fmt.Errorf("%w: OTH in marshal does not match OTH in marshalled", ErrJWKValidation)
+ }
+ for i, o := range j.marshal.OTH {
+ err = cmpBase64Int(o.R, marshalled.OTH[i].R, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
+ }
+ err = cmpBase64Int(o.D, marshalled.OTH[i].D, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
+ }
+ err = cmpBase64Int(o.T, marshalled.OTH[i].T, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
+ }
+ }
+ case KtyOct:
+ err = cmpBase64Int(j.marshal.K, marshalled.K, j.options.Validate.StrictPadding)
+ if err != nil {
+ return fmt.Errorf("%w: K in marshal does not match K in marshalled", errors.Join(ErrJWKValidation, err))
+ }
+ default:
+ return fmt.Errorf("%w: invalid or unsupported key type %q", ErrJWKValidation, j.marshal.KTY)
+ }
+
+ // Saved for last because it may involve a network request.
+ if j.marshal.X5U != "" || j.options.X509.X5U != "" {
+ if j.marshal.X5U != j.options.X509.X5U {
+ return fmt.Errorf("%w: X5U in marshal does not match X5U in options", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ u, err := url.ParseRequestURI(j.marshal.X5U)
+ if err != nil {
+ return fmt.Errorf("failed to parse X5U URI: %w", errors.Join(ErrJWKValidation, ErrOptions, err))
+ }
+ if !j.options.Validate.SkipX5UScheme && u.Scheme != "https" {
+ return fmt.Errorf("%w: X5U URI scheme must be https", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ if j.options.Validate.GetX5U != nil {
+ certs, err := j.options.Validate.GetX5U(u)
+ if err != nil {
+ return fmt.Errorf("failed to get X5U URI: %w", errors.Join(ErrJWKValidation, ErrOptions, err))
+ }
+ if len(certs) == 0 {
+ return fmt.Errorf("%w: X5U URI did not return any certificates", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ larger := certs
+ smaller := j.options.X509.X5C
+ if len(j.options.X509.X5C) > len(certs) {
+ larger = j.options.X509.X5C
+ smaller = certs
+ }
+ for i, c := range smaller {
+ if !c.Equal(larger[i]) {
+ return fmt.Errorf("%w: the X5C and X5U (remote resource) parameters are not a full or partial match", errors.Join(ErrJWKValidation, ErrOptions))
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// DefaultGetX5U is the default implementation of the GetX5U field for JWKValidateOptions.
+func DefaultGetX5U(u *url.URL) ([]*x509.Certificate, error) {
+ timeout := time.Minute
+ ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("%w: timeout of %s reached", ErrGetX5U, timeout.String()))
+ defer cancel()
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create X5U request: %w", errors.Join(ErrGetX5U, err))
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to do X5U request: %w", errors.Join(ErrGetX5U, err))
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("%w: X5U request returned status code %d", ErrGetX5U, resp.StatusCode)
+ }
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read X5U response body: %w", errors.Join(ErrGetX5U, err))
+ }
+ certs, err := LoadCertificates(b)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse X5U response body: %w", errors.Join(ErrGetX5U, err))
+ }
+ return certs, nil
+}
+
+func cmpBase64Int(first, second string, strictPadding bool) error {
+ if first == second {
+ return nil
+ }
+ b, err := base64.RawURLEncoding.DecodeString(first)
+ if err != nil {
+ return fmt.Errorf("failed to decode Base64 raw URL decode first string: %w", err)
+ }
+ fLen := len(b)
+ f := new(big.Int).SetBytes(b)
+ b, err = base64.RawURLEncoding.DecodeString(second)
+ if err != nil {
+ return fmt.Errorf("failed to decode Base64 raw URL decode second string: %w", err)
+ }
+ sLen := len(b)
+ s := new(big.Int).SetBytes(b)
+ if f.Cmp(s) != 0 {
+ return fmt.Errorf("%w: the parsed integers do not match", ErrJWKValidation)
+ }
+ if strictPadding && fLen != sLen {
+ return fmt.Errorf("%w: the Base64 raw URL inputs do not have matching padding", errors.Join(ErrJWKValidation, ErrPadding))
+ }
+ return nil
+}
diff --git a/vendor/github.com/MicahParks/jwkset/marshal.go b/vendor/github.com/MicahParks/jwkset/marshal.go
new file mode 100644
index 000000000..c604e22df
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/marshal.go
@@ -0,0 +1,511 @@
+package jwkset
+
+import (
+ "context"
+ "crypto/ecdh"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "math"
+ "math/big"
+ "slices"
+ "strings"
+)
+
+var (
+ // ErrGetX5U indicates there was an error getting the X5U remote resource.
+ ErrGetX5U = errors.New("failed to get X5U via given URI")
+ // ErrJWKValidation indicates that a JWK failed to validate.
+ ErrJWKValidation = errors.New("failed to validate JWK")
+ // ErrKeyUnmarshalParameter indicates that a JWK's attributes are invalid and cannot be unmarshaled.
+ ErrKeyUnmarshalParameter = errors.New("unable to unmarshal JWK due to invalid attributes")
+ // ErrOptions indicates that the given options caused an error.
+ ErrOptions = errors.New("the given options caused an error")
+ // ErrUnsupportedKey indicates a key is not supported.
+ ErrUnsupportedKey = errors.New("unsupported key")
+ // ErrX509Mismatch indicates that the X.509 certificate does not match the key.
+ ErrX509Mismatch = errors.New("the X.509 certificate does not match Golang key type")
+)
+
+// OtherPrimes is for RSA private keys that have more than 2 primes.
+// https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7
+type OtherPrimes struct {
+ R string `json:"r,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.1
+ D string `json:"d,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.2
+ T string `json:"t,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.3
+}
+
+// JWKMarshal is used to marshal or unmarshal a JSON Web Key.
+// https://www.rfc-editor.org/rfc/rfc7517
+// https://www.rfc-editor.org/rfc/rfc7518
+// https://www.rfc-editor.org/rfc/rfc8037
+//
+// You can find the full list at https://www.iana.org/assignments/jose/jose.xhtml under "JSON Web Key Parameters".
+type JWKMarshal struct {
+ KTY KTY `json:"kty,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.1
+ USE USE `json:"use,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.2
+ KEYOPS []KEYOPS `json:"key_ops,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.3
+ ALG ALG `json:"alg,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.4 and https://www.rfc-editor.org/rfc/rfc7518#section-4.1
+ KID string `json:"kid,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.5
+ X5U string `json:"x5u,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.6
+ X5C []string `json:"x5c,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.7
+ X5T string `json:"x5t,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.8
+ X5TS256 string `json:"x5t#S256,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.9
+ CRV CRV `json:"crv,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.1 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
+ X string `json:"x,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.2 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
+ Y string `json:"y,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.3
+ D string `json:"d,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.1 and https://www.rfc-editor.org/rfc/rfc7518#section-6.2.2.1 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
+ N string `json:"n,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.1
+ E string `json:"e,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.2
+ P string `json:"p,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.2
+ Q string `json:"q,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.3
+ DP string `json:"dp,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.4
+ DQ string `json:"dq,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.5
+ QI string `json:"qi,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.6
+ OTH []OtherPrimes `json:"oth,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7
+ K string `json:"k,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.4.1
+}
+
+// JWKSMarshal is used to marshal or unmarshal a JSON Web Key Set.
+type JWKSMarshal struct {
+ Keys []JWKMarshal `json:"keys"`
+}
+
+// JWKSlice converts the JWKSMarshal to a []JWK.
+func (j JWKSMarshal) JWKSlice() ([]JWK, error) {
+ slice := make([]JWK, len(j.Keys))
+ for i, key := range j.Keys {
+ marshalOptions := JWKMarshalOptions{
+ Private: true,
+ }
+ jwk, err := keyUnmarshal(key, marshalOptions, JWKValidateOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JWK: %w", err)
+ }
+ slice[i] = jwk
+ }
+ return slice, nil
+}
+
+// ToStorage converts the JWKSMarshal to a Storage.
+func (j JWKSMarshal) ToStorage() (Storage, error) {
+ m := NewMemoryStorage()
+ jwks, err := j.JWKSlice()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create a slice of JWK from JWKSMarshal: %w", err)
+ }
+ for _, jwk := range jwks {
+ err = m.KeyWrite(context.Background(), jwk)
+ if err != nil {
+ return nil, fmt.Errorf("failed to write JWK to storage: %w", err)
+ }
+ }
+ return m, nil
+}
+
+func keyMarshal(key any, options JWKOptions) (JWKMarshal, error) {
+ m := JWKMarshal{}
+ m.ALG = options.Metadata.ALG
+ switch key := key.(type) {
+ case *ecdh.PublicKey:
+ pub := key.Bytes()
+ m.CRV = CrvX25519
+ m.X = base64.RawURLEncoding.EncodeToString(pub)
+ m.KTY = KtyOKP
+ case *ecdh.PrivateKey:
+ pub := key.PublicKey().Bytes()
+ m.CRV = CrvX25519
+ m.X = base64.RawURLEncoding.EncodeToString(pub)
+ m.KTY = KtyOKP
+ if options.Marshal.Private {
+ priv := key.Bytes()
+ m.D = base64.RawURLEncoding.EncodeToString(priv)
+ }
+ case *ecdsa.PrivateKey:
+ pub := key.PublicKey
+ m.CRV = CRV(pub.Curve.Params().Name)
+ l := uint(pub.Curve.Params().BitSize / 8)
+ if pub.Curve.Params().BitSize%8 != 0 {
+ l++
+ }
+ m.X = bigIntToBase64RawURL(pub.X, l)
+ m.Y = bigIntToBase64RawURL(pub.Y, l)
+ m.KTY = KtyEC
+ if options.Marshal.Private {
+ params := key.Curve.Params()
+ f, _ := params.N.Float64()
+ l = uint(math.Ceil(math.Log2(f) / 8))
+ m.D = bigIntToBase64RawURL(key.D, l)
+ }
+ case *ecdsa.PublicKey:
+ l := uint(key.Curve.Params().BitSize / 8)
+ if key.Curve.Params().BitSize%8 != 0 {
+ l++
+ }
+ m.CRV = CRV(key.Curve.Params().Name)
+ m.X = bigIntToBase64RawURL(key.X, l)
+ m.Y = bigIntToBase64RawURL(key.Y, l)
+ m.KTY = KtyEC
+ case ed25519.PrivateKey:
+ pub := key.Public().(ed25519.PublicKey)
+ m.ALG = AlgEdDSA
+ m.CRV = CrvEd25519
+ m.X = base64.RawURLEncoding.EncodeToString(pub)
+ m.KTY = KtyOKP
+ if options.Marshal.Private {
+ m.D = base64.RawURLEncoding.EncodeToString(key[:32])
+ }
+ case ed25519.PublicKey:
+ m.ALG = AlgEdDSA
+ m.CRV = CrvEd25519
+ m.X = base64.RawURLEncoding.EncodeToString(key)
+ m.KTY = KtyOKP
+ case *rsa.PrivateKey:
+ pub := key.PublicKey
+ m.E = bigIntToBase64RawURL(big.NewInt(int64(pub.E)), 0)
+ m.N = bigIntToBase64RawURL(pub.N, 0)
+ m.KTY = KtyRSA
+ if options.Marshal.Private {
+ m.D = bigIntToBase64RawURL(key.D, 0)
+ m.P = bigIntToBase64RawURL(key.Primes[0], 0)
+ m.Q = bigIntToBase64RawURL(key.Primes[1], 0)
+ m.DP = bigIntToBase64RawURL(key.Precomputed.Dp, 0)
+ m.DQ = bigIntToBase64RawURL(key.Precomputed.Dq, 0)
+ m.QI = bigIntToBase64RawURL(key.Precomputed.Qinv, 0)
+ if len(key.Precomputed.CRTValues) > 0 {
+ m.OTH = make([]OtherPrimes, len(key.Precomputed.CRTValues))
+ for i := 0; i < len(key.Precomputed.CRTValues); i++ {
+ m.OTH[i] = OtherPrimes{
+ D: bigIntToBase64RawURL(key.Precomputed.CRTValues[i].Exp, 0),
+ T: bigIntToBase64RawURL(key.Precomputed.CRTValues[i].Coeff, 0),
+ R: bigIntToBase64RawURL(key.Primes[i+2], 0),
+ }
+ }
+ }
+ }
+ case *rsa.PublicKey:
+ m.E = bigIntToBase64RawURL(big.NewInt(int64(key.E)), 0)
+ m.N = bigIntToBase64RawURL(key.N, 0)
+ m.KTY = KtyRSA
+ case []byte:
+ if options.Marshal.Private {
+ m.KTY = KtyOct
+ m.K = base64.RawURLEncoding.EncodeToString(key)
+ } else {
+ return JWKMarshal{}, fmt.Errorf("%w: incorrect options to marshal symmetric key (oct)", ErrOptions)
+ }
+ default:
+ return JWKMarshal{}, fmt.Errorf("%w: %T", ErrUnsupportedKey, key)
+ }
+ haveX5C := len(options.X509.X5C) > 0
+ if haveX5C {
+ for i, cert := range options.X509.X5C {
+ m.X5C = append(m.X5C, base64.StdEncoding.EncodeToString(cert.Raw))
+ if i == 0 {
+ h1 := sha1.Sum(cert.Raw)
+ m.X5T = base64.RawURLEncoding.EncodeToString(h1[:])
+ h256 := sha256.Sum256(cert.Raw)
+ m.X5TS256 = base64.RawURLEncoding.EncodeToString(h256[:])
+ }
+ }
+ }
+ m.KID = options.Metadata.KID
+ m.KEYOPS = options.Metadata.KEYOPS
+ m.USE = options.Metadata.USE
+ m.X5U = options.X509.X5U
+ return m, nil
+}
+
+func keyUnmarshal(marshal JWKMarshal, options JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
+ marshalCopy := JWKMarshal{}
+ var key any
+ switch marshal.KTY {
+ case KtyEC:
+ if marshal.CRV == "" || marshal.X == "" || marshal.Y == "" {
+ return JWK{}, fmt.Errorf(`%w: %s requires parameters "crv", "x", and "y"`, ErrKeyUnmarshalParameter, KtyEC)
+ }
+ x, err := base64urlTrailingPadding(marshal.X)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "x": %w`, KtyEC, err)
+ }
+ y, err := base64urlTrailingPadding(marshal.Y)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "y": %w`, KtyEC, err)
+ }
+ publicKey := &ecdsa.PublicKey{
+ X: new(big.Int).SetBytes(x),
+ Y: new(big.Int).SetBytes(y),
+ }
+ switch marshal.CRV {
+ case CrvP256:
+ publicKey.Curve = elliptic.P256()
+ case CrvP384:
+ publicKey.Curve = elliptic.P384()
+ case CrvP521:
+ publicKey.Curve = elliptic.P521()
+ default:
+ return JWK{}, fmt.Errorf("%w: unsupported curve type %q", ErrKeyUnmarshalParameter, marshal.CRV)
+ }
+ marshalCopy.CRV = marshal.CRV
+ marshalCopy.X = marshal.X
+ marshalCopy.Y = marshal.Y
+ if options.Private && marshal.D != "" {
+ d, err := base64urlTrailingPadding(marshal.D)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyEC, err)
+ }
+ privateKey := &ecdsa.PrivateKey{
+ PublicKey: *publicKey,
+ D: new(big.Int).SetBytes(d),
+ }
+ key = privateKey
+ marshalCopy.D = marshal.D
+ } else {
+ key = publicKey
+ }
+ case KtyOKP:
+ if marshal.CRV == "" || marshal.X == "" {
+ return JWK{}, fmt.Errorf(`%w: %s requires parameters "crv" and "x"`, ErrKeyUnmarshalParameter, KtyOKP)
+ }
+ public, err := base64urlTrailingPadding(marshal.X)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "x": %w`, KtyOKP, err)
+ }
+ marshalCopy.CRV = marshal.CRV
+ marshalCopy.X = marshal.X
+ var private []byte
+ if options.Private && marshal.D != "" {
+ private, err = base64urlTrailingPadding(marshal.D)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyOKP, err)
+ }
+ }
+ switch marshal.CRV {
+ case CrvEd25519:
+ if len(public) != ed25519.PublicKeySize {
+ return JWK{}, fmt.Errorf("%w: %s key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, ed25519.PublicKeySize)
+ }
+ if options.Private && marshal.D != "" {
+ private = append(private, public...)
+ if len(private) != ed25519.PrivateKeySize {
+ return JWK{}, fmt.Errorf("%w: %s key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, ed25519.PrivateKeySize)
+ }
+ key = ed25519.PrivateKey(private)
+ marshalCopy.D = marshal.D
+ } else {
+ key = ed25519.PublicKey(public)
+ }
+ case CrvX25519:
+ const x25519PublicKeySize = 32
+ if len(public) != x25519PublicKeySize {
+ return JWK{}, fmt.Errorf("%w: %s with curve %s public key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, CrvEd25519, x25519PublicKeySize)
+ }
+ if options.Private && marshal.D != "" {
+ const x25519PrivateKeySize = 32
+ if len(private) != x25519PrivateKeySize {
+ return JWK{}, fmt.Errorf("%w: %s with curve %s private key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, CrvEd25519, x25519PrivateKeySize)
+ }
+ key, err = ecdh.X25519().NewPrivateKey(private)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to create X25519 private key: %w", err)
+ }
+ marshalCopy.D = marshal.D
+ } else {
+ key, err = ecdh.X25519().NewPublicKey(public)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to create X25519 public key: %w", err)
+ }
+ }
+ default:
+ return JWK{}, fmt.Errorf("%w: unsupported curve type %q", ErrKeyUnmarshalParameter, marshal.CRV)
+ }
+ case KtyRSA:
+ if marshal.N == "" || marshal.E == "" {
+ return JWK{}, fmt.Errorf(`%w: %s requires parameters "n" and "e"`, ErrKeyUnmarshalParameter, KtyRSA)
+ }
+ n, err := base64urlTrailingPadding(marshal.N)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "n": %w`, KtyRSA, err)
+ }
+ e, err := base64urlTrailingPadding(marshal.E)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "e": %w`, KtyRSA, err)
+ }
+ publicKey := rsa.PublicKey{
+ N: new(big.Int).SetBytes(n),
+ E: int(new(big.Int).SetBytes(e).Uint64()),
+ }
+ marshalCopy.N = marshal.N
+ marshalCopy.E = marshal.E
+ if options.Private && marshal.D != "" && marshal.P != "" && marshal.Q != "" && marshal.DP != "" && marshal.DQ != "" && marshal.QI != "" { // TODO Only "d" is required, but if one of the others is present, they all must be.
+ d, err := base64urlTrailingPadding(marshal.D)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyRSA, err)
+ }
+ p, err := base64urlTrailingPadding(marshal.P)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "p": %w`, KtyRSA, err)
+ }
+ q, err := base64urlTrailingPadding(marshal.Q)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "q": %w`, KtyRSA, err)
+ }
+ dp, err := base64urlTrailingPadding(marshal.DP)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "dp": %w`, KtyRSA, err)
+ }
+ dq, err := base64urlTrailingPadding(marshal.DQ)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "dq": %w`, KtyRSA, err)
+ }
+ qi, err := base64urlTrailingPadding(marshal.QI)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "qi": %w`, KtyRSA, err)
+ }
+ var oth []rsa.CRTValue
+ primes := []*big.Int{
+ new(big.Int).SetBytes(p),
+ new(big.Int).SetBytes(q),
+ }
+ if len(marshal.OTH) > 0 {
+ oth = make([]rsa.CRTValue, len(marshal.OTH))
+ for i, otherPrimes := range marshal.OTH {
+ if otherPrimes.R == "" || otherPrimes.D == "" || otherPrimes.T == "" {
+ return JWK{}, fmt.Errorf(`%w: %s requires parameters "r", "d", and "t" for each "oth"`, ErrKeyUnmarshalParameter, KtyRSA)
+ }
+ othD, err := base64urlTrailingPadding(otherPrimes.D)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyRSA, err)
+ }
+ othT, err := base64urlTrailingPadding(otherPrimes.T)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "t": %w`, KtyRSA, err)
+ }
+ othR, err := base64urlTrailingPadding(otherPrimes.R)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "r": %w`, KtyRSA, err)
+ }
+ primes = append(primes, new(big.Int).SetBytes(othR))
+ oth[i] = rsa.CRTValue{
+ Exp: new(big.Int).SetBytes(othD),
+ Coeff: new(big.Int).SetBytes(othT),
+ R: new(big.Int).SetBytes(othR),
+ }
+ }
+ }
+ privateKey := &rsa.PrivateKey{
+ PublicKey: publicKey,
+ D: new(big.Int).SetBytes(d),
+ Primes: primes,
+ Precomputed: rsa.PrecomputedValues{
+ Dp: new(big.Int).SetBytes(dp),
+ Dq: new(big.Int).SetBytes(dq),
+ Qinv: new(big.Int).SetBytes(qi),
+ CRTValues: oth,
+ },
+ }
+ err = privateKey.Validate()
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to validate %s key: %w`, KtyRSA, err)
+ }
+ key = privateKey
+ marshalCopy.D = marshal.D
+ marshalCopy.P = marshal.P
+ marshalCopy.Q = marshal.Q
+ marshalCopy.DP = marshal.DP
+ marshalCopy.DQ = marshal.DQ
+ marshalCopy.QI = marshal.QI
+ marshalCopy.OTH = slices.Clone(marshal.OTH)
+ } else {
+ key = &publicKey
+ }
+ case KtyOct:
+ if options.Private {
+ if marshal.K == "" {
+ return JWK{}, fmt.Errorf(`%w: %s requires parameter "k"`, ErrKeyUnmarshalParameter, KtyOct)
+ }
+ k, err := base64urlTrailingPadding(marshal.K)
+ if err != nil {
+ return JWK{}, fmt.Errorf(`failed to decode %s key parameter "k": %w`, KtyOct, err)
+ }
+ key = k
+ marshalCopy.K = marshal.K
+ } else {
+ return JWK{}, fmt.Errorf("%w: incorrect options to unmarshal symmetric key (%s)", ErrOptions, KtyOct)
+ }
+ default:
+ return JWK{}, fmt.Errorf("%w: %s (kty)", ErrUnsupportedKey, marshal.KTY)
+ }
+ marshalCopy.KTY = marshal.KTY
+ x5c := make([]*x509.Certificate, len(marshal.X5C))
+ for i, cert := range marshal.X5C {
+ raw, err := base64.StdEncoding.DecodeString(cert)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to Base64 decode X.509 certificate: %w", err)
+ }
+ x5c[i], err = x509.ParseCertificate(raw)
+ if err != nil {
+ return JWK{}, fmt.Errorf("failed to parse X.509 certificate: %w", err)
+ }
+ }
+ jwkX509 := JWKX509Options{
+ X5C: x5c,
+ X5U: marshal.X5U,
+ }
+ marshalCopy.X5C = slices.Clone(marshal.X5C)
+ marshalCopy.X5T = marshal.X5T
+ marshalCopy.X5TS256 = marshal.X5TS256
+ marshalCopy.X5U = marshal.X5U
+ metadata := JWKMetadataOptions{
+ ALG: marshal.ALG,
+ KID: marshal.KID,
+ KEYOPS: slices.Clone(marshal.KEYOPS),
+ USE: marshal.USE,
+ }
+ marshalCopy.ALG = marshal.ALG
+ marshalCopy.KID = marshal.KID
+ marshalCopy.KEYOPS = slices.Clone(marshal.KEYOPS)
+ marshalCopy.USE = marshal.USE
+ opts := JWKOptions{
+ Metadata: metadata,
+ Marshal: options,
+ Validate: validateOptions,
+ X509: jwkX509,
+ }
+ j := JWK{
+ key: key,
+ marshal: marshalCopy,
+ options: opts,
+ }
+ return j, nil
+}
+
+// base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
+// JWKS contain padding at the end values for base64url encoded public keys.
+//
+// Trailing padding is required to be removed from base64url encoded keys.
+// RFC 7517 defines base64url the same as RFC 7515 Section 2:
+// https://datatracker.ietf.org/doc/html/rfc7517#section-1.1
+// https://datatracker.ietf.org/doc/html/rfc7515#section-2
+func base64urlTrailingPadding(s string) ([]byte, error) {
+ s = strings.TrimRight(s, "=")
+ return base64.RawURLEncoding.DecodeString(s)
+}
+
+func bigIntToBase64RawURL(i *big.Int, l uint) string {
+ var b []byte
+ if l != 0 {
+ b = make([]byte, l)
+ i.FillBytes(b)
+ } else {
+ b = i.Bytes()
+ }
+ return base64.RawURLEncoding.EncodeToString(b)
+}
diff --git a/vendor/github.com/MicahParks/jwkset/storage.go b/vendor/github.com/MicahParks/jwkset/storage.go
new file mode 100644
index 000000000..e66efe9bd
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/storage.go
@@ -0,0 +1,314 @@
+package jwkset
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "slices"
+ "sync"
+ "time"
+)
+
+var (
+ // ErrKeyNotFound is returned by a Storage implementation when a key is not found.
+ ErrKeyNotFound = errors.New("key not found")
+ // ErrInvalidHTTPStatusCode is returned when the HTTP status code is invalid.
+ ErrInvalidHTTPStatusCode = errors.New("invalid HTTP status code")
+)
+
+// Storage handles storage operations for a JWKSet.
+type Storage interface {
+ // KeyDelete deletes a key from the storage. It will return ok as true if the key was present for deletion.
+ KeyDelete(ctx context.Context, keyID string) (ok bool, err error)
+ // KeyRead reads a key from the storage. If the key is not present, it returns ErrKeyNotFound. Any pointers returned
+ // should be considered read-only.
+ KeyRead(ctx context.Context, keyID string) (JWK, error)
+ // KeyReadAll reads a snapshot of all keys from storage. As with ReadKey, any pointers returned should be
+ // considered read-only.
+ KeyReadAll(ctx context.Context) ([]JWK, error)
+ // KeyWrite writes a key to the storage. If the key already exists, it will be overwritten. After writing a key,
+ // any pointers written should be considered owned by the underlying storage.
+ KeyWrite(ctx context.Context, jwk JWK) error
+
+ // JSON creates the JSON representation of the JWKSet.
+ JSON(ctx context.Context) (json.RawMessage, error)
+ // JSONPublic creates the JSON representation of the public keys in JWKSet.
+ JSONPublic(ctx context.Context) (json.RawMessage, error)
+ // JSONPrivate creates the JSON representation of the JWKSet public and private key material.
+ JSONPrivate(ctx context.Context) (json.RawMessage, error)
+ // JSONWithOptions creates the JSON representation of the JWKSet with the given options. These options override whatever
+ // options are set on the individual JWKs.
+ JSONWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (json.RawMessage, error)
+ // Marshal transforms the JWK Set's current state into a Go type that can be marshaled into JSON.
+ Marshal(ctx context.Context) (JWKSMarshal, error)
+ // MarshalWithOptions transforms the JWK Set's current state into a Go type that can be marshaled into JSON with the
+ // given options. These options override whatever options are set on the individual JWKs.
+ MarshalWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (JWKSMarshal, error)
+}
+
+var _ Storage = &MemoryJWKSet{}
+
+type MemoryJWKSet struct {
+ set []JWK
+ mux sync.RWMutex
+}
+
+// NewMemoryStorage creates a new in-memory Storage implementation.
+func NewMemoryStorage() *MemoryJWKSet {
+ return &MemoryJWKSet{}
+}
+
+func (m *MemoryJWKSet) KeyDelete(_ context.Context, keyID string) (ok bool, err error) {
+ m.mux.Lock()
+ defer m.mux.Unlock()
+ for i, jwk := range m.set {
+ if jwk.Marshal().KID == keyID {
+ m.set = append(m.set[:i], m.set[i+1:]...)
+ return true, nil
+ }
+ }
+ return ok, nil
+}
+func (m *MemoryJWKSet) KeyRead(_ context.Context, keyID string) (JWK, error) {
+ m.mux.RLock()
+ defer m.mux.RUnlock()
+ for _, jwk := range m.set {
+ if jwk.Marshal().KID == keyID {
+ return jwk, nil
+ }
+ }
+ return JWK{}, fmt.Errorf("%w: kid %q", ErrKeyNotFound, keyID)
+}
+func (m *MemoryJWKSet) KeyReadAll(_ context.Context) ([]JWK, error) {
+ m.mux.RLock()
+ defer m.mux.RUnlock()
+ return slices.Clone(m.set), nil
+}
+func (m *MemoryJWKSet) KeyWrite(_ context.Context, jwk JWK) error {
+ m.mux.Lock()
+ defer m.mux.Unlock()
+ m.set = append(m.set, jwk)
+ return nil
+}
+func (m *MemoryJWKSet) JSON(ctx context.Context) (json.RawMessage, error) {
+ jwks, err := m.Marshal(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal JWK Set: %w", err)
+ }
+ return json.Marshal(jwks)
+}
+func (m *MemoryJWKSet) JSONPublic(ctx context.Context) (json.RawMessage, error) {
+ return m.JSONWithOptions(ctx, JWKMarshalOptions{}, JWKValidateOptions{})
+}
+func (m *MemoryJWKSet) JSONPrivate(ctx context.Context) (json.RawMessage, error) {
+ marshalOptions := JWKMarshalOptions{
+ Private: true,
+ }
+ return m.JSONWithOptions(ctx, marshalOptions, JWKValidateOptions{})
+}
+func (m *MemoryJWKSet) JSONWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (json.RawMessage, error) {
+ jwks, err := m.MarshalWithOptions(ctx, marshalOptions, validationOptions)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal JWK Set with options: %w", err)
+ }
+ return json.Marshal(jwks)
+}
+func (m *MemoryJWKSet) Marshal(ctx context.Context) (JWKSMarshal, error) {
+ keys, err := m.KeyReadAll(ctx)
+ if err != nil {
+ return JWKSMarshal{}, fmt.Errorf("failed to read snapshot of all keys from storage: %w", err)
+ }
+ jwks := JWKSMarshal{}
+ for _, key := range keys {
+ jwks.Keys = append(jwks.Keys, key.Marshal())
+ }
+ return jwks, nil
+}
+func (m *MemoryJWKSet) MarshalWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (JWKSMarshal, error) {
+ jwks := JWKSMarshal{}
+
+ keys, err := m.KeyReadAll(ctx)
+ if err != nil {
+ return JWKSMarshal{}, fmt.Errorf("failed to read snapshot of all keys from storage: %w", err)
+ }
+
+ for _, key := range keys {
+ options := key.options
+ options.Marshal = marshalOptions
+ options.Validate = validationOptions
+ marshal, err := keyMarshal(key.Key(), options)
+ if err != nil {
+ if errors.Is(err, ErrOptions) {
+ continue
+ }
+ return JWKSMarshal{}, fmt.Errorf("failed to marshal key: %w", err)
+ }
+ jwks.Keys = append(jwks.Keys, marshal)
+ }
+
+ return jwks, nil
+}
+
+// HTTPClientStorageOptions are used to configure the behavior of NewStorageFromHTTP.
+type HTTPClientStorageOptions struct {
+ // Client is the HTTP client to use for requests.
+ //
+ // This defaults to http.DefaultClient.
+ Client *http.Client
+
+ // Ctx is used when performing HTTP requests. It is also used to end the refresh goroutine when it's no longer
+ // needed.
+ //
+ // This defaults to context.Background().
+ Ctx context.Context
+
+ // HTTPExpectedStatus is the expected HTTP status code for the HTTP request.
+ //
+ // This defaults to http.StatusOK.
+ HTTPExpectedStatus int
+
+ // HTTPMethod is the HTTP method to use for the HTTP request.
+ //
+ // This defaults to http.MethodGet.
+ HTTPMethod string
+
+ // HTTPTimeout is the timeout for the HTTP request. When the Ctx option is also provided, this value is used for a
+ // child context.
+ //
+ // This defaults to time.Minute.
+ HTTPTimeout time.Duration
+
+ // NoErrorReturnFirstHTTPReq will create the Storage without error if the first HTTP request fails.
+ NoErrorReturnFirstHTTPReq bool
+
+ // RefreshErrorHandler is a function that consumes errors that happen during an HTTP refresh. This is only effectual
+ // if RefreshInterval is set.
+ //
+ // If NoErrorReturnFirstHTTPReq is set, this function will be called when if the first HTTP request fails.
+ RefreshErrorHandler func(ctx context.Context, err error)
+
+ // RefreshInterval is the interval at which the HTTP URL is refreshed and the JWK Set is processed. This option will
+ // launch a "refresh goroutine" to refresh the remote HTTP resource at the given interval.
+ //
+ // Provide the Ctx option to end the goroutine when it's no longer needed.
+ RefreshInterval time.Duration
+
+ // ValidateOptions are the options to use when validating the JWKs.
+ ValidateOptions JWKValidateOptions
+}
+
+type httpStorage struct {
+ options HTTPClientStorageOptions
+ refresh func(ctx context.Context) error
+ Storage
+}
+
+// NewStorageFromHTTP creates a new Storage implementation that processes a remote HTTP resource for a JWK Set. If
+// the RefreshInterval option is not set, the remote HTTP resource will be requested and processed before returning. If
+// the RefreshInterval option is set, a background goroutine will be launched to refresh the remote HTTP resource and
+// not block the return of this function.
+func NewStorageFromHTTP(remoteJWKSetURL string, options HTTPClientStorageOptions) (Storage, error) {
+ if options.Client == nil {
+ fmt.Println("***** USING DEFAULT HTTP CLIENT *****")
+ options.Client = http.DefaultClient
+ } else {
+ fmt.Println("***** USING PROVIDED HTTP CLIENT *****")
+ }
+ if options.Ctx == nil {
+ options.Ctx = context.Background()
+ }
+ if options.HTTPExpectedStatus == 0 {
+ options.HTTPExpectedStatus = http.StatusOK
+ }
+ if options.HTTPTimeout == 0 {
+ options.HTTPTimeout = time.Minute
+ }
+ if options.HTTPMethod == "" {
+ options.HTTPMethod = http.MethodGet
+ }
+ store := NewMemoryStorage()
+ _, err := url.ParseRequestURI(remoteJWKSetURL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse given URL %q: %w", remoteJWKSetURL, err)
+ }
+
+ refresh := func(ctx context.Context) error {
+ req, err := http.NewRequestWithContext(ctx, options.HTTPMethod, remoteJWKSetURL, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create HTTP request for JWK Set refresh: %w", err)
+ }
+ resp, err := options.Client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to perform HTTP request for JWK Set refresh: %w", err)
+ }
+ //goland:noinspection GoUnhandledErrorResult
+ defer resp.Body.Close()
+ if resp.StatusCode != options.HTTPExpectedStatus {
+ return fmt.Errorf("%w: %d", ErrInvalidHTTPStatusCode, resp.StatusCode)
+ }
+ var jwks JWKSMarshal
+ err = json.NewDecoder(resp.Body).Decode(&jwks)
+ if err != nil {
+ return fmt.Errorf("failed to decode JWK Set response: %w", err)
+ }
+ store.mux.Lock()
+ defer store.mux.Unlock()
+ store.set = make([]JWK, len(jwks.Keys)) // Clear local cache in case of key revocation.
+ for i, marshal := range jwks.Keys {
+ marshalOptions := JWKMarshalOptions{
+ Private: true,
+ }
+ jwk, err := NewJWKFromMarshal(marshal, marshalOptions, options.ValidateOptions)
+ if err != nil {
+ return fmt.Errorf("failed to create JWK from JWK Marshal: %w", err)
+ }
+ store.set[i] = jwk
+ }
+ return nil
+ }
+
+ if options.RefreshInterval != 0 {
+ go func() { // Refresh goroutine.
+ ticker := time.NewTicker(options.RefreshInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-options.Ctx.Done():
+ return
+ case <-ticker.C:
+ ctx, cancel := context.WithTimeout(options.Ctx, options.HTTPTimeout)
+ err := refresh(ctx)
+ cancel()
+ if err != nil && options.RefreshErrorHandler != nil {
+ options.RefreshErrorHandler(ctx, err)
+ }
+ }
+ }
+ }()
+ }
+
+ s := httpStorage{
+ options: options,
+ refresh: refresh,
+ Storage: store,
+ }
+
+ ctx, cancel := context.WithTimeout(options.Ctx, options.HTTPTimeout)
+ defer cancel()
+ err = refresh(ctx)
+ cancel()
+ if err != nil {
+ if options.NoErrorReturnFirstHTTPReq {
+ if options.RefreshErrorHandler != nil {
+ options.RefreshErrorHandler(ctx, err)
+ }
+ return s, nil
+ }
+ return nil, fmt.Errorf("failed to perform first HTTP request for JWK Set: %w", err)
+ }
+
+ return s, nil
+}
diff --git a/vendor/github.com/MicahParks/jwkset/x509.go b/vendor/github.com/MicahParks/jwkset/x509.go
new file mode 100644
index 000000000..b89a3a6ea
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/x509.go
@@ -0,0 +1,125 @@
+package jwkset
+
+import (
+ "crypto/ecdh"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrX509Infer is returned when the key type cannot be inferred from the PEM block type.
+ ErrX509Infer = errors.New("failed to infer X509 key type")
+)
+
+// LoadCertificate loads an X509 certificate from a PEM block.
+func LoadCertificate(pemBlock []byte) (*x509.Certificate, error) {
+ cert, err := x509.ParseCertificate(pemBlock)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse certificates: %w", err)
+ }
+ switch cert.PublicKey.(type) {
+ case *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
+ default:
+ return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, cert.PublicKey)
+ }
+ return cert, nil
+}
+
+// LoadCertificates loads X509 certificates from raw PEM data. It can be useful in loading X5U remote resources.
+func LoadCertificates(rawPEM []byte) ([]*x509.Certificate, error) {
+ b := make([]byte, 0)
+ for {
+ block, rest := pem.Decode(rawPEM)
+ if block == nil {
+ break
+ }
+ rawPEM = rest
+ if block.Type == "CERTIFICATE" {
+ b = append(b, block.Bytes...)
+ }
+ }
+ certs, err := x509.ParseCertificates(b)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse certificates: %w", err)
+ }
+ for _, cert := range certs {
+ switch cert.PublicKey.(type) {
+ case *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
+ default:
+ return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, cert.PublicKey)
+ }
+ }
+ return certs, nil
+}
+
+// LoadX509KeyInfer loads an X509 key from a PEM block.
+func LoadX509KeyInfer(pemBlock *pem.Block) (key any, err error) {
+ switch pemBlock.Type {
+ case "EC PRIVATE KEY":
+ key, err = loadECPrivate(pemBlock)
+ case "RSA PRIVATE KEY":
+ key, err = loadPKCS1Private(pemBlock)
+ case "RSA PUBLIC KEY":
+ key, err = loadPKCS1Public(pemBlock)
+ case "PRIVATE KEY":
+ key, err = loadPKCS8Private(pemBlock)
+ case "PUBLIC KEY":
+ key, err = loadPKIXPublic(pemBlock)
+ default:
+ return nil, ErrX509Infer
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to load key from inferred format %q: %w", key, err)
+ }
+ return key, nil
+}
+func loadECPrivate(pemBlock *pem.Block) (priv *ecdsa.PrivateKey, err error) {
+ priv, err = x509.ParseECPrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse EC private key: %w", err)
+ }
+ return priv, nil
+}
+func loadPKCS1Public(pemBlock *pem.Block) (pub *rsa.PublicKey, err error) {
+ pub, err = x509.ParsePKCS1PublicKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse PKCS1 public key: %w", err)
+ }
+ return pub, nil
+}
+func loadPKCS1Private(pemBlock *pem.Block) (priv *rsa.PrivateKey, err error) {
+ priv, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse PKCS1 private key: %w", err)
+ }
+ return priv, nil
+}
+func loadPKCS8Private(pemBlock *pem.Block) (priv any, err error) {
+ priv, err = x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse PKCS8 private key: %w", err)
+ }
+ switch priv.(type) {
+ case *ecdh.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, *rsa.PrivateKey:
+ default:
+ return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, priv)
+ }
+ return priv, nil
+}
+func loadPKIXPublic(pemBlock *pem.Block) (pub any, err error) {
+ pub, err = x509.ParsePKIXPublicKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse PKIX public key: %w", err)
+ }
+ switch pub.(type) {
+ case *ecdh.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
+ default:
+ return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, pub)
+ }
+ return pub, nil
+}
diff --git a/vendor/github.com/MicahParks/jwkset/x509_gen.sh b/vendor/github.com/MicahParks/jwkset/x509_gen.sh
new file mode 100644
index 000000000..79cc315c1
--- /dev/null
+++ b/vendor/github.com/MicahParks/jwkset/x509_gen.sh
@@ -0,0 +1,15 @@
+# OpenSSL 3.0.10 1 Aug 2023 (Library: OpenSSL 3.0.10 1 Aug 2023)
+openssl req -newkey EC -pkeyopt ec_paramgen_curve:P-521 -noenc -keyout ec521.pem -x509 -out ec521.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
+openssl req -newkey ED25519 -noenc -keyout ed25519.pem -x509 -out ed25519.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
+openssl req -newkey RSA:4096 -noenc -keyout rsa4096.pem -x509 -out rsa4096.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
+
+openssl pkey -in ec521.pem -pubout -out ec521pub.pem
+openssl pkey -in ed25519.pem -pubout -out ed25519pub.pem
+openssl pkey -in rsa4096.pem -pubout -out rsa4096pub.pem
+
+# For the "RSA PRIVATE KEY" (PKCS#1) and "EC PRIVATE KEY" (SEC1) formats, the PEM files are generated using the
+# cmd/gen_pkcs1 and cmd/gen_ec Golang programs, respectively.
+
+openssl dsaparam -out dsaparam.pem 2048
+openssl gendsa -out dsa.pem dsaparam.pem
+openssl dsa -in dsa.pem -pubout -out dsa_pub.pem
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/LICENSE b/vendor/github.com/MicahParks/keyfunc/v3/LICENSE
new file mode 100644
index 000000000..06dd4f210
--- /dev/null
+++ b/vendor/github.com/MicahParks/keyfunc/v3/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2021 Micah Parks
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/README.md b/vendor/github.com/MicahParks/keyfunc/v3/README.md
new file mode 100644
index 000000000..e6e304dc3
--- /dev/null
+++ b/vendor/github.com/MicahParks/keyfunc/v3/README.md
@@ -0,0 +1,81 @@
+[](https://pkg.go.dev/github.com/MicahParks/keyfunc/v3)
+
+# keyfunc
+
+The purpose of this package is to provide a
+[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc) for the
+[github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) package using a JSON Web Key Set (JWK Set) for parsing
+and verifying JSON Web Tokens (JWTs).
+
+It's common for an identity providers, particularly those
+using [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749)
+or [OpenID Connect](https://openid.net/developers/how-connect-works/), such
+as [Keycloak](https://github.com/MicahParks/keyfunc/blob/master/examples/keycloak/main.go)
+or [Amazon Cognito (AWS)](https://github.com/MicahParks/keyfunc/blob/master/examples/aws_cognito/main.go) to expose a
+JWK Set via an HTTPS endpoint. This package has the ability to consume that JWK Set and produce a
+[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc). It is important that a JWK Set endpoint is
+using HTTPS to ensure the keys are from the correct trusted source.
+
+## Basic usage
+
+For complete examples, please see the `examples` directory.
+
+```go
+import "github.com/MicahParks/keyfunc/v3"
+```
+
+### Step 1: Create the `keyfunc.Keyfunc`
+
+The below example is for a remote HTTP resource.
+See [`examples/json/main.go`](https://github.com/MicahParks/keyfunc/blob/master/examples/json/main.go) for a JSON
+example.
+
+```go
+// Create the keyfunc.Keyfunc.
+k, err := keyfunc.NewDefaultCtx(ctx, []string{server.URL}) // Context is used to end the refresh goroutine.
+if err != nil {
+ log.Fatalf("Failed to create a keyfunc.Keyfunc from the server's URL.\nError: %s", err)
+}
+```
+
+When using the `keyfunc.NewDefault` function, the JWK Set will be automatically refreshed using
+[`jwkset.NewDefaultHTTPClient`](https://pkg.go.dev/github.com/MicahParks/jwkset#NewHTTPClient). This does launch a "
+refresh goroutine". If you want the ability to end this goroutine, use the `keyfunc.NewDefaultCtx` function.
+
+It is also possible to create a `keyfunc.Keyfunc` from given keys like HMAC shared secrets. See `examples/hmac/main.go`.
+
+### Step 2: Use the `keyfunc.Keyfunc` to parse and verify JWTs
+
+```go
+// Parse the JWT.
+parsed, err := jwt.Parse(signed, k.Keyfunc)
+if err != nil {
+ log.Fatalf("Failed to parse the JWT.\nError: %s", err)
+}
+```
+
+## Additional features
+
+This project's primary purpose is to provide a [`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc)
+implementation for JWK Sets.
+
+Since version `3.X.X`, this project has become a thin wrapper
+around [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset). Newer versions contain a superset of
+features available in versions `2.X.X` and earlier, but some of the deep customization has been moved to the `jwkset`
+project. The intention behind this is to make `keyfunc` easier to use for most use cases.
+
+Access the [`jwkset.Storage`](https://pkg.go.dev/github.com/MicahParks/jwkset#Storage) from a `keyfunc.Keyfunc` via
+the `.Storage()` method. Using the [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) package
+provides the below features, and more:
+
+* An HTTP client that automatically updates one or more remote JWK Set resources.
+* An automatic refresh of remote HTTP resources when an unknown key ID (`kid`) is encountered.
+* X.509 URIs or embedded [certificate chains](https://pkg.go.dev/crypto/x509#Certificate), when a JWK contains them.
+* Support for private asymmetric keys.
+* Specified key operations and usage.
+
+## Related projects
+
+### [`github.com/MicahParks/jwkset`](https://github.com/MicahParks/jwkset):
+
+A JWK Set implementation. The `keyfunc` project is a wrapper around this project.
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go b/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
new file mode 100644
index 000000000..1725731d6
--- /dev/null
+++ b/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
@@ -0,0 +1,177 @@
+package keyfunc
+
+import (
+ "context"
+ "crypto"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/MicahParks/jwkset"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+var (
+ // ErrKeyfunc is returned when a keyfunc error occurs.
+ ErrKeyfunc = errors.New("failed keyfunc")
+)
+
+// Keyfunc is meant to be used as the jwt.Keyfunc function for github.com/golang-jwt/jwt/v5. It uses
+// github.com/MicahParks/jwkset as a JWK Set storage.
+type Keyfunc interface {
+ Keyfunc(token *jwt.Token) (any, error)
+ KeyfuncCtx(ctx context.Context) jwt.Keyfunc
+ Storage() jwkset.Storage
+}
+
+// Options are used to create a new Keyfunc.
+type Options struct {
+ Ctx context.Context
+ Storage jwkset.Storage
+ UseWhitelist []jwkset.USE
+}
+
+type keyfunc struct {
+ ctx context.Context
+ storage jwkset.Storage
+ useWhitelist []jwkset.USE
+}
+
+// New creates a new Keyfunc.
+func New(options Options) (Keyfunc, error) {
+ ctx := options.Ctx
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ if options.Storage == nil {
+ return nil, fmt.Errorf("%w: no JWK Set storage given in options", ErrKeyfunc)
+ }
+ k := keyfunc{
+ ctx: ctx,
+ storage: options.Storage,
+ useWhitelist: options.UseWhitelist,
+ }
+ return k, nil
+}
+
+// NewDefault creates a new Keyfunc with a default JWK Set storage and options.
+//
+// This will launch "refresh goroutine" to automatically refresh the remote HTTP resources.
+func NewDefault(urls []string) (Keyfunc, error) {
+ return NewDefaultCtx(context.Background(), urls)
+}
+
+// NewDefaultCtx creates a new Keyfunc with a default JWK Set storage and options. The context is used to end the
+// "refresh goroutine".
+//
+// This will launch "refresh goroutine" to automatically refresh the remote HTTP resources.
+func NewDefaultCtx(ctx context.Context, urls []string) (Keyfunc, error) {
+ client, err := jwkset.NewDefaultHTTPClientCtx(ctx, urls)
+ if err != nil {
+ return nil, err
+ }
+ options := Options{
+ Storage: client,
+ }
+ return New(options)
+}
+
+// NewJWKJSON creates a new Keyfunc from raw JWK JSON.
+func NewJWKJSON(raw json.RawMessage) (Keyfunc, error) {
+ marshalOptions := jwkset.JWKMarshalOptions{
+ Private: true,
+ }
+ jwk, err := jwkset.NewJWKFromRawJSON(raw, marshalOptions, jwkset.JWKValidateOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not create JWK from raw JSON", errors.Join(err, ErrKeyfunc))
+ }
+ store := jwkset.NewMemoryStorage()
+ err = store.KeyWrite(context.Background(), jwk)
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not write JWK to storage", errors.Join(err, ErrKeyfunc))
+ }
+ options := Options{
+ Storage: store,
+ }
+ return New(options)
+}
+
+// NewJWKSetJSON creates a new Keyfunc from raw JWK Set JSON.
+func NewJWKSetJSON(raw json.RawMessage) (Keyfunc, error) {
+ var jwks jwkset.JWKSMarshal
+ err := json.Unmarshal(raw, &jwks)
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not unmarshal raw JWK Set JSON", errors.Join(err, ErrKeyfunc))
+ }
+ store, err := jwks.ToStorage()
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not create JWK Set storage", errors.Join(err, ErrKeyfunc))
+ }
+ options := Options{
+ Storage: store,
+ }
+ return New(options)
+}
+
+func (k keyfunc) KeyfuncCtx(ctx context.Context) jwt.Keyfunc {
+ return func(token *jwt.Token) (any, error) {
+ kidInter, ok := token.Header[jwkset.HeaderKID]
+ if !ok {
+ return nil, fmt.Errorf("%w: could not find kid in JWT header", ErrKeyfunc)
+ }
+ kid, ok := kidInter.(string)
+ if !ok {
+ return nil, fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKeyfunc)
+ }
+ algInter, ok := token.Header["alg"]
+ if !ok {
+ return nil, fmt.Errorf("%w: could not find alg in JWT header", ErrKeyfunc)
+ }
+ alg, ok := algInter.(string)
+ if !ok {
+ // For test coverage purposes, this should be impossible to reach because the JWT package rejects a token
+ // without an alg parameter in the header before calling jwt.Keyfunc.
+ return nil, fmt.Errorf(`%w: the JWT header did not contain the "alg" parameter, which is required by RFC 7515 section 4.1.1`, ErrKeyfunc)
+ }
+
+ jwk, err := k.storage.KeyRead(ctx, kid)
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not read JWK from storage", errors.Join(err, ErrKeyfunc))
+ }
+
+ if a := jwk.Marshal().ALG.String(); a != "" && a != alg {
+ return nil, fmt.Errorf(`%w: JWK "alg" parameter value %q does not match token "alg" parameter value %q`, ErrKeyfunc, a, alg)
+ }
+ if len(k.useWhitelist) > 0 {
+ found := false
+ for _, u := range k.useWhitelist {
+ if jwk.Marshal().USE == u {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, fmt.Errorf(`%w: JWK "use" parameter value %q is not in whitelist`, ErrKeyfunc, jwk.Marshal().USE)
+ }
+ }
+
+ type publicKeyer interface {
+ Public() crypto.PublicKey
+ }
+
+ key := jwk.Key()
+ pk, ok := key.(publicKeyer)
+ if ok {
+ key = pk.Public()
+ }
+
+ return key, nil
+ }
+}
+func (k keyfunc) Keyfunc(token *jwt.Token) (any, error) {
+ keyF := k.KeyfuncCtx(k.ctx)
+ return keyF(token)
+}
+func (k keyfunc) Storage() jwkset.Storage {
+ return k.storage
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 9c4068de1..129388b5c 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -37,9 +37,15 @@ github.com/Masterminds/semver/v3
# github.com/Masterminds/sprig v2.22.0+incompatible
## explicit
github.com/Masterminds/sprig
+# github.com/MicahParks/jwkset v0.8.0
+## explicit; go 1.21
+github.com/MicahParks/jwkset
# github.com/MicahParks/keyfunc/v2 v2.1.0
## explicit; go 1.18
github.com/MicahParks/keyfunc/v2
+# github.com/MicahParks/keyfunc/v3 v3.3.11
+## explicit; go 1.21
+github.com/MicahParks/keyfunc/v3
# github.com/Microsoft/go-winio v0.6.2
## explicit; go 1.21
github.com/Microsoft/go-winio