diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3ace6efb7c..66e0ab0dd3 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -137,8 +137,15 @@
"OC_SERVICE_ACCOUNT_ID": "service-account-id",
"OC_SERVICE_ACCOUNT_SECRET": "service-account-secret",
- "OC_ADD_RUN_SERVICES": "groupware",
- "GROUPWARE_LOG_LEVEL": "trace"
+ "OC_ADD_RUN_SERVICES": "auth-api,groupware",
+ "GROUPWARE_LOG_LEVEL": "trace",
+
+ "GROUPWARE_JMAP_MASTER_USERNAME": "",
+ "GROUPWARE_JMAP_MASTER_PASSWORD": "admin",
+
+ "AUTHAPI_HTTP_ADDR": "0.0.0.0:10000",
+ "AUTHAPI_AUTH_REQUIRE_SHARED_SECRET": "true",
+ "AUTHAPI_AUTH_SHARED_SECRETS": "stalwart=maethaR9eiXaiph8ahn8ohH6dahPiequ;unused=eeyaigh6hae1zo5ahGeete6oohaiquei",
}
},
{
diff --git a/devtools/deployments/opencloud_full/.env b/devtools/deployments/opencloud_full/.env
index fe94283028..27c4398a78 100644
--- a/devtools/deployments/opencloud_full/.env
+++ b/devtools/deployments/opencloud_full/.env
@@ -311,11 +311,11 @@ KEYCLOAK_ADMIN_PASSWORD=
# Domain of Stalwart
# Defaults to "stalwart.opencloud.test"
STALWART_DOMAIN=
-
# LDAP configuration to use for Stalwart:
# Can either be either
-# - idmldap: for the built-in IDP/IDM
-# - ldap: when using KeyCloak and OpenLDAP
+# - idmldap: for the built-in IDP/IDM, using Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
+# - idmoidc: built-in IDP/IDM, using OIDC Userinfo between Groupware and Stalwart
+# - ldap: when using KeyCloak and OpenLDAP, with Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
STALWART_AUTH_DIRECTORY=idmldap
## IMPORTANT ##
diff --git a/devtools/deployments/opencloud_full/config/stalwart/idmoidc.toml b/devtools/deployments/opencloud_full/config/stalwart/idmoidc.toml
new file mode 100644
index 0000000000..2cf88b3fff
--- /dev/null
+++ b/devtools/deployments/opencloud_full/config/stalwart/idmoidc.toml
@@ -0,0 +1,67 @@
+authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
+authentication.fallback-admin.user = "mailadmin"
+authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
+authentication.master.user = "master"
+directory.oidc.cache.size = 1048576
+directory.oidc.cache.ttl.negative = "10m"
+directory.oidc.cache.ttl.positive = "1h"
+directory.oidc.endpoint.method = "userinfo"
+directory.oidc.endpoint.url = "http://172.17.0.1:10000/auth/maethaR9eiXaiph8ahn8ohH6dahPiequ"
+directory.oidc.fields.email = "email"
+directory.oidc.fields.full-name = "name"
+directory.oidc.fields.username = "preferred_username"
+directory.oidc.timeout = "15s"
+directory.oidc.type = "oidc"
+http.allowed-endpoint = 200
+http.hsts = true
+http.permissive-cors = false
+http.url = "'https://' + config_get('server.hostname')"
+http.use-x-forwarded = true
+metrics.prometheus.auth.secret = "secret"
+metrics.prometheus.auth.username = "metrics"
+metrics.prometheus.enable = true
+server.listener.http.bind = "0.0.0.0:8080"
+server.listener.http.protocol = "http"
+server.listener.https.bind = "0.0.0.0:443"
+server.listener.https.protocol = "http"
+server.listener.https.tls.implicit = true
+server.listener.imap.bind = "0.0.0.0:143"
+server.listener.imap.protocol = "imap"
+server.listener.imaptls.bind = "0.0.0.0:993"
+server.listener.imaptls.protocol = "imap"
+server.listener.imaptls.tls.implicit = true
+server.listener.pop3.bind = "0.0.0.0:110"
+server.listener.pop3.protocol = "pop3"
+server.listener.pop3s.bind = "0.0.0.0:995"
+server.listener.pop3s.protocol = "pop3"
+server.listener.pop3s.tls.implicit = true
+server.listener.sieve.bind = "0.0.0.0:4190"
+server.listener.sieve.protocol = "managesieve"
+server.listener.smtp.bind = "0.0.0.0:25"
+server.listener.smtp.protocol = "smtp"
+server.listener.submission.bind = "0.0.0.0:587"
+server.listener.submission.protocol = "smtp"
+server.listener.submissions.bind = "0.0.0.0:465"
+server.listener.submissions.protocol = "smtp"
+server.listener.submissions.tls.implicit = true
+server.max-connections = 8192
+server.socket.backlog = 1024
+server.socket.nodelay = true
+server.socket.reuse-addr = true
+server.socket.reuse-port = true
+sharing.allow-directory-query = false
+storage.blob = "rocksdb"
+storage.data = "rocksdb"
+storage.directory = "oidc"
+storage.fts = "rocksdb"
+storage.lookup = "rocksdb"
+store.rocksdb.compression = "lz4"
+store.rocksdb.path = "/opt/stalwart/data"
+store.rocksdb.type = "rocksdb"
+tracer.console.ansi = true
+tracer.console.buffered = true
+tracer.console.enable = true
+tracer.console.level = "trace"
+tracer.console.lossy = false
+tracer.console.multiline = false
+tracer.console.type = "stdout"
diff --git a/devtools/deployments/opencloud_full/opencloud.yml b/devtools/deployments/opencloud_full/opencloud.yml
index c3cafc3a42..aac58f7763 100644
--- a/devtools/deployments/opencloud_full/opencloud.yml
+++ b/devtools/deployments/opencloud_full/opencloud.yml
@@ -64,6 +64,8 @@ services:
GROUPS_LDAP_BIND_PASSWORD: "admin"
IDM_LDAPS_ADDR: 0.0.0.0:9235
GROUPWARE_JMAP_BASE_URL: https://${STALWART_DOMAIN:-stalwart.opencloud.test}
+ GROUPWARE_JMAP_MASTER_USERNAME: "master"
+ GROUPWARE_JMAP_MASTER_PASSWORD: "admin"
volumes:
- ./config/opencloud/app-registry.yaml:/etc/opencloud/app-registry.yaml
- ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml
diff --git a/go.mod b/go.mod
index 2d145dd9a2..755937cd3b 100644
--- a/go.mod
+++ b/go.mod
@@ -7,9 +7,7 @@ require (
github.com/CiscoM31/godata v1.0.11
github.com/KimMachineGun/automemlimit v0.7.5
github.com/Masterminds/semver v1.5.0
- github.com/MicahParks/jwkset v0.8.0
github.com/MicahParks/keyfunc/v2 v2.1.0
- github.com/MicahParks/keyfunc/v3 v3.3.11
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/ProtonMail/go-crypto v1.1.6
github.com/bbalet/stopwords v1.0.0
@@ -103,7 +101,6 @@ require (
github.com/tidwall/sjson v1.2.5
github.com/tus/tusd/v2 v2.9.2
github.com/unrolled/secure v1.16.0
- github.com/urfave/cli/v2 v2.27.7
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/wk8/go-ordered-map v1.0.0
github.com/xhit/go-simple-mail/v2 v2.16.0
@@ -383,6 +380,7 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/trustelem/zxcvbn v1.0.1 // indirect
+ github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
diff --git a/go.sum b/go.sum
index b39d1029c5..bd42cb9e94 100644
--- a/go.sum
+++ b/go.sum
@@ -80,12 +80,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
-github.com/MicahParks/jwkset v0.8.0 h1:jHtclI38Gibmu17XMI6+6/UB59srp58pQVxePHRK5o8=
-github.com/MicahParks/jwkset v0.8.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
-github.com/MicahParks/keyfunc/v3 v3.3.11 h1:eA6wNltwdSRX2gtpTwZseBCC9nGeBkI9KxHtTyZbDbo=
-github.com/MicahParks/keyfunc/v3 v3.3.11/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 3f3982e92d..e016ede045 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -89,6 +89,7 @@ type Config struct {
AppProvider *appProvider.Config `yaml:"app_provider"`
AppRegistry *appRegistry.Config `yaml:"app_registry"`
Audit *audit.Config `yaml:"audit"`
+ AuthApi *authapi.Config `yaml:"auth_api"`
AuthApp *authapp.Config `yaml:"auth_app"`
AuthBasic *authbasic.Config `yaml:"auth_basic"`
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
@@ -126,5 +127,4 @@ type Config struct {
WebDAV *webdav.Config `yaml:"webdav"`
Webfinger *webfinger.Config `yaml:"webfinger"`
Search *search.Config `yaml:"search"`
- AuthApi *authapi.Config `yaml:"authapi"`
}
diff --git a/pkg/jmap/api.go b/pkg/jmap/api.go
index c5578b28fc..7b049e1f68 100644
--- a/pkg/jmap/api.go
+++ b/pkg/jmap/api.go
@@ -23,12 +23,12 @@ type WsClient interface {
}
type WsClientFactory interface {
- EnableNotifications(pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
+ EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
io.Closer
}
type SessionClient interface {
- GetSession(baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
+ GetSession(ctx context.Context, baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
io.Closer
}
diff --git a/pkg/jmap/api_ws.go b/pkg/jmap/api_ws.go
index 3af91c454f..9c8f6ac72c 100644
--- a/pkg/jmap/api_ws.go
+++ b/pkg/jmap/api_ws.go
@@ -1,7 +1,9 @@
package jmap
-func (j *Client) EnablePushNotifications(pushState State, sessionProvider func() (*Session, error)) (WsClient, error) {
- return j.ws.EnableNotifications(pushState, sessionProvider, j)
+import "context"
+
+func (j *Client) EnablePushNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error)) (WsClient, error) {
+ return j.ws.EnableNotifications(ctx, pushState, sessionProvider, j)
}
func (j *Client) AddWsPushListener(listener WsPushListener) {
diff --git a/pkg/jmap/client.go b/pkg/jmap/client.go
index b16d4a012d..86feb3ecf7 100644
--- a/pkg/jmap/client.go
+++ b/pkg/jmap/client.go
@@ -1,6 +1,7 @@
package jmap
import (
+ "context"
"errors"
"io"
"net/url"
@@ -55,8 +56,8 @@ func (j *Client) OnNotification(username string, stateChange StateChange) {
}
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
-func (j *Client) FetchSession(sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
- wk, err := j.session.GetSession(sessionUrl, username, logger)
+func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
+ wk, err := j.session.GetSession(ctx, sessionUrl, username, logger)
if err != nil {
return Session{}, err
}
diff --git a/pkg/jmap/http.go b/pkg/jmap/http.go
index a7851f8c12..21c62c992f 100644
--- a/pkg/jmap/http.go
+++ b/pkg/jmap/http.go
@@ -22,11 +22,10 @@ import (
// Implementation of ApiClient, SessionClient and BlobClient that uses
// HTTP to perform JMAP operations.
type HttpJmapClient struct {
- client *http.Client
- masterUser string
- masterPassword string
- userAgent string
- listener HttpJmapApiClientEventListener
+ client *http.Client
+ userAgent string
+ authenticator HttpJmapClientAuthenticator
+ listener HttpJmapApiClientEventListener
}
var (
@@ -49,12 +48,6 @@ const (
logTypePush = "push"
)
-/*
-func bearer(req *http.Request, token string) {
- req.Header.Add("Authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte(token)))
-}
-*/
-
// Record JMAP HTTP execution events that may occur, e.g. using metrics.
type HttpJmapApiClientEventListener interface {
OnSuccessfulRequest(endpoint string, status int)
@@ -86,18 +79,45 @@ func (l nullHttpJmapApiClientEventListener) OnFailedWsHandshakeRequestWithStatus
var _ HttpJmapApiClientEventListener = nullHttpJmapApiClientEventListener{}
+type HttpJmapClientAuthenticator interface {
+ Authenticate(ctx context.Context, username string, logger *log.Logger, req *http.Request) Error
+ AuthenticateWS(ctx context.Context, username string, logger *log.Logger, headers http.Header) Error
+}
+
+type MasterAuthHttpJmapClientAuthenticator struct {
+ masterUser string
+ masterPassword string
+}
+
+func NewMasterAuthHttpJmapClientAuthenticator(masterUser string, masterPassword string) HttpJmapClientAuthenticator {
+ return &MasterAuthHttpJmapClientAuthenticator{masterUser: masterUser, masterPassword: masterPassword}
+}
+
+var _ HttpJmapClientAuthenticator = &MasterAuthHttpJmapClientAuthenticator{}
+
+func (h *MasterAuthHttpJmapClientAuthenticator) Authenticate(ctx context.Context, username string, _ *log.Logger, req *http.Request) Error {
+ masterUsername := username + "%" + h.masterUser
+ req.SetBasicAuth(masterUsername, h.masterPassword)
+ return nil
+}
+
+func (h *MasterAuthHttpJmapClientAuthenticator) AuthenticateWS(ctx context.Context, username string, _ *log.Logger, headers http.Header) Error {
+ masterUsername := username + "%" + h.masterUser
+ headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(masterUsername+":"+h.masterPassword)))
+ return nil
+}
+
// An implementation of HttpJmapApiClientMetricsRecorder that does nothing.
func NullHttpJmapApiClientEventListener() HttpJmapApiClientEventListener {
return nullHttpJmapApiClientEventListener{}
}
-func NewHttpJmapClient(client *http.Client, masterUser string, masterPassword string, listener HttpJmapApiClientEventListener) *HttpJmapClient {
+func NewHttpJmapClient(client *http.Client, authenticator HttpJmapClientAuthenticator, listener HttpJmapApiClientEventListener) *HttpJmapClient {
return &HttpJmapClient{
- client: client,
- masterUser: masterUser,
- masterPassword: masterPassword,
- userAgent: "OpenCloud/" + version.GetString(),
- listener: listener,
+ client: client,
+ authenticator: authenticator,
+ userAgent: "OpenCloud/" + version.GetString(),
+ listener: listener,
}
}
@@ -117,17 +137,15 @@ func (e AuthenticationError) Unwrap() error {
return e.Err
}
-func (h *HttpJmapClient) auth(username string, _ *log.Logger, req *http.Request) error {
- masterUsername := username + "%" + h.masterUser
- req.SetBasicAuth(masterUsername, h.masterPassword)
- return nil
+func (h *HttpJmapClient) auth(ctx context.Context, username string, logger *log.Logger, req *http.Request) Error {
+ return h.authenticator.Authenticate(ctx, username, logger, req)
}
var (
errNilBaseUrl = errors.New("sessionUrl is nil")
)
-func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
+func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
if sessionUrl == nil {
logger.Error().Msg("sessionUrl is nil")
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: errNilBaseUrl}
@@ -147,7 +165,9 @@ func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger
logger.Error().Err(err).Msgf("failed to create GET request for %v", sessionUrl)
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
}
- h.auth(username, logger, req)
+ if err := h.auth(ctx, username, logger, req); err != nil {
+ return SessionResponse{}, err
+ }
req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation
res, err := h.client.Do(req)
@@ -222,7 +242,9 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
logger.Trace().Str(logEndpoint, endpoint).Str(logProto, logProtoJmap).Str(logType, logTypeRequest).Msg(string(requestBytes))
}
}
- h.auth(session.Username, logger, req)
+ if err := h.auth(ctx, session.Username, logger, req); err != nil {
+ return nil, "", err
+ }
res, err := h.client.Do(req)
if err != nil {
@@ -286,7 +308,9 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
}
}
- h.auth(session.Username, logger, req)
+ if err := h.auth(ctx, session.Username, logger, req); err != nil {
+ return UploadedBlob{}, "", err
+ }
res, err := h.client.Do(req)
if err != nil {
@@ -357,7 +381,10 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
logger.Trace().Str(logEndpoint, endpoint).Str(logProto, logProtoJmap).Str(logType, logTypeRequest).Msg(string(requestBytes))
}
}
- h.auth(session.Username, logger, req)
+
+ if err := h.auth(ctx, session.Username, logger, req); err != nil {
+ return nil, "", err
+ }
res, err := h.client.Do(req)
if err != nil {
@@ -435,16 +462,15 @@ type WebSocketPushDisable struct {
}
type HttpWsClientFactory struct {
- dialer *websocket.Dialer
- masterUser string
- masterPassword string
- logger *log.Logger
- eventListener HttpJmapApiClientEventListener
+ dialer *websocket.Dialer
+ authenticator HttpJmapClientAuthenticator
+ logger *log.Logger
+ eventListener HttpJmapApiClientEventListener
}
var _ WsClientFactory = &HttpWsClientFactory{}
-func NewHttpWsClientFactory(dialer *websocket.Dialer, masterUser string, masterPassword string, logger *log.Logger,
+func NewHttpWsClientFactory(dialer *websocket.Dialer, authenticator HttpJmapClientAuthenticator, logger *log.Logger,
eventListener HttpJmapApiClientEventListener) (*HttpWsClientFactory, error) {
// RFC 8887: Section 4.2:
// Otherwise, the client MUST make an authenticated HTTP request [RFC7235] on the encrypted connection
@@ -453,21 +479,18 @@ func NewHttpWsClientFactory(dialer *websocket.Dialer, masterUser string, masterP
dialer.Subprotocols = []string{"jmap"}
return &HttpWsClientFactory{
- dialer: dialer,
- masterUser: masterUser,
- masterPassword: masterPassword,
- logger: logger,
- eventListener: eventListener,
+ dialer: dialer,
+ authenticator: authenticator,
+ logger: logger,
+ eventListener: eventListener,
}, nil
}
-func (w *HttpWsClientFactory) auth(username string, h http.Header) error {
- masterUsername := username + "%" + w.masterUser
- h.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(masterUsername+":"+w.masterPassword)))
- return nil
+func (w *HttpWsClientFactory) auth(ctx context.Context, username string, logger *log.Logger, h http.Header) Error {
+ return w.authenticator.AuthenticateWS(ctx, username, logger, h)
}
-func (w *HttpWsClientFactory) connect(sessionProvider func() (*Session, error)) (*websocket.Conn, string, string, Error) {
+func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func() (*Session, error)) (*websocket.Conn, string, string, Error) {
logger := w.logger
session, err := sessionProvider()
@@ -486,10 +509,8 @@ func (w *HttpWsClientFactory) connect(sessionProvider func() (*Session, error))
u := session.WebsocketUrl
endpoint := session.WebsocketEndpoint
- ctx := context.Background() // TODO WS connection context with a timeout?
-
h := http.Header{}
- w.auth(username, h)
+ w.auth(ctx, username, logger, h)
c, res, err := w.dialer.DialContext(ctx, u.String(), h)
if err != nil {
return nil, "", endpoint, SimpleError{code: JmapErrorFailedToEstablishWssConnection, err: err}
@@ -582,8 +603,8 @@ func (w *HttpWsClient) readPump() {
}
}
-func (w *HttpWsClientFactory) EnableNotifications(pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
- c, username, endpoint, jerr := w.connect(sessionProvider)
+func (w *HttpWsClientFactory) EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
+ c, username, endpoint, jerr := w.connect(ctx, sessionProvider)
if jerr != nil {
return nil, jerr
}
diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go
index dbc8a17ea0..2de2c4a5c6 100644
--- a/pkg/jmap/integration_test.go
+++ b/pkg/jmap/integration_test.go
@@ -175,7 +175,7 @@ func (s *StalwartTest) Close() error {
}
func (s *StalwartTest) Session(username string) *Session {
- session, jerr := s.client.FetchSession(s.sessionUrl, username, s.logger)
+ session, jerr := s.client.FetchSession(s.ctx, s.sessionUrl, username, s.logger)
require.NoError(s.t, jerr)
require.NotNil(s.t, session.Capabilities.Mail)
require.NotNil(s.t, session.Capabilities.Calendars)
@@ -328,14 +328,11 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
eventListener := nullHttpJmapApiClientEventListener{}
- api := NewHttpJmapClient(
- &jh,
- masterUsername,
- masterPassword,
- eventListener,
- )
+ auth := NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword)
- wscf, err := NewHttpWsClientFactory(wsd, masterUsername, masterPassword, logger, eventListener)
+ api := NewHttpJmapClient(&jh, auth, eventListener)
+
+ wscf, err := NewHttpWsClientFactory(wsd, auth, logger, eventListener)
if err != nil {
return nil, err
}
@@ -455,7 +452,7 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
{
// check whether we can fetch a session for the provisioned users
for _, user := range users {
- session, err := j.FetchSession(sessionUrl, user.name, logger)
+ session, err := j.FetchSession(ctx, sessionUrl, user.name, logger)
require.NoError(t, err, "failed to retrieve JMAP session for newly created principal '%s'", user.name)
require.Equal(t, user.name, session.Username)
}
diff --git a/pkg/jmap/integration_ws_test.go b/pkg/jmap/integration_ws_test.go
index 673949775d..065e9827ae 100644
--- a/pkg/jmap/integration_ws_test.go
+++ b/pkg/jmap/integration_ws_test.go
@@ -1,6 +1,7 @@
package jmap
import (
+ "context"
"sync"
"sync/atomic"
"testing"
@@ -60,6 +61,8 @@ func TestWs(t *testing.T) {
require := require.New(t)
+ ctx := context.Background()
+
s, err := newStalwartTest(t)
require.NoError(err)
defer s.Close()
@@ -111,7 +114,7 @@ func TestWs(t *testing.T) {
require.Empty(changes.Updated)
}
- wsc, err := s.client.EnablePushNotifications(initialState, func() (*Session, error) { return session, nil })
+ wsc, err := s.client.EnablePushNotifications(ctx, initialState, func() (*Session, error) { return session, nil })
require.NoError(err)
defer wsc.Close()
diff --git a/services/auth-api/pkg/auth-api/authapi.go b/services/auth-api/pkg/auth-api/authapi.go
new file mode 100644
index 0000000000..21a140bd44
--- /dev/null
+++ b/services/auth-api/pkg/auth-api/authapi.go
@@ -0,0 +1,329 @@
+package auth_api
+
+import (
+ "encoding/json"
+ "net/http"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/golang-jwt/jwt/v5"
+
+ oteltrace "go.opentelemetry.io/otel/trace"
+
+ "github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/opencloud-eu/opencloud/pkg/version"
+
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
+)
+
+const defaultLeeway int64 = 5
+
+type appId string
+
+type AuthApi struct {
+ mux *chi.Mux
+ logger *log.Logger
+ metrics *metrics.Metrics
+ tracer oteltrace.Tracer
+ parser *jwt.Parser
+ keyFunc func(token *jwt.Token) (any, error)
+ audiences []string
+ requireSharedSecret bool
+ sharedSecrets map[string]appId
+}
+
+func parseSecrets(s string) (map[string]appId, error) {
+ s = strings.TrimSpace(s)
+ if len(s) == 0 {
+ return map[string]appId{}, nil
+ }
+ result := map[string]appId{}
+ for item := range strings.SplitSeq(s, ";") {
+ item = strings.TrimSpace(item)
+ parts := strings.Split(item, "=")
+ switch len(parts) {
+ case 0:
+ case 1:
+ result[item] = appId("")
+ case 2:
+ result[parts[1]] = appId(parts[0])
+ default:
+ result[strings.Join(parts[1:], "=")] = appId(parts[0])
+ }
+ }
+ return result, nil
+}
+
+func NewAuthApi(
+ config *config.Config,
+ logger *log.Logger,
+ tracerProvider oteltrace.TracerProvider,
+ metrics *metrics.Metrics,
+ mux *chi.Mux,
+) (*AuthApi, error) {
+ jwtSecret := []byte(config.TokenManager.JWTSecret)
+ parser := jwt.NewParser(jwt.WithAudience("reva"), jwt.WithLeeway(time.Duration(defaultLeeway)*time.Second))
+
+ var tracer oteltrace.Tracer
+ if tracerProvider != nil {
+ tracer = tracerProvider.Tracer("instrumentation/" + config.HTTP.Namespace + "/" + config.Service.Name)
+ }
+
+ metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1)
+
+ sharedSecrets, err := parseSecrets(config.Auth.SharedSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ return &AuthApi{
+ mux: mux,
+ logger: logger,
+ metrics: metrics,
+ tracer: tracer,
+ parser: parser,
+ keyFunc: func(token *jwt.Token) (any, error) { return jwtSecret, nil },
+ audiences: config.Auth.Audiences,
+ requireSharedSecret: config.Auth.RequireSharedSecret,
+ sharedSecrets: sharedSecrets,
+ }, nil
+}
+
+func (a *AuthApi) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ a.mux.ServeHTTP(w, r)
+}
+
+func (a *AuthApi) Route(r chi.Router) {
+ r.Get("/", a.Unauthenticated)
+ r.Post("/", a.Unauthenticated)
+ r.Get("/{secret}", a.Authenticated)
+ r.Post("/{secret}", a.Authenticated)
+}
+
+type StalwartClaims struct {
+ Audience []string `json:"aud"`
+ Subject string `json:"sub"`
+ Name string `json:"name"`
+ Username string `json:"preferred_username"`
+ Nickname string `json:"nickname,omitempty"`
+ Email string `json:"email"`
+ Expires *jwt.NumericDate `json:"exp"`
+ IssuedAt *jwt.NumericDate `json:"iat"`
+ Issuer string `json:"iss,omitempty"`
+ NotBefore *jwt.NumericDate `json:"nbf,omitzero"`
+}
+
+func invalidRequest(w http.ResponseWriter) {
+ w.Header().Add("WWW-Authenticate", `Bearer error="invalid_request"`)
+ w.WriteHeader(http.StatusBadRequest)
+}
+
+func invalidToken(w http.ResponseWriter) {
+ w.Header().Add("WWW-Authenticate", `Bearer error="invalid_token"`)
+ w.WriteHeader(http.StatusUnauthorized)
+}
+
+func (a *AuthApi) unsupportedAuth() {
+ a.metrics.Attempts.WithLabelValues(
+ metrics.AttemptFailureOutcome,
+ metrics.UnsupportedType,
+ ).Inc()
+}
+func (a *AuthApi) failedAuth(duration time.Duration) {
+ a.metrics.Attempts.WithLabelValues(
+ metrics.AttemptFailureOutcome,
+ metrics.BearerType,
+ ).Inc()
+ a.metrics.Duration.WithLabelValues(
+ metrics.AttemptFailureOutcome,
+ ).Observe(duration.Seconds())
+}
+func (a *AuthApi) succeededAuth(duration time.Duration) {
+ a.metrics.Attempts.WithLabelValues(
+ metrics.AttemptSuccessOutcome,
+ metrics.BearerType,
+ ).Inc()
+ a.metrics.Duration.WithLabelValues(
+ metrics.AttemptSuccessOutcome,
+ ).Observe(duration.Seconds())
+}
+
+func (a *AuthApi) Unauthenticated(w http.ResponseWriter, r *http.Request) {
+ if a.requireSharedSecret {
+ a.unsupportedAuth()
+ a.logger.Warn().Str("reason", "missing-shared-secret").Msgf("authentication failure: request did not provide a required shared secret")
+ invalidRequest(w)
+ return
+ } else {
+ a.authenticate(w, r, "")
+ }
+}
+
+func (a *AuthApi) Authenticated(w http.ResponseWriter, r *http.Request) {
+ secret := chi.URLParam(r, "secret")
+ if app, ok := a.sharedSecrets[secret]; ok {
+ a.authenticate(w, r, app)
+ } else {
+ a.unsupportedAuth()
+ a.logger.Warn().Str("reason", "invalid-shared-secret").Msgf("authentication failure: request did not provide a valid shared secret")
+ invalidRequest(w)
+ return
+ }
+}
+
+func (a *AuthApi) authenticate(w http.ResponseWriter, r *http.Request, app appId) {
+ start := time.Now()
+ logger := a.logger
+ if app != "" {
+ logger = log.From(logger.With().Str("app", log.SafeString(string(app))))
+ }
+
+ var span oteltrace.Span = nil
+ if a.tracer != nil {
+ _, span = a.tracer.Start(r.Context(), "authenticate")
+ defer span.End()
+ }
+
+ auth := r.Header.Get("Authorization")
+ if auth == "" {
+ a.unsupportedAuth()
+ logger.Warn().Str("reason", "missing-authorization-header").Msgf("authentication failure: missing 'Authorization' header")
+ invalidRequest(w)
+ return
+ }
+ if !strings.HasPrefix(auth, "Bearer ") {
+ a.unsupportedAuth()
+ logger.Warn().Str("reason", "authorization-header-not-bearer").Msgf("authentication failure: 'Authorization' header does not start with 'Bearer '")
+ invalidRequest(w)
+ return
+ }
+
+ tokenStr := auth[len("Bearer "):]
+
+ claims := jwt.MapClaims{}
+ token, err := a.parser.ParseWithClaims(tokenStr, claims, a.keyFunc)
+ //token, _, err := jwt.NewParser().ParseUnverified(tokenStr, claims)
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-parsing-failed").Msgf("authentication failure: failed to parse token")
+ invalidToken(w)
+ return
+ }
+ if !token.Valid {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid").Msgf("authentication failure: the token is invalid")
+ invalidToken(w)
+ return
+ }
+
+ user, ok := claims["user"].(map[string]any)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-user").Msgf("authentication failure: token has no 'user' claim")
+ invalidToken(w)
+ return
+ }
+ id, ok := user["id"].(map[string]any)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-id").Msgf("authentication failure: token has no 'id' attribute in the 'user' claim")
+ invalidToken(w)
+ return
+ }
+ opaqueId, ok := id["opaque_id"].(string)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-id-opaqueid").Msgf("authentication failure: token has no 'id/opaque_id' attribute in the 'user' claim")
+ invalidToken(w)
+ return
+ }
+ username, ok := user["username"].(string)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-username").Msgf("authentication failure: token has no 'username' attribute in the 'user' claim")
+ invalidToken(w)
+ return
+ }
+ displayName, ok := user["display_name"].(string)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-displayname").Msgf("authentication failure: token has no 'display_name' attribute in the 'user' claim")
+ invalidToken(w)
+ return
+ }
+ mail, ok := user["mail"].(string)
+ if !ok {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-missing-mail").Msgf("authentication failure: token has no 'mail' attribute in the 'user' claim")
+ invalidToken(w)
+ return
+ }
+
+ exp, err := token.Claims.GetExpirationTime()
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid-exp").Msgf("authentication failure: token has invalid 'exp'")
+ invalidToken(w)
+ return
+ }
+ iat, err := token.Claims.GetIssuedAt()
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid-iat").Msgf("authentication failure: token has invalid 'iat'")
+ invalidToken(w)
+ return
+ }
+ nbf, err := token.Claims.GetNotBefore()
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid-nbf").Msgf("authentication failure: token has invalid 'nbf'")
+ invalidToken(w)
+ return
+ }
+ iss, err := token.Claims.GetIssuer()
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid-iss").Msgf("authentication failure: token has invalid 'iss'")
+ invalidToken(w)
+ return
+ }
+ aud, err := claims.GetAudience()
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "token-invalid-aud").Msgf("authentication failure: token has invalid 'aud'")
+ invalidToken(w)
+ return
+ }
+
+ audiences := aud
+ if len(a.audiences) > 0 {
+ audiences = slices.Concat(aud, a.audiences)
+ }
+
+ rc := StalwartClaims{
+ Audience: audiences,
+ Subject: opaqueId,
+ Name: displayName,
+ Username: username,
+ Email: mail,
+ Expires: exp,
+ IssuedAt: iat,
+ NotBefore: nbf,
+ Issuer: iss,
+ }
+ response, err := json.Marshal(rc)
+ if err != nil {
+ a.failedAuth(time.Since(start))
+ logger.Warn().Err(err).Str("reason", "response-serialization-failure").Msgf("authentication failure: failed to serialize response")
+ invalidToken(w)
+ return
+ }
+
+ logger.Debug().Str("username", username).Msg("successfully authenticated token")
+ a.succeededAuth(time.Since(start))
+ w.WriteHeader(http.StatusOK)
+ w.Write(response)
+}
diff --git a/services/auth-api/pkg/auth-api/authapi_test.go b/services/auth-api/pkg/auth-api/authapi_test.go
new file mode 100644
index 0000000000..71a39d6949
--- /dev/null
+++ b/services/auth-api/pkg/auth-api/authapi_test.go
@@ -0,0 +1,40 @@
+package auth_api
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseSecrets(t *testing.T) {
+ require := require.New(t)
+
+ {
+ m, err := parseSecrets("")
+ require.NoError(err)
+ require.Empty(m)
+ }
+ {
+ m, err := parseSecrets("app=123")
+ require.NoError(err)
+ require.Len(m, 1)
+ require.Contains(m, "123")
+ require.Equal(appId("app"), m["123"])
+ }
+ {
+ m, err := parseSecrets("app1=123;app2=23456")
+ require.NoError(err)
+ require.Len(m, 2)
+ require.Contains(m, "123")
+ require.Equal(appId("app1"), m["123"])
+ require.Contains(m, "23456")
+ require.Equal(appId("app2"), m["23456"])
+ }
+ {
+ m, err := parseSecrets("app=123=456")
+ require.NoError(err)
+ require.Len(m, 1)
+ require.Contains(m, "123=456")
+ require.Equal(appId("app"), m["123=456"])
+ }
+}
diff --git a/services/auth-api/pkg/command/server.go b/services/auth-api/pkg/command/server.go
index f4879c577c..a4d142b15f 100644
--- a/services/auth-api/pkg/command/server.go
+++ b/services/auth-api/pkg/command/server.go
@@ -6,11 +6,10 @@ import (
"github.com/oklog/run"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
- "github.com/opencloud-eu/opencloud/pkg/version"
+ "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/config/parser"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/logging"
- "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/server/debug"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/server/http"
@@ -28,16 +27,18 @@ func Server(cfg *config.Config) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
+ tracerProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
+ if err != nil {
+ return err
+ }
+
var (
gr = run.Group{}
ctx, cancel = context.WithCancel(context.Background())
- m = metrics.New()
)
defer cancel()
- m.BuildInfo.WithLabelValues(version.GetString()).Set(1)
-
server, err := debug.Server(
debug.Logger(logger),
debug.Config(cfg),
@@ -54,11 +55,10 @@ func Server(cfg *config.Config) *cobra.Command {
})
httpServer, err := http.Server(
- http.Logger(logger),
- http.Context(ctx),
- http.Config(cfg),
- http.Metrics(m),
- http.Namespace(cfg.HTTP.Namespace),
+ &logger,
+ ctx,
+ cfg,
+ tracerProvider,
)
if err != nil {
logger.Info().
diff --git a/services/auth-api/pkg/config/auth.go b/services/auth-api/pkg/config/auth.go
new file mode 100644
index 0000000000..f1e2186e0d
--- /dev/null
+++ b/services/auth-api/pkg/config/auth.go
@@ -0,0 +1,7 @@
+package config
+
+type Auth struct {
+ Audiences []string `yaml:"auds" env:"AUTHAPI_AUTH_AUDS" desc:"Additional audiences to inject into the userinfo response claims" introductionVersion:"1.0.0"`
+ RequireSharedSecret bool `yaml:"require_shared_secret" env:"AUTHAPI_AUTH_REQUIRE_SHARED_SECRET" desc:"Whether to require a shared secret or not" introductionVersion:"1.0.0"`
+ SharedSecrets string `yaml:"shared_secrets" env:"AUTHAPI_AUTH_SHARED_SECRETS" desc:"Shared secret values" introductionVersion:"1.0.0"`
+}
diff --git a/services/auth-api/pkg/config/config.go b/services/auth-api/pkg/config/config.go
index 60e8f539be..16f3165217 100644
--- a/services/auth-api/pkg/config/config.go
+++ b/services/auth-api/pkg/config/config.go
@@ -17,11 +17,9 @@ type Config struct {
HTTP HTTP `yaml:"http"`
- Authentication AuthenticationAPI `yaml:"authentication_api"`
-
Context context.Context `yaml:"-"`
-}
-type AuthenticationAPI struct {
- JwkEndpoint string `yaml:"jwk_endpoint"`
+ TokenManager *TokenManager `yaml:"token_manager"`
+
+ Auth Auth `yaml:"auth"`
}
diff --git a/services/auth-api/pkg/config/defaults/defaultconfig.go b/services/auth-api/pkg/config/defaults/defaultconfig.go
index 7860c2900f..69af1e407e 100644
--- a/services/auth-api/pkg/config/defaults/defaultconfig.go
+++ b/services/auth-api/pkg/config/defaults/defaultconfig.go
@@ -31,8 +31,10 @@ func DefaultConfig() *config.Config {
Service: config.Service{
Name: "auth-api",
},
- Authentication: config.AuthenticationAPI{
- JwkEndpoint: "https://keycloak.opencloud.test/realms/openCloud/protocol/openid-connect/certs",
+ Auth: config.Auth{
+ Audiences: []string{"stalwart"},
+ RequireSharedSecret: false,
+ SharedSecrets: "",
},
}
}
@@ -50,10 +52,16 @@ func EnsureDefaults(cfg *config.Config) {
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
-
if cfg.Commons != nil {
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
}
+ if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
+ cfg.TokenManager = &config.TokenManager{
+ JWTSecret: cfg.Commons.TokenManager.JWTSecret,
+ }
+ } else if cfg.TokenManager == nil {
+ cfg.TokenManager = &config.TokenManager{}
+ }
}
// Sanitize sanitized the configuration
diff --git a/services/auth-api/pkg/config/parser/parse.go b/services/auth-api/pkg/config/parser/parse.go
index 8dbefde027..e3548f1526 100644
--- a/services/auth-api/pkg/config/parser/parse.go
+++ b/services/auth-api/pkg/config/parser/parse.go
@@ -4,6 +4,7 @@ import (
"errors"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
+ "github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/defaults"
@@ -34,6 +35,10 @@ func ParseConfig(cfg *config.Config) error {
}
// Validate can validate the configuration
-func Validate(_ *config.Config) error {
+func Validate(cfg *config.Config) error {
+ if cfg.TokenManager.JWTSecret == "" {
+ return shared.MissingJWTTokenError(cfg.Service.Name)
+ }
+
return nil
}
diff --git a/services/auth-api/pkg/config/reva.go b/services/auth-api/pkg/config/reva.go
new file mode 100644
index 0000000000..a33af14389
--- /dev/null
+++ b/services/auth-api/pkg/config/reva.go
@@ -0,0 +1,6 @@
+package config
+
+// TokenManager is the config for using the reva token manager
+type TokenManager struct {
+ JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;AUTHAPI_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
+}
diff --git a/services/auth-api/pkg/metrics/metrics.go b/services/auth-api/pkg/metrics/metrics.go
index c684316cd1..680e3ca96e 100644
--- a/services/auth-api/pkg/metrics/metrics.go
+++ b/services/auth-api/pkg/metrics/metrics.go
@@ -1,6 +1,9 @@
package metrics
-import "github.com/prometheus/client_golang/prometheus"
+import (
+ "github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/prometheus/client_golang/prometheus"
+)
var (
// Namespace defines the namespace for the defines metrics.
@@ -27,10 +30,7 @@ const (
AttemptFailureOutcome = "failure"
)
-// New initializes the available metrics.
-func New(opts ...Option) *Metrics {
- options := newOptions(opts...)
-
+func New(logger *log.Logger) (*Metrics, error) {
m := &Metrics{
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
@@ -43,32 +43,35 @@ func New(opts ...Option) *Metrics {
Subsystem: Subsystem,
Name: "authentication_duration_seconds",
Help: "Authentication processing time in seconds",
- }, []string{"type"}),
+ }, []string{"outcome"}),
Attempts: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
- Name: "athentication_attempts_total",
+ Name: "authentication_attempts_total",
Help: "How many authentication attempts were processed",
- }, []string{"outcome"}),
+ }, []string{"outcome", "type"}),
}
if err := prometheus.Register(m.BuildInfo); err != nil {
- options.Logger.Error().
+ logger.Error().
Err(err).
Str("metric", "BuildInfo").
Msg("Failed to register prometheus metric")
+ return nil, err
}
if err := prometheus.Register(m.Duration); err != nil {
- options.Logger.Error().
+ logger.Error().
Err(err).
Str("metric", "Duration").
Msg("Failed to register prometheus metric")
+ return nil, err
}
if err := prometheus.Register(m.Attempts); err != nil {
- options.Logger.Error().
+ logger.Error().
Err(err).
Str("metric", "Attempts").
Msg("Failed to register prometheus metric")
+ return nil, err
}
- return m
+ return m, nil
}
diff --git a/services/auth-api/pkg/metrics/options.go b/services/auth-api/pkg/metrics/options.go
deleted file mode 100644
index 304456e7d1..0000000000
--- a/services/auth-api/pkg/metrics/options.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package metrics
-
-import (
- "github.com/opencloud-eu/opencloud/pkg/log"
-)
-
-// Option defines a single option function.
-type Option func(o *Options)
-
-// Options defines the available options for this package.
-type Options struct {
- Logger log.Logger
-}
-
-// newOptions initializes the available default options.
-func newOptions(opts ...Option) Options {
- opt := Options{}
-
- for _, o := range opts {
- o(&opt)
- }
-
- return opt
-}
-
-// Logger provides a function to set the logger option.
-func Logger(val log.Logger) Option {
- return func(o *Options) {
- o.Logger = val
- }
-}
diff --git a/services/auth-api/pkg/server/debug/server.go b/services/auth-api/pkg/server/debug/server.go
index 3f54c66012..11e2e4d44e 100644
--- a/services/auth-api/pkg/server/debug/server.go
+++ b/services/auth-api/pkg/server/debug/server.go
@@ -16,6 +16,10 @@ func Server(opts ...Option) (*http.Server, error) {
WithLogger(options.Logger)
return debug.NewService(
+ debug.Address(options.Config.Debug.Addr),
+ debug.Token(options.Config.Debug.Token),
+ debug.Pprof(options.Config.Debug.Pprof),
+ debug.Zpages(options.Config.Debug.Zpages),
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
diff --git a/services/auth-api/pkg/server/http/option.go b/services/auth-api/pkg/server/http/option.go
index cb4b0b5f75..9ae67048d4 100644
--- a/services/auth-api/pkg/server/http/option.go
+++ b/services/auth-api/pkg/server/http/option.go
@@ -5,10 +5,6 @@ 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"
- "github.com/urfave/cli/v2"
- "go.opentelemetry.io/otel/trace"
- "go.opentelemetry.io/otel/trace/noop"
)
// Option defines a single option function.
@@ -20,9 +16,6 @@ type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
- Metrics *metrics.Metrics
- Flags []cli.Flag
- TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
@@ -57,13 +50,6 @@ func Config(val *config.Config) Option {
}
}
-// Metrics provides a function to set the metrics option.
-func Metrics(val *metrics.Metrics) Option {
- return func(o *Options) {
- o.Metrics = val
- }
-}
-
// Namespace provides a function to set the Namespace option.
func Namespace(val string) Option {
return func(o *Options) {
@@ -71,13 +57,3 @@ func Namespace(val string) Option {
}
}
-// TraceProvider provides a function to configure the trace provider
-func TraceProvider(traceProvider trace.TracerProvider) Option {
- return func(o *Options) {
- if traceProvider != nil {
- o.TraceProvider = traceProvider
- } else {
- o.TraceProvider = noop.NewTracerProvider()
- }
- }
-}
diff --git a/services/auth-api/pkg/server/http/server.go b/services/auth-api/pkg/server/http/server.go
index 42f3d56dc2..9db35c7727 100644
--- a/services/auth-api/pkg/server/http/server.go
+++ b/services/auth-api/pkg/server/http/server.go
@@ -1,59 +1,70 @@
package http
import (
+ "context"
"fmt"
"github.com/go-chi/chi/v5/middleware"
+ "github.com/opencloud-eu/opencloud/pkg/log"
opencloudmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware"
"github.com/opencloud-eu/opencloud/pkg/service/http"
"github.com/opencloud-eu/opencloud/pkg/version"
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
+ "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
svc "github.com/opencloud-eu/opencloud/services/auth-api/pkg/service/http/v0"
"go-micro.dev/v4"
+ oteltrace "go.opentelemetry.io/otel/trace"
)
// Server initializes the http service and server.
-func Server(opts ...Option) (http.Service, error) {
- options := newOptions(opts...)
-
+func Server(
+ logger *log.Logger,
+ ctx context.Context,
+ cfg *config.Config,
+ traceProvider oteltrace.TracerProvider,
+) (http.Service, error) {
service, err := http.NewService(
- http.TLSConfig(options.Config.HTTP.TLS),
- http.Logger(options.Logger),
- http.Name(options.Config.Service.Name),
+ http.TLSConfig(cfg.HTTP.TLS),
+ http.Logger(*logger),
+ http.Name(cfg.Service.Name),
http.Version(version.GetString()),
- http.Namespace(options.Config.HTTP.Namespace),
- http.Address(options.Config.HTTP.Addr),
- http.Context(options.Context),
- http.TraceProvider(options.TraceProvider),
+ http.Namespace(cfg.HTTP.Namespace),
+ http.Address(cfg.HTTP.Addr),
+ http.Context(ctx),
+ http.TraceProvider(traceProvider),
)
if err != nil {
- options.Logger.Error().
+ logger.Error().
Err(err).
Msg("Error initializing http service")
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}
+ met, err := metrics.New(logger)
+ if err != nil {
+ return http.Service{}, err
+ }
+
handle, err := svc.NewService(
- svc.Logger(options.Logger),
- svc.Config(options.Config),
- svc.Metrics(options.Metrics),
- svc.TraceProvider(options.TraceProvider),
- svc.Middleware(
- middleware.RealIP,
- middleware.RequestID,
- opencloudmiddleware.Version(
- options.Config.Service.Name,
- version.GetString(),
- ),
- opencloudmiddleware.Logger(options.Logger),
+ logger,
+ met,
+ traceProvider,
+ cfg,
+ middleware.RealIP,
+ middleware.RequestID,
+ opencloudmiddleware.Version(
+ cfg.Service.Name,
+ version.GetString(),
),
+ opencloudmiddleware.Logger(*logger),
)
if err != nil {
return http.Service{}, err
}
{
- handle = svc.NewInstrument(handle, options.Metrics)
- handle = svc.NewLogging(handle, options.Logger)
+ //handle = svc.NewInstrument(handle, options.Metrics)
+ //handle = svc.NewLogging(handle, options.Logger)
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
diff --git a/services/auth-api/pkg/service/http/v0/instrument.go b/services/auth-api/pkg/service/http/v0/instrument.go
deleted file mode 100644
index d1a6663ecb..0000000000
--- a/services/auth-api/pkg/service/http/v0/instrument.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package svc
-
-import (
- "net/http"
-
- "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
-)
-
-// NewInstrument returns a service that instruments metrics.
-func NewInstrument(next Service, metrics *metrics.Metrics) Service {
- return instrument{
- next: next,
- metrics: metrics,
- }
-}
-
-type instrument struct {
- next Service
- metrics *metrics.Metrics
-}
-
-// ServeHTTP implements the Service interface.
-func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- i.next.ServeHTTP(w, r)
-}
diff --git a/services/auth-api/pkg/service/http/v0/logging.go b/services/auth-api/pkg/service/http/v0/logging.go
deleted file mode 100644
index c21734ce11..0000000000
--- a/services/auth-api/pkg/service/http/v0/logging.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package svc
-
-import (
- "net/http"
-
- "github.com/opencloud-eu/opencloud/pkg/log"
-)
-
-// NewLogging returns a service that logs messages.
-func NewLogging(next Service, logger log.Logger) Service {
- return logging{
- next: next,
- logger: logger,
- }
-}
-
-type logging struct {
- next Service
- logger log.Logger
-}
-
-// ServeHTTP implements the Service interface.
-func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- l.next.ServeHTTP(w, r)
-}
diff --git a/services/auth-api/pkg/service/http/v0/option.go b/services/auth-api/pkg/service/http/v0/option.go
deleted file mode 100644
index cb0e6615f9..0000000000
--- a/services/auth-api/pkg/service/http/v0/option.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package svc
-
-import (
- "net/http"
-
- "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"
-)
-
-// 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
- Config *config.Config
- Middleware []func(http.Handler) http.Handler
- Metrics *metrics.Metrics
- TraceProvider trace.TracerProvider
-}
-
-// 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
- }
-}
-
-// Config provides a function to set the config option.
-func Config(val *config.Config) Option {
- return func(o *Options) {
- o.Config = val
- }
-}
-
-// Middleware provides a function to set the middleware option.
-func Middleware(val ...func(http.Handler) http.Handler) Option {
- return func(o *Options) {
- o.Middleware = val
- }
-}
-
-func TraceProvider(tp trace.TracerProvider) Option {
- return func(o *Options) {
- o.TraceProvider = tp
- }
-}
-
-func Metrics(m *metrics.Metrics) Option {
- return func(o *Options) {
- o.Metrics = m
- }
-}
diff --git a/services/auth-api/pkg/service/http/v0/service.go b/services/auth-api/pkg/service/http/v0/service.go
index 0364182e30..35c3ec1419 100644
--- a/services/auth-api/pkg/service/http/v0/service.go
+++ b/services/auth-api/pkg/service/http/v0/service.go
@@ -1,24 +1,16 @@
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"
+ auth_api "github.com/opencloud-eu/opencloud/services/auth-api/pkg/auth-api"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
"github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics"
)
@@ -29,238 +21,33 @@ type Service interface {
}
// NewService returns a service implementation for Service.
-func NewService(opts ...Option) (Service, error) {
- options := newOptions(opts...)
-
- m := chi.NewMux()
- m.Use(options.Middleware...)
-
- m.Use(
- otelchi.Middleware(
- "auth-api",
- otelchi.WithChiRoutes(m),
- otelchi.WithTracerProvider(options.TraceProvider),
- otelchi.WithPropagators(tracing.GetPropagator()),
- otelchi.WithTraceResponseHeaders(otelchi.TraceHeaderConfig{}),
- ),
- )
-
- svc, err := NewAuthenticationApi(options.Config, &options.Logger, options.Metrics, options.TraceProvider, m)
- if err != nil {
- return nil, err
- }
-
- m.Route(options.Config.HTTP.Root, func(r chi.Router) {
- r.Post("/", svc.Authenticate)
- })
-
- _ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error {
- options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
- return nil
- })
-
- return svc, nil
-}
-
-type AuthenticationApi struct {
- 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,
+func NewService(
logger *log.Logger,
metrics *metrics.Metrics,
tracerProvider oteltrace.TracerProvider,
- mux *chi.Mux,
-) (*AuthenticationApi, error) {
+ config *config.Config,
+ middlewares ...func(http.Handler) http.Handler) (Service, error) {
+ m := chi.NewMux()
- tracer := tracerProvider.Tracer("instrumentation/" + config.HTTP.Namespace + "/" + config.Service.Name)
+ mwlist := []func(http.Handler) http.Handler{}
+ mwlist = append(mwlist, middlewares...)
+ o := otelchi.Middleware(
+ "auth-api",
+ otelchi.WithChiRoutes(m),
+ otelchi.WithTracerProvider(tracerProvider),
+ otelchi.WithPropagators(tracing.GetPropagator()),
+ otelchi.WithTraceResponseHeaders(otelchi.TraceHeaderConfig{}),
+ )
+ mwlist = append(mwlist, o)
- 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
- }
+ m.Use(mwlist...)
- 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{},
- })
+ authApi, err := auth_api.NewAuthApi(config, logger, tracerProvider, metrics, m)
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
- }
+ m.Route(config.HTTP.Root, authApi.Route)
- 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 SuccessfulAuthResponse struct {
- Subject string `json:"subject"`
- Roles []string `json:"roles,omitempty"`
-}
-
-func (SuccessfulAuthResponse) Render(w http.ResponseWriter, r *http.Request) error {
- return nil
-}
-
-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.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[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
- _ = 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()
-
- 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
- }
+ return authApi, nil
}
diff --git a/services/auth-api/pkg/service/http/v0/service_test.go b/services/auth-api/pkg/service/http/v0/service_test.go
deleted file mode 100644
index 23f3520ce6..0000000000
--- a/services/auth-api/pkg/service/http/v0/service_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package svc
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestRegex(t *testing.T) {
- require := require.New(t)
-
- matches := authRegex.FindStringSubmatch("Basic abc")
- require.NotNil(matches)
- require.Len(matches, 3)
- require.Equal("Basic", matches[1])
- require.Equal("abc", matches[2])
-}
diff --git a/services/groupware/DEVELOPER.md b/services/groupware/DEVELOPER.md
index 7814ce74d1..33814af7ef 100644
--- a/services/groupware/DEVELOPER.md
+++ b/services/groupware/DEVELOPER.md
@@ -79,16 +79,21 @@ done \
### Compose
-There are two options, either
+There are four options, either
-1. running the Groupware backend with OpenLDAP and Keycloak containers, more akin to a production setup;
-2. running the Groupware backend using the built-in LDAP and OIDC services, for a minimalistic setup that uses less resources and is more likely to be found in a home lab setup.
+1. running the Groupware backend with OpenLDAP and Keycloak containers, along with master authentication between the Groupware backend and Stalwart, more akin to a production setup;
+2. running the Groupware backend with OpenLDAP and Keycloak containers, along with OIDC Bearer token authentication between the Groupware backend and Stalwart, even more akin to a production setup;
+3. running the Groupware backend using the built-in LDAP and OIDC services, along with master authentication between the Groupware backend and Stalwart, for a minimalistic setup that uses less resources and is more likely to be found in a home lab setup;
+4. running the Groupware backend using the built-in LDAP and OIDC services, along with OIDC Bearer token authentication between the Groupware backend and Stalwart.
+
+> [!NOTE]
+> Note that option 2 is currently not implemented yet.
In either case, the Docker Compose configuration in `$OCDIR/opencloud/devtools/deployments/opencloud_full/` needs to be modified.
-#### Production Setup
+#### Production Setup with Master Authentication
-
+
```mermaid
---
@@ -109,7 +114,7 @@ flowchart LR
c --> kc
```
-Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the following changes (make sure to check out [the shell command-line that automates all of that, below](#automate-env-setup-prod)):
+Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the following changes (make sure to check out [the shell command-line that automates all of that, below](#automate-env-setup-prod-master)):
* change the container image to `opencloudeu/opencloud:dev`:
@@ -170,7 +175,7 @@ Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the fol
+#EXTERNALSITES=:web_extensions/externalsites.yml
```
-
+
All those changes above can be automated with the following script:
```bash
@@ -193,9 +198,10 @@ perl -pi -e '
' .env
```
-#### Homelab Setup
+#### Homelab Setup with Master Authentication
+
```mermaid
---
@@ -212,7 +218,7 @@ flowchart LR
```
-Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the following changes (make sure to check out [the shell command-line that automates all of that, below](#automate-env-setup-homelab)):
+Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the following changes (make sure to check out [the shell command-line that automates all of that, below](#automate-env-setup-homelab-master)):
* change the container image to `opencloudeu/opencloud:dev`:
@@ -280,7 +286,7 @@ Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the fol
+#EXTERNALSITES=:web_extensions/externalsites.yml
```
-
+
All those changes above can be automated with the following script:
```bash
@@ -308,6 +314,144 @@ perl -pi -e '
' .env
```
+#### Homelab Setup with OIDC Authentication
+
+
+
+```mermaid
+---
+title: Homelab Setup
+---
+flowchart LR
+ oc["`opencloud`"]
+ c["client"]
+ st["`stalwart`"]
+
+ c -- http --> oc
+ oc -- jmap --> st
+ st -- userinfo --> oc
+
+```
+
+Edit `$OCDIR/opencloud/devtools/deployments/opencloud_full/.env`, making the following changes (make sure to check out [the shell command-line that automates all of that, below](#automate-env-setup-homelab-oidc)):
+
+* change the container image to `opencloudeu/opencloud:dev`:
+
+```diff
+-OC_DOCKER_IMAGE=opencloudeu/opencloud-rolling
++OC_DOCKER_IMAGE=opencloudeu/opencloud
+-OC_DOCKER_TAG=
++OC_DOCKER_TAG=dev
+```
+
+* enable the creation of demo users:
+
+```diff
+-DEMO_USERS=
++DEMO_USERS=true
+```
+
+* add the `groupware` and the `auth-api` services to `START_ADDITIONAL_SERVICES`:
+
+```diff
+-START_ADDITIONAL_SERVICES="notifications"
++START_ADDITIONAL_SERVICES="notifications,groupware,auth-api"
+```
+
+* enable the Stalwart container:
+
+```diff
+-#STALWART=:stalwart.yml
++STALWART=:stalwart.yml
+```
+
+* change the authentication directory configuration for Stalwart to `idmoidc` in the `.env` file, using the variable `STALWART_AUTH_DIRECTORY`:
+
+```diff
+ # Domain of Stalwart
+ # Defaults to "stalwart.opencloud.test"
+ STALWART_DOMAIN=
+ # LDAP configuration to use for Stalwart:
+ # Can either be either
+ # - idmldap: for the built-in IDP/IDM, using Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
+ # - idmoidc: built-in IDP/IDM, using OIDC Userinfo between Groupware and Stalwart
+ # - ldap: when using KeyCloak and OpenLDAP, with Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
+-STALWART_AUTH_DIRECTORY=idmldap
++STALWART_AUTH_DIRECTORY=idmoidc
+```
+
+* while not required, it is recommended to enable basic authentication support which, while less secure, allows for easier tooling when developing and testing HTTP APIs, by adding `PROXY_ENABLE_BASIC_AUTH=true` somewhere before the last line of the `.env` file:
+
+```diff
++# Enable basic authentication to facilitate HTTP API testing
++# Do not do this in production.
++PROXY_ENABLE_BASIC_AUTH=true
++
+ ## IMPORTANT ##
+```
+
+* optionally disable the Collabora container
+
+```diff
+-COLLABORA=:collabora.yml
++#COLLABORA=:collabora.yml
+```
+
+* optionally disable UI containers
+
+```diff
+-UNZIP=:web_extensions/unzip.yml
+-DRAWIO=:web_extensions/drawio.yml
+-JSONVIEWER=:web_extensions/jsonviewer.yml
+-PROGRESSBARS=:web_extensions/progressbars.yml
+-EXTERNALSITES=:web_extensions/externalsites.yml
++#UNZIP=:web_extensions/unzip.yml
++#DRAWIO=:web_extensions/drawio.yml
++#JSONVIEWER=:web_extensions/jsonviewer.yml
++#PROGRESSBARS=:web_extensions/progressbars.yml
++#EXTERNALSITES=:web_extensions/externalsites.yml
+```
+
+
+All those changes above can be automated with the following script:
+
+```bash
+cd "$OCDIR/opencloud/devtools/deployments/opencloud_full/"
+perl -pi -e '
+ BEGIN{$basic_auth=0}
+ s|^(OC_DOCKER_IMAGE)=.*$|$1=opencloudeu/opencloud|;
+ s|^(OC_DOCKER_TAG)=.*$|$1=dev|;
+ s|^(START_ADDITIONAL_SERVICES=".*(? [!NOTE]
+> Unfortunately, as of now, the hostname or IP address of the host that runs the Groupware backend (or the OpenCloud single binary) needs to be configured manually
+> in `devtools/deployments/opencloud_full/config/stalwart/idmoidc.toml`, specifically in the variable `directory.oidc.endpoint.url` since it highly depends on whether
+> you are running it on the host (typically from an IDE)
+> or as another container in the Docker Compose project.
+> In the former case, it also depends on the operating system.
+> It is currently hard-wired to be `http://172.17.0.1:10000/auth/...`, which only works
+> * on Linux, where `172.17.0.1` _tends_ to be the gateway host IP address, for running the OpenCloud Groupware backend on the host
+> * when the environment variable `AUTHAPI_HTTP_ADDR` is set to `0.0.0.0:10000`, allowing for HTTP access to the auth-api backend, instead of limiting it to HTTPS through the proxy, which opens a whole can of worms with making Stalwart accept self-signed certificates
+
## Building
Build the `opencloudeu/opencloud:dev` image first:
@@ -358,7 +502,7 @@ To check whether the various services are running correctly:
#### Production Setup LDAP
-When using the “production” setup (as depicted in section [Production Setup](#prod-setup) above), queries can be performed directly against the \
+When using the “production” setup (as depicted in section [Production Setup](#prod-setup-master) above), queries can be performed directly against the \
OpenLDAP container (`opencloud_full-openldap-1`) since its LDAP ports are mapped onto the host (to `:389` and `:636` for LDAP and LDAPS, respectively).
When using the OpenLDAP container, the necessary LDAP parameters are as follows:
@@ -474,7 +618,7 @@ docker run --network 'opencloud_full_opencloud-net' --rm -ti alpine:3 \
### Testing Keycloak
> [!NOTE]
-> Only available in the [“production” setup](#prod-setup)
+> Only available in the [“production” setup](#prod-setup-master)
To check whether it works correctly, the following `curl` command:
diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go
index 042ed81a5d..03ce93c693 100644
--- a/services/groupware/pkg/config/defaults/defaultconfig.go
+++ b/services/groupware/pkg/config/defaults/defaultconfig.go
@@ -26,8 +26,8 @@ func DefaultConfig() *config.Config {
},
Mail: config.Mail{
Master: config.MailMasterAuth{
- Username: "master",
- Password: "admin",
+ Username: "",
+ Password: "",
},
BaseUrl: "https://stalwart.opencloud.test",
Timeout: 30 * time.Second,
diff --git a/services/groupware/pkg/groupware/dns.go b/services/groupware/pkg/groupware/dns.go
index 15ef97d1e2..3b726d5594 100644
--- a/services/groupware/pkg/groupware/dns.go
+++ b/services/groupware/pkg/groupware/dns.go
@@ -1,6 +1,7 @@
package groupware
import (
+ "context"
"errors"
"net"
"net/url"
@@ -17,7 +18,7 @@ var (
)
type DnsSessionUrlResolver struct {
- defaultSessionUrlSupplier func(string) (*url.URL, *GroupwareError)
+ defaultSessionUrlSupplier func(context.Context, string) (*url.URL, *GroupwareError)
defaultDomain string
domainGreenList []string
domainRedList []string
@@ -26,7 +27,7 @@ type DnsSessionUrlResolver struct {
}
func NewDnsSessionUrlResolver(
- defaultSessionUrlSupplier func(string) (*url.URL, *GroupwareError),
+ defaultSessionUrlSupplier func(context.Context, string) (*url.URL, *GroupwareError),
defaultDomain string,
config *dns.ClientConfig,
domainGreenList []string,
@@ -71,7 +72,7 @@ func (d DnsSessionUrlResolver) isRedListed(domain string) bool {
return !slices.Contains(d.domainRedList, domain)
}
-func (d DnsSessionUrlResolver) Resolve(username string) (*url.URL, *GroupwareError) {
+func (d DnsSessionUrlResolver) Resolve(ctx context.Context, username string) (*url.URL, *GroupwareError) {
// heuristic to detect whether the username is an email address
parts := strings.Split(username, "@")
domain := d.defaultDomain
@@ -80,7 +81,7 @@ func (d DnsSessionUrlResolver) Resolve(username string) (*url.URL, *GroupwareErr
// nevertheless then?
if d.defaultDomain == "" {
// we don't, then let's fall back to the static session URL instead
- return d.defaultSessionUrlSupplier(username)
+ return d.defaultSessionUrlSupplier(ctx, username)
}
} else {
domain = parts[len(parts)-1]
@@ -138,7 +139,7 @@ func (d DnsSessionUrlResolver) Resolve(username string) (*url.URL, *GroupwareErr
}
}
- return d.defaultSessionUrlSupplier(username)
+ return d.defaultSessionUrlSupplier(ctx, username)
}
func (d DnsSessionUrlResolver) dnsQuery(c *dns.Client, msg *dns.Msg) (*dns.Msg, error) {
diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go
index aaf5cce3b7..2198107ac8 100644
--- a/services/groupware/pkg/groupware/framework.go
+++ b/services/groupware/pkg/groupware/framework.go
@@ -177,17 +177,6 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
sessionUrl := baseUrl.JoinPath(".well-known", "jmap")
- masterUsername := config.Mail.Master.Username
- if masterUsername == "" {
- logger.Error().Msg("failed to parse empty Mail.Master.Username")
- return nil, GroupwareInitializationError{Message: "Mail.Master.Username is empty"}
- }
- masterPassword := config.Mail.Master.Password
- if masterPassword == "" {
- logger.Error().Msg("failed to parse empty Mail.Master.Password")
- return nil, GroupwareInitializationError{Message: "Mail.Master.Password is empty"}
- }
-
defaultEmailLimit := max(config.Mail.DefaultEmailLimit, 0)
maxBodyValueBytes := max(config.Mail.MaxBodyValueBytes, 0)
defaultContactLimit := max(config.Mail.DefaultContactLimit, 0)
@@ -218,6 +207,17 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
var jmapClient jmap.Client
{
+ var auth jmap.HttpJmapClientAuthenticator
+ {
+ masterUsername := config.Mail.Master.Username
+ masterPassword := config.Mail.Master.Password
+ if masterUsername != "" && masterPassword != "" {
+ auth = jmap.NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword)
+ } else {
+ auth = newRevaBearerHttpJmapClientAuthenticator()
+ }
+ }
+
var api *jmap.HttpJmapClient
{
// TODO add timeouts and other meaningful configuration settings for the HTTP client
@@ -235,8 +235,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
api = jmap.NewHttpJmapClient(
&httpClient,
- masterUsername,
- masterPassword,
+ auth,
jmapMetricsAdapter,
)
}
@@ -250,7 +249,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
- wsf, err = jmap.NewHttpWsClientFactory(wsDialer, masterUsername, masterPassword, logger, jmapMetricsAdapter)
+ wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter)
if err != nil {
logger.Error().Err(err).Msg("failed to create websocket client")
return nil, GroupwareInitializationError{Message: "failed to create websocket client", Err: err}
@@ -478,7 +477,7 @@ func (g *Groupware) ServeSSE(w http.ResponseWriter, r *http.Request) {
}
// Provide a JMAP Session for the given user
-func (g *Groupware) session(user user, logger *log.Logger) (jmap.Session, bool, *GroupwareError, time.Time) {
+func (g *Groupware) session(ctx context.Context, user user, logger *log.Logger) (jmap.Session, bool, *GroupwareError, time.Time) {
if user == nil {
logger.Warn().Msg("user is nil")
return jmap.Session{}, false, nil, time.Time{}
@@ -490,7 +489,7 @@ func (g *Groupware) session(user user, logger *log.Logger) (jmap.Session, bool,
}
// first look into the session cache
- s := g.sessionCache.Get(name)
+ s := g.sessionCache.Get(ctx, name)
if s != nil {
if s.Success() {
return s.Get(), true, nil, s.Until()
@@ -587,7 +586,7 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler
// retrieve a JMAP Session for that user
var session jmap.Session
{
- s, ok, gwerr, retryAfter := g.session(user, logger)
+ s, ok, gwerr, retryAfter := g.session(ctx, user, logger)
if gwerr != nil {
g.metrics.SessionFailureCounter.Inc()
errorId := errorId(r, ctx)
@@ -721,7 +720,7 @@ func (g *Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(
logger = log.From(logger.With().Str(logUserId, log.SafeString(user.GetId())))
- session, ok, gwerr, retryAfter := g.session(user, logger)
+ session, ok, gwerr, retryAfter := g.session(ctx, user, logger)
if gwerr != nil {
errorId := errorId(r, ctx)
logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Str(logErrorId, errorId).Msg("failed to determine JMAP session")
diff --git a/services/groupware/pkg/groupware/reva.go b/services/groupware/pkg/groupware/reva.go
index 4e2704f4ce..41335c66b3 100644
--- a/services/groupware/pkg/groupware/reva.go
+++ b/services/groupware/pkg/groupware/reva.go
@@ -6,28 +6,27 @@ import (
"net/http"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
// UsernameProvider implementation that uses Reva's enrichment of the Context
// to retrieve the current username.
-type revaContextUserProvider struct {
+type revaContextUsernameProvider struct {
}
-var _ userProvider = revaContextUserProvider{}
+var _ userProvider = revaContextUsernameProvider{}
func newRevaContextUsernameProvider() userProvider {
- return revaContextUserProvider{}
+ return revaContextUsernameProvider{}
}
-// var errUserNotInContext = fmt.Errorf("user not in context")
-
var (
errUserNotInRevaContext = errors.New("failed to find user in reva context")
)
-func (r revaContextUserProvider) GetUser(req *http.Request, ctx context.Context, logger *log.Logger) (user, error) {
+func (r revaContextUsernameProvider) GetUser(req *http.Request, ctx context.Context, logger *log.Logger) (user, error) {
u, ok := revactx.ContextGetUser(ctx)
if !ok {
err := errUserNotInRevaContext
@@ -50,3 +49,66 @@ func (r revaUser) GetId() string {
}
var _ user = revaUser{}
+
+type RevaBearerHttpJmapClientAuthenticator struct {
+}
+
+func newRevaBearerHttpJmapClientAuthenticator() jmap.HttpJmapClientAuthenticator {
+ return &RevaBearerHttpJmapClientAuthenticator{}
+}
+
+var _ jmap.HttpJmapClientAuthenticator = &RevaBearerHttpJmapClientAuthenticator{}
+
+type RevaError struct {
+ code int
+ err error
+}
+
+var _ jmap.Error = &RevaError{}
+
+func (e RevaError) Code() int {
+ return e.code
+}
+func (e RevaError) Unwrap() error {
+ return e.err
+}
+func (e RevaError) Error() string {
+ if e.err != nil {
+ return e.err.Error()
+ } else {
+ return ""
+ }
+}
+
+const (
+ revaErrorTokenMissingInRevaContext = iota + 10000
+)
+
+var tokenMissingInRevaContext = RevaError{
+ code: revaErrorTokenMissingInRevaContext,
+ err: errors.New("Token is missing from Reva context"),
+}
+
+func (h *RevaBearerHttpJmapClientAuthenticator) Authenticate(ctx context.Context, username string, logger *log.Logger, req *http.Request) jmap.Error {
+ token, ok := revactx.ContextGetToken(ctx)
+ if !ok {
+ err := tokenMissingInRevaContext
+ logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in reva context: %v", ctx)
+ return err
+ } else {
+ req.Header.Add("Authorization", "Bearer "+token)
+ return nil
+ }
+}
+
+func (h *RevaBearerHttpJmapClientAuthenticator) AuthenticateWS(ctx context.Context, username string, logger *log.Logger, headers http.Header) jmap.Error {
+ token, ok := revactx.ContextGetToken(ctx)
+ if !ok {
+ err := tokenMissingInRevaContext
+ logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in reva context: %v", ctx)
+ return err
+ } else {
+ headers.Add("Authorization", "Bearer "+token)
+ return nil
+ }
+}
diff --git a/services/groupware/pkg/groupware/session.go b/services/groupware/pkg/groupware/session.go
index 9b1f0eab9b..0942096e93 100644
--- a/services/groupware/pkg/groupware/session.go
+++ b/services/groupware/pkg/groupware/session.go
@@ -98,49 +98,17 @@ func (s failedSession) Until() time.Time {
return s.until
}
-// Implements the ttlcache.Loader interface, by loading JMAP Sessions for users
-// using the jmap.Client.
-type sessionCacheLoader struct {
- logger *log.Logger
- // A minimalistic contract for supplying the JMAP Session URL for a given username.
- sessionUrlProvider func(username string) (*url.URL, *GroupwareError)
- // A minimalistic contract for supplying JMAP Sessions using various input parameters.
- sessionSupplier func(sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error)
- errorTtl time.Duration
-}
-
-var _ ttlcache.Loader[sessionCacheKey, cachedSession] = &sessionCacheLoader{}
-
-func (l *sessionCacheLoader) Load(c *ttlcache.Cache[sessionCacheKey, cachedSession], key sessionCacheKey) *ttlcache.Item[sessionCacheKey, cachedSession] {
- username := key.username()
- sessionUrl, gwerr := l.sessionUrlProvider(username)
- if gwerr != nil {
- l.logger.Warn().Str("username", username).Str("code", gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
- now := time.Now()
- until := now.Add(l.errorTtl)
- return c.Set(key, failedSession{since: now, until: until, err: gwerr}, l.errorTtl)
- }
- session, jerr := l.sessionSupplier(sessionUrl, username, l.logger)
- if jerr != nil {
- l.logger.Warn().Str("username", username).Err(jerr).Msgf("failed to create session for '%v'", key)
- now := time.Now()
- until := now.Add(l.errorTtl)
- return c.Set(key, failedSession{since: now, until: until, err: groupwareErrorFromJmap(jerr)}, l.errorTtl)
- } else {
- l.logger.Debug().Str("username", username).Msgf("successfully created session for '%v'", key)
- now := time.Now()
- until := now.Add(ttlcache.DefaultTTL)
- return c.Set(key, succeededSession{since: now, until: until, session: session}, ttlcache.DefaultTTL) // use the TTL configured on the Cache
- }
-}
-
type sessionCache interface {
- Get(username string) cachedSession
+ Get(ctx context.Context, username string) cachedSession
jmap.SessionEventListener
}
type ttlcacheSessionCache struct {
sessionCache *ttlcache.Cache[sessionCacheKey, cachedSession]
+ sessionUrlProvider func(ctx context.Context, username string) (*url.URL, *GroupwareError)
+ sessionSupplier func(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error)
+ successTtl time.Duration
+ errorTtl time.Duration
outdatedSessionCounter prometheus.Counter
logger *log.Logger
}
@@ -148,10 +116,45 @@ type ttlcacheSessionCache struct {
var _ sessionCache = &ttlcacheSessionCache{}
var _ jmap.SessionEventListener = &ttlcacheSessionCache{}
-func (c *ttlcacheSessionCache) Get(username string) cachedSession {
- item := c.sessionCache.Get(toSessionCacheKey(username))
+func (l *ttlcacheSessionCache) load(c *ttlcache.Cache[sessionCacheKey, cachedSession], key sessionCacheKey, ctx context.Context) cachedSession {
+ username := key.username()
+ sessionUrl, gwerr := l.sessionUrlProvider(ctx, username)
+ if gwerr != nil {
+ l.logger.Warn().Str("username", username).Str("code", gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
+ now := time.Now()
+ until := now.Add(l.errorTtl)
+ return failedSession{since: now, until: until, err: gwerr}
+ }
+ session, jerr := l.sessionSupplier(ctx, sessionUrl, username, l.logger)
+ if jerr != nil {
+ l.logger.Warn().Str("username", username).Err(jerr).Msgf("failed to create session for '%v'", key)
+ now := time.Now()
+ until := now.Add(l.errorTtl)
+ return failedSession{since: now, until: until, err: groupwareErrorFromJmap(jerr)}
+ } else {
+ l.logger.Debug().Str("username", username).Msgf("successfully created session for '%v'", key)
+ now := time.Now()
+ until := now.Add(l.successTtl)
+ return succeededSession{since: now, until: until, session: session}
+ }
+}
+
+func (c *ttlcacheSessionCache) Get(ctx context.Context, username string) cachedSession {
+ key := toSessionCacheKey(username)
+ item, cached := c.sessionCache.GetOrSetFunc(key, func() cachedSession {
+ return c.load(c.sessionCache, key, ctx) // TODO can't set the TTL on the cached item
+ })
if item != nil {
- return item.Value()
+ value := item.Value()
+ if !cached {
+ if !value.Success() && c.errorTtl != c.successTtl {
+ // not great, but will do for now:
+ // - when the result is successful, the default TTL is used
+ // - when the result is a failure to retrieve the session, a (most probably shorter) TTL must be used instead
+ c.sessionCache.Set(key, value, c.errorTtl)
+ }
+ }
+ return value
} else {
return nil
}
@@ -159,9 +162,9 @@ func (c *ttlcacheSessionCache) Get(username string) cachedSession {
type sessionCacheBuilder struct {
logger *log.Logger
- sessionSupplier func(sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error)
- defaultUrlResolver func(string) (*url.URL, *GroupwareError)
- sessionUrlResolverFactory func() (func(string) (*url.URL, *GroupwareError), *GroupwareInitializationError)
+ sessionSupplier func(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error)
+ defaultUrlResolver func(context.Context, string) (*url.URL, *GroupwareError)
+ sessionUrlResolverFactory func() (func(context.Context, string) (*url.URL, *GroupwareError), *GroupwareInitializationError)
prometheusRegistry prometheus.Registerer
m *metrics.Metrics
sessionCacheMaxCapacity uint64
@@ -172,14 +175,14 @@ type sessionCacheBuilder struct {
func newSessionCacheBuilder(
sessionUrl *url.URL,
logger *log.Logger,
- sessionSupplier func(sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error),
+ sessionSupplier func(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error),
prometheusRegistry prometheus.Registerer,
m *metrics.Metrics,
sessionCacheMaxCapacity uint64,
sessionCacheTtl time.Duration,
sessionFailureCacheTtl time.Duration,
) *sessionCacheBuilder {
- defaultUrlResolver := func(_ string) (*url.URL, *GroupwareError) {
+ defaultUrlResolver := func(_ context.Context, _ string) (*url.URL, *GroupwareError) {
return sessionUrl, nil
}
@@ -187,7 +190,7 @@ func newSessionCacheBuilder(
logger: logger,
sessionSupplier: sessionSupplier,
defaultUrlResolver: defaultUrlResolver,
- sessionUrlResolverFactory: func() (func(string) (*url.URL, *GroupwareError), *GroupwareInitializationError) {
+ sessionUrlResolverFactory: func() (func(context.Context, string) (*url.URL, *GroupwareError), *GroupwareInitializationError) {
return defaultUrlResolver, nil
},
prometheusRegistry: prometheusRegistry,
@@ -206,7 +209,7 @@ func (b *sessionCacheBuilder) withDnsAutoDiscovery(
domainGreenList []string,
domainRedList []string,
) *sessionCacheBuilder {
- dnsSessionUrlResolverFactory := func() (func(string) (*url.URL, *GroupwareError), *GroupwareInitializationError) {
+ dnsSessionUrlResolverFactory := func() (func(context.Context, string) (*url.URL, *GroupwareError), *GroupwareInitializationError) {
d, err := NewDnsSessionUrlResolver(
b.defaultUrlResolver,
defaultSessionDomain,
@@ -234,18 +237,10 @@ func (b sessionCacheBuilder) build() (sessionCache, error) {
return nil, err
}
- sessionLoader := &sessionCacheLoader{
- logger: b.logger,
- sessionSupplier: b.sessionSupplier,
- errorTtl: b.sessionFailureCacheTtl,
- sessionUrlProvider: sessionUrlResolver,
- }
-
cache = ttlcache.New(
ttlcache.WithCapacity[sessionCacheKey, cachedSession](b.sessionCacheMaxCapacity),
ttlcache.WithTTL[sessionCacheKey, cachedSession](b.sessionCacheTtl),
ttlcache.WithDisableTouchOnHit[sessionCacheKey, cachedSession](),
- ttlcache.WithLoader(sessionLoader),
)
b.prometheusRegistry.Register(sessionCacheMetricsCollector{desc: b.m.SessionCacheDesc, supply: cache.Metrics})
@@ -277,6 +272,10 @@ func (b sessionCacheBuilder) build() (sessionCache, error) {
s := &ttlcacheSessionCache{
sessionCache: cache,
+ sessionSupplier: b.sessionSupplier,
+ successTtl: b.sessionCacheTtl,
+ errorTtl: b.sessionFailureCacheTtl,
+ sessionUrlProvider: sessionUrlResolver,
logger: b.logger,
outdatedSessionCounter: b.m.OutdatedSessionsCounter,
}
diff --git a/services/groupware/pkg/server/http/option.go b/services/groupware/pkg/server/http/option.go
index ed4d76a76f..f559d4ed6f 100644
--- a/services/groupware/pkg/server/http/option.go
+++ b/services/groupware/pkg/server/http/option.go
@@ -6,7 +6,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/groupware/pkg/config"
"github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics"
- "github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
@@ -21,7 +20,6 @@ type Options struct {
Context context.Context
Config *config.Config
Metrics *metrics.HttpMetrics
- Flags []cli.Flag
TraceProvider trace.TracerProvider
}
diff --git a/vendor/github.com/MicahParks/jwkset/.gitignore b/vendor/github.com/MicahParks/jwkset/.gitignore
deleted file mode 100644
index 040ac50a4b..0000000000
--- a/vendor/github.com/MicahParks/jwkset/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-config.*json
-node_modules
diff --git a/vendor/github.com/MicahParks/jwkset/LICENSE b/vendor/github.com/MicahParks/jwkset/LICENSE
deleted file mode 100644
index 05f2ccbd2a..0000000000
--- a/vendor/github.com/MicahParks/jwkset/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2022 Micah Parks
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/vendor/github.com/MicahParks/jwkset/README.md b/vendor/github.com/MicahParks/jwkset/README.md
deleted file mode 100644
index e9d511eb3b..0000000000
--- a/vendor/github.com/MicahParks/jwkset/README.md
+++ /dev/null
@@ -1,133 +0,0 @@
-[](https://pkg.go.dev/github.com/MicahParks/jwkset)
-
-# JWK Set (JSON Web Key Set)
-
-This is a JWK Set (JSON Web Key Set) implementation written in Golang.
-
-The goal of this project is to provide a complete implementation of JWK and JWK Sets within the constraints of the
-Golang standard library, without implementing any cryptographic algorithms. For example, `Ed25519` is supported, but
-`Ed448` is not, because the Go standard library does not have a high level implementation of `Ed448`.
-
-If you would like to generate or validate a JWK without writing any Golang code, please visit
-the [Generate a JWK Set](#generate-a-jwk-set) section.
-
-If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the
-[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the
-language of your choice using an OpenAPI interface.
-
-# Generate a JWK Set
-
-If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK
-Set from:
-
-* PEM encoded X.509 Certificates
-* PEM encoded public keys
-* PEM encoded private keys
-
-The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section
-for a list of supported cryptographic key types.
-
-## Website
-
-Visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this website
-by following the instructions in the `README.md` in
-the [website](https://github.com/MicahParks/jwkset/tree/master/website) directory.
-
-## Command line
-
-Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set.
-
-**Install**
-
-```
-go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest
-```
-
-**Usage**
-
-```
-jwksetinfer mykey.pem mycert.crt
-```
-
-## Custom server
-
-This project can be used in creating a custom JWK Set server. A good place to start is `examples/http_server/main.go`.
-
-# Golang JWK Set client
-
-If you are using [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) take a look
-at [`github.com/MicahParks/keyfunc/v3`](https://github.com/MicahParks/keyfunc).
-
-This project can be used to create JWK Set clients. An HTTP client is provided. See a snippet of the usage
-from `examples/default_http_client/main.go` below.
-
-## Create a JWK Set client from the server's HTTP URL.
-
-```go
-jwks, err := jwkset.NewDefaultHTTPClient([]string{server.URL})
-if err != nil {
- log.Fatalf("Failed to create client JWK set. Error: %s", err)
-}
-```
-
-## Read a key from the client.
-
-```go
-jwk, err = jwks.KeyRead(ctx, myKeyID)
-if err != nil {
- log.Fatalf("Failed to read key from client JWK set. Error: %s", err)
-}
-```
-
-# Supported keys
-
-This project supports the following key types:
-
-* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only)
- * Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey`
-* [Elliptic-curve Diffie–Hellman (ECDH)](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) (X25519
- only)
- * Go Types: `*ecdh.PrivateKey` and `*ecdh.PublicKey`
-* [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
- * Go Types: `*ecdsa.PrivateKey` and `*ecdsa.PublicKey`
-* [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem))
- * Go Types: `*rsa.PrivateKey` and `*rsa.PublicKey`
-* [HMAC](https://en.wikipedia.org/wiki/HMAC), [AES Key Wrap](https://en.wikipedia.org/wiki/Key_Wrap), and other
- symmetric keys
- * Go Type: `[]byte`
-
-Cryptographic keys can be added, deleted, and read from the JWK Set. A JSON representation of the JWK Set can be created
-for hosting via HTTPS. This project includes an in-memory storage implementation, but an interface is provided for more
-advanced use cases.
-
-# Notes
-
-This project aims to implement the relevant RFCs to the fullest extent possible using the Go standard library, but does
-not implement any cryptographic algorithms itself.
-
-* RFC 8037 adds support for `Ed448`, `X448`, and `secp256k1`, but there is no Golang standard library support for these
- key types.
-* In order to be compatible with non-RFC compliant JWK Set providers, this project does not strictly enforce JWK
- parameters that are integers and have extra or missing leading padding. See the release notes
- of [`v0.5.15`](https://github.com/MicahParks/jwkset/releases/tag/v0.5.15) for details.
-* `Base64url Encoding` requires that all trailing `=` characters be removed. This project automatically strips any
- trailing `=` characters in an attempt to be compatible with improper implementations of JWK.
-* This project does not currently support JWK Set encryption using JWE. This would involve implementing the relevant JWE
- specifications. It may be implemented in the future if there is interest. Open a GitHub issue to express interest.
-
-# Related projects
-
-## [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc)
-
-A JWK Set client for the [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) project.
-
-## [`github.com/MicahParks/jcp`](https://github.com/MicahParks/jcp)
-
-A JWK Set client proxy. JCP for short. This project is a standalone service that uses keyfunc under the hood. It
-primarily exists for these use cases:
-
-The language or shell a program is written in does not have an adequate JWK Set client. Validate JWTs with curl? Why
-not?
-Restrictive networking policies prevent a program from accessing the remote JWK Set directly.
-Many co-located services need to validate JWTs that were signed by a key that lives in a remote JWK Set.
-If you can integrate keyfunc directly into your program, you likely don't need JCP.
diff --git a/vendor/github.com/MicahParks/jwkset/constants.go b/vendor/github.com/MicahParks/jwkset/constants.go
deleted file mode 100644
index 15e219e0d1..0000000000
--- a/vendor/github.com/MicahParks/jwkset/constants.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package jwkset
-
-const (
- // HeaderKID is a JWT header for the key ID.
- HeaderKID = "kid"
-)
-
-// These are string constants set in https://www.iana.org/assignments/jose/jose.xhtml
-// See their respective types for more information.
-const (
- AlgHS256 ALG = "HS256"
- AlgHS384 ALG = "HS384"
- AlgHS512 ALG = "HS512"
- AlgRS256 ALG = "RS256"
- AlgRS384 ALG = "RS384"
- AlgRS512 ALG = "RS512"
- AlgES256 ALG = "ES256"
- AlgES384 ALG = "ES384"
- AlgES512 ALG = "ES512"
- AlgPS256 ALG = "PS256"
- AlgPS384 ALG = "PS384"
- AlgPS512 ALG = "PS512"
- AlgNone ALG = "none"
- AlgRSA1_5 ALG = "RSA1_5"
- AlgRSAOAEP ALG = "RSA-OAEP"
- AlgRSAOAEP256 ALG = "RSA-OAEP-256"
- AlgA128KW ALG = "A128KW"
- AlgA192KW ALG = "A192KW"
- AlgA256KW ALG = "A256KW"
- AlgDir ALG = "dir"
- AlgECDHES ALG = "ECDH-ES"
- AlgECDHESA128KW ALG = "ECDH-ES+A128KW"
- AlgECDHESA192KW ALG = "ECDH-ES+A192KW"
- AlgECDHESA256KW ALG = "ECDH-ES+A256KW"
- AlgA128GCMKW ALG = "A128GCMKW"
- AlgA192GCMKW ALG = "A192GCMKW"
- AlgA256GCMKW ALG = "A256GCMKW"
- AlgPBES2HS256A128KW ALG = "PBES2-HS256+A128KW"
- AlgPBES2HS384A192KW ALG = "PBES2-HS384+A192KW"
- AlgPBES2HS512A256KW ALG = "PBES2-HS512+A256KW"
- AlgA128CBCHS256 ALG = "A128CBC-HS256"
- AlgA192CBCHS384 ALG = "A192CBC-HS384"
- AlgA256CBCHS512 ALG = "A256CBC-HS512"
- AlgA128GCM ALG = "A128GCM"
- AlgA192GCM ALG = "A192GCM"
- AlgA256GCM ALG = "A256GCM"
- AlgEdDSA ALG = "EdDSA"
- AlgRS1 ALG = "RS1" // Prohibited.
- AlgRSAOAEP384 ALG = "RSA-OAEP-384"
- AlgRSAOAEP512 ALG = "RSA-OAEP-512"
- AlgA128CBC ALG = "A128CBC" // Prohibited.
- AlgA192CBC ALG = "A192CBC" // Prohibited.
- AlgA256CBC ALG = "A256CBC" // Prohibited.
- AlgA128CTR ALG = "A128CTR" // Prohibited.
- AlgA192CTR ALG = "A192CTR" // Prohibited.
- AlgA256CTR ALG = "A256CTR" // Prohibited.
- AlgHS1 ALG = "HS1" // Prohibited.
- AlgES256K ALG = "ES256K"
-
- CrvP256 CRV = "P-256"
- CrvP384 CRV = "P-384"
- CrvP521 CRV = "P-521"
- CrvEd25519 CRV = "Ed25519"
- CrvEd448 CRV = "Ed448"
- CrvX25519 CRV = "X25519"
- CrvX448 CRV = "X448"
- CrvSECP256K1 CRV = "secp256k1"
-
- KeyOpsSign KEYOPS = "sign"
- KeyOpsVerify KEYOPS = "verify"
- KeyOpsEncrypt KEYOPS = "encrypt"
- KeyOpsDecrypt KEYOPS = "decrypt"
- KeyOpsWrapKey KEYOPS = "wrapKey"
- KeyOpsUnwrapKey KEYOPS = "unwrapKey"
- KeyOpsDeriveKey KEYOPS = "deriveKey"
- KeyOpsDeriveBits KEYOPS = "deriveBits"
-
- KtyEC KTY = "EC"
- KtyOKP KTY = "OKP"
- KtyRSA KTY = "RSA"
- KtyOct KTY = "oct"
-
- UseEnc USE = "enc"
- UseSig USE = "sig"
-)
-
-// ALG is a set of "JSON Web Signature and Encryption Algorithms" types from
-// https://www.iana.org/assignments/jose/jose.xhtml as defined in
-// https://www.rfc-editor.org/rfc/rfc7518#section-7.1
-type ALG string
-
-func (alg ALG) IANARegistered() bool {
- switch alg {
- case AlgHS256, AlgHS384, AlgHS512, AlgRS256, AlgRS384, AlgRS512, AlgES256, AlgES384, AlgES512, AlgPS256, AlgPS384,
- AlgPS512, AlgNone, AlgRSA1_5, AlgRSAOAEP, AlgRSAOAEP256, AlgA128KW, AlgA192KW, AlgA256KW, AlgDir, AlgECDHES,
- AlgECDHESA128KW, AlgECDHESA192KW, AlgECDHESA256KW, AlgA128GCMKW, AlgA192GCMKW, AlgA256GCMKW,
- AlgPBES2HS256A128KW, AlgPBES2HS384A192KW, AlgPBES2HS512A256KW, AlgA128CBCHS256, AlgA192CBCHS384,
- AlgA256CBCHS512, AlgA128GCM, AlgA192GCM, AlgA256GCM, AlgEdDSA, AlgRS1, AlgRSAOAEP384, AlgRSAOAEP512, AlgA128CBC,
- AlgA192CBC, AlgA256CBC, AlgA128CTR, AlgA192CTR, AlgA256CTR, AlgHS1, AlgES256K, "":
- return true
- }
- return false
-}
-func (alg ALG) String() string {
- return string(alg)
-}
-
-// CRV is a set of "JSON Web Key Elliptic Curve" types from https://www.iana.org/assignments/jose/jose.xhtml as
-// mentioned in https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1
-type CRV string
-
-func (crv CRV) IANARegistered() bool {
- switch crv {
- case CrvP256, CrvP384, CrvP521, CrvEd25519, CrvEd448, CrvX25519, CrvX448, CrvSECP256K1, "":
- return true
- }
- return false
-}
-func (crv CRV) String() string {
- return string(crv)
-}
-
-// KEYOPS is a set of "JSON Web Key Operations" from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
-// https://www.rfc-editor.org/rfc/rfc7517#section-4.3
-type KEYOPS string
-
-func (keyopts KEYOPS) IANARegistered() bool {
- switch keyopts {
- case KeyOpsSign, KeyOpsVerify, KeyOpsEncrypt, KeyOpsDecrypt, KeyOpsWrapKey, KeyOpsUnwrapKey, KeyOpsDeriveKey,
- KeyOpsDeriveBits:
- return true
- }
- return false
-}
-func (keyopts KEYOPS) String() string {
- return string(keyopts)
-}
-
-// KTY is a set of "JSON Web Key Types" from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
-// https://www.rfc-editor.org/rfc/rfc7517#section-4.1
-type KTY string
-
-func (kty KTY) IANARegistered() bool {
- switch kty {
- case KtyEC, KtyOKP, KtyRSA, KtyOct:
- return true
- }
- return false
-}
-func (kty KTY) String() string {
- return string(kty)
-}
-
-// USE is a set of "JSON Web Key Use" types from https://www.iana.org/assignments/jose/jose.xhtml as mentioned in
-// https://www.rfc-editor.org/rfc/rfc7517#section-4.2
-type USE string
-
-func (use USE) IANARegistered() bool {
- switch use {
- case UseEnc, UseSig, "":
- return true
- }
- return false
-}
-func (use USE) String() string {
- return string(use)
-}
diff --git a/vendor/github.com/MicahParks/jwkset/http.go b/vendor/github.com/MicahParks/jwkset/http.go
deleted file mode 100644
index 36f151c240..0000000000
--- a/vendor/github.com/MicahParks/jwkset/http.go
+++ /dev/null
@@ -1,276 +0,0 @@
-package jwkset
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log/slog"
- "time"
-
- "golang.org/x/time/rate"
-)
-
-var (
- // ErrNewClient fails to create a new JWK Set client.
- ErrNewClient = errors.New("failed to create new JWK Set client")
-)
-
-// HTTPClientOptions are options for creating a new JWK Set client.
-type HTTPClientOptions struct {
- // Given contains keys known from outside HTTP URLs.
- Given Storage
- // HTTPURLs are a mapping of HTTP URLs to JWK Set endpoints to storage implementations for the keys located at the
- // URL. If empty, HTTP will not be used.
- HTTPURLs map[string]Storage
- // PrioritizeHTTP is a flag that indicates whether keys from the HTTP URL should be prioritized over keys from the
- // given storage.
- PrioritizeHTTP bool
- // RateLimitWaitMax is the timeout for waiting for rate limiting to end.
- RateLimitWaitMax time.Duration
- // RefreshUnknownKID is non-nil to indicate that remote HTTP resources should be refreshed if a key with an unknown
- // key ID is trying to be read. This makes reading methods block until the context is over, a key with the matching
- // key ID is found in a refreshed remote resource, or all refreshes complete.
- RefreshUnknownKID *rate.Limiter
-}
-
-// Client is a JWK Set client.
-type httpClient struct {
- given Storage
- httpURLs map[string]Storage
- prioritizeHTTP bool
- rateLimitWaitMax time.Duration
- refreshUnknownKID *rate.Limiter
-}
-
-// NewHTTPClient creates a new JWK Set client from remote HTTP resources.
-func NewHTTPClient(options HTTPClientOptions) (Storage, error) {
- if options.Given == nil && len(options.HTTPURLs) == 0 {
- return nil, fmt.Errorf("%w: no given keys or HTTP URLs", ErrNewClient)
- }
- for u, store := range options.HTTPURLs {
- if store == nil {
- var err error
- options.HTTPURLs[u], err = NewStorageFromHTTP(u, HTTPClientStorageOptions{})
- if err != nil {
- return nil, fmt.Errorf("failed to create HTTP client storage for %q: %w", u, errors.Join(err, ErrNewClient))
- }
- }
- }
- given := options.Given
- if given == nil {
- given = NewMemoryStorage()
- }
- c := httpClient{
- given: given,
- httpURLs: options.HTTPURLs,
- prioritizeHTTP: options.PrioritizeHTTP,
- rateLimitWaitMax: options.RateLimitWaitMax,
- refreshUnknownKID: options.RefreshUnknownKID,
- }
- return c, nil
-}
-
-// NewDefaultHTTPClient creates a new JWK Set client with default options from remote HTTP resources.
-//
-// The default behavior is to:
-// 1. Refresh remote HTTP resources every hour.
-// 2. Prioritize keys from remote HTTP resources over keys from the given storage.
-// 3. Refresh remote HTTP resources if a key with an unknown key ID is trying to be read, with a rate limit of 5 minutes.
-// 4. Log to slog.Default() if a refresh fails.
-func NewDefaultHTTPClient(urls []string) (Storage, error) {
- return NewDefaultHTTPClientCtx(context.Background(), urls)
-}
-
-// NewDefaultHTTPClientCtx is the same as NewDefaultHTTPClient, but with a context that can end the refresh goroutine.
-func NewDefaultHTTPClientCtx(ctx context.Context, urls []string) (Storage, error) {
- clientOptions := HTTPClientOptions{
- HTTPURLs: make(map[string]Storage),
- RateLimitWaitMax: time.Minute,
- RefreshUnknownKID: rate.NewLimiter(rate.Every(5*time.Minute), 1),
- }
- for _, u := range urls {
- refreshErrorHandler := func(ctx context.Context, err error) {
- slog.Default().ErrorContext(ctx, "Failed to refresh HTTP JWK Set from remote HTTP resource.",
- "error", err,
- "url", u,
- )
- }
- options := HTTPClientStorageOptions{
- Ctx: ctx,
- NoErrorReturnFirstHTTPReq: true,
- RefreshErrorHandler: refreshErrorHandler,
- RefreshInterval: time.Hour,
- }
- c, err := NewStorageFromHTTP(u, options)
- if err != nil {
- return nil, fmt.Errorf("failed to create HTTP client storage for %q: %w", u, errors.Join(err, ErrNewClient))
- }
- clientOptions.HTTPURLs[u] = c
- }
- return NewHTTPClient(clientOptions)
-}
-
-func (c httpClient) KeyDelete(ctx context.Context, keyID string) (ok bool, err error) {
- ok, err = c.given.KeyDelete(ctx, keyID)
- if err != nil && !errors.Is(err, ErrKeyNotFound) {
- return false, fmt.Errorf("failed to delete key with ID %q from given storage due to error: %w", keyID, err)
- }
- if ok {
- return true, nil
- }
- for _, store := range c.httpURLs {
- ok, err = store.KeyDelete(ctx, keyID)
- if err != nil && !errors.Is(err, ErrKeyNotFound) {
- return false, fmt.Errorf("failed to delete key with ID %q from HTTP storage due to error: %w", keyID, err)
- }
- if ok {
- return true, nil
- }
- }
- return false, nil
-}
-func (c httpClient) KeyRead(ctx context.Context, keyID string) (jwk JWK, err error) {
- if !c.prioritizeHTTP {
- jwk, err = c.given.KeyRead(ctx, keyID)
- switch {
- case errors.Is(err, ErrKeyNotFound):
- // Do nothing.
- case err != nil:
- return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in given storage due to error: %w", keyID, err)
- default:
- return jwk, nil
- }
- }
- for _, store := range c.httpURLs {
- jwk, err = store.KeyRead(ctx, keyID)
- switch {
- case errors.Is(err, ErrKeyNotFound):
- continue
- case err != nil:
- return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in HTTP storage due to error: %w", keyID, err)
- default:
- return jwk, nil
- }
- }
- if c.prioritizeHTTP {
- jwk, err = c.given.KeyRead(ctx, keyID)
- switch {
- case errors.Is(err, ErrKeyNotFound):
- // Do nothing.
- case err != nil:
- return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in given storage due to error: %w", keyID, err)
- default:
- return jwk, nil
- }
- }
- if c.refreshUnknownKID != nil {
- var cancel context.CancelFunc = func() {}
- if c.rateLimitWaitMax > 0 {
- ctx, cancel = context.WithTimeout(ctx, c.rateLimitWaitMax)
- }
- defer cancel()
- err = c.refreshUnknownKID.Wait(ctx)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to wait for JWK Set refresh rate limiter due to error: %w", err)
- }
- for _, store := range c.httpURLs {
- s, ok := store.(httpStorage)
- if !ok {
- continue
- }
- err = s.refresh(ctx)
- if err != nil {
- if s.options.RefreshErrorHandler != nil {
- s.options.RefreshErrorHandler(ctx, err)
- }
- continue
- }
- jwk, err = store.KeyRead(ctx, keyID)
- switch {
- case errors.Is(err, ErrKeyNotFound):
- // Do nothing.
- case err != nil:
- return JWK{}, fmt.Errorf("failed to find JWT key with ID %q in HTTP storage due to error: %w", keyID, err)
- default:
- return jwk, nil
- }
- }
- }
- return JWK{}, fmt.Errorf("%w %q", ErrKeyNotFound, keyID)
-}
-func (c httpClient) KeyReadAll(ctx context.Context) ([]JWK, error) {
- jwks, err := c.given.KeyReadAll(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to snapshot given keys due to error: %w", err)
- }
- for u, store := range c.httpURLs {
- j, err := store.KeyReadAll(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to snapshot HTTP keys from %q due to error: %w", u, err)
- }
- jwks = append(jwks, j...)
- }
- return jwks, nil
-}
-func (c httpClient) KeyWrite(ctx context.Context, jwk JWK) error {
- return c.given.KeyWrite(ctx, jwk)
-}
-
-func (c httpClient) JSON(ctx context.Context) (json.RawMessage, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.JSON(ctx)
-}
-func (c httpClient) JSONPublic(ctx context.Context) (json.RawMessage, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.JSONPublic(ctx)
-}
-func (c httpClient) JSONPrivate(ctx context.Context) (json.RawMessage, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.JSONPrivate(ctx)
-}
-func (c httpClient) JSONWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (json.RawMessage, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.JSONWithOptions(ctx, marshalOptions, validationOptions)
-}
-func (c httpClient) Marshal(ctx context.Context) (JWKSMarshal, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return JWKSMarshal{}, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.Marshal(ctx)
-}
-func (c httpClient) MarshalWithOptions(ctx context.Context, marshalOptions JWKMarshalOptions, validationOptions JWKValidateOptions) (JWKSMarshal, error) {
- m, err := c.combineStorage(ctx)
- if err != nil {
- return JWKSMarshal{}, fmt.Errorf("failed to combine storage due to error: %w", err)
- }
- return m.MarshalWithOptions(ctx, marshalOptions, validationOptions)
-}
-
-func (c httpClient) combineStorage(ctx context.Context) (Storage, error) {
- jwks, err := c.KeyReadAll(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to snapshot keys due to error: %w", err)
- }
- m := NewMemoryStorage()
- for _, jwk := range jwks {
- err = m.KeyWrite(ctx, jwk)
- if err != nil {
- return nil, fmt.Errorf("failed to write key to memory storage due to error: %w", err)
- }
- }
- return m, nil
-}
diff --git a/vendor/github.com/MicahParks/jwkset/jwk.go b/vendor/github.com/MicahParks/jwkset/jwk.go
deleted file mode 100644
index 9fd55580d1..0000000000
--- a/vendor/github.com/MicahParks/jwkset/jwk.go
+++ /dev/null
@@ -1,494 +0,0 @@
-package jwkset
-
-import (
- "bytes"
- "context"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
- "crypto/x509"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "math/big"
- "net/http"
- "net/url"
- "slices"
- "time"
-)
-
-var (
- // ErrPadding indicates that there is invalid padding.
- ErrPadding = errors.New("padding error")
-)
-
-// JWK represents a JSON Web Key.
-type JWK struct {
- key any
- marshal JWKMarshal
- options JWKOptions
-}
-
-// JWKMarshalOptions are used to specify options for JSON marshaling a JWK.
-type JWKMarshalOptions struct {
- // Private is used to indicate that the JWK's private key material should be JSON marshaled and unmarshalled. This
- // includes symmetric and asymmetric keys. Setting this to true is the only way to marshal and unmarshal symmetric
- // keys.
- Private bool
-}
-
-// JWKX509Options holds the X.509 certificate information for a JWK. This data structure is not used for JSON marshaling.
-type JWKX509Options struct {
- // X5C contains a chain of one or more PKIX certificates. The PKIX certificate containing the key value MUST be the
- // first certificate.
- X5C []*x509.Certificate // The PKIX certificate containing the key value MUST be the first certificate.
-
- // X5T is calculated automatically.
- // X5TS256 is calculated automatically.
-
- // X5U Is a URI that refers to a resource for an X.509 public key certificate or certificate chain.
- X5U string // https://www.rfc-editor.org/rfc/rfc7517#section-4.6
-}
-
-// JWKValidateOptions are used to specify options for validating a JWK.
-type JWKValidateOptions struct {
- /*
- This package intentionally does not confirm if certificate's usage or compare that to the JWK's use parameter.
- Please open a GitHub issue if you think this should be an option.
- */
- // CheckX509ValidTime is used to indicate that the X.509 certificate's valid time should be checked.
- CheckX509ValidTime bool
- // GetX5U is used to get and validate the X.509 certificate from the X5U URI. Use DefaultGetX5U for the default
- // behavior.
- GetX5U func(x5u *url.URL) ([]*x509.Certificate, error)
- // SkipAll is used to skip all validation.
- SkipAll bool
- // SkipKeyOps is used to skip validation of the key operations (key_ops).
- SkipKeyOps bool
- // SkipMetadata skips checking if the JWKMetadataOptions match the JWKMarshal.
- SkipMetadata bool
- // SkipUse is used to skip validation of the key use (use).
- SkipUse bool
- // SkipX5UScheme is used to skip checking if the X5U URI scheme is https.
- SkipX5UScheme bool
- // StrictPadding is used to indicate that the JWK should be validated with strict padding.
- StrictPadding bool
-}
-
-// JWKMetadataOptions are direct passthroughs into the JWKMarshal.
-type JWKMetadataOptions struct {
- // ALG is the algorithm (alg).
- ALG ALG
- // KID is the key ID (kid).
- KID string
- // KEYOPS is the key operations (key_ops).
- KEYOPS []KEYOPS
- // USE is the key use (use).
- USE USE
-}
-
-// JWKOptions are used to specify options for marshaling a JSON Web Key.
-type JWKOptions struct {
- Marshal JWKMarshalOptions
- Metadata JWKMetadataOptions
- Validate JWKValidateOptions
- X509 JWKX509Options
-}
-
-// NewJWKFromKey uses the given key and options to create a JWK. It is possible to provide a private key with an X.509
-// certificate, which will be validated to contain the correct public key.
-func NewJWKFromKey(key any, options JWKOptions) (JWK, error) {
- marshal, err := keyMarshal(key, options)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to marshal JSON Web Key: %w", err)
- }
- switch key.(type) {
- case ed25519.PrivateKey, ed25519.PublicKey:
- if options.Metadata.ALG == "" {
- options.Metadata.ALG = AlgEdDSA
- } else if options.Metadata.ALG != AlgEdDSA {
- return JWK{}, fmt.Errorf("%w: invalid ALG for Ed25519 key: %q", ErrOptions, options.Metadata.ALG)
- }
- }
- j := JWK{
- key: key,
- marshal: marshal,
- options: options,
- }
- err = j.Validate()
- if err != nil {
- return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
- }
- return j, nil
-}
-
-// NewJWKFromRawJSON uses the given raw JSON to create a JWK.
-func NewJWKFromRawJSON(j json.RawMessage, marshalOptions JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
- marshal := JWKMarshal{}
- err := json.Unmarshal(j, &marshal)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to unmarshal JSON Web Key: %w", err)
- }
- return NewJWKFromMarshal(marshal, marshalOptions, validateOptions)
-}
-
-// NewJWKFromMarshal transforms a JWKMarshal into a JWK.
-func NewJWKFromMarshal(marshal JWKMarshal, marshalOptions JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
- j, err := keyUnmarshal(marshal, marshalOptions, validateOptions)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to unmarshal JSON Web Key: %w", err)
- }
- err = j.Validate()
- if err != nil {
- return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
- }
- return j, nil
-}
-
-// NewJWKFromX5C uses the X.509 X5C information in the options to create a JWK.
-func NewJWKFromX5C(options JWKOptions) (JWK, error) {
- if len(options.X509.X5C) == 0 {
- return JWK{}, fmt.Errorf("%w: no X.509 certificates provided", ErrOptions)
- }
- cert := options.X509.X5C[0]
- marshal, err := keyMarshal(cert.PublicKey, options)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to marshal JSON Web Key: %w", err)
- }
-
- if cert.PublicKeyAlgorithm == x509.Ed25519 {
- if options.Metadata.ALG != "" && options.Metadata.ALG != AlgEdDSA {
- return JWK{}, fmt.Errorf("%w: ALG in metadata does not match ALG in X.509 certificate", errors.Join(ErrOptions, ErrX509Mismatch))
- }
- options.Metadata.ALG = AlgEdDSA
- }
-
- j := JWK{
- key: options.X509.X5C[0].PublicKey,
- marshal: marshal,
- options: options,
- }
- err = j.Validate()
- if err != nil {
- return JWK{}, fmt.Errorf("failed to validate JSON Web Key: %w", err)
- }
- return j, nil
-}
-
-// NewJWKFromX5U uses the X.509 X5U information in the options to create a JWK.
-func NewJWKFromX5U(options JWKOptions) (JWK, error) {
- if options.X509.X5U == "" {
- return JWK{}, fmt.Errorf("%w: no X.509 URI provided", ErrOptions)
- }
- u, err := url.ParseRequestURI(options.X509.X5U)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to parse X5U URI: %w", errors.Join(ErrOptions, err))
- }
- if !options.Validate.SkipX5UScheme && u.Scheme != "https" {
- return JWK{}, fmt.Errorf("%w: X5U URI scheme must be https", errors.Join(ErrOptions))
- }
- get := options.Validate.GetX5U
- if get == nil {
- get = DefaultGetX5U
- }
- certs, err := get(u)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to get X5U URI: %w", err)
- }
- options.X509.X5C = certs
- jwk, err := NewJWKFromX5C(options)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to create JWK from fetched X5U assets: %w", err)
- }
- return jwk, nil
-}
-
-// Key returns the public or private cryptographic key associated with the JWK.
-func (j JWK) Key() any {
- return j.key
-}
-
-// Marshal returns Go type that can be marshalled into JSON.
-func (j JWK) Marshal() JWKMarshal {
- return j.marshal
-}
-
-// X509 returns the X.509 certificate information for the JWK.
-func (j JWK) X509() JWKX509Options {
- return j.options.X509
-}
-
-// Validate validates the JWK. The JWK is automatically validated when created from a function in this package.
-func (j JWK) Validate() error {
- if j.options.Validate.SkipAll {
- return nil
- }
- if !j.marshal.KTY.IANARegistered() {
- return fmt.Errorf("%w: invalid or unsupported key type %q", ErrJWKValidation, j.marshal.KTY)
- }
-
- if !j.options.Validate.SkipUse && !j.marshal.USE.IANARegistered() {
- return fmt.Errorf("%w: invalid or unsupported key use %q", ErrJWKValidation, j.marshal.USE)
- }
-
- if !j.options.Validate.SkipKeyOps {
- for _, o := range j.marshal.KEYOPS {
- if !o.IANARegistered() {
- return fmt.Errorf("%w: invalid or unsupported key_opt %q", ErrJWKValidation, o)
- }
- }
- }
-
- if !j.options.Validate.SkipMetadata {
- if j.marshal.ALG != j.options.Metadata.ALG {
- return fmt.Errorf("%w: ALG in marshal does not match ALG in options", errors.Join(ErrJWKValidation, ErrOptions))
- }
- if j.marshal.KID != j.options.Metadata.KID {
- return fmt.Errorf("%w: KID in marshal does not match KID in options", errors.Join(ErrJWKValidation, ErrOptions))
- }
- if !slices.Equal(j.marshal.KEYOPS, j.options.Metadata.KEYOPS) {
- return fmt.Errorf("%w: KEYOPS in marshal does not match KEYOPS in options", errors.Join(ErrJWKValidation, ErrOptions))
- }
- if j.marshal.USE != j.options.Metadata.USE {
- return fmt.Errorf("%w: USE in marshal does not match USE in options", errors.Join(ErrJWKValidation, ErrOptions))
- }
- }
-
- if len(j.options.X509.X5C) > 0 {
- cert := j.options.X509.X5C[0]
- i := cert.PublicKey
- switch k := j.key.(type) {
- // ECDH keys are not used to sign certificates.
- case *ecdsa.PublicKey:
- pub, ok := i.(*ecdsa.PublicKey)
- if !ok {
- return fmt.Errorf("%w: Golang key is type *ecdsa.Public but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
- }
- if !k.Equal(pub) {
- return fmt.Errorf("%w: Golang *ecdsa.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
- }
- case ed25519.PublicKey:
- pub, ok := i.(ed25519.PublicKey)
- if !ok {
- return fmt.Errorf("%w: Golang key is type ed25519.PublicKey but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
- }
- if !bytes.Equal(k, pub) {
- return fmt.Errorf("%w: Golang ed25519.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
- }
- case *rsa.PublicKey:
- pub, ok := i.(*rsa.PublicKey)
- if !ok {
- return fmt.Errorf("%w: Golang key is type *rsa.PublicKey but X.509 public key was of type %T", errors.Join(ErrJWKValidation, ErrX509Mismatch), i)
- }
- if !k.Equal(pub) {
- return fmt.Errorf("%w: Golang *rsa.PublicKey does not match the X.509 public key", errors.Join(ErrJWKValidation, ErrX509Mismatch))
- }
- default:
- return fmt.Errorf("%w: Golang key is type %T, which is not supported, so it cannot be compared to given X.509 certificates", errors.Join(ErrJWKValidation, ErrUnsupportedKey, ErrX509Mismatch), j.key)
- }
- if cert.PublicKeyAlgorithm == x509.Ed25519 {
- if j.marshal.ALG != AlgEdDSA {
- return fmt.Errorf("%w: ALG in marshal does not match ALG in X.509 certificate", errors.Join(ErrJWKValidation, ErrX509Mismatch))
- }
- }
- if j.options.Validate.CheckX509ValidTime {
- now := time.Now()
- if now.Before(cert.NotBefore) {
- return fmt.Errorf("%w: X.509 certificate is not yet valid", ErrJWKValidation)
- }
- if now.After(cert.NotAfter) {
- return fmt.Errorf("%w: X.509 certificate is expired", ErrJWKValidation)
- }
- }
- }
-
- marshalled, err := keyMarshal(j.key, j.options)
- if err != nil {
- return fmt.Errorf("failed to marshal JSON Web Key: %w", errors.Join(ErrJWKValidation, err))
- }
-
- // Remove automatically computed thumbprints if not set in given JWK.
- if j.marshal.X5T == "" {
- marshalled.X5T = ""
- }
- if j.marshal.X5TS256 == "" {
- marshalled.X5TS256 = ""
- }
-
- canComputeThumbprint := len(j.marshal.X5C) > 0
- if j.marshal.X5T != marshalled.X5T && canComputeThumbprint {
- return fmt.Errorf("%w: X5T in marshal does not match X5T in marshalled", ErrJWKValidation)
- }
- if j.marshal.X5TS256 != marshalled.X5TS256 && canComputeThumbprint {
- return fmt.Errorf("%w: X5TS256 in marshal does not match X5TS256 in marshalled", ErrJWKValidation)
- }
- if j.marshal.CRV != marshalled.CRV {
- return fmt.Errorf("%w: CRV in marshal does not match CRV in marshalled", ErrJWKValidation)
- }
- switch j.marshal.KTY {
- case KtyEC:
- err = cmpBase64Int(j.marshal.X, marshalled.X, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: X in marshal does not match X in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.Y, marshalled.Y, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: Y in marshal does not match Y in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.D, marshalled.D, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: D in marshal does not match D in marshalled", errors.Join(ErrJWKValidation, err))
- }
- case KtyOKP:
- if j.marshal.X != marshalled.X {
- return fmt.Errorf("%w: X in marshal does not match X in marshalled", ErrJWKValidation)
- }
- if j.marshal.D != marshalled.D {
- return fmt.Errorf("%w: D in marshal does not match D in marshalled", ErrJWKValidation)
- }
- case KtyRSA:
- err = cmpBase64Int(j.marshal.D, marshalled.D, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: D in marshal does not match D in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.N, marshalled.N, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: N in marshal does not match N in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.E, marshalled.E, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: E in marshal does not match E in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.P, marshalled.P, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: P in marshal does not match P in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.Q, marshalled.Q, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: Q in marshal does not match Q in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.DP, marshalled.DP, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: DP in marshal does not match DP in marshalled", errors.Join(ErrJWKValidation, err))
- }
- err = cmpBase64Int(j.marshal.DQ, marshalled.DQ, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: DQ in marshal does not match DQ in marshalled", errors.Join(ErrJWKValidation, err))
- }
- if len(j.marshal.OTH) != len(marshalled.OTH) {
- return fmt.Errorf("%w: OTH in marshal does not match OTH in marshalled", ErrJWKValidation)
- }
- for i, o := range j.marshal.OTH {
- err = cmpBase64Int(o.R, marshalled.OTH[i].R, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
- }
- err = cmpBase64Int(o.D, marshalled.OTH[i].D, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
- }
- err = cmpBase64Int(o.T, marshalled.OTH[i].T, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: OTH index %d in marshal does not match OTH in marshalled", errors.Join(ErrJWKValidation, err), i)
- }
- }
- case KtyOct:
- err = cmpBase64Int(j.marshal.K, marshalled.K, j.options.Validate.StrictPadding)
- if err != nil {
- return fmt.Errorf("%w: K in marshal does not match K in marshalled", errors.Join(ErrJWKValidation, err))
- }
- default:
- return fmt.Errorf("%w: invalid or unsupported key type %q", ErrJWKValidation, j.marshal.KTY)
- }
-
- // Saved for last because it may involve a network request.
- if j.marshal.X5U != "" || j.options.X509.X5U != "" {
- if j.marshal.X5U != j.options.X509.X5U {
- return fmt.Errorf("%w: X5U in marshal does not match X5U in options", errors.Join(ErrJWKValidation, ErrOptions))
- }
- u, err := url.ParseRequestURI(j.marshal.X5U)
- if err != nil {
- return fmt.Errorf("failed to parse X5U URI: %w", errors.Join(ErrJWKValidation, ErrOptions, err))
- }
- if !j.options.Validate.SkipX5UScheme && u.Scheme != "https" {
- return fmt.Errorf("%w: X5U URI scheme must be https", errors.Join(ErrJWKValidation, ErrOptions))
- }
- if j.options.Validate.GetX5U != nil {
- certs, err := j.options.Validate.GetX5U(u)
- if err != nil {
- return fmt.Errorf("failed to get X5U URI: %w", errors.Join(ErrJWKValidation, ErrOptions, err))
- }
- if len(certs) == 0 {
- return fmt.Errorf("%w: X5U URI did not return any certificates", errors.Join(ErrJWKValidation, ErrOptions))
- }
- larger := certs
- smaller := j.options.X509.X5C
- if len(j.options.X509.X5C) > len(certs) {
- larger = j.options.X509.X5C
- smaller = certs
- }
- for i, c := range smaller {
- if !c.Equal(larger[i]) {
- return fmt.Errorf("%w: the X5C and X5U (remote resource) parameters are not a full or partial match", errors.Join(ErrJWKValidation, ErrOptions))
- }
- }
- }
- }
-
- return nil
-}
-
-// DefaultGetX5U is the default implementation of the GetX5U field for JWKValidateOptions.
-func DefaultGetX5U(u *url.URL) ([]*x509.Certificate, error) {
- timeout := time.Minute
- ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("%w: timeout of %s reached", ErrGetX5U, timeout.String()))
- defer cancel()
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create X5U request: %w", errors.Join(ErrGetX5U, err))
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to do X5U request: %w", errors.Join(ErrGetX5U, err))
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("%w: X5U request returned status code %d", ErrGetX5U, resp.StatusCode)
- }
- b, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read X5U response body: %w", errors.Join(ErrGetX5U, err))
- }
- certs, err := LoadCertificates(b)
- if err != nil {
- return nil, fmt.Errorf("failed to parse X5U response body: %w", errors.Join(ErrGetX5U, err))
- }
- return certs, nil
-}
-
-func cmpBase64Int(first, second string, strictPadding bool) error {
- if first == second {
- return nil
- }
- b, err := base64.RawURLEncoding.DecodeString(first)
- if err != nil {
- return fmt.Errorf("failed to decode Base64 raw URL decode first string: %w", err)
- }
- fLen := len(b)
- f := new(big.Int).SetBytes(b)
- b, err = base64.RawURLEncoding.DecodeString(second)
- if err != nil {
- return fmt.Errorf("failed to decode Base64 raw URL decode second string: %w", err)
- }
- sLen := len(b)
- s := new(big.Int).SetBytes(b)
- if f.Cmp(s) != 0 {
- return fmt.Errorf("%w: the parsed integers do not match", ErrJWKValidation)
- }
- if strictPadding && fLen != sLen {
- return fmt.Errorf("%w: the Base64 raw URL inputs do not have matching padding", errors.Join(ErrJWKValidation, ErrPadding))
- }
- return nil
-}
diff --git a/vendor/github.com/MicahParks/jwkset/marshal.go b/vendor/github.com/MicahParks/jwkset/marshal.go
deleted file mode 100644
index c604e22df1..0000000000
--- a/vendor/github.com/MicahParks/jwkset/marshal.go
+++ /dev/null
@@ -1,511 +0,0 @@
-package jwkset
-
-import (
- "context"
- "crypto/ecdh"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/elliptic"
- "crypto/rsa"
- "crypto/sha1"
- "crypto/sha256"
- "crypto/x509"
- "encoding/base64"
- "errors"
- "fmt"
- "math"
- "math/big"
- "slices"
- "strings"
-)
-
-var (
- // ErrGetX5U indicates there was an error getting the X5U remote resource.
- ErrGetX5U = errors.New("failed to get X5U via given URI")
- // ErrJWKValidation indicates that a JWK failed to validate.
- ErrJWKValidation = errors.New("failed to validate JWK")
- // ErrKeyUnmarshalParameter indicates that a JWK's attributes are invalid and cannot be unmarshaled.
- ErrKeyUnmarshalParameter = errors.New("unable to unmarshal JWK due to invalid attributes")
- // ErrOptions indicates that the given options caused an error.
- ErrOptions = errors.New("the given options caused an error")
- // ErrUnsupportedKey indicates a key is not supported.
- ErrUnsupportedKey = errors.New("unsupported key")
- // ErrX509Mismatch indicates that the X.509 certificate does not match the key.
- ErrX509Mismatch = errors.New("the X.509 certificate does not match Golang key type")
-)
-
-// OtherPrimes is for RSA private keys that have more than 2 primes.
-// https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7
-type OtherPrimes struct {
- R string `json:"r,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.1
- D string `json:"d,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.2
- T string `json:"t,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7.3
-}
-
-// JWKMarshal is used to marshal or unmarshal a JSON Web Key.
-// https://www.rfc-editor.org/rfc/rfc7517
-// https://www.rfc-editor.org/rfc/rfc7518
-// https://www.rfc-editor.org/rfc/rfc8037
-//
-// You can find the full list at https://www.iana.org/assignments/jose/jose.xhtml under "JSON Web Key Parameters".
-type JWKMarshal struct {
- KTY KTY `json:"kty,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.1
- USE USE `json:"use,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.2
- KEYOPS []KEYOPS `json:"key_ops,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.3
- ALG ALG `json:"alg,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.4 and https://www.rfc-editor.org/rfc/rfc7518#section-4.1
- KID string `json:"kid,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.5
- X5U string `json:"x5u,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.6
- X5C []string `json:"x5c,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.7
- X5T string `json:"x5t,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.8
- X5TS256 string `json:"x5t#S256,omitempty"` // https://www.rfc-editor.org/rfc/rfc7517#section-4.9
- CRV CRV `json:"crv,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.1 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
- X string `json:"x,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.2 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
- Y string `json:"y,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1.3
- D string `json:"d,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.1 and https://www.rfc-editor.org/rfc/rfc7518#section-6.2.2.1 and https://www.rfc-editor.org/rfc/rfc8037.html#section-2
- N string `json:"n,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.1
- E string `json:"e,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.2
- P string `json:"p,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.2
- Q string `json:"q,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.3
- DP string `json:"dp,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.4
- DQ string `json:"dq,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.5
- QI string `json:"qi,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.6
- OTH []OtherPrimes `json:"oth,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.3.2.7
- K string `json:"k,omitempty"` // https://www.rfc-editor.org/rfc/rfc7518#section-6.4.1
-}
-
-// JWKSMarshal is used to marshal or unmarshal a JSON Web Key Set.
-type JWKSMarshal struct {
- Keys []JWKMarshal `json:"keys"`
-}
-
-// JWKSlice converts the JWKSMarshal to a []JWK.
-func (j JWKSMarshal) JWKSlice() ([]JWK, error) {
- slice := make([]JWK, len(j.Keys))
- for i, key := range j.Keys {
- marshalOptions := JWKMarshalOptions{
- Private: true,
- }
- jwk, err := keyUnmarshal(key, marshalOptions, JWKValidateOptions{})
- if err != nil {
- return nil, fmt.Errorf("failed to unmarshal JWK: %w", err)
- }
- slice[i] = jwk
- }
- return slice, nil
-}
-
-// ToStorage converts the JWKSMarshal to a Storage.
-func (j JWKSMarshal) ToStorage() (Storage, error) {
- m := NewMemoryStorage()
- jwks, err := j.JWKSlice()
- if err != nil {
- return nil, fmt.Errorf("failed to create a slice of JWK from JWKSMarshal: %w", err)
- }
- for _, jwk := range jwks {
- err = m.KeyWrite(context.Background(), jwk)
- if err != nil {
- return nil, fmt.Errorf("failed to write JWK to storage: %w", err)
- }
- }
- return m, nil
-}
-
-func keyMarshal(key any, options JWKOptions) (JWKMarshal, error) {
- m := JWKMarshal{}
- m.ALG = options.Metadata.ALG
- switch key := key.(type) {
- case *ecdh.PublicKey:
- pub := key.Bytes()
- m.CRV = CrvX25519
- m.X = base64.RawURLEncoding.EncodeToString(pub)
- m.KTY = KtyOKP
- case *ecdh.PrivateKey:
- pub := key.PublicKey().Bytes()
- m.CRV = CrvX25519
- m.X = base64.RawURLEncoding.EncodeToString(pub)
- m.KTY = KtyOKP
- if options.Marshal.Private {
- priv := key.Bytes()
- m.D = base64.RawURLEncoding.EncodeToString(priv)
- }
- case *ecdsa.PrivateKey:
- pub := key.PublicKey
- m.CRV = CRV(pub.Curve.Params().Name)
- l := uint(pub.Curve.Params().BitSize / 8)
- if pub.Curve.Params().BitSize%8 != 0 {
- l++
- }
- m.X = bigIntToBase64RawURL(pub.X, l)
- m.Y = bigIntToBase64RawURL(pub.Y, l)
- m.KTY = KtyEC
- if options.Marshal.Private {
- params := key.Curve.Params()
- f, _ := params.N.Float64()
- l = uint(math.Ceil(math.Log2(f) / 8))
- m.D = bigIntToBase64RawURL(key.D, l)
- }
- case *ecdsa.PublicKey:
- l := uint(key.Curve.Params().BitSize / 8)
- if key.Curve.Params().BitSize%8 != 0 {
- l++
- }
- m.CRV = CRV(key.Curve.Params().Name)
- m.X = bigIntToBase64RawURL(key.X, l)
- m.Y = bigIntToBase64RawURL(key.Y, l)
- m.KTY = KtyEC
- case ed25519.PrivateKey:
- pub := key.Public().(ed25519.PublicKey)
- m.ALG = AlgEdDSA
- m.CRV = CrvEd25519
- m.X = base64.RawURLEncoding.EncodeToString(pub)
- m.KTY = KtyOKP
- if options.Marshal.Private {
- m.D = base64.RawURLEncoding.EncodeToString(key[:32])
- }
- case ed25519.PublicKey:
- m.ALG = AlgEdDSA
- m.CRV = CrvEd25519
- m.X = base64.RawURLEncoding.EncodeToString(key)
- m.KTY = KtyOKP
- case *rsa.PrivateKey:
- pub := key.PublicKey
- m.E = bigIntToBase64RawURL(big.NewInt(int64(pub.E)), 0)
- m.N = bigIntToBase64RawURL(pub.N, 0)
- m.KTY = KtyRSA
- if options.Marshal.Private {
- m.D = bigIntToBase64RawURL(key.D, 0)
- m.P = bigIntToBase64RawURL(key.Primes[0], 0)
- m.Q = bigIntToBase64RawURL(key.Primes[1], 0)
- m.DP = bigIntToBase64RawURL(key.Precomputed.Dp, 0)
- m.DQ = bigIntToBase64RawURL(key.Precomputed.Dq, 0)
- m.QI = bigIntToBase64RawURL(key.Precomputed.Qinv, 0)
- if len(key.Precomputed.CRTValues) > 0 {
- m.OTH = make([]OtherPrimes, len(key.Precomputed.CRTValues))
- for i := 0; i < len(key.Precomputed.CRTValues); i++ {
- m.OTH[i] = OtherPrimes{
- D: bigIntToBase64RawURL(key.Precomputed.CRTValues[i].Exp, 0),
- T: bigIntToBase64RawURL(key.Precomputed.CRTValues[i].Coeff, 0),
- R: bigIntToBase64RawURL(key.Primes[i+2], 0),
- }
- }
- }
- }
- case *rsa.PublicKey:
- m.E = bigIntToBase64RawURL(big.NewInt(int64(key.E)), 0)
- m.N = bigIntToBase64RawURL(key.N, 0)
- m.KTY = KtyRSA
- case []byte:
- if options.Marshal.Private {
- m.KTY = KtyOct
- m.K = base64.RawURLEncoding.EncodeToString(key)
- } else {
- return JWKMarshal{}, fmt.Errorf("%w: incorrect options to marshal symmetric key (oct)", ErrOptions)
- }
- default:
- return JWKMarshal{}, fmt.Errorf("%w: %T", ErrUnsupportedKey, key)
- }
- haveX5C := len(options.X509.X5C) > 0
- if haveX5C {
- for i, cert := range options.X509.X5C {
- m.X5C = append(m.X5C, base64.StdEncoding.EncodeToString(cert.Raw))
- if i == 0 {
- h1 := sha1.Sum(cert.Raw)
- m.X5T = base64.RawURLEncoding.EncodeToString(h1[:])
- h256 := sha256.Sum256(cert.Raw)
- m.X5TS256 = base64.RawURLEncoding.EncodeToString(h256[:])
- }
- }
- }
- m.KID = options.Metadata.KID
- m.KEYOPS = options.Metadata.KEYOPS
- m.USE = options.Metadata.USE
- m.X5U = options.X509.X5U
- return m, nil
-}
-
-func keyUnmarshal(marshal JWKMarshal, options JWKMarshalOptions, validateOptions JWKValidateOptions) (JWK, error) {
- marshalCopy := JWKMarshal{}
- var key any
- switch marshal.KTY {
- case KtyEC:
- if marshal.CRV == "" || marshal.X == "" || marshal.Y == "" {
- return JWK{}, fmt.Errorf(`%w: %s requires parameters "crv", "x", and "y"`, ErrKeyUnmarshalParameter, KtyEC)
- }
- x, err := base64urlTrailingPadding(marshal.X)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "x": %w`, KtyEC, err)
- }
- y, err := base64urlTrailingPadding(marshal.Y)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "y": %w`, KtyEC, err)
- }
- publicKey := &ecdsa.PublicKey{
- X: new(big.Int).SetBytes(x),
- Y: new(big.Int).SetBytes(y),
- }
- switch marshal.CRV {
- case CrvP256:
- publicKey.Curve = elliptic.P256()
- case CrvP384:
- publicKey.Curve = elliptic.P384()
- case CrvP521:
- publicKey.Curve = elliptic.P521()
- default:
- return JWK{}, fmt.Errorf("%w: unsupported curve type %q", ErrKeyUnmarshalParameter, marshal.CRV)
- }
- marshalCopy.CRV = marshal.CRV
- marshalCopy.X = marshal.X
- marshalCopy.Y = marshal.Y
- if options.Private && marshal.D != "" {
- d, err := base64urlTrailingPadding(marshal.D)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyEC, err)
- }
- privateKey := &ecdsa.PrivateKey{
- PublicKey: *publicKey,
- D: new(big.Int).SetBytes(d),
- }
- key = privateKey
- marshalCopy.D = marshal.D
- } else {
- key = publicKey
- }
- case KtyOKP:
- if marshal.CRV == "" || marshal.X == "" {
- return JWK{}, fmt.Errorf(`%w: %s requires parameters "crv" and "x"`, ErrKeyUnmarshalParameter, KtyOKP)
- }
- public, err := base64urlTrailingPadding(marshal.X)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "x": %w`, KtyOKP, err)
- }
- marshalCopy.CRV = marshal.CRV
- marshalCopy.X = marshal.X
- var private []byte
- if options.Private && marshal.D != "" {
- private, err = base64urlTrailingPadding(marshal.D)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyOKP, err)
- }
- }
- switch marshal.CRV {
- case CrvEd25519:
- if len(public) != ed25519.PublicKeySize {
- return JWK{}, fmt.Errorf("%w: %s key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, ed25519.PublicKeySize)
- }
- if options.Private && marshal.D != "" {
- private = append(private, public...)
- if len(private) != ed25519.PrivateKeySize {
- return JWK{}, fmt.Errorf("%w: %s key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, ed25519.PrivateKeySize)
- }
- key = ed25519.PrivateKey(private)
- marshalCopy.D = marshal.D
- } else {
- key = ed25519.PublicKey(public)
- }
- case CrvX25519:
- const x25519PublicKeySize = 32
- if len(public) != x25519PublicKeySize {
- return JWK{}, fmt.Errorf("%w: %s with curve %s public key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, CrvEd25519, x25519PublicKeySize)
- }
- if options.Private && marshal.D != "" {
- const x25519PrivateKeySize = 32
- if len(private) != x25519PrivateKeySize {
- return JWK{}, fmt.Errorf("%w: %s with curve %s private key should be %d bytes", ErrKeyUnmarshalParameter, KtyOKP, CrvEd25519, x25519PrivateKeySize)
- }
- key, err = ecdh.X25519().NewPrivateKey(private)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to create X25519 private key: %w", err)
- }
- marshalCopy.D = marshal.D
- } else {
- key, err = ecdh.X25519().NewPublicKey(public)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to create X25519 public key: %w", err)
- }
- }
- default:
- return JWK{}, fmt.Errorf("%w: unsupported curve type %q", ErrKeyUnmarshalParameter, marshal.CRV)
- }
- case KtyRSA:
- if marshal.N == "" || marshal.E == "" {
- return JWK{}, fmt.Errorf(`%w: %s requires parameters "n" and "e"`, ErrKeyUnmarshalParameter, KtyRSA)
- }
- n, err := base64urlTrailingPadding(marshal.N)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "n": %w`, KtyRSA, err)
- }
- e, err := base64urlTrailingPadding(marshal.E)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "e": %w`, KtyRSA, err)
- }
- publicKey := rsa.PublicKey{
- N: new(big.Int).SetBytes(n),
- E: int(new(big.Int).SetBytes(e).Uint64()),
- }
- marshalCopy.N = marshal.N
- marshalCopy.E = marshal.E
- if options.Private && marshal.D != "" && marshal.P != "" && marshal.Q != "" && marshal.DP != "" && marshal.DQ != "" && marshal.QI != "" { // TODO Only "d" is required, but if one of the others is present, they all must be.
- d, err := base64urlTrailingPadding(marshal.D)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyRSA, err)
- }
- p, err := base64urlTrailingPadding(marshal.P)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "p": %w`, KtyRSA, err)
- }
- q, err := base64urlTrailingPadding(marshal.Q)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "q": %w`, KtyRSA, err)
- }
- dp, err := base64urlTrailingPadding(marshal.DP)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "dp": %w`, KtyRSA, err)
- }
- dq, err := base64urlTrailingPadding(marshal.DQ)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "dq": %w`, KtyRSA, err)
- }
- qi, err := base64urlTrailingPadding(marshal.QI)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "qi": %w`, KtyRSA, err)
- }
- var oth []rsa.CRTValue
- primes := []*big.Int{
- new(big.Int).SetBytes(p),
- new(big.Int).SetBytes(q),
- }
- if len(marshal.OTH) > 0 {
- oth = make([]rsa.CRTValue, len(marshal.OTH))
- for i, otherPrimes := range marshal.OTH {
- if otherPrimes.R == "" || otherPrimes.D == "" || otherPrimes.T == "" {
- return JWK{}, fmt.Errorf(`%w: %s requires parameters "r", "d", and "t" for each "oth"`, ErrKeyUnmarshalParameter, KtyRSA)
- }
- othD, err := base64urlTrailingPadding(otherPrimes.D)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "d": %w`, KtyRSA, err)
- }
- othT, err := base64urlTrailingPadding(otherPrimes.T)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "t": %w`, KtyRSA, err)
- }
- othR, err := base64urlTrailingPadding(otherPrimes.R)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "r": %w`, KtyRSA, err)
- }
- primes = append(primes, new(big.Int).SetBytes(othR))
- oth[i] = rsa.CRTValue{
- Exp: new(big.Int).SetBytes(othD),
- Coeff: new(big.Int).SetBytes(othT),
- R: new(big.Int).SetBytes(othR),
- }
- }
- }
- privateKey := &rsa.PrivateKey{
- PublicKey: publicKey,
- D: new(big.Int).SetBytes(d),
- Primes: primes,
- Precomputed: rsa.PrecomputedValues{
- Dp: new(big.Int).SetBytes(dp),
- Dq: new(big.Int).SetBytes(dq),
- Qinv: new(big.Int).SetBytes(qi),
- CRTValues: oth,
- },
- }
- err = privateKey.Validate()
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to validate %s key: %w`, KtyRSA, err)
- }
- key = privateKey
- marshalCopy.D = marshal.D
- marshalCopy.P = marshal.P
- marshalCopy.Q = marshal.Q
- marshalCopy.DP = marshal.DP
- marshalCopy.DQ = marshal.DQ
- marshalCopy.QI = marshal.QI
- marshalCopy.OTH = slices.Clone(marshal.OTH)
- } else {
- key = &publicKey
- }
- case KtyOct:
- if options.Private {
- if marshal.K == "" {
- return JWK{}, fmt.Errorf(`%w: %s requires parameter "k"`, ErrKeyUnmarshalParameter, KtyOct)
- }
- k, err := base64urlTrailingPadding(marshal.K)
- if err != nil {
- return JWK{}, fmt.Errorf(`failed to decode %s key parameter "k": %w`, KtyOct, err)
- }
- key = k
- marshalCopy.K = marshal.K
- } else {
- return JWK{}, fmt.Errorf("%w: incorrect options to unmarshal symmetric key (%s)", ErrOptions, KtyOct)
- }
- default:
- return JWK{}, fmt.Errorf("%w: %s (kty)", ErrUnsupportedKey, marshal.KTY)
- }
- marshalCopy.KTY = marshal.KTY
- x5c := make([]*x509.Certificate, len(marshal.X5C))
- for i, cert := range marshal.X5C {
- raw, err := base64.StdEncoding.DecodeString(cert)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to Base64 decode X.509 certificate: %w", err)
- }
- x5c[i], err = x509.ParseCertificate(raw)
- if err != nil {
- return JWK{}, fmt.Errorf("failed to parse X.509 certificate: %w", err)
- }
- }
- jwkX509 := JWKX509Options{
- X5C: x5c,
- X5U: marshal.X5U,
- }
- marshalCopy.X5C = slices.Clone(marshal.X5C)
- marshalCopy.X5T = marshal.X5T
- marshalCopy.X5TS256 = marshal.X5TS256
- marshalCopy.X5U = marshal.X5U
- metadata := JWKMetadataOptions{
- ALG: marshal.ALG,
- KID: marshal.KID,
- KEYOPS: slices.Clone(marshal.KEYOPS),
- USE: marshal.USE,
- }
- marshalCopy.ALG = marshal.ALG
- marshalCopy.KID = marshal.KID
- marshalCopy.KEYOPS = slices.Clone(marshal.KEYOPS)
- marshalCopy.USE = marshal.USE
- opts := JWKOptions{
- Metadata: metadata,
- Marshal: options,
- Validate: validateOptions,
- X509: jwkX509,
- }
- j := JWK{
- key: key,
- marshal: marshalCopy,
- options: opts,
- }
- return j, nil
-}
-
-// base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
-// JWKS contain padding at the end values for base64url encoded public keys.
-//
-// Trailing padding is required to be removed from base64url encoded keys.
-// RFC 7517 defines base64url the same as RFC 7515 Section 2:
-// https://datatracker.ietf.org/doc/html/rfc7517#section-1.1
-// https://datatracker.ietf.org/doc/html/rfc7515#section-2
-func base64urlTrailingPadding(s string) ([]byte, error) {
- s = strings.TrimRight(s, "=")
- return base64.RawURLEncoding.DecodeString(s)
-}
-
-func bigIntToBase64RawURL(i *big.Int, l uint) string {
- var b []byte
- if l != 0 {
- b = make([]byte, l)
- i.FillBytes(b)
- } else {
- b = i.Bytes()
- }
- return base64.RawURLEncoding.EncodeToString(b)
-}
diff --git a/vendor/github.com/MicahParks/jwkset/storage.go b/vendor/github.com/MicahParks/jwkset/storage.go
deleted file mode 100644
index 3a057d2428..0000000000
--- a/vendor/github.com/MicahParks/jwkset/storage.go
+++ /dev/null
@@ -1,311 +0,0 @@
-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 {
- options.Client = http.DefaultClient
- }
- if options.Ctx == nil {
- options.Ctx = context.Background()
- }
- if options.HTTPExpectedStatus == 0 {
- options.HTTPExpectedStatus = http.StatusOK
- }
- if options.HTTPTimeout == 0 {
- options.HTTPTimeout = time.Minute
- }
- if options.HTTPMethod == "" {
- options.HTTPMethod = http.MethodGet
- }
- store := NewMemoryStorage()
- _, err := url.ParseRequestURI(remoteJWKSetURL)
- if err != nil {
- return nil, fmt.Errorf("failed to parse given URL %q: %w", remoteJWKSetURL, err)
- }
-
- refresh := func(ctx context.Context) error {
- req, err := http.NewRequestWithContext(ctx, options.HTTPMethod, remoteJWKSetURL, nil)
- if err != nil {
- return fmt.Errorf("failed to create HTTP request for JWK Set refresh: %w", err)
- }
- resp, err := options.Client.Do(req)
- if err != nil {
- return fmt.Errorf("failed to perform HTTP request for JWK Set refresh: %w", err)
- }
- //goland:noinspection GoUnhandledErrorResult
- defer resp.Body.Close()
- if resp.StatusCode != options.HTTPExpectedStatus {
- return fmt.Errorf("%w: %d", ErrInvalidHTTPStatusCode, resp.StatusCode)
- }
- var jwks JWKSMarshal
- err = json.NewDecoder(resp.Body).Decode(&jwks)
- if err != nil {
- return fmt.Errorf("failed to decode JWK Set response: %w", err)
- }
- store.mux.Lock()
- defer store.mux.Unlock()
- store.set = make([]JWK, len(jwks.Keys)) // Clear local cache in case of key revocation.
- for i, marshal := range jwks.Keys {
- marshalOptions := JWKMarshalOptions{
- Private: true,
- }
- jwk, err := NewJWKFromMarshal(marshal, marshalOptions, options.ValidateOptions)
- if err != nil {
- return fmt.Errorf("failed to create JWK from JWK Marshal: %w", err)
- }
- store.set[i] = jwk
- }
- return nil
- }
-
- if options.RefreshInterval != 0 {
- go func() { // Refresh goroutine.
- ticker := time.NewTicker(options.RefreshInterval)
- defer ticker.Stop()
- for {
- select {
- case <-options.Ctx.Done():
- return
- case <-ticker.C:
- ctx, cancel := context.WithTimeout(options.Ctx, options.HTTPTimeout)
- err := refresh(ctx)
- cancel()
- if err != nil && options.RefreshErrorHandler != nil {
- options.RefreshErrorHandler(ctx, err)
- }
- }
- }
- }()
- }
-
- s := httpStorage{
- options: options,
- refresh: refresh,
- Storage: store,
- }
-
- ctx, cancel := context.WithTimeout(options.Ctx, options.HTTPTimeout)
- defer cancel()
- err = refresh(ctx)
- cancel()
- if err != nil {
- if options.NoErrorReturnFirstHTTPReq {
- if options.RefreshErrorHandler != nil {
- options.RefreshErrorHandler(ctx, err)
- }
- return s, nil
- }
- return nil, fmt.Errorf("failed to perform first HTTP request for JWK Set: %w", err)
- }
-
- return s, nil
-}
diff --git a/vendor/github.com/MicahParks/jwkset/x509.go b/vendor/github.com/MicahParks/jwkset/x509.go
deleted file mode 100644
index b89a3a6ea9..0000000000
--- a/vendor/github.com/MicahParks/jwkset/x509.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package jwkset
-
-import (
- "crypto/ecdh"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "fmt"
-)
-
-var (
- // ErrX509Infer is returned when the key type cannot be inferred from the PEM block type.
- ErrX509Infer = errors.New("failed to infer X509 key type")
-)
-
-// LoadCertificate loads an X509 certificate from a PEM block.
-func LoadCertificate(pemBlock []byte) (*x509.Certificate, error) {
- cert, err := x509.ParseCertificate(pemBlock)
- if err != nil {
- return nil, fmt.Errorf("failed to parse certificates: %w", err)
- }
- switch cert.PublicKey.(type) {
- case *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
- default:
- return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, cert.PublicKey)
- }
- return cert, nil
-}
-
-// LoadCertificates loads X509 certificates from raw PEM data. It can be useful in loading X5U remote resources.
-func LoadCertificates(rawPEM []byte) ([]*x509.Certificate, error) {
- b := make([]byte, 0)
- for {
- block, rest := pem.Decode(rawPEM)
- if block == nil {
- break
- }
- rawPEM = rest
- if block.Type == "CERTIFICATE" {
- b = append(b, block.Bytes...)
- }
- }
- certs, err := x509.ParseCertificates(b)
- if err != nil {
- return nil, fmt.Errorf("failed to parse certificates: %w", err)
- }
- for _, cert := range certs {
- switch cert.PublicKey.(type) {
- case *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
- default:
- return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, cert.PublicKey)
- }
- }
- return certs, nil
-}
-
-// LoadX509KeyInfer loads an X509 key from a PEM block.
-func LoadX509KeyInfer(pemBlock *pem.Block) (key any, err error) {
- switch pemBlock.Type {
- case "EC PRIVATE KEY":
- key, err = loadECPrivate(pemBlock)
- case "RSA PRIVATE KEY":
- key, err = loadPKCS1Private(pemBlock)
- case "RSA PUBLIC KEY":
- key, err = loadPKCS1Public(pemBlock)
- case "PRIVATE KEY":
- key, err = loadPKCS8Private(pemBlock)
- case "PUBLIC KEY":
- key, err = loadPKIXPublic(pemBlock)
- default:
- return nil, ErrX509Infer
- }
- if err != nil {
- return nil, fmt.Errorf("failed to load key from inferred format %q: %w", key, err)
- }
- return key, nil
-}
-func loadECPrivate(pemBlock *pem.Block) (priv *ecdsa.PrivateKey, err error) {
- priv, err = x509.ParseECPrivateKey(pemBlock.Bytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse EC private key: %w", err)
- }
- return priv, nil
-}
-func loadPKCS1Public(pemBlock *pem.Block) (pub *rsa.PublicKey, err error) {
- pub, err = x509.ParsePKCS1PublicKey(pemBlock.Bytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse PKCS1 public key: %w", err)
- }
- return pub, nil
-}
-func loadPKCS1Private(pemBlock *pem.Block) (priv *rsa.PrivateKey, err error) {
- priv, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse PKCS1 private key: %w", err)
- }
- return priv, nil
-}
-func loadPKCS8Private(pemBlock *pem.Block) (priv any, err error) {
- priv, err = x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse PKCS8 private key: %w", err)
- }
- switch priv.(type) {
- case *ecdh.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, *rsa.PrivateKey:
- default:
- return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, priv)
- }
- return priv, nil
-}
-func loadPKIXPublic(pemBlock *pem.Block) (pub any, err error) {
- pub, err = x509.ParsePKIXPublicKey(pemBlock.Bytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse PKIX public key: %w", err)
- }
- switch pub.(type) {
- case *ecdh.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PublicKey:
- default:
- return nil, fmt.Errorf("%w: %T", ErrUnsupportedKey, pub)
- }
- return pub, nil
-}
diff --git a/vendor/github.com/MicahParks/jwkset/x509_gen.sh b/vendor/github.com/MicahParks/jwkset/x509_gen.sh
deleted file mode 100644
index 79cc315c14..0000000000
--- a/vendor/github.com/MicahParks/jwkset/x509_gen.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-# OpenSSL 3.0.10 1 Aug 2023 (Library: OpenSSL 3.0.10 1 Aug 2023)
-openssl req -newkey EC -pkeyopt ec_paramgen_curve:P-521 -noenc -keyout ec521.pem -x509 -out ec521.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
-openssl req -newkey ED25519 -noenc -keyout ed25519.pem -x509 -out ed25519.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
-openssl req -newkey RSA:4096 -noenc -keyout rsa4096.pem -x509 -out rsa4096.crt -subj "/C=US/ST=Virginia/L=Richmond/O=Micah Parks/OU=Self/CN=example.com"
-
-openssl pkey -in ec521.pem -pubout -out ec521pub.pem
-openssl pkey -in ed25519.pem -pubout -out ed25519pub.pem
-openssl pkey -in rsa4096.pem -pubout -out rsa4096pub.pem
-
-# For the "RSA PRIVATE KEY" (PKCS#1) and "EC PRIVATE KEY" (SEC1) formats, the PEM files are generated using the
-# cmd/gen_pkcs1 and cmd/gen_ec Golang programs, respectively.
-
-openssl dsaparam -out dsaparam.pem 2048
-openssl gendsa -out dsa.pem dsaparam.pem
-openssl dsa -in dsa.pem -pubout -out dsa_pub.pem
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/LICENSE b/vendor/github.com/MicahParks/keyfunc/v3/LICENSE
deleted file mode 100644
index 06dd4f2104..0000000000
--- a/vendor/github.com/MicahParks/keyfunc/v3/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2021 Micah Parks
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/README.md b/vendor/github.com/MicahParks/keyfunc/v3/README.md
deleted file mode 100644
index e6e304dc39..0000000000
--- a/vendor/github.com/MicahParks/keyfunc/v3/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-[](https://pkg.go.dev/github.com/MicahParks/keyfunc/v3)
-
-# keyfunc
-
-The purpose of this package is to provide a
-[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc) for the
-[github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) package using a JSON Web Key Set (JWK Set) for parsing
-and verifying JSON Web Tokens (JWTs).
-
-It's common for an identity providers, particularly those
-using [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749)
-or [OpenID Connect](https://openid.net/developers/how-connect-works/), such
-as [Keycloak](https://github.com/MicahParks/keyfunc/blob/master/examples/keycloak/main.go)
-or [Amazon Cognito (AWS)](https://github.com/MicahParks/keyfunc/blob/master/examples/aws_cognito/main.go) to expose a
-JWK Set via an HTTPS endpoint. This package has the ability to consume that JWK Set and produce a
-[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc). It is important that a JWK Set endpoint is
-using HTTPS to ensure the keys are from the correct trusted source.
-
-## Basic usage
-
-For complete examples, please see the `examples` directory.
-
-```go
-import "github.com/MicahParks/keyfunc/v3"
-```
-
-### Step 1: Create the `keyfunc.Keyfunc`
-
-The below example is for a remote HTTP resource.
-See [`examples/json/main.go`](https://github.com/MicahParks/keyfunc/blob/master/examples/json/main.go) for a JSON
-example.
-
-```go
-// Create the keyfunc.Keyfunc.
-k, err := keyfunc.NewDefaultCtx(ctx, []string{server.URL}) // Context is used to end the refresh goroutine.
-if err != nil {
- log.Fatalf("Failed to create a keyfunc.Keyfunc from the server's URL.\nError: %s", err)
-}
-```
-
-When using the `keyfunc.NewDefault` function, the JWK Set will be automatically refreshed using
-[`jwkset.NewDefaultHTTPClient`](https://pkg.go.dev/github.com/MicahParks/jwkset#NewHTTPClient). This does launch a "
-refresh goroutine". If you want the ability to end this goroutine, use the `keyfunc.NewDefaultCtx` function.
-
-It is also possible to create a `keyfunc.Keyfunc` from given keys like HMAC shared secrets. See `examples/hmac/main.go`.
-
-### Step 2: Use the `keyfunc.Keyfunc` to parse and verify JWTs
-
-```go
-// Parse the JWT.
-parsed, err := jwt.Parse(signed, k.Keyfunc)
-if err != nil {
- log.Fatalf("Failed to parse the JWT.\nError: %s", err)
-}
-```
-
-## Additional features
-
-This project's primary purpose is to provide a [`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc)
-implementation for JWK Sets.
-
-Since version `3.X.X`, this project has become a thin wrapper
-around [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset). Newer versions contain a superset of
-features available in versions `2.X.X` and earlier, but some of the deep customization has been moved to the `jwkset`
-project. The intention behind this is to make `keyfunc` easier to use for most use cases.
-
-Access the [`jwkset.Storage`](https://pkg.go.dev/github.com/MicahParks/jwkset#Storage) from a `keyfunc.Keyfunc` via
-the `.Storage()` method. Using the [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) package
-provides the below features, and more:
-
-* An HTTP client that automatically updates one or more remote JWK Set resources.
-* An automatic refresh of remote HTTP resources when an unknown key ID (`kid`) is encountered.
-* X.509 URIs or embedded [certificate chains](https://pkg.go.dev/crypto/x509#Certificate), when a JWK contains them.
-* Support for private asymmetric keys.
-* Specified key operations and usage.
-
-## Related projects
-
-### [`github.com/MicahParks/jwkset`](https://github.com/MicahParks/jwkset):
-
-A JWK Set implementation. The `keyfunc` project is a wrapper around this project.
diff --git a/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go b/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
deleted file mode 100644
index 1725731d6d..0000000000
--- a/vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package keyfunc
-
-import (
- "context"
- "crypto"
- "encoding/json"
- "errors"
- "fmt"
-
- "github.com/MicahParks/jwkset"
- "github.com/golang-jwt/jwt/v5"
-)
-
-var (
- // ErrKeyfunc is returned when a keyfunc error occurs.
- ErrKeyfunc = errors.New("failed keyfunc")
-)
-
-// Keyfunc is meant to be used as the jwt.Keyfunc function for github.com/golang-jwt/jwt/v5. It uses
-// github.com/MicahParks/jwkset as a JWK Set storage.
-type Keyfunc interface {
- Keyfunc(token *jwt.Token) (any, error)
- KeyfuncCtx(ctx context.Context) jwt.Keyfunc
- Storage() jwkset.Storage
-}
-
-// Options are used to create a new Keyfunc.
-type Options struct {
- Ctx context.Context
- Storage jwkset.Storage
- UseWhitelist []jwkset.USE
-}
-
-type keyfunc struct {
- ctx context.Context
- storage jwkset.Storage
- useWhitelist []jwkset.USE
-}
-
-// New creates a new Keyfunc.
-func New(options Options) (Keyfunc, error) {
- ctx := options.Ctx
- if ctx == nil {
- ctx = context.Background()
- }
- if options.Storage == nil {
- return nil, fmt.Errorf("%w: no JWK Set storage given in options", ErrKeyfunc)
- }
- k := keyfunc{
- ctx: ctx,
- storage: options.Storage,
- useWhitelist: options.UseWhitelist,
- }
- return k, nil
-}
-
-// NewDefault creates a new Keyfunc with a default JWK Set storage and options.
-//
-// This will launch "refresh goroutine" to automatically refresh the remote HTTP resources.
-func NewDefault(urls []string) (Keyfunc, error) {
- return NewDefaultCtx(context.Background(), urls)
-}
-
-// NewDefaultCtx creates a new Keyfunc with a default JWK Set storage and options. The context is used to end the
-// "refresh goroutine".
-//
-// This will launch "refresh goroutine" to automatically refresh the remote HTTP resources.
-func NewDefaultCtx(ctx context.Context, urls []string) (Keyfunc, error) {
- client, err := jwkset.NewDefaultHTTPClientCtx(ctx, urls)
- if err != nil {
- return nil, err
- }
- options := Options{
- Storage: client,
- }
- return New(options)
-}
-
-// NewJWKJSON creates a new Keyfunc from raw JWK JSON.
-func NewJWKJSON(raw json.RawMessage) (Keyfunc, error) {
- marshalOptions := jwkset.JWKMarshalOptions{
- Private: true,
- }
- jwk, err := jwkset.NewJWKFromRawJSON(raw, marshalOptions, jwkset.JWKValidateOptions{})
- if err != nil {
- return nil, fmt.Errorf("%w: could not create JWK from raw JSON", errors.Join(err, ErrKeyfunc))
- }
- store := jwkset.NewMemoryStorage()
- err = store.KeyWrite(context.Background(), jwk)
- if err != nil {
- return nil, fmt.Errorf("%w: could not write JWK to storage", errors.Join(err, ErrKeyfunc))
- }
- options := Options{
- Storage: store,
- }
- return New(options)
-}
-
-// NewJWKSetJSON creates a new Keyfunc from raw JWK Set JSON.
-func NewJWKSetJSON(raw json.RawMessage) (Keyfunc, error) {
- var jwks jwkset.JWKSMarshal
- err := json.Unmarshal(raw, &jwks)
- if err != nil {
- return nil, fmt.Errorf("%w: could not unmarshal raw JWK Set JSON", errors.Join(err, ErrKeyfunc))
- }
- store, err := jwks.ToStorage()
- if err != nil {
- return nil, fmt.Errorf("%w: could not create JWK Set storage", errors.Join(err, ErrKeyfunc))
- }
- options := Options{
- Storage: store,
- }
- return New(options)
-}
-
-func (k keyfunc) KeyfuncCtx(ctx context.Context) jwt.Keyfunc {
- return func(token *jwt.Token) (any, error) {
- kidInter, ok := token.Header[jwkset.HeaderKID]
- if !ok {
- return nil, fmt.Errorf("%w: could not find kid in JWT header", ErrKeyfunc)
- }
- kid, ok := kidInter.(string)
- if !ok {
- return nil, fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKeyfunc)
- }
- algInter, ok := token.Header["alg"]
- if !ok {
- return nil, fmt.Errorf("%w: could not find alg in JWT header", ErrKeyfunc)
- }
- alg, ok := algInter.(string)
- if !ok {
- // For test coverage purposes, this should be impossible to reach because the JWT package rejects a token
- // without an alg parameter in the header before calling jwt.Keyfunc.
- return nil, fmt.Errorf(`%w: the JWT header did not contain the "alg" parameter, which is required by RFC 7515 section 4.1.1`, ErrKeyfunc)
- }
-
- jwk, err := k.storage.KeyRead(ctx, kid)
- if err != nil {
- return nil, fmt.Errorf("%w: could not read JWK from storage", errors.Join(err, ErrKeyfunc))
- }
-
- if a := jwk.Marshal().ALG.String(); a != "" && a != alg {
- return nil, fmt.Errorf(`%w: JWK "alg" parameter value %q does not match token "alg" parameter value %q`, ErrKeyfunc, a, alg)
- }
- if len(k.useWhitelist) > 0 {
- found := false
- for _, u := range k.useWhitelist {
- if jwk.Marshal().USE == u {
- found = true
- break
- }
- }
- if !found {
- return nil, fmt.Errorf(`%w: JWK "use" parameter value %q is not in whitelist`, ErrKeyfunc, jwk.Marshal().USE)
- }
- }
-
- type publicKeyer interface {
- Public() crypto.PublicKey
- }
-
- key := jwk.Key()
- pk, ok := key.(publicKeyer)
- if ok {
- key = pk.Public()
- }
-
- return key, nil
- }
-}
-func (k keyfunc) Keyfunc(token *jwt.Token) (any, error) {
- keyF := k.KeyfuncCtx(k.ctx)
- return keyF(token)
-}
-func (k keyfunc) Storage() jwkset.Storage {
- return k.storage
-}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8e28eac5eb..e484e61491 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -38,15 +38,9 @@ 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