From 653796fa11d9f886482d9e31def981b1275e9694 Mon Sep 17 00:00:00 2001 From: Hannes Gustafsson Date: Tue, 30 Apr 2024 18:16:55 +0100 Subject: [PATCH] Add authproxy connector HMAC support The default signature header Gap-Signature is used by Buzzfeed [1]. The upstream hmac library used to calculate the signature is very old (as of writing last update 2017) but it minimizes the work to do here as implementation is trying to match the signing process of Buzzfeed [2]. It seems that signature is not calculated in a standard way; client chooses what to include in the calculation of the signature and the server must use the same input to get a matching signature. [1] https://github.com/buzzfeed/sso/blob/549155a64d6c5f8916ed909cfa4e340734056284/internal/proxy/oauthproxy.go#L25 [2] https://github.com/buzzfeed/sso/blob/549155a64d6c5f8916ed909cfa4e340734056284/docs/sso_config.md?plain=1#L105 --- connector/authproxy/authproxy.go | 37 ++++++++++++++++++++---- connector/authproxy/authproxy_test.go | 41 +++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 4 +++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go index 465c3e3d9e..848994fb3d 100644 --- a/connector/authproxy/authproxy.go +++ b/connector/authproxy/authproxy.go @@ -4,11 +4,14 @@ package authproxy import ( + "crypto" "fmt" "net/http" "net/url" "strings" + "github.com/18F/hmacauth" + "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/log" ) @@ -19,11 +22,14 @@ import ( // Headers retrieved to fetch user's email and group can be configured // with userHeader and groupHeader. type Config struct { - UserIDHeader string `json:"userIDHeader"` - UserHeader string `json:"userHeader"` - EmailHeader string `json:"emailHeader"` - GroupHeader string `json:"groupHeader"` - Groups []string `json:"staticGroups"` + UserIDHeader string `json:"userIDHeader"` + UserHeader string `json:"userHeader"` + EmailHeader string `json:"emailHeader"` + GroupHeader string `json:"groupHeader"` + Groups []string `json:"staticGroups"` + HMACSignatureHeader string `json:"hmacSignatureHeader"` + HMACSignedHeaders []string `json:"hmacSignedHeaders"` + HMACKey string `json:"hmacKey"` } // Open returns an authentication strategy which requires no user interaction. @@ -44,6 +50,16 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) if groupHeader == "" { groupHeader = "X-Remote-Group" } + hmacHeader := c.HMACSignatureHeader + if hmacHeader == "" { + hmacHeader = "Gap-Signature" + } + + var hasher hmacauth.HmacAuth + if c.HMACKey != "" { + hasher = hmacauth.NewHmacAuth(crypto.SHA256, []byte(c.HMACKey), + hmacHeader, c.HMACSignedHeaders) + } return &callback{ userIDHeader: userIDHeader, @@ -51,6 +67,8 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) emailHeader: emailHeader, groupHeader: groupHeader, groups: c.Groups, + hmacHeader: hmacHeader, + hmacAuth: hasher, logger: logger, pathSuffix: "/" + id, }, nil @@ -64,6 +82,8 @@ type callback struct { emailHeader string groupHeader string groups []string + hmacAuth hmacauth.HmacAuth + hmacHeader string logger log.Logger pathSuffix string } @@ -83,6 +103,13 @@ func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (stri // HandleCallback parses the request and returns the user's identity func (m *callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) { + if m.hmacAuth != nil { + hmacResult, _, _ := m.hmacAuth.AuthenticateRequest(r) + if hmacResult != hmacauth.ResultMatch { + return connector.Identity{}, fmt.Errorf("unexpected HMAC signature in header %q", m.hmacHeader) + } + } + remoteUser := r.Header.Get(m.userHeader) if remoteUser == "" { return connector.Identity{}, fmt.Errorf("required HTTP header %s is not set", m.userHeader) diff --git a/connector/authproxy/authproxy_test.go b/connector/authproxy/authproxy_test.go index 5e09872299..173ef4a4f3 100644 --- a/connector/authproxy/authproxy_test.go +++ b/connector/authproxy/authproxy_test.go @@ -1,11 +1,15 @@ package authproxy import ( + "bytes" + "crypto" "io" "net/http" + "net/http/httptest" "reflect" "testing" + "github.com/18F/hmacauth" "github.com/sirupsen/logrus" "github.com/dexidp/dex/connector" @@ -158,6 +162,43 @@ func TestStaticGroup(t *testing.T) { expectEquals(t, ident.Groups[5], testStaticGroup2) } +func TestHMAC_Valid(t *testing.T) { + c := Config{ + UserHeader: "X-Remote-User", + HMACSignatureHeader: "Gap-Signature", + HMACKey: "key", + } + hmacAuth := hmacauth.NewHmacAuth(crypto.SHA256, []byte(c.HMACKey), c.HMACSignatureHeader, c.HMACSignedHeaders) + conn := callback{userHeader: c.UserHeader, hmacAuth: hmacAuth, hmacHeader: c.HMACSignatureHeader} + + req := httptest.NewRequest(http.MethodGet, "/", bytes.NewBuffer([]byte(`{}`))) + hmacAuth.SignRequest(req) + req.Header.Set(c.UserHeader, "x") + + scopes := connector.Scopes{} + _, err := conn.HandleCallback(scopes, req) + expectNil(t, err) +} + +func TestHMAC_Invalid(t *testing.T) { + c := Config{ + UserHeader: "X-Remote-User", + HMACSignatureHeader: "Gap-Signature", + HMACKey: "key", + } + hmacAuth := hmacauth.NewHmacAuth(crypto.SHA256, []byte(c.HMACKey), c.HMACSignatureHeader, c.HMACSignedHeaders) + conn := callback{userHeader: c.UserHeader, hmacAuth: hmacAuth, hmacHeader: c.HMACSignatureHeader} + + req := httptest.NewRequest(http.MethodGet, "/", bytes.NewBuffer([]byte(`{}`))) + req.Header.Set(c.UserHeader, "x") + + scopes := connector.Scopes{} + _, err := conn.HandleCallback(scopes, req) + if err == nil { + t.Errorf("expected HMAC error") + } +} + func expectNil(t *testing.T, a interface{}) { if a != nil { t.Errorf("Expected %+v to equal nil", a) diff --git a/go.mod b/go.mod index 2b149e0a8a..b808aa8727 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( entgo.io/ent v0.13.1 + github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73 github.com/AppsFlyer/go-sundheit v0.5.0 github.com/Masterminds/semver v1.5.0 github.com/Masterminds/sprig/v3 v3.2.3 @@ -52,6 +53,7 @@ require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect diff --git a/go.sum b/go.sum index 99fb9c9e4a..56ed8f9437 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE= entgo.io/ent v0.13.1/go.mod h1:qCEmo+biw3ccBn9OyL4ZK5dfpwg++l1Gxwac5B1206A= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73 h1:FBZKuR39QOQwCah/AzpwDtgveu3ammIVGsxSKcuThT4= +github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73/go.mod h1:sOPSg0kyHhwq42XLYw8MpZQ6RpHXcr08rZSfRxg6ZE8= github.com/AppsFlyer/go-sundheit v0.5.0 h1:/VxpyigCfJrq1r97mn9HPiAB2qrhcTFHwNIIDr15CZM= github.com/AppsFlyer/go-sundheit v0.5.0/go.mod h1:2ZM0BnfqT/mljBQO224VbL5XH06TgWuQ6Cn+cTtCpTY= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= @@ -35,6 +37,8 @@ github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU= github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=