From d56685d52bed6031d27b2657268f3f5b9175145b Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Sat, 1 Jul 2023 23:09:10 +0000 Subject: [PATCH 01/17] Refactored metadata parsing Includes validation Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/metadata.go | 134 ++++++++++++++++++ middleware/http/oauth2/oauth2_middleware.go | 33 ++--- .../http/oauth2/oauth2_middleware_test.go | 2 +- 3 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 middleware/http/oauth2/metadata.go diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go new file mode 100644 index 0000000000..37022fba58 --- /dev/null +++ b/middleware/http/oauth2/metadata.go @@ -0,0 +1,134 @@ +/* +Copyright 2023 The Dapr Authors +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. +*/ + +package oauth2 + +import ( + "crypto/hmac" + "crypto/sha256" + "errors" + "strings" + + mdutils "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/middleware" +) + +const ( + defaultAuthHeaderName = "Authorization" + defaultTokenStorage = "memory" + defaultCookieName = "_dapr_oauth2" + + // Key used to derive cookie encryption keys using HMAC. + hmacKey = "dapr_oauth2" +) + +// Metadata is the oAuth middleware config. +type oAuth2MiddlewareMetadata struct { + // Client ID of the OAuth2 application. + // Required. + ClientID string `json:"clientID" mapstructure:"clientID"` + // Client secret of the OAuth2 application. + // Required. + ClientSecret string `json:"clientSecret" mapstructure:"clientSecret"` + // Scopes to request, as a comma-separated string + Scopes string `json:"scopes" mapstructure:"scopes"` + // URL of the OAuth2 authorization server. + // Required. + AuthURL string `json:"authURL" mapstructure:"authURL"` + // URL of the OAuth2 token endpoint, used to exchange an authorization code for an access token. + // Required. + TokenURL string `json:"tokenURL" mapstructure:"tokenURL"` + // Name of the header forwarded to the application, containing the token. + // Default: "Authorization". + AuthHeaderName string `json:"authHeaderName" mapstructure:"authHeaderName"` + // The URL of your application that the authorization server should redirect to once the user has authenticated. + // Required. + RedirectURL string `json:"redirectURL" mapstructure:"redirectURL"` + // Forces the use of TLS/HTTPS for the redirect URL. + // Defaults to false. + ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` + // Configure token storage. + // Possible values: "memory" (default) and "cookie" + TokenStorage string `json:"tokenStorage" mapstructure:"tokenStorage"` + // Name of the cookie where Dapr will store the encrypted access token, when storing tokens in cookies. + // Defaults to "_dapr_oauth2". + CookieName string `json:"cookieName" mapstructure:"cookieName"` + // Cookie encryption key. + // Required if storing access tokens in cookies. + CookieEncryptionKey string `json:"cookieEncryptionKey" mapstructure:"cookieEncryptionKey"` +} + +// Parse the component's metadata into the object. +func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata) error { + // Set default values + if md.AuthHeaderName == "" { + md.AuthHeaderName = defaultAuthHeaderName + } + if md.CookieName == "" { + md.CookieName = defaultCookieName + } + + // Decode the properties + err := mdutils.DecodeMetadata(metadata.Properties, md) + if err != nil { + return err + } + + // Check required fields + if md.ClientID == "" { + return errors.New("required field 'clientID' is empty") + } + if md.ClientSecret == "" { + return errors.New("required field 'clientSecret' is empty") + } + if md.AuthURL == "" { + return errors.New("required field 'authURL' is empty") + } + if md.TokenURL == "" { + return errors.New("required field 'tokenURL' is empty") + } + if md.RedirectURL == "" { + return errors.New("required field 'redirectURL' is empty") + } + + switch strings.ToLower(md.TokenStorage) { + case "cookie": + // Re-set to ensure it's lowercase + md.TokenStorage = "cookie" + if md.CookieEncryptionKey == "" { + return errors.New("field 'cookieEncryptionKey' is required when storing tokens in cookies") + } + case "memory", "": // Default value + // Re-set to ensure it's lowercase + md.TokenStorage = "memory" + default: + return errors.New("invalid value for property 'tokenStorage'; supported values: 'memory', 'cookie'") + } + + return nil +} + +// GetCookieEncryptionKey derives a 128-bit cookie encryption key from the user-defined value. +func (md *oAuth2MiddlewareMetadata) GetCookieEncryptionKey() []byte { + if md.CookieEncryptionKey == "" { + // This should never happen as the validation method ensures that cookieEncryptionKey isn't empty + return nil + } + + h := hmac.New(sha256.New, []byte(hmacKey)) + h.Write([]byte(md.CookieEncryptionKey)) + res := h.Sum(nil) + + // Return the first 16 bytes only + return res[:16] +} diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 7a7cb78c72..e982abf7d0 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +Copyright 2023 The Dapr Authors 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 @@ -25,29 +25,16 @@ import ( "golang.org/x/oauth2" "github.com/dapr/components-contrib/internal/httputils" - "github.com/dapr/components-contrib/internal/utils" mdutils "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/middleware" "github.com/dapr/kit/logger" ) -// Metadata is the oAuth middleware config. -type oAuth2MiddlewareMetadata struct { - ClientID string `json:"clientID" mapstructure:"clientID"` - ClientSecret string `json:"clientSecret" mapstructure:"clientSecret"` - Scopes string `json:"scopes" mapstructure:"scopes"` - AuthURL string `json:"authURL" mapstructure:"authURL"` - TokenURL string `json:"tokenURL" mapstructure:"tokenURL"` - AuthHeaderName string `json:"authHeaderName" mapstructure:"authHeaderName"` - RedirectURL string `json:"redirectURL" mapstructure:"redirectURL"` - ForceHTTPS string `json:"forceHTTPS" mapstructure:"forceHTTPS"` -} - // NewOAuth2Middleware returns a new oAuth2 middleware. func NewOAuth2Middleware(log logger.Logger) middleware.Middleware { - m := &Middleware{logger: log} - - return m + return &Middleware{ + logger: log, + } } // Middleware is an oAuth2 authentication middleware. @@ -56,20 +43,18 @@ type Middleware struct { } const ( - stateParam = "state" savedState = "auth-state" redirectPath = "redirect-url" - codeParam = "code" ) // GetHandler retruns the HTTP handler provided by the middleware. func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - meta, err := m.getNativeMetadata(metadata) + meta := oAuth2MiddlewareMetadata{} + err := meta.fromMetadata(metadata) if err != nil { return nil, err } - forceHTTPS := utils.IsTruthy(meta.ForceHTTPS) conf := &oauth2.Config{ ClientID: meta.ClientID, ClientSecret: meta.ClientSecret, @@ -92,7 +77,7 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat } // Redirect to the auth server - state := r.URL.Query().Get(stateParam) + state := r.URL.Query().Get("state") if state == "" { id, err := uuid.NewRandom() if err != nil { @@ -116,7 +101,7 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat return } - if forceHTTPS { + if meta.ForceHTTPS { redirectURL.Scheme = "https" } @@ -125,7 +110,7 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat return } - code := r.URL.Query().Get(codeParam) + code := r.URL.Query().Get("code") if code == "" { httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "code not found") return diff --git a/middleware/http/oauth2/oauth2_middleware_test.go b/middleware/http/oauth2/oauth2_middleware_test.go index 69fe8d2fd9..e47ee35f2f 100644 --- a/middleware/http/oauth2/oauth2_middleware_test.go +++ b/middleware/http/oauth2/oauth2_middleware_test.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +Copyright 2023 The Dapr Authors 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 From e7ec18441da0e259476f61e21079575c96219dbd Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Sat, 1 Jul 2023 23:49:03 +0000 Subject: [PATCH 02/17] WIP - some more refactorings Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/metadata.go | 51 ++++--- middleware/http/oauth2/oauth2_middleware.go | 139 ++++++++++---------- 2 files changed, 102 insertions(+), 88 deletions(-) diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go index 37022fba58..f394149642 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -15,12 +15,14 @@ package oauth2 import ( "crypto/hmac" + "crypto/rand" "crypto/sha256" "errors" - "strings" + "io" mdutils "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/middleware" + "github.com/dapr/kit/logger" ) const ( @@ -57,19 +59,18 @@ type oAuth2MiddlewareMetadata struct { // Forces the use of TLS/HTTPS for the redirect URL. // Defaults to false. ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` - // Configure token storage. - // Possible values: "memory" (default) and "cookie" - TokenStorage string `json:"tokenStorage" mapstructure:"tokenStorage"` // Name of the cookie where Dapr will store the encrypted access token, when storing tokens in cookies. // Defaults to "_dapr_oauth2". CookieName string `json:"cookieName" mapstructure:"cookieName"` // Cookie encryption key. - // Required if storing access tokens in cookies. + // Required to allow sessions to persist across restarts of the Dapr runtime and to allow multiple instances of Dapr to access the session. + // Not setting an explicit encryption key is deprecated, and this field will become required in Dapr 1.13. + // TODO @ItalyPaleAle: make required in Dapr 1.13. CookieEncryptionKey string `json:"cookieEncryptionKey" mapstructure:"cookieEncryptionKey"` } // Parse the component's metadata into the object. -func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata) error { +func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata, log logger.Logger) error { // Set default values if md.AuthHeaderName == "" { md.AuthHeaderName = defaultAuthHeaderName @@ -101,18 +102,10 @@ func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata) e return errors.New("required field 'redirectURL' is empty") } - switch strings.ToLower(md.TokenStorage) { - case "cookie": - // Re-set to ensure it's lowercase - md.TokenStorage = "cookie" - if md.CookieEncryptionKey == "" { - return errors.New("field 'cookieEncryptionKey' is required when storing tokens in cookies") - } - case "memory", "": // Default value - // Re-set to ensure it's lowercase - md.TokenStorage = "memory" - default: - return errors.New("invalid value for property 'tokenStorage'; supported values: 'memory', 'cookie'") + // If there's no cookie encryption key, show a warning + // TODO @ItalyPaleAle: make required in Dapr 1.13. + if md.CookieEncryptionKey == "" { + log.Warnf("[DEPRECATION NOTICE] Initializing the OAuth2 middleware with an empty 'cookieEncryptionKey' is deprecated, and the field will become required in Dapr 1.13. Setting an explicit 'cookieEncryptionKey' is required to allow sessions to be shared across multiple instances of Dapr and to survive a restart of Dapr.") } return nil @@ -121,8 +114,26 @@ func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata) e // GetCookieEncryptionKey derives a 128-bit cookie encryption key from the user-defined value. func (md *oAuth2MiddlewareMetadata) GetCookieEncryptionKey() []byte { if md.CookieEncryptionKey == "" { - // This should never happen as the validation method ensures that cookieEncryptionKey isn't empty - return nil + // TODO @ItalyPaleAle: uncomment for Dapr 1.13 and remove existing code in this block + /* + // This should never happen as the validation method ensures that cookieEncryptionKey isn't empty + // So if we're here, it means there was a development-time error. + panic("cookie encryption key is empty") + */ + + // If the user didn't provide a cookie encryption key, generate a random one + // Naturally, this means that the cookie encryption key is unique to this process and cookies cannot be decrypted by other instances of Dapr or if the process is restarted + // This is not good, but it is no different than how this component behaved in Dapr 1.11. + // This behavior is deprecated and will be removed in Dapr 1.13. + cek := make([]byte, 16) + _, err := io.ReadFull(rand.Reader, cek) + if err != nil { + // This makes Dapr panic, but it's ok here because: + // 1. This code is temporary and will be removed in Dapr 1.13. I would rather not change the interface of this method to return an error since it won't be needed in the future + // 2. Errors from io.ReadFull above are possible but highly unlikely (only if the kernel doesn't have enough entropy) + panic("Failed to generate a random cookie encryption key: " + err.Error()) + } + return cek } h := hmac.New(sha256.New, []byte(hmacKey)) diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index e982abf7d0..1d429553e6 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -15,6 +15,7 @@ package oauth2 import ( "context" + "fmt" "net/http" "net/url" "reflect" @@ -40,6 +41,7 @@ func NewOAuth2Middleware(log logger.Logger) middleware.Middleware { // Middleware is an oAuth2 authentication middleware. type Middleware struct { logger logger.Logger + meta oAuth2MiddlewareMetadata } const ( @@ -49,86 +51,87 @@ const ( // GetHandler retruns the HTTP handler provided by the middleware. func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - meta := oAuth2MiddlewareMetadata{} - err := meta.fromMetadata(metadata) + err := m.meta.fromMetadata(metadata, m.logger) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid metadata: %w", err) } - conf := &oauth2.Config{ - ClientID: meta.ClientID, - ClientSecret: meta.ClientSecret, - Scopes: strings.Split(meta.Scopes, ","), - RedirectURL: meta.RedirectURL, + return m.getHandler, nil +} + +func (m *Middleware) getHandler(next http.Handler) http.Handler { + conf := oauth2.Config{ + ClientID: m.meta.ClientID, + ClientSecret: m.meta.ClientSecret, + Scopes: strings.Split(m.meta.Scopes, ","), + RedirectURL: m.meta.RedirectURL, Endpoint: oauth2.Endpoint{ - AuthURL: meta.AuthURL, - TokenURL: meta.TokenURL, + AuthURL: m.meta.AuthURL, + TokenURL: m.meta.TokenURL, }, } - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := sessions.Start(w, r) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := sessions.Start(w, r) + + if session.GetString(m.meta.AuthHeaderName) != "" { + r.Header.Add(m.meta.AuthHeaderName, session.GetString(m.meta.AuthHeaderName)) + next.ServeHTTP(w, r) + return + } + + // Redirect to the auth server + state := r.URL.Query().Get("state") + if state == "" { + id, err := uuid.NewRandom() + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Failed to generate UUID: %v", err) + return + } + idStr := id.String() + + session.Set(savedState, idStr) + session.Set(redirectPath, r.URL) + + url := conf.AuthCodeURL(idStr, oauth2.AccessTypeOffline) + httputils.RespondWithRedirect(w, http.StatusFound, url) + } else { + authState := session.GetString(savedState) + redirectURL, ok := session.Get(redirectPath).(*url.URL) + if !ok { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Value saved in state key '%s' is not a *url.URL", redirectPath) + return + } + + if m.meta.ForceHTTPS { + redirectURL.Scheme = "https" + } - if session.GetString(meta.AuthHeaderName) != "" { - r.Header.Add(meta.AuthHeaderName, session.GetString(meta.AuthHeaderName)) - next.ServeHTTP(w, r) + if state != authState { + httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "invalid state") return } - // Redirect to the auth server - state := r.URL.Query().Get("state") - if state == "" { - id, err := uuid.NewRandom() - if err != nil { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to generate UUID: %v", err) - return - } - idStr := id.String() - - session.Set(savedState, idStr) - session.Set(redirectPath, r.URL) - - url := conf.AuthCodeURL(idStr, oauth2.AccessTypeOffline) - httputils.RespondWithRedirect(w, http.StatusFound, url) - } else { - authState := session.GetString(savedState) - redirectURL, ok := session.Get(redirectPath).(*url.URL) - if !ok { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Value saved in state key '%s' is not a *url.URL", redirectPath) - return - } - - if meta.ForceHTTPS { - redirectURL.Scheme = "https" - } - - if state != authState { - httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "invalid state") - return - } - - code := r.URL.Query().Get("code") - if code == "" { - httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "code not found") - return - } - - token, err := conf.Exchange(r.Context(), code) - if err != nil { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Error("Failed to exchange token") - return - } - - authHeader := token.Type() + " " + token.AccessToken - session.Set(meta.AuthHeaderName, authHeader) - httputils.RespondWithRedirect(w, http.StatusFound, redirectURL.String()) + code := r.URL.Query().Get("code") + if code == "" { + httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "code not found") + return } - }) - }, nil + + token, err := conf.Exchange(r.Context(), code) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Error("Failed to exchange token") + return + } + + authHeader := token.Type() + " " + token.AccessToken + session.Set(m.meta.AuthHeaderName, authHeader) + httputils.RespondWithRedirect(w, http.StatusFound, redirectURL.String()) + } + }) } func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*oAuth2MiddlewareMetadata, error) { From 3cc968bc5afe55df6e52514f959e79db1bab6e52 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Sun, 2 Jul 2023 01:47:41 +0000 Subject: [PATCH 03/17] Working on it Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/metadata.go | 90 +++++-- middleware/http/oauth2/oauth2_middleware.go | 281 +++++++++++++++----- 2 files changed, 281 insertions(+), 90 deletions(-) diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go index f394149642..efc66f65c1 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -14,11 +14,18 @@ limitations under the License. package oauth2 import ( + "crypto" "crypto/hmac" "crypto/rand" "crypto/sha256" + "encoding/base64" "errors" + "fmt" "io" + "strings" + + "github.com/lestrrat-go/jwx/v2/jwk" + "golang.org/x/oauth2" mdutils "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/middleware" @@ -62,11 +69,19 @@ type oAuth2MiddlewareMetadata struct { // Name of the cookie where Dapr will store the encrypted access token, when storing tokens in cookies. // Defaults to "_dapr_oauth2". CookieName string `json:"cookieName" mapstructure:"cookieName"` - // Cookie encryption key. + // Cookie encryption and signing key (technically, seed used to derive those two). + // It is recommended to provide a random string with sufficient entropy. // Required to allow sessions to persist across restarts of the Dapr runtime and to allow multiple instances of Dapr to access the session. // Not setting an explicit encryption key is deprecated, and this field will become required in Dapr 1.13. // TODO @ItalyPaleAle: make required in Dapr 1.13. - CookieEncryptionKey string `json:"cookieEncryptionKey" mapstructure:"cookieEncryptionKey"` + CookieKey string `json:"cookieKey" mapstructure:"cookieKey"` + + // Internal: cookie encryption key + cek jwk.Key + // Internal: cookie signing key + csk jwk.Key + // Internal: OAuth2 configuration object + oauth2Conf oauth2.Config } // Parse the component's metadata into the object. @@ -104,42 +119,73 @@ func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata, l // If there's no cookie encryption key, show a warning // TODO @ItalyPaleAle: make required in Dapr 1.13. - if md.CookieEncryptionKey == "" { - log.Warnf("[DEPRECATION NOTICE] Initializing the OAuth2 middleware with an empty 'cookieEncryptionKey' is deprecated, and the field will become required in Dapr 1.13. Setting an explicit 'cookieEncryptionKey' is required to allow sessions to be shared across multiple instances of Dapr and to survive a restart of Dapr.") + if md.CookieKey == "" { + log.Warnf("[DEPRECATION NOTICE] Initializing the OAuth2 middleware with an empty 'cookieKey' is deprecated, and the field will become required in Dapr 1.13. Setting an explicit 'cookieKey' is required to allow sessions to be shared across multiple instances of Dapr and to survive a restart of Dapr.") } return nil } -// GetCookieEncryptionKey derives a 128-bit cookie encryption key from the user-defined value. -func (md *oAuth2MiddlewareMetadata) GetCookieEncryptionKey() []byte { - if md.CookieEncryptionKey == "" { +// Derives a 128-bit cookie encryption key and a 256-bit cookie signing key from the user-provided value. +func (md *oAuth2MiddlewareMetadata) setCookieKeys() (err error) { + var b []byte + + if md.CookieKey == "" { // TODO @ItalyPaleAle: uncomment for Dapr 1.13 and remove existing code in this block /* - // This should never happen as the validation method ensures that cookieEncryptionKey isn't empty + // This should never happen as the validation method ensures that cookieKey isn't empty // So if we're here, it means there was a development-time error. panic("cookie encryption key is empty") */ - // If the user didn't provide a cookie encryption key, generate a random one - // Naturally, this means that the cookie encryption key is unique to this process and cookies cannot be decrypted by other instances of Dapr or if the process is restarted + // If the user didn't provide a cookie key, generate a random one + // Naturally, this means that the cookie key is unique to this process and cookies cannot be decrypted by other instances of Dapr or if the process is restarted // This is not good, but it is no different than how this component behaved in Dapr 1.11. // This behavior is deprecated and will be removed in Dapr 1.13. - cek := make([]byte, 16) - _, err := io.ReadFull(rand.Reader, cek) + b = make([]byte, 48) + _, err := io.ReadFull(rand.Reader, b) if err != nil { - // This makes Dapr panic, but it's ok here because: - // 1. This code is temporary and will be removed in Dapr 1.13. I would rather not change the interface of this method to return an error since it won't be needed in the future - // 2. Errors from io.ReadFull above are possible but highly unlikely (only if the kernel doesn't have enough entropy) - panic("Failed to generate a random cookie encryption key: " + err.Error()) + return fmt.Errorf("failed to generate a random cookie key: %w", err) } - return cek + } else { + // Derive 48 bytes from the cookie key using HMAC with a fixed "HMAC key" + h := hmac.New(crypto.SHA384.New, []byte(hmacKey)) + h.Write([]byte(md.CookieKey)) + b = h.Sum(nil) } - h := hmac.New(sha256.New, []byte(hmacKey)) - h.Write([]byte(md.CookieEncryptionKey)) - res := h.Sum(nil) + // We must set a kid for the jwx library to work + kidH := sha256.New224() + kidH.Write(b) + kid := base64.RawURLEncoding.EncodeToString(kidH.Sum(nil)) - // Return the first 16 bytes only - return res[:16] + // Cookie encryption key uses 128 bits + md.cek, err = jwk.FromRaw(b[:16]) + if err != nil { + return fmt.Errorf("failed to import cookie encryption key: %w", err) + } + md.cek.Set("kid", kid) + + // Cookie signing key uses 256 bits + md.csk, err = jwk.FromRaw(b[16:]) + if err != nil { + return fmt.Errorf("failed to import cookie signing key: %w", err) + } + md.csk.Set("kid", kid) + + return nil +} + +// Sets the oauth2Conf property in the object. +func (md *oAuth2MiddlewareMetadata) setOAuth2Conf() { + md.oauth2Conf = oauth2.Config{ + ClientID: md.ClientID, + ClientSecret: md.ClientSecret, + Scopes: strings.Split(md.Scopes, ","), + RedirectURL: md.RedirectURL, + Endpoint: oauth2.Endpoint{ + AuthURL: md.AuthURL, + TokenURL: md.TokenURL, + }, + } } diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 1d429553e6..af8879f1ff 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -15,14 +15,18 @@ package oauth2 import ( "context" + "errors" "fmt" "net/http" "net/url" "reflect" - "strings" + "time" - "github.com/fasthttp-contrib/sessions" "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwe" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/spf13/cast" "golang.org/x/oauth2" "github.com/dapr/components-contrib/internal/httputils" @@ -31,6 +35,19 @@ import ( "github.com/dapr/kit/logger" ) +const ( + // Timeout for authenticating with the IdP + authenticationTimeout = 10 * time.Minute + // Allowed clock skew for validating JWTs + allowedClockSkew = 5 * time.Minute + // Issuer for JWTs + jwtIssuer = "oauth2.dapr.io" + + claimToken = "token" + claimRedirect = "redirect" + claimState = "state" +) + // NewOAuth2Middleware returns a new oAuth2 middleware. func NewOAuth2Middleware(log logger.Logger) middleware.Middleware { return &Middleware{ @@ -56,81 +73,209 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat return nil, fmt.Errorf("invalid metadata: %w", err) } - return m.getHandler, nil -} - -func (m *Middleware) getHandler(next http.Handler) http.Handler { - conf := oauth2.Config{ - ClientID: m.meta.ClientID, - ClientSecret: m.meta.ClientSecret, - Scopes: strings.Split(m.meta.Scopes, ","), - RedirectURL: m.meta.RedirectURL, - Endpoint: oauth2.Endpoint{ - AuthURL: m.meta.AuthURL, - TokenURL: m.meta.TokenURL, - }, + // Derive the cookie keys + err = m.meta.setCookieKeys() + if err != nil { + return nil, fmt.Errorf("failed to derive cookie keys: %w", err) } + // Create the OAuth2 configuration object + m.meta.setOAuth2Conf() + + return m.handler, nil +} + +func (m *Middleware) handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := sessions.Start(w, r) + // To check if the request is coming in using HTTPS, we check if the scheme of the URL is "https" + // Checking for `r.TLS` alone may not work if Dapr is behind a proxy that does TLS termination + secureCookie := r.URL.Scheme == "https" - if session.GetString(m.meta.AuthHeaderName) != "" { - r.Header.Add(m.meta.AuthHeaderName, session.GetString(m.meta.AuthHeaderName)) - next.ServeHTTP(w, r) + // Get the token from the cookie + claims, err := m.getClaimsFromCookie(r) + if err != nil { + // If the cookie is invalid, redirect to the auth endpoint again + // This will overwrite the old cookie + m.logger.Debugf("Invalid session cookie: %v", err) + m.redirectToAuthenticationEndpoint(w, r.URL, secureCookie) return } - // Redirect to the auth server + // If we already have a token, forward the request to the app + if claims[claimToken] != "" { + r.Header.Add(m.meta.AuthHeaderName, claims[claimToken]) + next.ServeHTTP(w, r) + } + + // If we have the "state" and "code" parameter, we need to exchange the authorization code for the access token + code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") - if state == "" { - id, err := uuid.NewRandom() - if err != nil { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to generate UUID: %v", err) - return - } - idStr := id.String() - - session.Set(savedState, idStr) - session.Set(redirectPath, r.URL) - - url := conf.AuthCodeURL(idStr, oauth2.AccessTypeOffline) - httputils.RespondWithRedirect(w, http.StatusFound, url) - } else { - authState := session.GetString(savedState) - redirectURL, ok := session.Get(redirectPath).(*url.URL) - if !ok { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Value saved in state key '%s' is not a *url.URL", redirectPath) - return - } - - if m.meta.ForceHTTPS { - redirectURL.Scheme = "https" - } - - if state != authState { - httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "invalid state") - return - } - - code := r.URL.Query().Get("code") - if code == "" { - httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "code not found") - return - } - - token, err := conf.Exchange(r.Context(), code) - if err != nil { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Error("Failed to exchange token") - return - } - - authHeader := token.Type() + " " + token.AccessToken - session.Set(m.meta.AuthHeaderName, authHeader) - httputils.RespondWithRedirect(w, http.StatusFound, redirectURL.String()) + if code != "" && state != "" { + m.exchangeAccessCode(r.Context(), w, claims, code, state, secureCookie) + return } + + // Redirect to the auhentication endpoint + m.redirectToAuthenticationEndpoint(w, r.URL, secureCookie) + }) +} + +func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, redirectURL *url.URL, secureCookie bool) { + // Generate a new state token + stateObj, err := uuid.NewRandom() + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Failed to generate UUID: %v", err) + return + } + state := stateObj.String() + + if m.meta.ForceHTTPS { + redirectURL.Scheme = "https" + } + + // Set the cookie with the state and redirect URL + err = m.setSecureCookie(w, map[string]string{ + claimState: state, + claimRedirect: redirectURL.String(), + }, authenticationTimeout, secureCookie) + + // Redirect to the auth endpoint + url := m.meta.oauth2Conf.AuthCodeURL(state, oauth2.AccessTypeOffline) + httputils.RespondWithRedirect(w, http.StatusFound, url) +} + +func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, secureCookie bool) { + if claims[claimRedirect] == "" { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Error("Missing claim 'redirect'") + return + } + + if claims[claimState] == "" || state != claims[claimState] { + httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "invalid state") + return + } + + // Exchange the authorization code for a token + token, err := m.meta.oauth2Conf.Exchange(ctx, code) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Error("Failed to exchange token") + return + } + + // If we don't have an expiration, assume it's 1 hour + exp := time.Until(token.Expiry) + if exp <= time.Second { + exp = time.Hour + } + + // Set the cookie + saveClaims := map[string]string{ + claimToken: token.Type() + " " + token.AccessToken, + } + err = m.setSecureCookie(w, saveClaims, exp, secureCookie) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Failed to set secure cookie: %w", err) + return + } + + // Redirect to the URL set in the request + httputils.RespondWithRedirect(w, http.StatusFound, claims[claimRedirect]) +} + +func (m *Middleware) getClaimsFromCookie(r *http.Request) (map[string]string, error) { + // Get the cookie, which should contain a JWE + cookie, err := r.Cookie(m.meta.CookieName) + if errors.Is(err, http.ErrNoCookie) || cookie.Valid() != nil || cookie.Value == "" { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("failed to retrieve cookie: %w", err) + } + + // Decrypt the encrypted (JWE) cookie + dec, err := jwe.Decrypt( + []byte(cookie.Value), + jwe.WithKey(jwa.A128KW, m.meta.cek), + ) + if err != nil { + return nil, fmt.Errorf("failed to decrypt cookie: %w", err) + } + + // Validate the JWT from the decrypted cookie + token, err := jwt.Parse(dec, + jwt.WithKey(jwa.HS256, m.meta.csk), + jwt.WithIssuer(jwtIssuer), + jwt.WithAudience(m.meta.ClientID), // Use the client ID as audience + jwt.WithAcceptableSkew(allowedClockSkew), + ) + if err != nil { + return nil, fmt.Errorf("failed to validate JWT token: %w", err) + } + + return cast.ToStringMapString(token.PrivateClaims()), nil +} + +func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, isSecure bool) error { + now := time.Now() + exp := now.Add(ttl) + + // Build the JWT + builder := jwt.NewBuilder(). + IssuedAt(now). + Issuer(jwtIssuer). + Audience([]string{m.meta.ClientID}). + Expiration(exp). + NotBefore(now) + for k, v := range claims { + builder.Claim(k, v) + } + token, err := builder.Build() + if err != nil { + return fmt.Errorf("error building JWT: %w", err) + } + + // Generate the encrypted JWT + val, err := jwt.NewSerializer(). + Sign( + jwt.WithKey(jwa.HS256, m.meta.csk), + ). + Encrypt( + jwt.WithKey(jwa.A128KW, m.meta.cek), + jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), + ). + Serialize(token) + if err != nil { + return fmt.Errorf("failed to serialize token: %w", err) + } + + // Set the cookie + http.SetCookie(w, &http.Cookie{ + Name: m.meta.CookieName, + Value: string(val), + Expires: exp, + HttpOnly: true, + Secure: isSecure, + Path: "/", + SameSite: http.SameSiteLaxMode, + }) + + return nil +} + +func (m *Middleware) unsetCookie(w http.ResponseWriter, isSecure bool) { + // To delete the cookie, create a new cookie with the same name, but no value and that has already expired + http.SetCookie(w, &http.Cookie{ + Name: m.meta.CookieName, + Value: "", + Expires: time.Now().Add(-24 * time.Hour), + MaxAge: -1, + HttpOnly: true, + Secure: isSecure, + Path: "/", + SameSite: http.SameSiteLaxMode, }) } From 3e9f6583ec011a39ccca6b677da252f3fa46660a Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Sun, 2 Jul 2023 02:59:46 +0000 Subject: [PATCH 04/17] WIP Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/oauth2_middleware.go | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index af8879f1ff..747c728048 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -105,13 +105,14 @@ func (m *Middleware) handler(next http.Handler) http.Handler { if claims[claimToken] != "" { r.Header.Add(m.meta.AuthHeaderName, claims[claimToken]) next.ServeHTTP(w, r) + return } // If we have the "state" and "code" parameter, we need to exchange the authorization code for the access token code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") if code != "" && state != "" { - m.exchangeAccessCode(r.Context(), w, claims, code, state, secureCookie) + m.exchangeAccessCode(r.Context(), w, claims, code, state, r.URL.Host, secureCookie) return } @@ -138,14 +139,19 @@ func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, red err = m.setSecureCookie(w, map[string]string{ claimState: state, claimRedirect: redirectURL.String(), - }, authenticationTimeout, secureCookie) + }, authenticationTimeout, redirectURL.Host, secureCookie) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Failed to set secure cookie: %w", err) + return + } // Redirect to the auth endpoint url := m.meta.oauth2Conf.AuthCodeURL(state, oauth2.AccessTypeOffline) httputils.RespondWithRedirect(w, http.StatusFound, url) } -func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, secureCookie bool) { +func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, domain string, secureCookie bool) { if claims[claimRedirect] == "" { httputils.RespondWithError(w, http.StatusInternalServerError) m.logger.Error("Missing claim 'redirect'") @@ -172,10 +178,9 @@ func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWrit } // Set the cookie - saveClaims := map[string]string{ + err = m.setSecureCookie(w, map[string]string{ claimToken: token.Type() + " " + token.AccessToken, - } - err = m.setSecureCookie(w, saveClaims, exp, secureCookie) + }, exp, domain, secureCookie) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) m.logger.Errorf("Failed to set secure cookie: %w", err) @@ -218,7 +223,7 @@ func (m *Middleware) getClaimsFromCookie(r *http.Request) (map[string]string, er return cast.ToStringMapString(token.PrivateClaims()), nil } -func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, isSecure bool) error { +func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, isSecure bool) error { now := time.Now() exp := now.Add(ttl) @@ -255,9 +260,10 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st http.SetCookie(w, &http.Cookie{ Name: m.meta.CookieName, Value: string(val), - Expires: exp, + MaxAge: int(ttl.Seconds()), HttpOnly: true, Secure: isSecure, + Domain: domain, Path: "/", SameSite: http.SameSiteLaxMode, }) From d190ba6875b5503f01b9700f7a90c8501027db07 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 00:00:39 +0000 Subject: [PATCH 05/17] Enable compression in JWTs Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- go.mod | 3 ++ go.sum | 5 ++- middleware/http/oauth2/oauth2_middleware.go | 37 ++++++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ae0d907fa8..e64a9a9180 100644 --- a/go.mod +++ b/go.mod @@ -413,3 +413,6 @@ replace github.com/Shopify/sarama => github.com/Shopify/sarama v1.37.2 // this is a fork which addresses a performance issues due to go routines. replace dubbo.apache.org/dubbo-go/v3 => dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3 + +// TODO: Remove once a new release of jwx that fixes https://github.com/lestrrat-go/jwx/pull/952 is merged +replace github.com/lestrrat-go/jwx/v2 => github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93 diff --git a/go.sum b/go.sum index 404e04880a..78a28c0875 100644 --- a/go.sum +++ b/go.sum @@ -1315,6 +1315,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93 h1:pMCEn1Bkv3o53Qw0wMPZtRzUKS3VA5Ydcl8EthnAVeA= +github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93/go.mod h1:SmVQUhXxinEHWAFVPIMbDXG7JsBWTn03HwVivS5bp/g= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -1449,8 +1451,6 @@ github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbq github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx v1.2.24/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= -github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ= -github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -2126,7 +2126,6 @@ golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 747c728048..099ec608fd 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -43,9 +43,10 @@ const ( // Issuer for JWTs jwtIssuer = "oauth2.dapr.io" - claimToken = "token" - claimRedirect = "redirect" - claimState = "state" + claimToken = "tkn" + claimTokenType = "tkt" + claimRedirect = "redirect" + claimState = "state" ) // NewOAuth2Middleware returns a new oAuth2 middleware. @@ -103,7 +104,11 @@ func (m *Middleware) handler(next http.Handler) http.Handler { // If we already have a token, forward the request to the app if claims[claimToken] != "" { - r.Header.Add(m.meta.AuthHeaderName, claims[claimToken]) + if claims[claimTokenType] != "" { + r.Header.Add(m.meta.AuthHeaderName, claims[claimTokenType]+" "+claims[claimToken]) + } else { + r.Header.Add(m.meta.AuthHeaderName, claims[claimToken]) + } next.ServeHTTP(w, r) return } @@ -179,7 +184,8 @@ func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWrit // Set the cookie err = m.setSecureCookie(w, map[string]string{ - claimToken: token.Type() + " " + token.AccessToken, + claimTokenType: token.Type(), + claimToken: token.AccessToken, }, exp, domain, secureCookie) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) @@ -234,8 +240,10 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st Audience([]string{m.meta.ClientID}). Expiration(exp). NotBefore(now) + var claimsSize int for k, v := range claims { builder.Claim(k, v) + claimsSize += len(v) } token, err := builder.Build() if err != nil { @@ -243,14 +251,25 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st } // Generate the encrypted JWT + var encryptOpts []jwt.EncryptOption + if claimsSize > 800 { + // If the total size of the claims is more than 800 bytes, we should enable compression + encryptOpts = []jwt.EncryptOption{ + jwt.WithKey(jwa.A128KW, m.meta.cek), + jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), + jwt.WithEncryptOption(jwe.WithCompress(jwa.Deflate)), + } + } else { + encryptOpts = []jwt.EncryptOption{ + jwt.WithKey(jwa.A128KW, m.meta.cek), + jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), + } + } val, err := jwt.NewSerializer(). Sign( jwt.WithKey(jwa.HS256, m.meta.csk), ). - Encrypt( - jwt.WithKey(jwa.A128KW, m.meta.cek), - jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), - ). + Encrypt(encryptOpts...). Serialize(token) if err != nil { return fmt.Errorf("failed to serialize token: %w", err) From 37f3542966b25db7a62cc2bed60ac1fa91da2501 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 00:11:10 +0000 Subject: [PATCH 06/17] Show an error if cookie is too large Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/oauth2_middleware.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 099ec608fd..8b5fe72ef6 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -147,7 +147,7 @@ func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, red }, authenticationTimeout, redirectURL.Host, secureCookie) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to set secure cookie: %w", err) + m.logger.Errorf("Failed to set secure cookie: %v", err) return } @@ -189,7 +189,7 @@ func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWrit }, exp, domain, secureCookie) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to set secure cookie: %w", err) + m.logger.Errorf("Failed to set secure cookie: %v", err) return } @@ -275,8 +275,8 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st return fmt.Errorf("failed to serialize token: %w", err) } - // Set the cookie - http.SetCookie(w, &http.Cookie{ + // Generate the cookie + cookie := http.Cookie{ Name: m.meta.CookieName, Value: string(val), MaxAge: int(ttl.Seconds()), @@ -285,7 +285,17 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st Domain: domain, Path: "/", SameSite: http.SameSiteLaxMode, - }) + } + cookieStr := cookie.String() + + // Browsers have a maximum size of about 4KB, and if the cookie is larger than that (by looking at the entire value of the header), it is silently rejected + // Some info: https://stackoverflow.com/a/4604212/192024 + if len(cookieStr) > 4<<10 { + return errors.New("token is too large to be stored in a cookie") + } + + // Finally set the cookie + w.Header().Add("Set-Cookie", cookieStr) return nil } From 383aac595e6b2e5abd74a852a7393a87ce16a1db Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 00:46:55 +0000 Subject: [PATCH 07/17] =?UTF-8?q?=F0=9F=92=84=20and=20remove=20now-ineffec?= =?UTF-8?q?tive=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- go.mod | 15 ----- go.sum | 34 ---------- middleware/http/oauth2/metadata.go | 2 +- middleware/http/oauth2/oauth2_middleware.go | 17 +---- .../http/oauth2/oauth2_middleware_test.go | 64 ------------------- 5 files changed, 3 insertions(+), 129 deletions(-) delete mode 100644 middleware/http/oauth2/oauth2_middleware_test.go diff --git a/go.mod b/go.mod index e64a9a9180..5cdadd875f 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,6 @@ require ( github.com/dapr/kit v0.11.3-0.20230615225244-804821bb8f2d github.com/didip/tollbooth/v7 v7.0.1 github.com/eclipse/paho.mqtt.golang v1.4.2 - github.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.7.1 @@ -150,7 +149,6 @@ require ( github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect - github.com/ajg/form v1.5.1 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect @@ -202,7 +200,6 @@ require ( github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/gavv/httpexpect v2.0.0+incompatible // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect @@ -237,7 +234,6 @@ require ( github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect @@ -256,7 +252,6 @@ require ( github.com/hashicorp/raft v1.4.0 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/imdario/mergo v0.3.13 // indirect - github.com/imkira/go-interpol v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -271,8 +266,6 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/k0kubun/pp v3.0.1+incompatible // indirect - github.com/kataras/go-errors v0.0.3 // indirect - github.com/kataras/go-serializer v0.0.4 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.16.3 // indirect github.com/knadh/koanf v1.4.1 // indirect @@ -291,7 +284,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect github.com/miekg/dns v1.1.43 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -300,7 +292,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.0 // indirect - github.com/moul/http2curl v1.0.0 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -328,10 +319,8 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron v1.2.0 // indirect github.com/rs/zerolog v1.28.0 // indirect - github.com/russross/blackfriday v1.6.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect - github.com/sergi/go-diff v1.2.0 // indirect github.com/shirou/gopsutil/v3 v3.22.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sony/gobreaker v0.5.0 // indirect @@ -350,11 +339,8 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - github.com/yudai/gojsondiff v1.0.0 // indirect - github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect go.etcd.io/etcd/api/v3 v3.5.5 // indirect @@ -381,7 +367,6 @@ require ( gopkg.in/gorethink/gorethink.v4 v4.1.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/kataras/go-serializer.v0 v0.0.4 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.80.1 // indirect diff --git a/go.sum b/go.sum index 78a28c0875..27d6afd5e0 100644 --- a/go.sum +++ b/go.sum @@ -495,8 +495,6 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -613,7 +611,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f h1:Pf0BjJDga7C98f0vhw+Ip5EaiE07S3lTKpIYPNS0nMo= github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f/go.mod h1:SghidfnxvX7ribW6nHI7T+IBbc9puZ9kk5Tx/88h8P4= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -870,8 +867,6 @@ github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yi github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= -github.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5 h1:M4CVMQ5ueVmGZAtkW2bsO+ftesCYpfxl27JTqtzKBzE= -github.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5/go.mod h1:MQXNGeXkpojWTxbN7vXoE3f7EmlA11MlJbsrJpVBINA= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -879,7 +874,6 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -898,8 +892,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getkin/kin-openapi v0.2.0/go.mod h1:V1z9xl9oF5Wt7v32ne4FmiF1alpS4dM6mNzoywPOXlk= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= @@ -1105,8 +1097,6 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -1166,7 +1156,6 @@ github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3 github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -1306,8 +1295,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb-client-go v1.4.0 h1:+KavOkwhLClHFfYcJMHHnTL5CZQhXJzOm5IKHI9BqJk= github.com/influxdata/influxdb-client-go v1.4.0/go.mod h1:S+oZsPivqbcP1S9ur+T+QqXvrYS3NCZeMQtBoH4D1dw= @@ -1384,10 +1371,6 @@ github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/k0kubun/pp/v3 v3.1.0/go.mod h1:vIrP5CF0n78pKHm2Ku6GVerpZBJvscg48WepUYEk2gw= -github.com/kataras/go-errors v0.0.3 h1:RQSGEb5AHjsGbwhNW8mFC7a9JrgoCLHC8CBQ4keXJYU= -github.com/kataras/go-errors v0.0.3/go.mod h1:K3ncz8UzwI3bpuksXt5tQLmrRlgxfv+52ARvAu1+I+o= -github.com/kataras/go-serializer v0.0.4 h1:isugggrY3DSac67duzQ/tn31mGAUtYqNpE2ob6Xt/SY= -github.com/kataras/go-serializer v0.0.4/go.mod h1:/EyLBhXKQOJ12dZwpUZZje3lGy+3wnvG7QKaVJtm/no= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -1511,8 +1494,6 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/microsoft/go-mssqldb v0.21.0 h1:p2rpHIL7TlSv1QrbXJUAcbyRKnIT0C9rRkH2E4OjLn8= github.com/microsoft/go-mssqldb v0.21.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -1560,8 +1541,6 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mrz1836/postmark v1.4.0 h1:lh4y5eIxBNdnRWMWPbFWv/BA2BzgJyu5ITKc5dKcbkY= github.com/mrz1836/postmark v1.4.0/go.mod h1:bgRfHzpUSl+zrQ8e2yh7zhQSJBwcZXywBVWkSa+6PFw= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= @@ -1776,8 +1755,6 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= @@ -1795,8 +1772,6 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg= github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks= @@ -1957,20 +1932,13 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -2911,8 +2879,6 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/kataras/go-serializer.v0 v0.0.4 h1:mVy3gjU4zZZBe+8JbZDRTMPJdrB0lzBNsLLREBcKGgU= -gopkg.in/kataras/go-serializer.v0 v0.0.4/go.mod h1:v2jHg/3Wp7uncDNzenTsX75PRDxhzlxoo/qDvM4ZGxk= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go index efc66f65c1..00c942f344 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -143,7 +143,7 @@ func (md *oAuth2MiddlewareMetadata) setCookieKeys() (err error) { // This is not good, but it is no different than how this component behaved in Dapr 1.11. // This behavior is deprecated and will be removed in Dapr 1.13. b = make([]byte, 48) - _, err := io.ReadFull(rand.Reader, b) + _, err = io.ReadFull(rand.Reader, b) if err != nil { return fmt.Errorf("failed to generate a random cookie key: %w", err) } diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 8b5fe72ef6..73899b2f1d 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -62,11 +62,6 @@ type Middleware struct { meta oAuth2MiddlewareMetadata } -const ( - savedState = "auth-state" - redirectPath = "redirect-url" -) - // GetHandler retruns the HTTP handler provided by the middleware. func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { err := m.meta.fromMetadata(metadata, m.logger) @@ -201,6 +196,7 @@ func (m *Middleware) getClaimsFromCookie(r *http.Request) (map[string]string, er // Get the cookie, which should contain a JWE cookie, err := r.Cookie(m.meta.CookieName) if errors.Is(err, http.ErrNoCookie) || cookie.Valid() != nil || cookie.Value == "" { + //nolint:nilerr return nil, nil } else if err != nil { return nil, fmt.Errorf("failed to retrieve cookie: %w", err) @@ -300,7 +296,7 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st return nil } -func (m *Middleware) unsetCookie(w http.ResponseWriter, isSecure bool) { +func (m *Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { // To delete the cookie, create a new cookie with the same name, but no value and that has already expired http.SetCookie(w, &http.Cookie{ Name: m.meta.CookieName, @@ -314,15 +310,6 @@ func (m *Middleware) unsetCookie(w http.ResponseWriter, isSecure bool) { }) } -func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*oAuth2MiddlewareMetadata, error) { - var middlewareMetadata oAuth2MiddlewareMetadata - err := mdutils.DecodeMetadata(metadata.Properties, &middlewareMetadata) - if err != nil { - return nil, err - } - return &middlewareMetadata, nil -} - func (m *Middleware) GetComponentMetadata() map[string]string { metadataStruct := oAuth2MiddlewareMetadata{} metadataInfo := map[string]string{} diff --git a/middleware/http/oauth2/oauth2_middleware_test.go b/middleware/http/oauth2/oauth2_middleware_test.go deleted file mode 100644 index e47ee35f2f..0000000000 --- a/middleware/http/oauth2/oauth2_middleware_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2023 The Dapr Authors -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. -*/ - -package oauth2 - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/fasthttp-contrib/sessions" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/dapr/components-contrib/middleware" - "github.com/dapr/kit/logger" -) - -func TestOAuth2CreatesAuthorizationHeaderWhenInSessionState(t *testing.T) { - var metadata middleware.Metadata - metadata.Properties = map[string]string{ - "clientID": "testId", - "clientSecret": "testSecret", - "scopes": "ascope", - "authURL": "https://idp:9999", - "tokenURL": "https://idp:9999", - "redirectUrl": "https://localhost:9999", - "authHeaderName": "someHeader", - } - - log := logger.NewLogger("oauth2.test") - handler, err := NewOAuth2Middleware(log).GetHandler(context.Background(), metadata) - require.NoError(t, err) - - // Create request and recorder - r := httptest.NewRequest(http.MethodGet, "http://dapr.io", nil) - w := httptest.NewRecorder() - session := sessions.Start(w, r) - session.Set("someHeader", "Bearer abcd") - - // Copy the session cookie to the request - cookie := w.Header().Get("Set-Cookie") - r.Header.Add("Cookie", cookie) - - handler( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("from mock")) - }), - ).ServeHTTP(w, r) - - assert.Equal(t, "Bearer abcd", r.Header.Get("someHeader")) -} From b05f23fcd6714c15ce2bfbc46630820917f75aa6 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 20:09:09 +0000 Subject: [PATCH 08/17] Split cookie component into its own Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- .build-tools/component-folders.json | 2 + middleware/http/oauth2/cookie/cookie.go | 68 +++++++ middleware/http/oauth2/{ => impl}/metadata.go | 73 ++++--- .../oauth2/{ => impl}/oauth2_middleware.go | 178 ++++++++++-------- 4 files changed, 208 insertions(+), 113 deletions(-) create mode 100644 middleware/http/oauth2/cookie/cookie.go rename middleware/http/oauth2/{ => impl}/metadata.go (69%) rename middleware/http/oauth2/{ => impl}/oauth2_middleware.go (60%) diff --git a/.build-tools/component-folders.json b/.build-tools/component-folders.json index ecea136ffb..b679eedabe 100644 --- a/.build-tools/component-folders.json +++ b/.build-tools/component-folders.json @@ -24,6 +24,8 @@ "configuration/redis/internal", "crypto/azure", "crypto/kubernetes", + "middleware/http/oauth2", + "middleware/http/oauth2/impl", "pubsub/aws", "pubsub/azure", "pubsub/azure/servicebus", diff --git a/middleware/http/oauth2/cookie/cookie.go b/middleware/http/oauth2/cookie/cookie.go new file mode 100644 index 0000000000..bddb87af9d --- /dev/null +++ b/middleware/http/oauth2/cookie/cookie.go @@ -0,0 +1,68 @@ +/* +Copyright 2023 The Dapr Authors +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. +*/ + +// Package oauth2cookie is a concrete implementation of the oauth2 middleware that stores the token in a cookie in the client. +package oauth2cookie + +import ( + "context" + "fmt" + "net/http" + "reflect" + + mdutils "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/middleware" + "github.com/dapr/components-contrib/middleware/http/oauth2/impl" + "github.com/dapr/kit/logger" +) + +type OAuth2CookieMiddlewareMetadata struct { + impl.OAuth2MiddlewareMetadata `mapstructure:",squash"` +} + +// NewOAuth2CookieMiddleware returns a new oAuth2 middleware. +func NewOAuth2CookieMiddleware(log logger.Logger) middleware.Middleware { + mw := &OAuth2CookieMiddleware{ + OAuth2Middleware: impl.OAuth2Middleware{}, + logger: log, + } + mw.OAuth2Middleware.GetClaimsFn = mw.GetClaimsFromCookie + mw.OAuth2Middleware.SetClaimsFn = mw.SetSecureCookie + return mw +} + +// OAuth2CookieMiddleware is an OAuth2 authentication middleware that stores session data in a cookie. +type OAuth2CookieMiddleware struct { + impl.OAuth2Middleware + + logger logger.Logger + meta OAuth2CookieMiddlewareMetadata +} + +// GetHandler retruns the HTTP handler provided by the middleware. +func (m *OAuth2CookieMiddleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { + err := m.meta.FromMetadata(metadata, m.logger) + if err != nil { + return nil, fmt.Errorf("invalid metadata: %w", err) + } + m.OAuth2Middleware.SetMetadata(m.meta.OAuth2MiddlewareMetadata) + + return m.OAuth2Middleware.GetHandler(ctx) +} + +func (m *OAuth2CookieMiddleware) GetComponentMetadata() map[string]string { + metadataStruct := OAuth2CookieMiddlewareMetadata{} + metadataInfo := map[string]string{} + mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) + return metadataInfo +} diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/impl/metadata.go similarity index 69% rename from middleware/http/oauth2/metadata.go rename to middleware/http/oauth2/impl/metadata.go index 00c942f344..9df59842e0 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/impl/metadata.go @@ -11,7 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package oauth2 +package impl import ( "crypto" @@ -34,15 +34,14 @@ import ( const ( defaultAuthHeaderName = "Authorization" - defaultTokenStorage = "memory" defaultCookieName = "_dapr_oauth2" // Key used to derive cookie encryption keys using HMAC. hmacKey = "dapr_oauth2" ) -// Metadata is the oAuth middleware config. -type oAuth2MiddlewareMetadata struct { +// OAuth2MiddlewareMetadata is the OAuth2 middleware config. +type OAuth2MiddlewareMetadata struct { // Client ID of the OAuth2 application. // Required. ClientID string `json:"clientID" mapstructure:"clientID"` @@ -66,26 +65,26 @@ type oAuth2MiddlewareMetadata struct { // Forces the use of TLS/HTTPS for the redirect URL. // Defaults to false. ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` - // Name of the cookie where Dapr will store the encrypted access token, when storing tokens in cookies. - // Defaults to "_dapr_oauth2". - CookieName string `json:"cookieName" mapstructure:"cookieName"` - // Cookie encryption and signing key (technically, seed used to derive those two). + // Token encryption and signing key (technically, seed used to derive those two). // It is recommended to provide a random string with sufficient entropy. // Required to allow sessions to persist across restarts of the Dapr runtime and to allow multiple instances of Dapr to access the session. // Not setting an explicit encryption key is deprecated, and this field will become required in Dapr 1.13. // TODO @ItalyPaleAle: make required in Dapr 1.13. - CookieKey string `json:"cookieKey" mapstructure:"cookieKey"` + TokenEncryptionKey string `json:"tokenEncryptionKey" mapstructure:"tokenEncryptionKey"` + // Name of the cookie where Dapr will store the session state during authentication (and when using cookies for storage, the encrypted access token too). + // Defaults to "_dapr_oauth2". + CookieName string `json:"cookieName" mapstructure:"cookieName"` - // Internal: cookie encryption key - cek jwk.Key - // Internal: cookie signing key - csk jwk.Key + // Internal: token encryption key + encKey jwk.Key + // Internal: token signing key + sigKey jwk.Key // Internal: OAuth2 configuration object oauth2Conf oauth2.Config } // Parse the component's metadata into the object. -func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata, log logger.Logger) error { +func (md *OAuth2MiddlewareMetadata) FromMetadata(metadata middleware.Metadata, log logger.Logger) error { // Set default values if md.AuthHeaderName == "" { md.AuthHeaderName = defaultAuthHeaderName @@ -117,40 +116,40 @@ func (md *oAuth2MiddlewareMetadata) fromMetadata(metadata middleware.Metadata, l return errors.New("required field 'redirectURL' is empty") } - // If there's no cookie encryption key, show a warning + // If there's no token encryption key, show a warning // TODO @ItalyPaleAle: make required in Dapr 1.13. - if md.CookieKey == "" { - log.Warnf("[DEPRECATION NOTICE] Initializing the OAuth2 middleware with an empty 'cookieKey' is deprecated, and the field will become required in Dapr 1.13. Setting an explicit 'cookieKey' is required to allow sessions to be shared across multiple instances of Dapr and to survive a restart of Dapr.") + if md.TokenEncryptionKey == "" { + log.Warnf("[DEPRECATION NOTICE] Initializing the OAuth2 middleware with an empty 'tokenEncryptionKey' is deprecated, and the field will become required in Dapr 1.13. Setting an explicit 'tokenEncryptionKey' is required to allow sessions to be shared across multiple instances of Dapr and to survive a restart of Dapr.") } return nil } -// Derives a 128-bit cookie encryption key and a 256-bit cookie signing key from the user-provided value. -func (md *oAuth2MiddlewareMetadata) setCookieKeys() (err error) { +// setTokenKeys derives a 128-bit token encryption key and a 256-bit token signing key from the user-provided value. +func (md *OAuth2MiddlewareMetadata) setTokenKeys() (err error) { var b []byte - if md.CookieKey == "" { + if md.TokenEncryptionKey == "" { // TODO @ItalyPaleAle: uncomment for Dapr 1.13 and remove existing code in this block /* - // This should never happen as the validation method ensures that cookieKey isn't empty + // This should never happen as the validation method ensures that tokenEncryptionKey isn't empty // So if we're here, it means there was a development-time error. - panic("cookie encryption key is empty") + panic("token encryption key is empty") */ - // If the user didn't provide a cookie key, generate a random one - // Naturally, this means that the cookie key is unique to this process and cookies cannot be decrypted by other instances of Dapr or if the process is restarted + // If the user didn't provide an encryption key, generate a random one + // Naturally, this means that the key is unique to this process and sessions cannot be decrypted by other instances of Dapr or if the process is restarted // This is not good, but it is no different than how this component behaved in Dapr 1.11. // This behavior is deprecated and will be removed in Dapr 1.13. b = make([]byte, 48) _, err = io.ReadFull(rand.Reader, b) if err != nil { - return fmt.Errorf("failed to generate a random cookie key: %w", err) + return fmt.Errorf("failed to generate a random encryption key: %w", err) } } else { - // Derive 48 bytes from the cookie key using HMAC with a fixed "HMAC key" + // Derive 48 bytes from the token encryption key using HMAC with a fixed "HMAC key" h := hmac.New(crypto.SHA384.New, []byte(hmacKey)) - h.Write([]byte(md.CookieKey)) + h.Write([]byte(md.TokenEncryptionKey)) b = h.Sum(nil) } @@ -159,25 +158,25 @@ func (md *oAuth2MiddlewareMetadata) setCookieKeys() (err error) { kidH.Write(b) kid := base64.RawURLEncoding.EncodeToString(kidH.Sum(nil)) - // Cookie encryption key uses 128 bits - md.cek, err = jwk.FromRaw(b[:16]) + // Token encryption key uses 128 bits + md.encKey, err = jwk.FromRaw(b[:16]) if err != nil { - return fmt.Errorf("failed to import cookie encryption key: %w", err) + return fmt.Errorf("failed to import token encryption key: %w", err) } - md.cek.Set("kid", kid) + md.encKey.Set("kid", kid) - // Cookie signing key uses 256 bits - md.csk, err = jwk.FromRaw(b[16:]) + // Token signing key uses 256 bits + md.sigKey, err = jwk.FromRaw(b[16:]) if err != nil { - return fmt.Errorf("failed to import cookie signing key: %w", err) + return fmt.Errorf("failed to import token signing key: %w", err) } - md.csk.Set("kid", kid) + md.sigKey.Set("kid", kid) return nil } -// Sets the oauth2Conf property in the object. -func (md *oAuth2MiddlewareMetadata) setOAuth2Conf() { +// setOAuth2Conf sets the oauth2Conf property in the object. +func (md *OAuth2MiddlewareMetadata) setOAuth2Conf() { md.oauth2Conf = oauth2.Config{ ClientID: md.ClientID, ClientSecret: md.ClientSecret, diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/impl/oauth2_middleware.go similarity index 60% rename from middleware/http/oauth2/oauth2_middleware.go rename to middleware/http/oauth2/impl/oauth2_middleware.go index 73899b2f1d..01eb19ec80 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/impl/oauth2_middleware.go @@ -11,7 +11,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -package oauth2 +// Package impl contains the abstract, shared implementation for the OAuth2 middleware. +package impl import ( "context" @@ -31,7 +32,6 @@ import ( "github.com/dapr/components-contrib/internal/httputils" mdutils "github.com/dapr/components-contrib/metadata" - "github.com/dapr/components-contrib/middleware" "github.com/dapr/kit/logger" ) @@ -49,30 +49,31 @@ const ( claimState = "state" ) -// NewOAuth2Middleware returns a new oAuth2 middleware. -func NewOAuth2Middleware(log logger.Logger) middleware.Middleware { - return &Middleware{ - logger: log, - } -} +// ErrTokenTooLargeForCookie is returned by SetSecureCookie when the token is too large to be stored in a cookie +var ErrTokenTooLargeForCookie = errors.New("token is too large to be stored in a cookie") -// Middleware is an oAuth2 authentication middleware. -type Middleware struct { +// OAuth2Middleware is an oAuth2 authentication middleware. +type OAuth2Middleware struct { logger logger.Logger - meta oAuth2MiddlewareMetadata + meta OAuth2MiddlewareMetadata + + // GetClaimsFn is the function invoked to retrieve the claims from the request. + GetClaimsFn func(r *http.Request) (map[string]string, error) + // SetClaimsFn is the function invoked to store claims in the response. + SetClaimsFn func(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, secureContext bool) error } -// GetHandler retruns the HTTP handler provided by the middleware. -func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - err := m.meta.fromMetadata(metadata, m.logger) - if err != nil { - return nil, fmt.Errorf("invalid metadata: %w", err) - } +// SetMetadata sets the metadata in the object. +func (m *OAuth2Middleware) SetMetadata(meta OAuth2MiddlewareMetadata) { + m.meta = meta +} - // Derive the cookie keys - err = m.meta.setCookieKeys() +// GetHandler retruns the HTTP handler provided by the middleware. +func (m *OAuth2Middleware) GetHandler(ctx context.Context) (func(next http.Handler) http.Handler, error) { + // Derive the token keys + err := m.meta.setTokenKeys() if err != nil { - return nil, fmt.Errorf("failed to derive cookie keys: %w", err) + return nil, fmt.Errorf("failed to derive token keys: %w", err) } // Create the OAuth2 configuration object @@ -81,19 +82,37 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat return m.handler, nil } -func (m *Middleware) handler(next http.Handler) http.Handler { +func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // To check if the request is coming in using HTTPS, we check if the scheme of the URL is "https" // Checking for `r.TLS` alone may not work if Dapr is behind a proxy that does TLS termination - secureCookie := r.URL.Scheme == "https" + secureContext := r.URL.Scheme == "https" - // Get the token from the cookie - claims, err := m.getClaimsFromCookie(r) + // If we have the "state" and "code" parameter, we need to exchange the authorization code for the access token + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + if code != "" && state != "" { + // Always get the claims from the cookies in this case + claims, err := m.GetClaimsFromCookie(r) + if err != nil { + // If the cookie is invalid, redirect to the auth endpoint again + // This will overwrite the old cookie + m.logger.Debugf("Invalid session cookie: %v", err) + m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) + return + } + + m.exchangeAccessCode(r.Context(), w, claims, code, state, r.URL.Host, secureContext) + return + } + + // Get the token from the request + // This uses the function provided by the implementation + claims, err := m.GetClaimsFn(r) if err != nil { - // If the cookie is invalid, redirect to the auth endpoint again - // This will overwrite the old cookie + // If the function returns an error, redirect to the auth endpoint again m.logger.Debugf("Invalid session cookie: %v", err) - m.redirectToAuthenticationEndpoint(w, r.URL, secureCookie) + m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) return } @@ -108,20 +127,12 @@ func (m *Middleware) handler(next http.Handler) http.Handler { return } - // If we have the "state" and "code" parameter, we need to exchange the authorization code for the access token - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - if code != "" && state != "" { - m.exchangeAccessCode(r.Context(), w, claims, code, state, r.URL.Host, secureCookie) - return - } - // Redirect to the auhentication endpoint - m.redirectToAuthenticationEndpoint(w, r.URL, secureCookie) + m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) }) } -func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, redirectURL *url.URL, secureCookie bool) { +func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, redirectURL *url.URL, secureContext bool) { // Generate a new state token stateObj, err := uuid.NewRandom() if err != nil { @@ -136,10 +147,10 @@ func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, red } // Set the cookie with the state and redirect URL - err = m.setSecureCookie(w, map[string]string{ + err = m.SetSecureCookie(w, map[string]string{ claimState: state, claimRedirect: redirectURL.String(), - }, authenticationTimeout, redirectURL.Host, secureCookie) + }, authenticationTimeout, redirectURL.Host, secureContext) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) m.logger.Errorf("Failed to set secure cookie: %v", err) @@ -151,8 +162,8 @@ func (m *Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, red httputils.RespondWithRedirect(w, http.StatusFound, url) } -func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, domain string, secureCookie bool) { - if claims[claimRedirect] == "" { +func (m *OAuth2Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, domain string, secureContext bool) { + if len(claims) == 0 || claims[claimRedirect] == "" { httputils.RespondWithError(w, http.StatusInternalServerError) m.logger.Error("Missing claim 'redirect'") return @@ -177,14 +188,14 @@ func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWrit exp = time.Hour } - // Set the cookie - err = m.setSecureCookie(w, map[string]string{ + // Set the claims in the response + err = m.SetClaimsFn(w, map[string]string{ claimTokenType: token.Type(), claimToken: token.AccessToken, - }, exp, domain, secureCookie) + }, exp, domain, secureContext) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to set secure cookie: %v", err) + m.logger.Errorf("Failed to set token in the response: %v", err) return } @@ -192,28 +203,19 @@ func (m *Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWrit httputils.RespondWithRedirect(w, http.StatusFound, claims[claimRedirect]) } -func (m *Middleware) getClaimsFromCookie(r *http.Request) (map[string]string, error) { - // Get the cookie, which should contain a JWE - cookie, err := r.Cookie(m.meta.CookieName) - if errors.Is(err, http.ErrNoCookie) || cookie.Valid() != nil || cookie.Value == "" { - //nolint:nilerr - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("failed to retrieve cookie: %w", err) - } - - // Decrypt the encrypted (JWE) cookie +func (m *OAuth2Middleware) ParseToken(token string) (map[string]string, error) { + // Decrypt the encrypted (JWE) token dec, err := jwe.Decrypt( - []byte(cookie.Value), - jwe.WithKey(jwa.A128KW, m.meta.cek), + []byte(token), + jwe.WithKey(jwa.A128KW, m.meta.encKey), ) if err != nil { - return nil, fmt.Errorf("failed to decrypt cookie: %w", err) + return nil, fmt.Errorf("failed to decrypt token: %w", err) } - // Validate the JWT from the decrypted cookie - token, err := jwt.Parse(dec, - jwt.WithKey(jwa.HS256, m.meta.csk), + // Validate the JWT from the decrypted token + tk, err := jwt.Parse(dec, + jwt.WithKey(jwa.HS256, m.meta.sigKey), jwt.WithIssuer(jwtIssuer), jwt.WithAudience(m.meta.ClientID), // Use the client ID as audience jwt.WithAcceptableSkew(allowedClockSkew), @@ -222,10 +224,25 @@ func (m *Middleware) getClaimsFromCookie(r *http.Request) (map[string]string, er return nil, fmt.Errorf("failed to validate JWT token: %w", err) } - return cast.ToStringMapString(token.PrivateClaims()), nil + return cast.ToStringMapString(tk.PrivateClaims()), nil } -func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, isSecure bool) error { +// GetClaimsFromCookie retrieves the claims from the cookie. +func (m *OAuth2Middleware) GetClaimsFromCookie(r *http.Request) (map[string]string, error) { + // Get the cookie, which should contain a JWE + cookie, err := r.Cookie(m.meta.CookieName) + if errors.Is(err, http.ErrNoCookie) || cookie.Valid() != nil || cookie.Value == "" { + //nolint:nilerr + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("failed to retrieve cookie: %w", err) + } + + return m.ParseToken(cookie.Value) +} + +// CreateToken generates an encrypted JWT containing the claims. +func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Duration) (string, error) { now := time.Now() exp := now.Add(ttl) @@ -243,7 +260,7 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st } token, err := builder.Build() if err != nil { - return fmt.Errorf("error building JWT: %w", err) + return "", fmt.Errorf("error building JWT: %w", err) } // Generate the encrypted JWT @@ -251,30 +268,39 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st if claimsSize > 800 { // If the total size of the claims is more than 800 bytes, we should enable compression encryptOpts = []jwt.EncryptOption{ - jwt.WithKey(jwa.A128KW, m.meta.cek), + jwt.WithKey(jwa.A128KW, m.meta.encKey), jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), jwt.WithEncryptOption(jwe.WithCompress(jwa.Deflate)), } } else { encryptOpts = []jwt.EncryptOption{ - jwt.WithKey(jwa.A128KW, m.meta.cek), + jwt.WithKey(jwa.A128KW, m.meta.encKey), jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), } } val, err := jwt.NewSerializer(). Sign( - jwt.WithKey(jwa.HS256, m.meta.csk), + jwt.WithKey(jwa.HS256, m.meta.sigKey), ). Encrypt(encryptOpts...). Serialize(token) if err != nil { - return fmt.Errorf("failed to serialize token: %w", err) + return "", fmt.Errorf("failed to serialize token: %w", err) + } + + return string(val), nil +} + +func (m *OAuth2Middleware) SetSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, isSecure bool) error { + token, err := m.CreateToken(claims, ttl) + if err != nil { + return err } // Generate the cookie cookie := http.Cookie{ Name: m.meta.CookieName, - Value: string(val), + Value: token, MaxAge: int(ttl.Seconds()), HttpOnly: true, Secure: isSecure, @@ -284,10 +310,10 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st } cookieStr := cookie.String() - // Browsers have a maximum size of about 4KB, and if the cookie is larger than that (by looking at the entire value of the header), it is silently rejected + // Browsers have a maximum size of 4093 bytes, and if the cookie is larger than that (by looking at the entire value of the header), it is silently rejected // Some info: https://stackoverflow.com/a/4604212/192024 - if len(cookieStr) > 4<<10 { - return errors.New("token is too large to be stored in a cookie") + if len(cookieStr) > 4093 { + return ErrTokenTooLargeForCookie } // Finally set the cookie @@ -296,7 +322,7 @@ func (m *Middleware) setSecureCookie(w http.ResponseWriter, claims map[string]st return nil } -func (m *Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { +func (m *OAuth2Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { // To delete the cookie, create a new cookie with the same name, but no value and that has already expired http.SetCookie(w, &http.Cookie{ Name: m.meta.CookieName, @@ -310,8 +336,8 @@ func (m *Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { }) } -func (m *Middleware) GetComponentMetadata() map[string]string { - metadataStruct := oAuth2MiddlewareMetadata{} +func (m *OAuth2Middleware) GetComponentMetadata() map[string]string { + metadataStruct := OAuth2MiddlewareMetadata{} metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) return metadataInfo From 1b5e0be40d9828cfe42521a10593c51498b49a43 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 21:20:33 +0000 Subject: [PATCH 09/17] Created header component + fixes Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/cookie/cookie.go | 56 +++++++- middleware/http/oauth2/header/header.go | 106 +++++++++++++++ middleware/http/oauth2/impl/metadata.go | 3 - .../http/oauth2/impl/oauth2_middleware.go | 122 +++++++++--------- 4 files changed, 218 insertions(+), 69 deletions(-) create mode 100644 middleware/http/oauth2/header/header.go diff --git a/middleware/http/oauth2/cookie/cookie.go b/middleware/http/oauth2/cookie/cookie.go index bddb87af9d..9f4da8d012 100644 --- a/middleware/http/oauth2/cookie/cookie.go +++ b/middleware/http/oauth2/cookie/cookie.go @@ -19,25 +19,37 @@ import ( "fmt" "net/http" "reflect" + "time" + "github.com/dapr/components-contrib/internal/httputils" mdutils "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/middleware" "github.com/dapr/components-contrib/middleware/http/oauth2/impl" "github.com/dapr/kit/logger" ) +const ( + claimRedirect = "redirect" +) + type OAuth2CookieMiddlewareMetadata struct { impl.OAuth2MiddlewareMetadata `mapstructure:",squash"` + + // Forces the use of TLS/HTTPS for the redirect URL. + // Defaults to false. + ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` } // NewOAuth2CookieMiddleware returns a new oAuth2 middleware. func NewOAuth2CookieMiddleware(log logger.Logger) middleware.Middleware { mw := &OAuth2CookieMiddleware{ - OAuth2Middleware: impl.OAuth2Middleware{}, - logger: log, + OAuth2Middleware: impl.OAuth2Middleware{ + Logger: log, + }, } - mw.OAuth2Middleware.GetClaimsFn = mw.GetClaimsFromCookie - mw.OAuth2Middleware.SetClaimsFn = mw.SetSecureCookie + mw.OAuth2Middleware.GetTokenFn = mw.GetClaimsFromCookie + mw.OAuth2Middleware.SetTokenFn = mw.setTokenInResponse + mw.OAuth2Middleware.ClaimsForAuthFn = mw.claimsForAuth return mw } @@ -45,13 +57,12 @@ func NewOAuth2CookieMiddleware(log logger.Logger) middleware.Middleware { type OAuth2CookieMiddleware struct { impl.OAuth2Middleware - logger logger.Logger - meta OAuth2CookieMiddlewareMetadata + meta OAuth2CookieMiddlewareMetadata } // GetHandler retruns the HTTP handler provided by the middleware. func (m *OAuth2CookieMiddleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - err := m.meta.FromMetadata(metadata, m.logger) + err := m.meta.FromMetadata(metadata, m.Logger) if err != nil { return nil, fmt.Errorf("invalid metadata: %w", err) } @@ -60,6 +71,37 @@ func (m *OAuth2CookieMiddleware) GetHandler(ctx context.Context, metadata middle return m.OAuth2Middleware.GetHandler(ctx) } +func (m *OAuth2CookieMiddleware) claimsForAuth(r *http.Request) (map[string]string, error) { + // Get the redirect URL + redirectURL := r.URL + if m.meta.ForceHTTPS { + redirectURL.Scheme = "https" + } + + return map[string]string{ + claimRedirect: redirectURL.String(), + }, nil +} + +func (m *OAuth2CookieMiddleware) setTokenInResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { + if reqClaims[claimRedirect] == "" { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.Logger.Error("Missing claim 'redirect'") + return + } + + // Set the claims in the response + err := m.SetCookie(w, token, exp, r.URL.Host, impl.IsRequestSecure(r)) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.Logger.Errorf("Failed to set cookie in the response: %v", err) + return + } + + // Redirect to the URL set in the request + httputils.RespondWithRedirect(w, http.StatusFound, reqClaims[claimRedirect]) +} + func (m *OAuth2CookieMiddleware) GetComponentMetadata() map[string]string { metadataStruct := OAuth2CookieMiddlewareMetadata{} metadataInfo := map[string]string{} diff --git a/middleware/http/oauth2/header/header.go b/middleware/http/oauth2/header/header.go new file mode 100644 index 0000000000..fe86e6fdc5 --- /dev/null +++ b/middleware/http/oauth2/header/header.go @@ -0,0 +1,106 @@ +/* +Copyright 2023 The Dapr Authors +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. +*/ + +// Package oauth2header is a concrete implementation of the oauth2 middleware that expects the token to be present in the Authorization header. +package oauth2header + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "strings" + "time" + + mdutils "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/middleware" + "github.com/dapr/components-contrib/middleware/http/oauth2/impl" + "github.com/dapr/kit/logger" +) + +const ( + headerName = "Authorization" + bearerPrefix = "bearer " +) + +type OAuth2HeaderMiddlewareMetadata struct { + impl.OAuth2MiddlewareMetadata `mapstructure:",squash"` +} + +// NewOAuth2HeaderMiddleware returns a new OAuth2 middleware. +func NewOAuth2HeaderMiddleware(log logger.Logger) middleware.Middleware { + mw := &OAuth2HeaderMiddleware{ + OAuth2Middleware: impl.OAuth2Middleware{ + Logger: log, + }, + } + mw.OAuth2Middleware.GetTokenFn = mw.getClaimsFromHeader + mw.OAuth2Middleware.SetTokenFn = mw.setTokenResponse + return mw +} + +// OAuth2HeaderMiddleware is an OAuth2 authentication middleware that stores session data in a cookie. +type OAuth2HeaderMiddleware struct { + impl.OAuth2Middleware + + logger logger.Logger + meta OAuth2HeaderMiddlewareMetadata +} + +// GetHandler retruns the HTTP handler provided by the middleware. +func (m *OAuth2HeaderMiddleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { + err := m.meta.FromMetadata(metadata, m.logger) + if err != nil { + return nil, fmt.Errorf("invalid metadata: %w", err) + } + m.OAuth2Middleware.SetMetadata(m.meta.OAuth2MiddlewareMetadata) + + return m.OAuth2Middleware.GetHandler(ctx) +} + +func (m *OAuth2HeaderMiddleware) getClaimsFromHeader(r *http.Request) (map[string]string, error) { + // Get the header which should contain the JWE + // The "Bearer " prefix is optional + header := r.Header.Get(headerName) + if header == "" { + return nil, nil + } + if len(header) > len(bearerPrefix) && strings.ToLower(header[0:len(bearerPrefix)]) == bearerPrefix { + header = header[len(bearerPrefix):] + } + + return m.ParseToken(header) +} + +func (m *OAuth2HeaderMiddleware) setTokenResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { + // Delete the state token cookie + m.UnsetCookie(w, impl.IsRequestSecure(r)) + + // Set the response in the body + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + enc.Encode(map[string]string{ + headerName: token, + }) +} + +func (m *OAuth2HeaderMiddleware) GetComponentMetadata() map[string]string { + metadataStruct := OAuth2HeaderMiddlewareMetadata{} + metadataInfo := map[string]string{} + mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) + return metadataInfo +} diff --git a/middleware/http/oauth2/impl/metadata.go b/middleware/http/oauth2/impl/metadata.go index 9df59842e0..d8792b0ddd 100644 --- a/middleware/http/oauth2/impl/metadata.go +++ b/middleware/http/oauth2/impl/metadata.go @@ -62,9 +62,6 @@ type OAuth2MiddlewareMetadata struct { // The URL of your application that the authorization server should redirect to once the user has authenticated. // Required. RedirectURL string `json:"redirectURL" mapstructure:"redirectURL"` - // Forces the use of TLS/HTTPS for the redirect URL. - // Defaults to false. - ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` // Token encryption and signing key (technically, seed used to derive those two). // It is recommended to provide a random string with sufficient entropy. // Required to allow sessions to persist across restarts of the Dapr runtime and to allow multiple instances of Dapr to access the session. diff --git a/middleware/http/oauth2/impl/oauth2_middleware.go b/middleware/http/oauth2/impl/oauth2_middleware.go index 01eb19ec80..213879c844 100644 --- a/middleware/http/oauth2/impl/oauth2_middleware.go +++ b/middleware/http/oauth2/impl/oauth2_middleware.go @@ -19,8 +19,6 @@ import ( "errors" "fmt" "net/http" - "net/url" - "reflect" "time" "github.com/google/uuid" @@ -31,7 +29,6 @@ import ( "golang.org/x/oauth2" "github.com/dapr/components-contrib/internal/httputils" - mdutils "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -45,7 +42,6 @@ const ( claimToken = "tkn" claimTokenType = "tkt" - claimRedirect = "redirect" claimState = "state" ) @@ -54,13 +50,16 @@ var ErrTokenTooLargeForCookie = errors.New("token is too large to be stored in a // OAuth2Middleware is an oAuth2 authentication middleware. type OAuth2Middleware struct { - logger logger.Logger - meta OAuth2MiddlewareMetadata + Logger logger.Logger - // GetClaimsFn is the function invoked to retrieve the claims from the request. - GetClaimsFn func(r *http.Request) (map[string]string, error) - // SetClaimsFn is the function invoked to store claims in the response. - SetClaimsFn func(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, secureContext bool) error + // GetTokenFn is the function invoked to retrieve the token from the request. + GetTokenFn func(r *http.Request) (map[string]string, error) + // ClaimsForAuthFn is an optional function invoked before redirecting to the authorization endpoint that allows setting additional claims in the state cookie. + ClaimsForAuthFn func(r *http.Request) (map[string]string, error) + // SetTokenFn is the function invoked to store the token in the response. + SetTokenFn func(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) + + meta OAuth2MiddlewareMetadata } // SetMetadata sets the metadata in the object. @@ -84,10 +83,6 @@ func (m *OAuth2Middleware) GetHandler(ctx context.Context) (func(next http.Handl func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // To check if the request is coming in using HTTPS, we check if the scheme of the URL is "https" - // Checking for `r.TLS` alone may not work if Dapr is behind a proxy that does TLS termination - secureContext := r.URL.Scheme == "https" - // If we have the "state" and "code" parameter, we need to exchange the authorization code for the access token code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") @@ -97,22 +92,22 @@ func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { if err != nil { // If the cookie is invalid, redirect to the auth endpoint again // This will overwrite the old cookie - m.logger.Debugf("Invalid session cookie: %v", err) - m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) + m.Logger.Debugf("Invalid session cookie: %v", err) + m.redirectToAuthenticationEndpoint(w, r) return } - m.exchangeAccessCode(r.Context(), w, claims, code, state, r.URL.Host, secureContext) + m.exchangeAccessCode(w, r, claims, code, state) return } // Get the token from the request // This uses the function provided by the implementation - claims, err := m.GetClaimsFn(r) + claims, err := m.GetTokenFn(r) if err != nil { // If the function returns an error, redirect to the auth endpoint again - m.logger.Debugf("Invalid session cookie: %v", err) - m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) + m.Logger.Debugf("Invalid session: %v", err) + m.redirectToAuthenticationEndpoint(w, r) return } @@ -128,32 +123,43 @@ func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { } // Redirect to the auhentication endpoint - m.redirectToAuthenticationEndpoint(w, r.URL, secureContext) + m.redirectToAuthenticationEndpoint(w, r) }) } -func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, redirectURL *url.URL, secureContext bool) { +func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, r *http.Request) { + // Do this here in case ClaimsForAuthFn modifies the request object + domain := r.URL.Host + isSecureContext := IsRequestSecure(r) + // Generate a new state token stateObj, err := uuid.NewRandom() if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to generate UUID: %v", err) + m.Logger.Errorf("Failed to generate UUID: %v", err) return } state := stateObj.String() - if m.meta.ForceHTTPS { - redirectURL.Scheme = "https" + // Get additional claims from the implementation + var claims map[string]string + if m.ClaimsForAuthFn != nil { + claims, err = m.ClaimsForAuthFn(r) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.Logger.Errorf("Failed to retrieve claims for the authentication endpoint: %v", err) + return + } + } else { + claims = make(map[string]string, 1) } - // Set the cookie with the state and redirect URL - err = m.SetSecureCookie(w, map[string]string{ - claimState: state, - claimRedirect: redirectURL.String(), - }, authenticationTimeout, redirectURL.Host, secureContext) + // Set the cookie with the state token + claims[claimState] = state + err = m.setCookieWithClaims(w, claims, authenticationTimeout, domain, isSecureContext) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to set secure cookie: %v", err) + m.Logger.Errorf("Failed to set secure cookie: %v", err) return } @@ -162,45 +168,39 @@ func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWrite httputils.RespondWithRedirect(w, http.StatusFound, url) } -func (m *OAuth2Middleware) exchangeAccessCode(ctx context.Context, w http.ResponseWriter, claims map[string]string, code string, state string, domain string, secureContext bool) { - if len(claims) == 0 || claims[claimRedirect] == "" { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Error("Missing claim 'redirect'") - return - } - - if claims[claimState] == "" || state != claims[claimState] { +func (m *OAuth2Middleware) exchangeAccessCode(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, code string, state string) { + if len(reqClaims) == 0 || reqClaims[claimState] == "" || state != reqClaims[claimState] { httputils.RespondWithErrorAndMessage(w, http.StatusBadRequest, "invalid state") return } - // Exchange the authorization code for a token - token, err := m.meta.oauth2Conf.Exchange(ctx, code) + // Exchange the authorization code for a accessToken + accessToken, err := m.meta.oauth2Conf.Exchange(r.Context(), code) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Error("Failed to exchange token") + m.Logger.Error("Failed to exchange token") return } - // If we don't have an expiration, assume it's 1 hour - exp := time.Until(token.Expiry) + // If we don't have an expiration, set it to 1hr + exp := time.Until(accessToken.Expiry) if exp <= time.Second { exp = time.Hour } - // Set the claims in the response - err = m.SetClaimsFn(w, map[string]string{ - claimTokenType: token.Type(), - claimToken: token.AccessToken, - }, exp, domain, secureContext) + // Encrypt the token + token, err := m.CreateToken(map[string]string{ + claimTokenType: accessToken.Type(), + claimToken: accessToken.AccessToken, + }, exp) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.logger.Errorf("Failed to set token in the response: %v", err) + m.Logger.Errorf("Failed to generate token: %v", err) return } - // Redirect to the URL set in the request - httputils.RespondWithRedirect(w, http.StatusFound, claims[claimRedirect]) + // Allow the implementation to send the response + m.SetTokenFn(w, r, reqClaims, token, exp) } func (m *OAuth2Middleware) ParseToken(token string) (map[string]string, error) { @@ -291,16 +291,20 @@ func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Durati return string(val), nil } -func (m *OAuth2Middleware) SetSecureCookie(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, isSecure bool) error { +func (m *OAuth2Middleware) setCookieWithClaims(w http.ResponseWriter, claims map[string]string, ttl time.Duration, domain string, isSecure bool) error { token, err := m.CreateToken(claims, ttl) if err != nil { return err } + return m.SetCookie(w, token, ttl, domain, isSecure) +} + +func (m *OAuth2Middleware) SetCookie(w http.ResponseWriter, value string, ttl time.Duration, domain string, isSecure bool) error { // Generate the cookie cookie := http.Cookie{ Name: m.meta.CookieName, - Value: token, + Value: value, MaxAge: int(ttl.Seconds()), HttpOnly: true, Secure: isSecure, @@ -336,9 +340,9 @@ func (m *OAuth2Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { }) } -func (m *OAuth2Middleware) GetComponentMetadata() map[string]string { - metadataStruct := OAuth2MiddlewareMetadata{} - metadataInfo := map[string]string{} - mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo +// IsRequestSecure returns true if the request is using a secure context. +func IsRequestSecure(r *http.Request) bool { + // To check if the request is coming in using HTTPS, we check if the scheme of the URL is "https" + // Checking for `r.TLS` alone may not work if Dapr is behind a proxy that does TLS termination + return r.URL.Scheme == "https" } From dee80af5c811371bb0e10c0dc487a7f6d1fa2b3c Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 21:49:59 +0000 Subject: [PATCH 10/17] Added metadata.yaml for components Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/cookie/metadata.yaml | 96 +++++++++++++++++++++ middleware/http/oauth2/header/metadata.yaml | 88 +++++++++++++++++++ middleware/http/routeralias/metadata.yaml | 10 +-- 3 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 middleware/http/oauth2/cookie/metadata.yaml create mode 100644 middleware/http/oauth2/header/metadata.yaml diff --git a/middleware/http/oauth2/cookie/metadata.yaml b/middleware/http/oauth2/cookie/metadata.yaml new file mode 100644 index 0000000000..f126d24fc4 --- /dev/null +++ b/middleware/http/oauth2/cookie/metadata.yaml @@ -0,0 +1,96 @@ +# yaml-language-server: $schema=../../../../component-metadata-schema.json +schemaVersion: "v1" +type: "middleware" +name: "http.oauth2.cookie" +version: "v1" +status: "alpha" +title: "OAuth2 (Cookie)" +description: | + Enables the OAuth2 Authorization Code flow, storing the authentication token in a cookie on the client. +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-oauth2/ +metadata: + - name: clientID + description: | + Client ID of the OAuth2 application. + type: string + required: true + sensitive: false + example: | + "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" + - name: clientSecret + description: | + Client secret of the OAuth2 application. + type: string + required: true + sensitive: true + example: | + "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" + - name: scopes + description: | + Scopes to request, as a comma-separated string + type: string + required: false + default: "" + example: | + Google: "https://www.googleapis.com/auth/userinfo.email" + Microsoft Graph: "https://graph.microsoft.com/User.Read" + - name: authURL + description: | + URL of the OAuth2 authorization server. + type: string + required: true + example: | + Google: "https://accounts.google.com/o/oauth2/v2/auth" + Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/authorize" + - name: tokenURL + description: | + URL of the OAuth2 token endpoint, used to exchange an authorization code for an access token. + type: string + required: true + example: | + Google: "https://accounts.google.com/o/oauth2/token" + Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token" + - name: authHeaderName + description: | + Name of the header forwarded to the application, containing the token. + type: string + required: false + default: | + "Authorization" + example: | + "x-dapr-auth" + - name: redirectURL + description: | + The URL of your application that the authorization server should redirect to once the user has authenticated. + type: string + required: true + example: | + "https://myapp.com" + - name: forceHTTPS + description: | + Forces the use of TLS/HTTPS for the redirect URL. + type: bool + required: false + default: | + "false" + example: | + "true" + - name: "tokenEncryptionKey" + description: | + Token encryption and signing key (technically, seed used to derive those two). It is recommended to provide a random string with sufficient entropy. + This can for example be generated with `openssl rand -base64 24` (the value does not need to be base64-encoded). + type: string + required: true + example: | + "21af6uV80hocAzs7/Ok4oOcdMCqPQit6" + - name: "cookieName" + description: | + Name of the cookie used by Dapr to store the (encrypted) access token and session state during authentication. + type: string + required: false + default: | + "_dapr_oauth2" + example: | + "auth" diff --git a/middleware/http/oauth2/header/metadata.yaml b/middleware/http/oauth2/header/metadata.yaml new file mode 100644 index 0000000000..fd1d2a1747 --- /dev/null +++ b/middleware/http/oauth2/header/metadata.yaml @@ -0,0 +1,88 @@ +# yaml-language-server: $schema=../../../../component-metadata-schema.json +schemaVersion: "v1" +type: "middleware" +name: "http.oauth2.header" +version: "v1" +status: "alpha" +title: "OAuth2 (Header)" +description: | + Enables the OAuth2 Authorization Code flow, requiring access tokens to be passed in the "Authorization" header in requests to Dapr. +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-oauth2-header/ +metadata: + - name: clientID + description: | + Client ID of the OAuth2 application. + type: string + required: true + sensitive: false + example: | + "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" + - name: clientSecret + description: | + Client secret of the OAuth2 application. + type: string + required: true + sensitive: true + example: | + "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" + - name: scopes + description: | + Scopes to request, as a comma-separated string + type: string + required: false + default: "" + example: | + Google: "https://www.googleapis.com/auth/userinfo.email" + Microsoft Graph: "https://graph.microsoft.com/User.Read" + - name: authURL + description: | + URL of the OAuth2 authorization server. + type: string + required: true + example: | + Google: "https://accounts.google.com/o/oauth2/v2/auth" + Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/authorize" + - name: tokenURL + description: | + URL of the OAuth2 token endpoint, used to exchange an authorization code for an access token. + type: string + required: true + example: | + Google: "https://accounts.google.com/o/oauth2/token" + Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token" + - name: authHeaderName + description: | + Name of the header forwarded to the application, containing the token. + Note that this impacts only the name of the header forwarded to the application. Dapr always expects access tokens in the "Authorization" header. + type: string + required: false + default: | + "Authorization" + example: | + "x-dapr-auth" + - name: redirectURL + description: | + The URL of your application that the authorization server should redirect to once the user has authenticated. + type: string + required: true + example: | + "https://myapp.com" + - name: "tokenEncryptionKey" + description: | + Token encryption and signing key (technically, seed used to derive those two). It is recommended to provide a random string with sufficient entropy. + This can for example be generated with `openssl rand -base64 24` (the value does not need to be base64-encoded). + type: string + required: true + example: | + "21af6uV80hocAzs7/Ok4oOcdMCqPQit6" + - name: "cookieName" + description: | + Name of the cookie used by Dapr to store session state during authentication. + type: string + required: false + default: | + "_dapr_oauth2" + example: | + "auth" diff --git a/middleware/http/routeralias/metadata.yaml b/middleware/http/routeralias/metadata.yaml index 1db73ba014..d3c0f65e67 100644 --- a/middleware/http/routeralias/metadata.yaml +++ b/middleware/http/routeralias/metadata.yaml @@ -1,9 +1,9 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json -schemaVersion: v1 -type: middleware -name: routeralias -version: v1 -status: alpha +schemaVersion: "v1" +type: "middleware" +name: "http.routeralias" +version: "v1" +status: "alpha" title: "Router Alias" urls: - title: Reference From 5140149947ddc9b1e7faecfd9ad8bf7a9b3a564d Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 3 Jul 2023 21:54:43 +0000 Subject: [PATCH 11/17] Use official fork for github.com/lestrrat-go/jwx/v2 Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 5cdadd875f..1e75b08f70 100644 --- a/go.mod +++ b/go.mod @@ -399,5 +399,5 @@ replace github.com/Shopify/sarama => github.com/Shopify/sarama v1.37.2 // this is a fork which addresses a performance issues due to go routines. replace dubbo.apache.org/dubbo-go/v3 => dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3 -// TODO: Remove once a new release of jwx that fixes https://github.com/lestrrat-go/jwx/pull/952 is merged -replace github.com/lestrrat-go/jwx/v2 => github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93 +// TODO: Remove when version 2.0.12 is released +replace github.com/lestrrat-go/jwx/v2 => github.com/lestrrat-go/jwx/v2 v2.0.11-0.20230703012827-2d138a353358 diff --git a/go.sum b/go.sum index 27d6afd5e0..2db318a94f 100644 --- a/go.sum +++ b/go.sum @@ -1302,8 +1302,6 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93 h1:pMCEn1Bkv3o53Qw0wMPZtRzUKS3VA5Ydcl8EthnAVeA= -github.com/italypaleale/jwx/v2 v2.0.0-20230702235135-620113e5ff93/go.mod h1:SmVQUhXxinEHWAFVPIMbDXG7JsBWTn03HwVivS5bp/g= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -1434,6 +1432,8 @@ github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbq github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx v1.2.24/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/jwx/v2 v2.0.11-0.20230703012827-2d138a353358 h1:rhdk2XBbz5oAgZIQ9RynDu7eAF8xkqM6b7yjUvEfT+w= +github.com/lestrrat-go/jwx/v2 v2.0.11-0.20230703012827-2d138a353358/go.mod h1:SmVQUhXxinEHWAFVPIMbDXG7JsBWTn03HwVivS5bp/g= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= From a0c1548026e4ae708bb4576c8a17a5491efbef2e Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:23:22 +0000 Subject: [PATCH 12/17] Merged the two components into a single one Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/cookie/cookie.go | 110 ------------- middleware/http/oauth2/header/header.go | 106 ------------- middleware/http/oauth2/header/metadata.yaml | 88 ---------- middleware/http/oauth2/{impl => }/metadata.go | 30 +++- .../http/oauth2/{cookie => }/metadata.yaml | 14 +- .../oauth2/{impl => }/oauth2_middleware.go | 150 ++++++++++++++---- 6 files changed, 157 insertions(+), 341 deletions(-) delete mode 100644 middleware/http/oauth2/cookie/cookie.go delete mode 100644 middleware/http/oauth2/header/header.go delete mode 100644 middleware/http/oauth2/header/metadata.yaml rename middleware/http/oauth2/{impl => }/metadata.go (86%) rename middleware/http/oauth2/{cookie => }/metadata.yaml (80%) rename middleware/http/oauth2/{impl => }/oauth2_middleware.go (69%) diff --git a/middleware/http/oauth2/cookie/cookie.go b/middleware/http/oauth2/cookie/cookie.go deleted file mode 100644 index 9f4da8d012..0000000000 --- a/middleware/http/oauth2/cookie/cookie.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2023 The Dapr Authors -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. -*/ - -// Package oauth2cookie is a concrete implementation of the oauth2 middleware that stores the token in a cookie in the client. -package oauth2cookie - -import ( - "context" - "fmt" - "net/http" - "reflect" - "time" - - "github.com/dapr/components-contrib/internal/httputils" - mdutils "github.com/dapr/components-contrib/metadata" - "github.com/dapr/components-contrib/middleware" - "github.com/dapr/components-contrib/middleware/http/oauth2/impl" - "github.com/dapr/kit/logger" -) - -const ( - claimRedirect = "redirect" -) - -type OAuth2CookieMiddlewareMetadata struct { - impl.OAuth2MiddlewareMetadata `mapstructure:",squash"` - - // Forces the use of TLS/HTTPS for the redirect URL. - // Defaults to false. - ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` -} - -// NewOAuth2CookieMiddleware returns a new oAuth2 middleware. -func NewOAuth2CookieMiddleware(log logger.Logger) middleware.Middleware { - mw := &OAuth2CookieMiddleware{ - OAuth2Middleware: impl.OAuth2Middleware{ - Logger: log, - }, - } - mw.OAuth2Middleware.GetTokenFn = mw.GetClaimsFromCookie - mw.OAuth2Middleware.SetTokenFn = mw.setTokenInResponse - mw.OAuth2Middleware.ClaimsForAuthFn = mw.claimsForAuth - return mw -} - -// OAuth2CookieMiddleware is an OAuth2 authentication middleware that stores session data in a cookie. -type OAuth2CookieMiddleware struct { - impl.OAuth2Middleware - - meta OAuth2CookieMiddlewareMetadata -} - -// GetHandler retruns the HTTP handler provided by the middleware. -func (m *OAuth2CookieMiddleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - err := m.meta.FromMetadata(metadata, m.Logger) - if err != nil { - return nil, fmt.Errorf("invalid metadata: %w", err) - } - m.OAuth2Middleware.SetMetadata(m.meta.OAuth2MiddlewareMetadata) - - return m.OAuth2Middleware.GetHandler(ctx) -} - -func (m *OAuth2CookieMiddleware) claimsForAuth(r *http.Request) (map[string]string, error) { - // Get the redirect URL - redirectURL := r.URL - if m.meta.ForceHTTPS { - redirectURL.Scheme = "https" - } - - return map[string]string{ - claimRedirect: redirectURL.String(), - }, nil -} - -func (m *OAuth2CookieMiddleware) setTokenInResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { - if reqClaims[claimRedirect] == "" { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Error("Missing claim 'redirect'") - return - } - - // Set the claims in the response - err := m.SetCookie(w, token, exp, r.URL.Host, impl.IsRequestSecure(r)) - if err != nil { - httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Errorf("Failed to set cookie in the response: %v", err) - return - } - - // Redirect to the URL set in the request - httputils.RespondWithRedirect(w, http.StatusFound, reqClaims[claimRedirect]) -} - -func (m *OAuth2CookieMiddleware) GetComponentMetadata() map[string]string { - metadataStruct := OAuth2CookieMiddlewareMetadata{} - metadataInfo := map[string]string{} - mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo -} diff --git a/middleware/http/oauth2/header/header.go b/middleware/http/oauth2/header/header.go deleted file mode 100644 index fe86e6fdc5..0000000000 --- a/middleware/http/oauth2/header/header.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright 2023 The Dapr Authors -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. -*/ - -// Package oauth2header is a concrete implementation of the oauth2 middleware that expects the token to be present in the Authorization header. -package oauth2header - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "reflect" - "strings" - "time" - - mdutils "github.com/dapr/components-contrib/metadata" - "github.com/dapr/components-contrib/middleware" - "github.com/dapr/components-contrib/middleware/http/oauth2/impl" - "github.com/dapr/kit/logger" -) - -const ( - headerName = "Authorization" - bearerPrefix = "bearer " -) - -type OAuth2HeaderMiddlewareMetadata struct { - impl.OAuth2MiddlewareMetadata `mapstructure:",squash"` -} - -// NewOAuth2HeaderMiddleware returns a new OAuth2 middleware. -func NewOAuth2HeaderMiddleware(log logger.Logger) middleware.Middleware { - mw := &OAuth2HeaderMiddleware{ - OAuth2Middleware: impl.OAuth2Middleware{ - Logger: log, - }, - } - mw.OAuth2Middleware.GetTokenFn = mw.getClaimsFromHeader - mw.OAuth2Middleware.SetTokenFn = mw.setTokenResponse - return mw -} - -// OAuth2HeaderMiddleware is an OAuth2 authentication middleware that stores session data in a cookie. -type OAuth2HeaderMiddleware struct { - impl.OAuth2Middleware - - logger logger.Logger - meta OAuth2HeaderMiddlewareMetadata -} - -// GetHandler retruns the HTTP handler provided by the middleware. -func (m *OAuth2HeaderMiddleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { - err := m.meta.FromMetadata(metadata, m.logger) - if err != nil { - return nil, fmt.Errorf("invalid metadata: %w", err) - } - m.OAuth2Middleware.SetMetadata(m.meta.OAuth2MiddlewareMetadata) - - return m.OAuth2Middleware.GetHandler(ctx) -} - -func (m *OAuth2HeaderMiddleware) getClaimsFromHeader(r *http.Request) (map[string]string, error) { - // Get the header which should contain the JWE - // The "Bearer " prefix is optional - header := r.Header.Get(headerName) - if header == "" { - return nil, nil - } - if len(header) > len(bearerPrefix) && strings.ToLower(header[0:len(bearerPrefix)]) == bearerPrefix { - header = header[len(bearerPrefix):] - } - - return m.ParseToken(header) -} - -func (m *OAuth2HeaderMiddleware) setTokenResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { - // Delete the state token cookie - m.UnsetCookie(w, impl.IsRequestSecure(r)) - - // Set the response in the body - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - enc := json.NewEncoder(w) - enc.SetEscapeHTML(false) - enc.Encode(map[string]string{ - headerName: token, - }) -} - -func (m *OAuth2HeaderMiddleware) GetComponentMetadata() map[string]string { - metadataStruct := OAuth2HeaderMiddlewareMetadata{} - metadataInfo := map[string]string{} - mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo -} diff --git a/middleware/http/oauth2/header/metadata.yaml b/middleware/http/oauth2/header/metadata.yaml deleted file mode 100644 index fd1d2a1747..0000000000 --- a/middleware/http/oauth2/header/metadata.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# yaml-language-server: $schema=../../../../component-metadata-schema.json -schemaVersion: "v1" -type: "middleware" -name: "http.oauth2.header" -version: "v1" -status: "alpha" -title: "OAuth2 (Header)" -description: | - Enables the OAuth2 Authorization Code flow, requiring access tokens to be passed in the "Authorization" header in requests to Dapr. -urls: - - title: Reference - url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-oauth2-header/ -metadata: - - name: clientID - description: | - Client ID of the OAuth2 application. - type: string - required: true - sensitive: false - example: | - "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" - - name: clientSecret - description: | - Client secret of the OAuth2 application. - type: string - required: true - sensitive: true - example: | - "39cc4eaf-4ad9-4bb2-bf84-791678dbbf57" - - name: scopes - description: | - Scopes to request, as a comma-separated string - type: string - required: false - default: "" - example: | - Google: "https://www.googleapis.com/auth/userinfo.email" - Microsoft Graph: "https://graph.microsoft.com/User.Read" - - name: authURL - description: | - URL of the OAuth2 authorization server. - type: string - required: true - example: | - Google: "https://accounts.google.com/o/oauth2/v2/auth" - Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/authorize" - - name: tokenURL - description: | - URL of the OAuth2 token endpoint, used to exchange an authorization code for an access token. - type: string - required: true - example: | - Google: "https://accounts.google.com/o/oauth2/token" - Azure AD: "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token" - - name: authHeaderName - description: | - Name of the header forwarded to the application, containing the token. - Note that this impacts only the name of the header forwarded to the application. Dapr always expects access tokens in the "Authorization" header. - type: string - required: false - default: | - "Authorization" - example: | - "x-dapr-auth" - - name: redirectURL - description: | - The URL of your application that the authorization server should redirect to once the user has authenticated. - type: string - required: true - example: | - "https://myapp.com" - - name: "tokenEncryptionKey" - description: | - Token encryption and signing key (technically, seed used to derive those two). It is recommended to provide a random string with sufficient entropy. - This can for example be generated with `openssl rand -base64 24` (the value does not need to be base64-encoded). - type: string - required: true - example: | - "21af6uV80hocAzs7/Ok4oOcdMCqPQit6" - - name: "cookieName" - description: | - Name of the cookie used by Dapr to store session state during authentication. - type: string - required: false - default: | - "_dapr_oauth2" - example: | - "auth" diff --git a/middleware/http/oauth2/impl/metadata.go b/middleware/http/oauth2/metadata.go similarity index 86% rename from middleware/http/oauth2/impl/metadata.go rename to middleware/http/oauth2/metadata.go index d8792b0ddd..d46f90d0df 100644 --- a/middleware/http/oauth2/impl/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -11,7 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package impl +package oauth2 import ( "crypto" @@ -33,6 +33,9 @@ import ( ) const ( + modeCookie = "cookie" + modeHeader = "header" + defaultAuthHeaderName = "Authorization" defaultCookieName = "_dapr_oauth2" @@ -42,6 +45,10 @@ const ( // OAuth2MiddlewareMetadata is the OAuth2 middleware config. type OAuth2MiddlewareMetadata struct { + // Mode of operation: 'cookie' (default) or 'header' + // - In 'cookie' mode, access tokens are set in cookies, and clients are automatically redirected to the URL they requested before authenticating. + // - In 'header' mode, access tokens must be passed to Dapr in the "Authorization" header. Once the user has been authenticated, the access token is returned in the body of the response; clients are not automatically redirected. + Mode string `json:"mode" mapstructure:"mode"` // Client ID of the OAuth2 application. // Required. ClientID string `json:"clientID" mapstructure:"clientID"` @@ -71,6 +78,10 @@ type OAuth2MiddlewareMetadata struct { // Name of the cookie where Dapr will store the session state during authentication (and when using cookies for storage, the encrypted access token too). // Defaults to "_dapr_oauth2". CookieName string `json:"cookieName" mapstructure:"cookieName"` + // Forces the use of TLS/HTTPS for the redirect URL. + // Only used when "mode" is "cookie" + // Defaults to false. + ForceHTTPS bool `json:"forceHTTPS" mapstructure:"forceHTTPS"` // Internal: token encryption key encKey jwk.Key @@ -83,12 +94,9 @@ type OAuth2MiddlewareMetadata struct { // Parse the component's metadata into the object. func (md *OAuth2MiddlewareMetadata) FromMetadata(metadata middleware.Metadata, log logger.Logger) error { // Set default values - if md.AuthHeaderName == "" { - md.AuthHeaderName = defaultAuthHeaderName - } - if md.CookieName == "" { - md.CookieName = defaultCookieName - } + md.AuthHeaderName = defaultAuthHeaderName + md.CookieName = defaultCookieName + md.Mode = modeCookie // Decode the properties err := mdutils.DecodeMetadata(metadata.Properties, md) @@ -97,6 +105,14 @@ func (md *OAuth2MiddlewareMetadata) FromMetadata(metadata middleware.Metadata, l } // Check required fields + md.Mode = strings.ToLower(md.Mode) + switch md.Mode { + case "cookie", "header": + // Ok + default: + return errors.New("field 'mode' is invalid: must be 'cookie' (default) or 'header'") + } + if md.ClientID == "" { return errors.New("required field 'clientID' is empty") } diff --git a/middleware/http/oauth2/cookie/metadata.yaml b/middleware/http/oauth2/metadata.yaml similarity index 80% rename from middleware/http/oauth2/cookie/metadata.yaml rename to middleware/http/oauth2/metadata.yaml index f126d24fc4..31e3d31af9 100644 --- a/middleware/http/oauth2/cookie/metadata.yaml +++ b/middleware/http/oauth2/metadata.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../../../../component-metadata-schema.json +# yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: "v1" type: "middleware" name: "http.oauth2.cookie" @@ -11,6 +11,17 @@ urls: - title: Reference url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-oauth2/ metadata: + - name: mode + description: | + Mode of operation: 'cookie' (default) or 'header': + + - In 'cookie' mode, access tokens are set in cookies, and clients are automatically redirected to the URL they requested before authenticating. + - In 'header' mode, access tokens must be passed to Dapr in the "Authorization" header. Once the user has been authenticated, the access token is returned in the body of the response; clients are not automatically redirected. + required: false + type: string + allowedValues: ['cookie', 'header'] + default: '"cookie"' + example: '"cookie"' - name: clientID description: | Client ID of the OAuth2 application. @@ -71,6 +82,7 @@ metadata: - name: forceHTTPS description: | Forces the use of TLS/HTTPS for the redirect URL. + Only used when 'mode' is 'cookie'. type: bool required: false default: | diff --git a/middleware/http/oauth2/impl/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go similarity index 69% rename from middleware/http/oauth2/impl/oauth2_middleware.go rename to middleware/http/oauth2/oauth2_middleware.go index 213879c844..8425c86c19 100644 --- a/middleware/http/oauth2/impl/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -11,14 +11,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package impl contains the abstract, shared implementation for the OAuth2 middleware. -package impl +package oauth2 import ( "context" + "encoding/json" "errors" "fmt" "net/http" + "reflect" + "strings" "time" "github.com/google/uuid" @@ -29,6 +31,8 @@ import ( "golang.org/x/oauth2" "github.com/dapr/components-contrib/internal/httputils" + mdutils "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/middleware" "github.com/dapr/kit/logger" ) @@ -43,34 +47,44 @@ const ( claimToken = "tkn" claimTokenType = "tkt" claimState = "state" + claimRedirect = "redirect" + + headerName = "Authorization" + bearerPrefix = "bearer " ) // ErrTokenTooLargeForCookie is returned by SetSecureCookie when the token is too large to be stored in a cookie var ErrTokenTooLargeForCookie = errors.New("token is too large to be stored in a cookie") -// OAuth2Middleware is an oAuth2 authentication middleware. +// OAuth2Middleware is an OAuth2 authentication middleware. type OAuth2Middleware struct { - Logger logger.Logger - - // GetTokenFn is the function invoked to retrieve the token from the request. - GetTokenFn func(r *http.Request) (map[string]string, error) - // ClaimsForAuthFn is an optional function invoked before redirecting to the authorization endpoint that allows setting additional claims in the state cookie. - ClaimsForAuthFn func(r *http.Request) (map[string]string, error) - // SetTokenFn is the function invoked to store the token in the response. - SetTokenFn func(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) - - meta OAuth2MiddlewareMetadata + // Function invoked to retrieve the token from the request. + getTokenFn func(r *http.Request) (map[string]string, error) + // Optional function invoked before redirecting to the authorization endpoint that allows setting additional claims in the state cookie. + claimsForAuthFn func(r *http.Request) (map[string]string, error) + // Function invoked to store the token in the response. + setTokenFn func(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) + + logger logger.Logger + meta OAuth2MiddlewareMetadata } -// SetMetadata sets the metadata in the object. -func (m *OAuth2Middleware) SetMetadata(meta OAuth2MiddlewareMetadata) { - m.meta = meta +// NewOAuth2Middleware returns a new OAuth2 middleware. +func NewOAuth2Middleware(log logger.Logger) middleware.Middleware { + return &OAuth2Middleware{ + logger: log, + } } // GetHandler retruns the HTTP handler provided by the middleware. -func (m *OAuth2Middleware) GetHandler(ctx context.Context) (func(next http.Handler) http.Handler, error) { +func (m *OAuth2Middleware) GetHandler(ctx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) { + err := m.meta.FromMetadata(metadata, m.logger) + if err != nil { + return nil, fmt.Errorf("invalid metadata: %w", err) + } + // Derive the token keys - err := m.meta.setTokenKeys() + err = m.meta.setTokenKeys() if err != nil { return nil, fmt.Errorf("failed to derive token keys: %w", err) } @@ -78,6 +92,17 @@ func (m *OAuth2Middleware) GetHandler(ctx context.Context) (func(next http.Handl // Create the OAuth2 configuration object m.meta.setOAuth2Conf() + // Set the callbacks depending on the mode of operation + switch m.meta.Mode { + case modeCookie: + m.getTokenFn = m.GetClaimsFromCookie + m.setTokenFn = m.cookieModeSetTokenInResponse + m.claimsForAuthFn = m.cookieModeClaimsForAuth + case modeHeader: + m.getTokenFn = m.headerModeGetClaimsFromHeader + m.setTokenFn = m.headerModeSetTokenResponse + } + return m.handler, nil } @@ -92,7 +117,7 @@ func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { if err != nil { // If the cookie is invalid, redirect to the auth endpoint again // This will overwrite the old cookie - m.Logger.Debugf("Invalid session cookie: %v", err) + m.logger.Debugf("Invalid session cookie: %v", err) m.redirectToAuthenticationEndpoint(w, r) return } @@ -103,10 +128,10 @@ func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { // Get the token from the request // This uses the function provided by the implementation - claims, err := m.GetTokenFn(r) + claims, err := m.getTokenFn(r) if err != nil { // If the function returns an error, redirect to the auth endpoint again - m.Logger.Debugf("Invalid session: %v", err) + m.logger.Debugf("Invalid session: %v", err) m.redirectToAuthenticationEndpoint(w, r) return } @@ -136,18 +161,18 @@ func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWrite stateObj, err := uuid.NewRandom() if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Errorf("Failed to generate UUID: %v", err) + m.logger.Errorf("Failed to generate UUID: %v", err) return } state := stateObj.String() // Get additional claims from the implementation var claims map[string]string - if m.ClaimsForAuthFn != nil { - claims, err = m.ClaimsForAuthFn(r) + if m.claimsForAuthFn != nil { + claims, err = m.claimsForAuthFn(r) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Errorf("Failed to retrieve claims for the authentication endpoint: %v", err) + m.logger.Errorf("Failed to retrieve claims for the authentication endpoint: %v", err) return } } else { @@ -159,7 +184,7 @@ func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWrite err = m.setCookieWithClaims(w, claims, authenticationTimeout, domain, isSecureContext) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Errorf("Failed to set secure cookie: %v", err) + m.logger.Errorf("Failed to set secure cookie: %v", err) return } @@ -178,7 +203,7 @@ func (m *OAuth2Middleware) exchangeAccessCode(w http.ResponseWriter, r *http.Req accessToken, err := m.meta.oauth2Conf.Exchange(r.Context(), code) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Error("Failed to exchange token") + m.logger.Error("Failed to exchange token") return } @@ -195,12 +220,12 @@ func (m *OAuth2Middleware) exchangeAccessCode(w http.ResponseWriter, r *http.Req }, exp) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) - m.Logger.Errorf("Failed to generate token: %v", err) + m.logger.Errorf("Failed to generate token: %v", err) return } // Allow the implementation to send the response - m.SetTokenFn(w, r, reqClaims, token, exp) + m.setTokenFn(w, r, reqClaims, token, exp) } func (m *OAuth2Middleware) ParseToken(token string) (map[string]string, error) { @@ -340,6 +365,73 @@ func (m *OAuth2Middleware) UnsetCookie(w http.ResponseWriter, isSecure bool) { }) } +func (m *OAuth2Middleware) cookieModeClaimsForAuth(r *http.Request) (map[string]string, error) { + // Get the redirect URL + redirectURL := r.URL + if m.meta.ForceHTTPS { + redirectURL.Scheme = "https" + } + + return map[string]string{ + claimRedirect: redirectURL.String(), + }, nil +} + +func (m *OAuth2Middleware) cookieModeSetTokenInResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { + if reqClaims[claimRedirect] == "" { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Error("Missing claim 'redirect'") + return + } + + // Set the claims in the response + err := m.SetCookie(w, token, exp, r.URL.Host, IsRequestSecure(r)) + if err != nil { + httputils.RespondWithError(w, http.StatusInternalServerError) + m.logger.Errorf("Failed to set cookie in the response: %v", err) + return + } + + // Redirect to the URL set in the request + httputils.RespondWithRedirect(w, http.StatusFound, reqClaims[claimRedirect]) +} + +func (m *OAuth2Middleware) headerModeGetClaimsFromHeader(r *http.Request) (map[string]string, error) { + // Get the header which should contain the JWE + // The "Bearer " prefix is optional + header := r.Header.Get(headerName) + if header == "" { + return nil, nil + } + if len(header) > len(bearerPrefix) && strings.ToLower(header[0:len(bearerPrefix)]) == bearerPrefix { + header = header[len(bearerPrefix):] + } + + return m.ParseToken(header) +} + +func (m *OAuth2Middleware) headerModeSetTokenResponse(w http.ResponseWriter, r *http.Request, reqClaims map[string]string, token string, exp time.Duration) { + // Delete the state token cookie + m.UnsetCookie(w, IsRequestSecure(r)) + + // Set the response in the body + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + enc.Encode(map[string]string{ + headerName: token, + }) +} + +func (m *OAuth2Middleware) GetComponentMetadata() map[string]string { + metadataStruct := OAuth2MiddlewareMetadata{} + metadataInfo := map[string]string{} + mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) + return metadataInfo +} + // IsRequestSecure returns true if the request is using a secure context. func IsRequestSecure(r *http.Request) bool { // To check if the request is coming in using HTTPS, we check if the scheme of the URL is "https" From 4990d736fefa7331522aa7252a9c8cc361adf2bd Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:27:03 +0000 Subject: [PATCH 13/17] Fixed metadata Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- .build-tools/component-folders.json | 2 -- middleware/http/oauth2/metadata.yaml | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.build-tools/component-folders.json b/.build-tools/component-folders.json index b679eedabe..ecea136ffb 100644 --- a/.build-tools/component-folders.json +++ b/.build-tools/component-folders.json @@ -24,8 +24,6 @@ "configuration/redis/internal", "crypto/azure", "crypto/kubernetes", - "middleware/http/oauth2", - "middleware/http/oauth2/impl", "pubsub/aws", "pubsub/azure", "pubsub/azure/servicebus", diff --git a/middleware/http/oauth2/metadata.yaml b/middleware/http/oauth2/metadata.yaml index 31e3d31af9..db70c4a76c 100644 --- a/middleware/http/oauth2/metadata.yaml +++ b/middleware/http/oauth2/metadata.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: "v1" type: "middleware" -name: "http.oauth2.cookie" +name: "http.oauth2" version: "v1" status: "alpha" -title: "OAuth2 (Cookie)" +title: "OAuth2" description: | - Enables the OAuth2 Authorization Code flow, storing the authentication token in a cookie on the client. + Enables the OAuth2 Authorization Code flow. urls: - title: Reference url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-oauth2/ @@ -99,7 +99,7 @@ metadata: "21af6uV80hocAzs7/Ok4oOcdMCqPQit6" - name: "cookieName" description: | - Name of the cookie used by Dapr to store the (encrypted) access token and session state during authentication. + Name of the cookie used by Dapr to store the encrypted access token (if 'mode' is 'cookie') and session state during authentication. type: string required: false default: | From 77c2f875e7c39b6f3b8362e28e362b435f3115da Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:12:56 +0000 Subject: [PATCH 14/17] Make tokenCompressionThreshold a const Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/oauth2_middleware.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 8425c86c19..c46eda7849 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -43,6 +43,8 @@ const ( allowedClockSkew = 5 * time.Minute // Issuer for JWTs jwtIssuer = "oauth2.dapr.io" + // Min size of the access token returned by the IdP, in bytes, before it's compressed + tokenCompressionThreshold = 800 claimToken = "tkn" claimTokenType = "tkt" @@ -290,8 +292,8 @@ func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Durati // Generate the encrypted JWT var encryptOpts []jwt.EncryptOption - if claimsSize > 800 { - // If the total size of the claims is more than 800 bytes, we should enable compression + if claimsSize > tokenCompressionThreshold { + // If the total size of the claims is more than tokenCompressionThreshold, we should enable compression encryptOpts = []jwt.EncryptOption{ jwt.WithKey(jwa.A128KW, m.meta.encKey), jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), From 094dedf2420ff3e6b76547f061dde7e63cb870a0 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:18:41 +0000 Subject: [PATCH 15/17] Some unit tests Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/metadata.go | 6 +- middleware/http/oauth2/metadata_test.go | 173 ++++++++++++++++++++ middleware/http/oauth2/oauth2_middleware.go | 15 +- 3 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 middleware/http/oauth2/metadata_test.go diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go index d46f90d0df..9ccf2f4e46 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -166,10 +166,10 @@ func (md *OAuth2MiddlewareMetadata) setTokenKeys() (err error) { b = h.Sum(nil) } - // We must set a kid for the jwx library to work - kidH := sha256.New224() + // We must set a kid (Key ID) for the jwx library to work + kidH := sha256.New() kidH.Write(b) - kid := base64.RawURLEncoding.EncodeToString(kidH.Sum(nil)) + kid := base64.RawURLEncoding.EncodeToString(kidH.Sum(nil)[:8]) // Token encryption key uses 128 bits md.encKey, err = jwk.FromRaw(b[:16]) diff --git a/middleware/http/oauth2/metadata_test.go b/middleware/http/oauth2/metadata_test.go new file mode 100644 index 0000000000..592ff1551c --- /dev/null +++ b/middleware/http/oauth2/metadata_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2023 The Dapr Authors +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. +*/ + +package oauth2 + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/middleware" + "github.com/dapr/kit/logger" +) + +func TestMetadataValidation(t *testing.T) { + log := logger.NewLogger("test") + + allMeta := map[string]string{ + "clientID": "something", + "clientSecret": "something", + "authURL": "http://example.com", + "tokenURL": "http://example.com", + "redirectURL": "http://example.com", + "tokenEncryptionKey": "something", + } + + t.Run("clientID is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "clientID") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "clientID") + }) + + t.Run("clientSecret is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "clientSecret") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "clientSecret") + }) + + t.Run("authURL is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "authURL") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "authURL") + }) + + t.Run("tokenURL is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "tokenURL") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "tokenURL") + }) + + t.Run("redirectURL is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "redirectURL") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "redirectURL") + }) + + // TODO @ItalyPaleAle: uncomment for 1.13 when this becomes required + /* + t.Run("tokenEncryptionKey is required", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "tokenEncryptionKey") + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "tokenEncryptionKey") + }) + */ + + t.Run("default values", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: allMeta}}, log) + require.NoError(t, err) + + assert.Equal(t, modeCookie, md.Mode) + assert.Equal(t, defaultAuthHeaderName, md.AuthHeaderName) + assert.Equal(t, defaultCookieName, md.CookieName) + }) + + t.Run("modes", func(t *testing.T) { + t.Run("default is cookie", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + delete(meta, "mode") + + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.NoError(t, err) + + assert.Equal(t, modeCookie, md.Mode) + }) + + t.Run("set to cookie", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + meta["mode"] = "COOKIE" // Should be case-insensitive + + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.NoError(t, err) + + assert.Equal(t, modeCookie, md.Mode) + }) + + t.Run("set to header", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + meta["mode"] = "header" + + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.NoError(t, err) + + assert.Equal(t, modeHeader, md.Mode) + }) + + t.Run("invalid mode", func(t *testing.T) { + md := &OAuth2MiddlewareMetadata{} + meta := maps.Clone(allMeta) + meta["mode"] = "invalid" + + err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + require.ErrorContains(t, err, "mode") + }) + }) +} + +func TestMetadataKeys(t *testing.T) { + md := &OAuth2MiddlewareMetadata{ + TokenEncryptionKey: "Ciao.Mamma.Guarda.Come.Mi.Diverto", + } + + err := md.setTokenKeys() + require.NoError(t, err) + + require.NotNil(t, md.encKey) + require.NotNil(t, md.sigKey) + + var encKeyRaw []byte + err = md.encKey.Raw(&encKeyRaw) + require.NoError(t, err) + require.NotEmpty(t, encKeyRaw) + + var sigKeyRaw []byte + err = md.sigKey.Raw(&sigKeyRaw) + require.NoError(t, err) + require.NotEmpty(t, sigKeyRaw) + + // Given the "token encryption key" passed as metadata above, these values should be constants as there's no randomness involved + assert.Equal(t, "xM6r-SqHO14", md.encKey.KeyID()) + assert.Equal(t, "OV4ot3pasc17z5q4jOP5VA", base64.RawURLEncoding.EncodeToString(encKeyRaw)) + assert.Equal(t, "xM6r-SqHO14", md.sigKey.KeyID()) + assert.Equal(t, "R2w4G0ShOTKVb4KmBZyPych3BuPn4mOC6EYuNSXIiHU", base64.RawURLEncoding.EncodeToString(sigKeyRaw)) +} diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index c46eda7849..56a17b0efb 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -26,6 +26,7 @@ import ( "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwe" + "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/spf13/cast" "golang.org/x/oauth2" @@ -270,6 +271,12 @@ func (m *OAuth2Middleware) GetClaimsFromCookie(r *http.Request) (map[string]stri // CreateToken generates an encrypted JWT containing the claims. func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Duration) (string, error) { + return createEncryptedJWT(claims, ttl, []string{m.meta.ClientID}, m.meta.encKey, m.meta.sigKey) +} + +// Creates an encrypted JWT given the encryption and signing keys. +// Split into a separate function to allow better unit testing. +func createEncryptedJWT(claims map[string]string, ttl time.Duration, audience []string, encKey jwk.Key, sigKey jwk.Key) (string, error) { now := time.Now() exp := now.Add(ttl) @@ -277,7 +284,7 @@ func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Durati builder := jwt.NewBuilder(). IssuedAt(now). Issuer(jwtIssuer). - Audience([]string{m.meta.ClientID}). + Audience(audience). Expiration(exp). NotBefore(now) var claimsSize int @@ -295,19 +302,19 @@ func (m *OAuth2Middleware) CreateToken(claims map[string]string, ttl time.Durati if claimsSize > tokenCompressionThreshold { // If the total size of the claims is more than tokenCompressionThreshold, we should enable compression encryptOpts = []jwt.EncryptOption{ - jwt.WithKey(jwa.A128KW, m.meta.encKey), + jwt.WithKey(jwa.A128KW, encKey), jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), jwt.WithEncryptOption(jwe.WithCompress(jwa.Deflate)), } } else { encryptOpts = []jwt.EncryptOption{ - jwt.WithKey(jwa.A128KW, m.meta.encKey), + jwt.WithKey(jwa.A128KW, encKey), jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A128GCM)), } } val, err := jwt.NewSerializer(). Sign( - jwt.WithKey(jwa.HS256, m.meta.sigKey), + jwt.WithKey(jwa.HS256, sigKey), ). Encrypt(encryptOpts...). Serialize(token) From cdd00cee5cf716cfe7e2033be3f70cacb8aae01c Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Thu, 6 Jul 2023 18:11:09 +0000 Subject: [PATCH 16/17] Added tests Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- go.mod | 1 + go.sum | 2 + middleware/http/oauth2/metadata.go | 6 +- middleware/http/oauth2/oauth2_middleware.go | 11 +- .../http/oauth2/oauth2_middleware_test.go | 325 ++++++++++++++++++ 5 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 middleware/http/oauth2/oauth2_middleware_test.go diff --git a/go.mod b/go.mod index 1e75b08f70..ff92a4f24b 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/didip/tollbooth/v7 v7.0.1 github.com/eclipse/paho.mqtt.golang v1.4.2 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 + github.com/go-chi/chi/v5 v5.0.8 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.7.1 github.com/go-zookeeper/zk v1.0.3 diff --git a/go.sum b/go.sum index 2db318a94f..85dca01cab 100644 --- a/go.sum +++ b/go.sum @@ -903,6 +903,8 @@ github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-co-op/gocron v1.9.0/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97EXCRPr4k= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= diff --git a/middleware/http/oauth2/metadata.go b/middleware/http/oauth2/metadata.go index 9ccf2f4e46..72a1970800 100644 --- a/middleware/http/oauth2/metadata.go +++ b/middleware/http/oauth2/metadata.go @@ -63,12 +63,12 @@ type OAuth2MiddlewareMetadata struct { // URL of the OAuth2 token endpoint, used to exchange an authorization code for an access token. // Required. TokenURL string `json:"tokenURL" mapstructure:"tokenURL"` - // Name of the header forwarded to the application, containing the token. - // Default: "Authorization". - AuthHeaderName string `json:"authHeaderName" mapstructure:"authHeaderName"` // The URL of your application that the authorization server should redirect to once the user has authenticated. // Required. RedirectURL string `json:"redirectURL" mapstructure:"redirectURL"` + // Name of the header forwarded to the application, containing the token. + // Default: "Authorization". + AuthHeaderName string `json:"authHeaderName" mapstructure:"authHeaderName"` // Token encryption and signing key (technically, seed used to derive those two). // It is recommended to provide a random string with sufficient entropy. // Required to allow sessions to persist across restarts of the Dapr runtime and to allow multiple instances of Dapr to access the session. diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 56a17b0efb..7beeff6529 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -95,6 +95,11 @@ func (m *OAuth2Middleware) GetHandler(ctx context.Context, metadata middleware.M // Create the OAuth2 configuration object m.meta.setOAuth2Conf() + return m.getHandler(), nil +} + +// Returns the handler. This is split out to make unit testing easier. +func (m *OAuth2Middleware) getHandler() func(next http.Handler) http.Handler { // Set the callbacks depending on the mode of operation switch m.meta.Mode { case modeCookie: @@ -106,7 +111,7 @@ func (m *OAuth2Middleware) GetHandler(ctx context.Context, metadata middleware.M m.setTokenFn = m.headerModeSetTokenResponse } - return m.handler, nil + return m.handler } func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { @@ -157,7 +162,7 @@ func (m *OAuth2Middleware) handler(next http.Handler) http.Handler { func (m *OAuth2Middleware) redirectToAuthenticationEndpoint(w http.ResponseWriter, r *http.Request) { // Do this here in case ClaimsForAuthFn modifies the request object - domain := r.URL.Host + domain := r.URL.Hostname() isSecureContext := IsRequestSecure(r) // Generate a new state token @@ -394,7 +399,7 @@ func (m *OAuth2Middleware) cookieModeSetTokenInResponse(w http.ResponseWriter, r } // Set the claims in the response - err := m.SetCookie(w, token, exp, r.URL.Host, IsRequestSecure(r)) + err := m.SetCookie(w, token, exp, r.URL.Hostname(), IsRequestSecure(r)) if err != nil { httputils.RespondWithError(w, http.StatusInternalServerError) m.logger.Errorf("Failed to set cookie in the response: %v", err) diff --git a/middleware/http/oauth2/oauth2_middleware_test.go b/middleware/http/oauth2/oauth2_middleware_test.go new file mode 100644 index 0000000000..dad7f9f09b --- /dev/null +++ b/middleware/http/oauth2/oauth2_middleware_test.go @@ -0,0 +1,325 @@ +/* +Copyright 2023 The Dapr Authors +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. +*/ + +package oauth2 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "google.golang.org/grpc/test/bufconn" + + "github.com/dapr/kit/logger" +) + +const testAuthHeaderName = "x-dapr-token" + +var ( + log logger.Logger + testHTTPClient *http.Client +) + +func TestMain(m *testing.M) { + log = logger.NewLogger("test") + + // Start an internal server for returning tokens + ln := bufconn.Listen(1 << 20) // 1MB buffer size + + mux := chi.NewMux() + mux.Post("/token", func(w http.ResponseWriter, r *http.Request) { + reqBody := map[string]string{} + + switch r.Header.Get("Content-Type") { + case "application/x-www-form-urlencoded": + body, err := io.ReadAll(r.Body) + if err != nil { + log.Error("Failed to parse request body ", err) + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + + vals, err := url.ParseQuery(string(body)) + if err != nil { + log.Error("Failed to parse request body ", err) + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + for k, v := range vals { + reqBody[k] = v[0] + } + + case "application/json": + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + log.Error("Failed to parse request body ", err) + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + default: + log.Error("Invalid content type ", r.Header.Get("Content-Type")) + http.Error(w, "Invalid content type", http.StatusBadRequest) + return + } + + if reqBody["client_id"] != "client-id" || reqBody["client_secret"] != "client-secret" || + reqBody["code"] == "" || reqBody["grant_type"] != "authorization_code" { + log.Error("Invalid request body ", reqBody) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + switch reqBody["code"] { + case "bad": + http.Error(w, "Simulated error", http.StatusUnauthorized) + return + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + res := map[string]any{ + "access_token": "good-access-token", + "expires_in": 1800, + "token_type": "Bearer", + } + json.NewEncoder(w).Encode(res) + return + } + }) + srv := http.Server{ + Handler: mux, + } + go srv.Serve(ln) + defer srv.Close() + + testHTTPClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return ln.DialContext(ctx) + }, + }, + } + + os.Exit(m.Run()) +} + +func TestMiddlewareHeader(t *testing.T) { + md := OAuth2MiddlewareMetadata{ + Mode: modeHeader, + ClientID: "client-id", + ClientSecret: "client-secret", + Scopes: "profile,email", + AuthURL: "http://localhost:8000/auth", + TokenURL: "http://localhost:8000/token", + RedirectURL: "http://localhost:4000/", + AuthHeaderName: testAuthHeaderName, + TokenEncryptionKey: "Ciao.Mamma.Guarda.Come.Mi.Diverto", + CookieName: defaultCookieName, + } + md.setOAuth2Conf() + err := md.setTokenKeys() + require.NoError(t, err) + + mw := &OAuth2Middleware{ + logger: log, + meta: md, + } + + // Next handler + nextCh := make(chan string, 1) + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + nextCh <- r.Header.Get(testAuthHeaderName) + }) + + // Handler from the middleware component + handler := mw.getHandler()(nextHandler) + + // Create a context that has the custom HTTP client included, so the oauth2 package uses that + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, testHTTPClient) + + // Cookies, state token, and authorization token + var ( + cookies []*http.Cookie + state string + authToken string + ) + + assertRedirectToAuthEndpoint := func(t *testing.T, res *http.Response) { + // Should have a redirect to the auth URL + assert.Equal(t, http.StatusFound, res.StatusCode) + assert.NotEmpty(t, res.Header.Get("Location")) + loc, err := url.Parse(res.Header.Get("Location")) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(loc.String(), "http://localhost:8000/auth")) + assert.NotEmpty(t, loc.Query().Get("state")) + assert.Equal(t, "client-id", loc.Query().Get("client_id")) + assert.Equal(t, "http://localhost:4000/", loc.Query().Get("redirect_uri")) + assert.Equal(t, "profile email", loc.Query().Get("scope")) + + // Should have a cookie + // The cookie is an encrypted JWT so we won't verify it here + require.NotEmpty(t, res.Header.Get("Set-Cookie")) + cookies = res.Cookies() + require.Len(t, cookies, 1) + state = loc.Query().Get("state") + + // Handler should not have been called + select { + case <-time.After(100 * time.Millisecond): + // All good + case <-nextCh: + t.Fatalf("Received a signal on nextCh that was not expected") + } + } + + t.Run("missing token redirects to auth endpoint", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://localhost:4000/method", nil) + r = r.WithContext(ctx) + + handler.ServeHTTP(w, r) + + // Allow other goroutines first + runtime.Gosched() + + // Should redirect to auth endpoint + res := w.Result() + defer res.Body.Close() + assertRedirectToAuthEndpoint(t, res) + }) + + // Cannot continue if tests so far have failed + if t.Failed() { + return + } + + t.Run("token endpoint returns an error", func(t *testing.T) { + logDest := &bytes.Buffer{} + log.SetOutput(logDest) + defer log.SetOutput(os.Stdout) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://localhost:4000/method?state="+state+"&code=bad", nil) + r.AddCookie(cookies[0]) + r = r.WithContext(ctx) + + handler.ServeHTTP(w, r) + + // Allow other goroutines first + runtime.Gosched() + + // Should contain a message in the logs + assert.Contains(t, logDest.String(), "Failed to exchange token") + + // Should have an ISE in response + res := w.Result() + defer res.Body.Close() + + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("state in cookie doesn't match URL parameter", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://localhost:4000/method?state=badstate&code=good", nil) + r.AddCookie(cookies[0]) + r = r.WithContext(ctx) + + handler.ServeHTTP(w, r) + + // Allow other goroutines first + runtime.Gosched() + + // Response should be BadRequest + res := w.Result() + defer res.Body.Close() + + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + // Cannot continue if tests so far have failed + if t.Failed() { + return + } + + t.Run("obtain access token", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://localhost:4000/method?state="+state+"&code=good", nil) + r.AddCookie(cookies[0]) + r = r.WithContext(ctx) + + handler.ServeHTTP(w, r) + + // Allow other goroutines first + runtime.Gosched() + + // Response should contain the access token + res := w.Result() + defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + + body := map[string]string{} + err := json.NewDecoder(res.Body).Decode(&body) + require.NoError(t, err) + + require.NotEmpty(t, body["Authorization"]) + authToken = body["Authorization"] + + // Should delete the cookie + require.NotEmpty(t, res.Header.Get("Set-Cookie")) + cookies = res.Cookies() + require.Len(t, cookies, 1) + assert.LessOrEqual(t, cookies[0].MaxAge, 0) + + // Handler should not have been called + select { + case <-time.After(100 * time.Millisecond): + // All good + case <-nextCh: + t.Fatalf("Received a signal on nextCh that was not expected") + } + }) + + t.Run("requests with access token should succeed", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://localhost:4000/method", nil) + r = r.WithContext(ctx) + r.Header.Set("Authorization", authToken) + + handler.ServeHTTP(w, r) + + // Next handler should be invoked + select { + case <-time.After(500 * time.Millisecond): + t.Fatalf("Did not receive signal on nextCh within 500ms") + case header := <-nextCh: + assert.Equal(t, "Bearer good-access-token", header) + } + }) +} From 9cd9a8ecb5f73b2ba2e8a63d5e2404cb22eeab20 Mon Sep 17 00:00:00 2001 From: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Date: Thu, 6 Jul 2023 18:19:43 +0000 Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- middleware/http/oauth2/metadata_test.go | 22 +++++++++---------- .../http/oauth2/oauth2_middleware_test.go | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/middleware/http/oauth2/metadata_test.go b/middleware/http/oauth2/metadata_test.go index 592ff1551c..1f81e4446b 100644 --- a/middleware/http/oauth2/metadata_test.go +++ b/middleware/http/oauth2/metadata_test.go @@ -42,7 +42,7 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "clientID") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "clientID") }) @@ -50,7 +50,7 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "clientSecret") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "clientSecret") }) @@ -58,7 +58,7 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "authURL") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "authURL") }) @@ -66,7 +66,7 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "tokenURL") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "tokenURL") }) @@ -74,7 +74,7 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "redirectURL") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "redirectURL") }) @@ -84,14 +84,14 @@ func TestMetadataValidation(t *testing.T) { md := &OAuth2MiddlewareMetadata{} meta := maps.Clone(allMeta) delete(meta, "tokenEncryptionKey") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "tokenEncryptionKey") }) */ t.Run("default values", func(t *testing.T) { md := &OAuth2MiddlewareMetadata{} - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: allMeta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: allMeta}}, log) require.NoError(t, err) assert.Equal(t, modeCookie, md.Mode) @@ -105,7 +105,7 @@ func TestMetadataValidation(t *testing.T) { meta := maps.Clone(allMeta) delete(meta, "mode") - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.NoError(t, err) assert.Equal(t, modeCookie, md.Mode) @@ -116,7 +116,7 @@ func TestMetadataValidation(t *testing.T) { meta := maps.Clone(allMeta) meta["mode"] = "COOKIE" // Should be case-insensitive - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.NoError(t, err) assert.Equal(t, modeCookie, md.Mode) @@ -127,7 +127,7 @@ func TestMetadataValidation(t *testing.T) { meta := maps.Clone(allMeta) meta["mode"] = "header" - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.NoError(t, err) assert.Equal(t, modeHeader, md.Mode) @@ -138,7 +138,7 @@ func TestMetadataValidation(t *testing.T) { meta := maps.Clone(allMeta) meta["mode"] = "invalid" - err := md.FromMetadata(middleware.Metadata{metadata.Base{Properties: meta}}, log) + err := md.FromMetadata(middleware.Metadata{Base: metadata.Base{Properties: meta}}, log) require.ErrorContains(t, err, "mode") }) }) diff --git a/middleware/http/oauth2/oauth2_middleware_test.go b/middleware/http/oauth2/oauth2_middleware_test.go index dad7f9f09b..b2063872d3 100644 --- a/middleware/http/oauth2/oauth2_middleware_test.go +++ b/middleware/http/oauth2/oauth2_middleware_test.go @@ -110,6 +110,7 @@ func TestMain(m *testing.M) { return } }) + //nolint:gosec srv := http.Server{ Handler: mux, }