From b7b540a3c8dd6ef2a5a660c546d5f9c5d64a6604 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 10 Feb 2026 16:57:24 +0100 Subject: [PATCH] groupware: add OIDC authentication support between Groupware backend and Stalwart * re-implement the auth-api service to authenticate Reva tokens following the OIDC Userinfo endpoint specification * pass the context where necessary and add an authenticator interface to the JMAP HTTP driver, in order to select between master authentication (which is used when GROUPWARE_JMAP_MASTER_USERNAME and GROUPWARE_JMAP_MASTER_PASSWORD are both set) and OIDC token forwarding through bearer auth * add Stalwart directory configuration "idmoidc" which uses the OpenCloud auth-api service API (/auth/) to validate the token it received as bearer auth from the Groupware backend's JMAP client, using it as an OIDC Userinfo endpoint * implement optional additional shared secret to secure the Userinfo service, as an additional path parameter --- .vscode/launch.json | 11 +- devtools/deployments/opencloud_full/.env | 6 +- .../config/stalwart/idmoidc.toml | 67 +++ .../deployments/opencloud_full/opencloud.yml | 2 + go.mod | 4 +- go.sum | 4 - pkg/config/config.go | 2 +- pkg/jmap/api.go | 4 +- pkg/jmap/api_ws.go | 6 +- pkg/jmap/client.go | 5 +- pkg/jmap/http.go | 115 ++-- pkg/jmap/integration_test.go | 15 +- pkg/jmap/integration_ws_test.go | 5 +- services/auth-api/pkg/auth-api/authapi.go | 329 +++++++++++ .../auth-api/pkg/auth-api/authapi_test.go | 40 ++ services/auth-api/pkg/command/server.go | 20 +- services/auth-api/pkg/config/auth.go | 7 + services/auth-api/pkg/config/config.go | 8 +- .../pkg/config/defaults/defaultconfig.go | 14 +- services/auth-api/pkg/config/parser/parse.go | 7 +- services/auth-api/pkg/config/reva.go | 6 + services/auth-api/pkg/metrics/metrics.go | 27 +- services/auth-api/pkg/metrics/options.go | 31 -- services/auth-api/pkg/server/debug/server.go | 4 + services/auth-api/pkg/server/http/option.go | 24 - services/auth-api/pkg/server/http/server.go | 61 ++- .../pkg/service/http/v0/instrument.go | 25 - .../auth-api/pkg/service/http/v0/logging.go | 25 - .../auth-api/pkg/service/http/v0/option.go | 66 --- .../auth-api/pkg/service/http/v0/service.go | 251 +-------- .../pkg/service/http/v0/service_test.go | 17 - services/groupware/DEVELOPER.md | 168 +++++- .../pkg/config/defaults/defaultconfig.go | 4 +- services/groupware/pkg/groupware/dns.go | 11 +- services/groupware/pkg/groupware/framework.go | 35 +- services/groupware/pkg/groupware/reva.go | 74 ++- services/groupware/pkg/groupware/session.go | 109 ++-- services/groupware/pkg/server/http/option.go | 2 - .../github.com/MicahParks/jwkset/.gitignore | 2 - vendor/github.com/MicahParks/jwkset/LICENSE | 201 ------- vendor/github.com/MicahParks/jwkset/README.md | 133 ----- .../github.com/MicahParks/jwkset/constants.go | 167 ------ vendor/github.com/MicahParks/jwkset/http.go | 276 ---------- vendor/github.com/MicahParks/jwkset/jwk.go | 494 ----------------- .../github.com/MicahParks/jwkset/marshal.go | 511 ------------------ .../github.com/MicahParks/jwkset/storage.go | 311 ----------- vendor/github.com/MicahParks/jwkset/x509.go | 125 ----- .../github.com/MicahParks/jwkset/x509_gen.sh | 15 - .../github.com/MicahParks/keyfunc/v3/LICENSE | 201 ------- .../MicahParks/keyfunc/v3/README.md | 81 --- .../MicahParks/keyfunc/v3/keyfunc.go | 177 ------ vendor/modules.txt | 6 - 52 files changed, 959 insertions(+), 3352 deletions(-) create mode 100644 devtools/deployments/opencloud_full/config/stalwart/idmoidc.toml create mode 100644 services/auth-api/pkg/auth-api/authapi.go create mode 100644 services/auth-api/pkg/auth-api/authapi_test.go create mode 100644 services/auth-api/pkg/config/auth.go create mode 100644 services/auth-api/pkg/config/reva.go delete mode 100644 services/auth-api/pkg/metrics/options.go delete mode 100644 services/auth-api/pkg/service/http/v0/instrument.go delete mode 100644 services/auth-api/pkg/service/http/v0/logging.go delete mode 100644 services/auth-api/pkg/service/http/v0/option.go delete mode 100644 services/auth-api/pkg/service/http/v0/service_test.go delete mode 100644 vendor/github.com/MicahParks/jwkset/.gitignore delete mode 100644 vendor/github.com/MicahParks/jwkset/LICENSE delete mode 100644 vendor/github.com/MicahParks/jwkset/README.md delete mode 100644 vendor/github.com/MicahParks/jwkset/constants.go delete mode 100644 vendor/github.com/MicahParks/jwkset/http.go delete mode 100644 vendor/github.com/MicahParks/jwkset/jwk.go delete mode 100644 vendor/github.com/MicahParks/jwkset/marshal.go delete mode 100644 vendor/github.com/MicahParks/jwkset/storage.go delete mode 100644 vendor/github.com/MicahParks/jwkset/x509.go delete mode 100644 vendor/github.com/MicahParks/jwkset/x509_gen.sh delete mode 100644 vendor/github.com/MicahParks/keyfunc/v3/LICENSE delete mode 100644 vendor/github.com/MicahParks/keyfunc/v3/README.md delete mode 100644 vendor/github.com/MicahParks/keyfunc/v3/keyfunc.go 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 @@ -[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](https://pkg.go.dev/github.com/MicahParks/jwkset) - -# JWK Set (JSON Web Key Set) - -This is a JWK Set (JSON Web Key Set) implementation written in Golang. - -The goal of this project is to provide a complete implementation of JWK and JWK Sets within the constraints of the -Golang standard library, without implementing any cryptographic algorithms. For example, `Ed25519` is supported, but -`Ed448` is not, because the Go standard library does not have a high level implementation of `Ed448`. - -If you would like to generate or validate a JWK without writing any Golang code, please visit -the [Generate a JWK Set](#generate-a-jwk-set) section. - -If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the -[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the -language of your choice using an OpenAPI interface. - -# Generate a JWK Set - -If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK -Set from: - -* PEM encoded X.509 Certificates -* PEM encoded public keys -* PEM encoded private keys - -The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section -for a list of supported cryptographic key types. - -## Website - -Visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this website -by following the instructions in the `README.md` in -the [website](https://github.com/MicahParks/jwkset/tree/master/website) directory. - -## Command line - -Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set. - -**Install** - -``` -go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest -``` - -**Usage** - -``` -jwksetinfer mykey.pem mycert.crt -``` - -## Custom server - -This project can be used in creating a custom JWK Set server. A good place to start is `examples/http_server/main.go`. - -# Golang JWK Set client - -If you are using [`github.com/golang-jwt/jwt/v5`](https://github.com/golang-jwt/jwt) take a look -at [`github.com/MicahParks/keyfunc/v3`](https://github.com/MicahParks/keyfunc). - -This project can be used to create JWK Set clients. An HTTP client is provided. See a snippet of the usage -from `examples/default_http_client/main.go` below. - -## Create a JWK Set client from the server's HTTP URL. - -```go -jwks, err := jwkset.NewDefaultHTTPClient([]string{server.URL}) -if err != nil { - log.Fatalf("Failed to create client JWK set. Error: %s", err) -} -``` - -## Read a key from the client. - -```go -jwk, err = jwks.KeyRead(ctx, myKeyID) -if err != nil { - log.Fatalf("Failed to read key from client JWK set. Error: %s", err) -} -``` - -# Supported keys - -This project supports the following key types: - -* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only) - * Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey` -* [Elliptic-curve 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 @@ -[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/keyfunc/v3.svg)](https://pkg.go.dev/github.com/MicahParks/keyfunc/v3) - -# keyfunc - -The purpose of this package is to provide a -[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc) for the -[github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) package using a JSON Web Key Set (JWK Set) for parsing -and verifying JSON Web Tokens (JWTs). - -It's common for an identity providers, particularly those -using [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) -or [OpenID Connect](https://openid.net/developers/how-connect-works/), such -as [Keycloak](https://github.com/MicahParks/keyfunc/blob/master/examples/keycloak/main.go) -or [Amazon Cognito (AWS)](https://github.com/MicahParks/keyfunc/blob/master/examples/aws_cognito/main.go) to expose a -JWK Set via an HTTPS endpoint. This package has the ability to consume that JWK Set and produce a -[`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc). It is important that a JWK Set endpoint is -using HTTPS to ensure the keys are from the correct trusted source. - -## Basic usage - -For complete examples, please see the `examples` directory. - -```go -import "github.com/MicahParks/keyfunc/v3" -``` - -### Step 1: Create the `keyfunc.Keyfunc` - -The below example is for a remote HTTP resource. -See [`examples/json/main.go`](https://github.com/MicahParks/keyfunc/blob/master/examples/json/main.go) for a JSON -example. - -```go -// Create the keyfunc.Keyfunc. -k, err := keyfunc.NewDefaultCtx(ctx, []string{server.URL}) // Context is used to end the refresh goroutine. -if err != nil { - log.Fatalf("Failed to create a keyfunc.Keyfunc from the server's URL.\nError: %s", err) -} -``` - -When using the `keyfunc.NewDefault` function, the JWK Set will be automatically refreshed using -[`jwkset.NewDefaultHTTPClient`](https://pkg.go.dev/github.com/MicahParks/jwkset#NewHTTPClient). This does launch a " -refresh goroutine". If you want the ability to end this goroutine, use the `keyfunc.NewDefaultCtx` function. - -It is also possible to create a `keyfunc.Keyfunc` from given keys like HMAC shared secrets. See `examples/hmac/main.go`. - -### Step 2: Use the `keyfunc.Keyfunc` to parse and verify JWTs - -```go -// Parse the JWT. -parsed, err := jwt.Parse(signed, k.Keyfunc) -if err != nil { - log.Fatalf("Failed to parse the JWT.\nError: %s", err) -} -``` - -## Additional features - -This project's primary purpose is to provide a [`jwt.Keyfunc`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc) -implementation for JWK Sets. - -Since version `3.X.X`, this project has become a thin wrapper -around [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset). Newer versions contain a superset of -features available in versions `2.X.X` and earlier, but some of the deep customization has been moved to the `jwkset` -project. The intention behind this is to make `keyfunc` easier to use for most use cases. - -Access the [`jwkset.Storage`](https://pkg.go.dev/github.com/MicahParks/jwkset#Storage) from a `keyfunc.Keyfunc` via -the `.Storage()` method. Using the [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) package -provides the below features, and more: - -* An HTTP client that automatically updates one or more remote JWK Set resources. -* An automatic refresh of remote HTTP resources when an unknown key ID (`kid`) is encountered. -* X.509 URIs or embedded [certificate chains](https://pkg.go.dev/crypto/x509#Certificate), when a JWK contains them. -* Support for private asymmetric keys. -* Specified key operations and usage. - -## Related projects - -### [`github.com/MicahParks/jwkset`](https://github.com/MicahParks/jwkset): - -A JWK Set implementation. The `keyfunc` project is a wrapper around this project. 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