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
This commit is contained in:
Pascal Bleser
2025-05-12 11:14:21 +02:00
parent 4e6053cdbd
commit ebd58fcfdb
25 changed files with 3012 additions and 59 deletions

View File

@@ -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

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -24,4 +24,5 @@ type Config struct {
}
type AuthenticationAPI struct {
JwkEndpoint string `yaml:"jwk_endpoint"`
}

View File

@@ -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",
},
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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])
}

2
vendor/github.com/MicahParks/jwkset/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
config.*json
node_modules

201
vendor/github.com/MicahParks/jwkset/LICENSE generated vendored Normal file
View File

@@ -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.

133
vendor/github.com/MicahParks/jwkset/README.md generated vendored Normal file
View File

@@ -0,0 +1,133 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](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 DiffieHellman (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`
* [RivestShamirAdleman (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.

167
vendor/github.com/MicahParks/jwkset/constants.go generated vendored Normal file
View File

@@ -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)
}

276
vendor/github.com/MicahParks/jwkset/http.go generated vendored Normal file
View File

@@ -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
}

494
vendor/github.com/MicahParks/jwkset/jwk.go generated vendored Normal file
View File

@@ -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
}

511
vendor/github.com/MicahParks/jwkset/marshal.go generated vendored Normal file
View File

@@ -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)
}

314
vendor/github.com/MicahParks/jwkset/storage.go generated vendored Normal file
View File

@@ -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
}

125
vendor/github.com/MicahParks/jwkset/x509.go generated vendored Normal file
View File

@@ -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
}

15
vendor/github.com/MicahParks/jwkset/x509_gen.sh generated vendored Normal file
View File

@@ -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

201
vendor/github.com/MicahParks/keyfunc/v3/LICENSE generated vendored Normal file
View File

@@ -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.

81
vendor/github.com/MicahParks/keyfunc/v3/README.md generated vendored Normal file
View File

@@ -0,0 +1,81 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/keyfunc/v3.svg)](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.

177
vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go generated vendored Normal file
View File

@@ -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
}

6
vendor/modules.txt vendored
View File

@@ -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