mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-31 17:30:29 -06:00
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:
@@ -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
2
go.mod
@@ -127,6 +127,8 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/MicahParks/jwkset v0.8.0 // indirect
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.11 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -82,8 +82,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/MicahParks/jwkset v0.8.0 h1:jHtclI38Gibmu17XMI6+6/UB59srp58pQVxePHRK5o8=
|
||||
github.com/MicahParks/jwkset v0.8.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.11 h1:eA6wNltwdSRX2gtpTwZseBCC9nGeBkI9KxHtTyZbDbo=
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.11/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
|
||||
@@ -24,4 +24,5 @@ type Config struct {
|
||||
}
|
||||
|
||||
type AuthenticationAPI struct {
|
||||
JwkEndpoint string `yaml:"jwk_endpoint"`
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
31
services/auth-api/pkg/metrics/options.go
Normal file
31
services/auth-api/pkg/metrics/options.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
17
services/auth-api/pkg/service/http/v0/service_test.go
Normal file
17
services/auth-api/pkg/service/http/v0/service_test.go
Normal 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
2
vendor/github.com/MicahParks/jwkset/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
config.*json
|
||||
node_modules
|
||||
201
vendor/github.com/MicahParks/jwkset/LICENSE
generated
vendored
Normal file
201
vendor/github.com/MicahParks/jwkset/LICENSE
generated
vendored
Normal 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
133
vendor/github.com/MicahParks/jwkset/README.md
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
[](https://pkg.go.dev/github.com/MicahParks/jwkset)
|
||||
|
||||
# JWK Set (JSON Web Key Set)
|
||||
|
||||
This is a JWK Set (JSON Web Key Set) implementation written in Golang.
|
||||
|
||||
The goal of this project is to provide a complete implementation of JWK and JWK Sets within the constraints of the
|
||||
Golang standard library, without implementing any cryptographic algorithms. For example, `Ed25519` is supported, but
|
||||
`Ed448` is not, because the Go standard library does not have a high level implementation of `Ed448`.
|
||||
|
||||
If you would like to generate or validate a JWK without writing any Golang code, please visit
|
||||
the [Generate a JWK Set](#generate-a-jwk-set) section.
|
||||
|
||||
If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the
|
||||
[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the
|
||||
language of your choice using an OpenAPI interface.
|
||||
|
||||
# Generate a JWK Set
|
||||
|
||||
If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK
|
||||
Set from:
|
||||
|
||||
* PEM encoded X.509 Certificates
|
||||
* PEM encoded public keys
|
||||
* PEM encoded private keys
|
||||
|
||||
The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section
|
||||
for a list of supported cryptographic key types.
|
||||
|
||||
## Website
|
||||
|
||||
Visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this website
|
||||
by following the instructions in the `README.md` in
|
||||
the [website](https://github.com/MicahParks/jwkset/tree/master/website) directory.
|
||||
|
||||
## Command line
|
||||
|
||||
Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set.
|
||||
|
||||
**Install**
|
||||
|
||||
```
|
||||
go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
jwksetinfer mykey.pem mycert.crt
|
||||
```
|
||||
|
||||
## Custom server
|
||||
|
||||
This project can be used in creating a custom JWK Set server. A good place to start is `examples/http_server/main.go`.
|
||||
|
||||
# Golang JWK Set client
|
||||
|
||||
If you are using [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) take a look
|
||||
at [`github.com/MicahParks/keyfunc/v3`](https://github.com/MicahParks/keyfunc).
|
||||
|
||||
This project can be used to create JWK Set clients. An HTTP client is provided. See a snippet of the usage
|
||||
from `examples/default_http_client/main.go` below.
|
||||
|
||||
## Create a JWK Set client from the server's HTTP URL.
|
||||
|
||||
```go
|
||||
jwks, err := jwkset.NewDefaultHTTPClient([]string{server.URL})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client JWK set. Error: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Read a key from the client.
|
||||
|
||||
```go
|
||||
jwk, err = jwks.KeyRead(ctx, myKeyID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read key from client JWK set. Error: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
# Supported keys
|
||||
|
||||
This project supports the following key types:
|
||||
|
||||
* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only)
|
||||
* Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey`
|
||||
* [Elliptic-curve Diffie–Hellman (ECDH)](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) (X25519
|
||||
only)
|
||||
* Go Types: `*ecdh.PrivateKey` and `*ecdh.PublicKey`
|
||||
* [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
|
||||
* Go Types: `*ecdsa.PrivateKey` and `*ecdsa.PublicKey`
|
||||
* [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem))
|
||||
* Go Types: `*rsa.PrivateKey` and `*rsa.PublicKey`
|
||||
* [HMAC](https://en.wikipedia.org/wiki/HMAC), [AES Key Wrap](https://en.wikipedia.org/wiki/Key_Wrap), and other
|
||||
symmetric keys
|
||||
* Go Type: `[]byte`
|
||||
|
||||
Cryptographic keys can be added, deleted, and read from the JWK Set. A JSON representation of the JWK Set can be created
|
||||
for hosting via HTTPS. This project includes an in-memory storage implementation, but an interface is provided for more
|
||||
advanced use cases.
|
||||
|
||||
# Notes
|
||||
|
||||
This project aims to implement the relevant RFCs to the fullest extent possible using the Go standard library, but does
|
||||
not implement any cryptographic algorithms itself.
|
||||
|
||||
* RFC 8037 adds support for `Ed448`, `X448`, and `secp256k1`, but there is no Golang standard library support for these
|
||||
key types.
|
||||
* In order to be compatible with non-RFC compliant JWK Set providers, this project does not strictly enforce JWK
|
||||
parameters that are integers and have extra or missing leading padding. See the release notes
|
||||
of [`v0.5.15`](https://github.com/MicahParks/jwkset/releases/tag/v0.5.15) for details.
|
||||
* `Base64url Encoding` requires that all trailing `=` characters be removed. This project automatically strips any
|
||||
trailing `=` characters in an attempt to be compatible with improper implementations of JWK.
|
||||
* This project does not currently support JWK Set encryption using JWE. This would involve implementing the relevant JWE
|
||||
specifications. It may be implemented in the future if there is interest. Open a GitHub issue to express interest.
|
||||
|
||||
# Related projects
|
||||
|
||||
## [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc)
|
||||
|
||||
A JWK Set client for the [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) project.
|
||||
|
||||
## [`github.com/MicahParks/jcp`](https://github.com/MicahParks/jcp)
|
||||
|
||||
A JWK Set client proxy. JCP for short. This project is a standalone service that uses keyfunc under the hood. It
|
||||
primarily exists for these use cases:
|
||||
|
||||
The language or shell a program is written in does not have an adequate JWK Set client. Validate JWTs with curl? Why
|
||||
not?
|
||||
Restrictive networking policies prevent a program from accessing the remote JWK Set directly.
|
||||
Many co-located services need to validate JWTs that were signed by a key that lives in a remote JWK Set.
|
||||
If you can integrate keyfunc directly into your program, you likely don't need JCP.
|
||||
167
vendor/github.com/MicahParks/jwkset/constants.go
generated
vendored
Normal file
167
vendor/github.com/MicahParks/jwkset/constants.go
generated
vendored
Normal 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
276
vendor/github.com/MicahParks/jwkset/http.go
generated
vendored
Normal 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
494
vendor/github.com/MicahParks/jwkset/jwk.go
generated
vendored
Normal 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
511
vendor/github.com/MicahParks/jwkset/marshal.go
generated
vendored
Normal 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
314
vendor/github.com/MicahParks/jwkset/storage.go
generated
vendored
Normal 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
125
vendor/github.com/MicahParks/jwkset/x509.go
generated
vendored
Normal 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
15
vendor/github.com/MicahParks/jwkset/x509_gen.sh
generated
vendored
Normal 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
201
vendor/github.com/MicahParks/keyfunc/v3/LICENSE
generated
vendored
Normal 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
81
vendor/github.com/MicahParks/keyfunc/v3/README.md
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
[](https://pkg.go.dev/github.com/MicahParks/keyfunc/v3)
|
||||
|
||||
# keyfunc
|
||||
|
||||
The purpose of this package is to provide a
|
||||
[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc) for the
|
||||
[github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) package using a JSON Web Key Set (JWK Set) for parsing
|
||||
and verifying JSON Web Tokens (JWTs).
|
||||
|
||||
It's common for an identity providers, particularly those
|
||||
using [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749)
|
||||
or [OpenID Connect](https://openid.net/developers/how-connect-works/), such
|
||||
as [Keycloak](https://github.com/MicahParks/keyfunc/blob/master/examples/keycloak/main.go)
|
||||
or [Amazon Cognito (AWS)](https://github.com/MicahParks/keyfunc/blob/master/examples/aws_cognito/main.go) to expose a
|
||||
JWK Set via an HTTPS endpoint. This package has the ability to consume that JWK Set and produce a
|
||||
[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc). It is important that a JWK Set endpoint is
|
||||
using HTTPS to ensure the keys are from the correct trusted source.
|
||||
|
||||
## Basic usage
|
||||
|
||||
For complete examples, please see the `examples` directory.
|
||||
|
||||
```go
|
||||
import "github.com/MicahParks/keyfunc/v3"
|
||||
```
|
||||
|
||||
### Step 1: Create the `keyfunc.Keyfunc`
|
||||
|
||||
The below example is for a remote HTTP resource.
|
||||
See [`examples/json/main.go`](https://github.com/MicahParks/keyfunc/blob/master/examples/json/main.go) for a JSON
|
||||
example.
|
||||
|
||||
```go
|
||||
// Create the keyfunc.Keyfunc.
|
||||
k, err := keyfunc.NewDefaultCtx(ctx, []string{server.URL}) // Context is used to end the refresh goroutine.
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a keyfunc.Keyfunc from the server's URL.\nError: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
When using the `keyfunc.NewDefault` function, the JWK Set will be automatically refreshed using
|
||||
[`jwkset.NewDefaultHTTPClient`](https://pkg.go.dev/github.com/MicahParks/jwkset#NewHTTPClient). This does launch a "
|
||||
refresh goroutine". If you want the ability to end this goroutine, use the `keyfunc.NewDefaultCtx` function.
|
||||
|
||||
It is also possible to create a `keyfunc.Keyfunc` from given keys like HMAC shared secrets. See `examples/hmac/main.go`.
|
||||
|
||||
### Step 2: Use the `keyfunc.Keyfunc` to parse and verify JWTs
|
||||
|
||||
```go
|
||||
// Parse the JWT.
|
||||
parsed, err := jwt.Parse(signed, k.Keyfunc)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse the JWT.\nError: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Additional features
|
||||
|
||||
This project's primary purpose is to provide a [`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc)
|
||||
implementation for JWK Sets.
|
||||
|
||||
Since version `3.X.X`, this project has become a thin wrapper
|
||||
around [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset). Newer versions contain a superset of
|
||||
features available in versions `2.X.X` and earlier, but some of the deep customization has been moved to the `jwkset`
|
||||
project. The intention behind this is to make `keyfunc` easier to use for most use cases.
|
||||
|
||||
Access the [`jwkset.Storage`](https://pkg.go.dev/github.com/MicahParks/jwkset#Storage) from a `keyfunc.Keyfunc` via
|
||||
the `.Storage()` method. Using the [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) package
|
||||
provides the below features, and more:
|
||||
|
||||
* An HTTP client that automatically updates one or more remote JWK Set resources.
|
||||
* An automatic refresh of remote HTTP resources when an unknown key ID (`kid`) is encountered.
|
||||
* X.509 URIs or embedded [certificate chains](https://pkg.go.dev/crypto/x509#Certificate), when a JWK contains them.
|
||||
* Support for private asymmetric keys.
|
||||
* Specified key operations and usage.
|
||||
|
||||
## Related projects
|
||||
|
||||
### [`github.com/MicahParks/jwkset`](https://github.com/MicahParks/jwkset):
|
||||
|
||||
A JWK Set implementation. The `keyfunc` project is a wrapper around this project.
|
||||
177
vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
generated
vendored
Normal file
177
vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
generated
vendored
Normal 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
6
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user