From 64e332f0bedfaeca54a61c470b6a7496df0f2d5d Mon Sep 17 00:00:00 2001 From: Marlin Cremers Date: Thu, 16 Jun 2022 10:54:52 +0200 Subject: [PATCH] feat: introduce auth scheme and jumping to next authentication --- .schema/config.schema.json | 15 +++++++++++ helper/bearer.go | 17 +++++++++--- helper/bearer_test.go | 18 +++++++++++++ .../authn/authenticator_bearer_token_test.go | 27 +++++++++++++++++++ .../authn/authenticator_cookie_session.go | 5 ++++ 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 24e561a3d1..153ab8a21f 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -456,6 +456,11 @@ "title": "Header", "type": "string", "description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter or cookie." + }, + "auth_scheme": { + "title": "Auth scheme", + "type": "string", + "description": "The auth scheme to accept in case the header is set to Authorization, this is by default set to Bearer." } } }, @@ -619,6 +624,11 @@ "title": "Header", "type": "string", "description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter or cookie." + }, + "auth_scheme": { + "title": "Auth scheme", + "type": "string", + "description": "The auth scheme to accept in case the header is set to Authorization, this is by default set to Bearer." } } }, @@ -842,6 +852,11 @@ "title": "Header", "type": "string", "description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter or cookie." + }, + "auth_scheme": { + "title": "Auth scheme", + "type": "string", + "description": "The auth scheme to accept in case the header is set to Authorization, this is by default set to Bearer." } } }, diff --git a/helper/bearer.go b/helper/bearer.go index 64740987cf..6af7e9c0d2 100644 --- a/helper/bearer.go +++ b/helper/bearer.go @@ -31,6 +31,7 @@ const ( type BearerTokenLocation struct { Header *string `json:"header"` + AuthScheme *string `json:"auth_scheme"` QueryParameter *string `json:"query_parameter"` Cookie *string `json:"cookie"` } @@ -39,7 +40,11 @@ func BearerTokenFromRequest(r *http.Request, tokenLocation *BearerTokenLocation) if tokenLocation != nil { if tokenLocation.Header != nil { if *tokenLocation.Header == defaultAuthorizationHeader { - return DefaultBearerTokenFromRequest(r) + authScheme := "Bearer" + if tokenLocation.AuthScheme != nil { + authScheme = *tokenLocation.AuthScheme + } + return DefaultBearerTokenFromRequest(r, authScheme) } return r.Header.Get(*tokenLocation.Header) } else if tokenLocation.QueryParameter != nil { @@ -53,13 +58,17 @@ func BearerTokenFromRequest(r *http.Request, tokenLocation *BearerTokenLocation) } } - return DefaultBearerTokenFromRequest(r) + return DefaultBearerTokenFromRequest(r, "Bearer") } -func DefaultBearerTokenFromRequest(r *http.Request) string { +func DefaultBearerTokenFromRequest(r *http.Request, authScheme string) string { token := r.Header.Get(defaultAuthorizationHeader) + if authScheme == "" { + return token + } + split := strings.SplitN(token, " ", 2) - if len(split) != 2 || !strings.EqualFold(strings.ToLower(split[0]), "bearer") { + if len(split) != 2 || !strings.EqualFold(strings.ToLower(split[0]), strings.ToLower(authScheme)) { return "" } return split[1] diff --git a/helper/bearer_test.go b/helper/bearer_test.go index 1c3582e388..cdfae842a8 100644 --- a/helper/bearer_test.go +++ b/helper/bearer_test.go @@ -74,4 +74,22 @@ func TestBearerTokenFromRequest(t *testing.T) { token := helper.BearerTokenFromRequest(request, &tokenLocation) assert.Equal(t, expectedToken, token) }) + t.Run("case=token should be received from authorization header with custom auth scheme if custom location is set to header and token is present", func(t *testing.T) { + expectedToken := "token" + customHeaderName := "Authorization" + customAuthScheme := "AccessToken" + request := &http.Request{Header: http.Header{customHeaderName: {customAuthScheme + " " + expectedToken}}} + tokenLocation := helper.BearerTokenLocation{Header: &customHeaderName, AuthScheme: &customAuthScheme} + token := helper.BearerTokenFromRequest(request, &tokenLocation) + assert.Equal(t, expectedToken, token) + }) + t.Run("case=token should be received from authorization header with an empty custom auth scheme if custom location is set to header and token is present", func(t *testing.T) { + expectedToken := "token" + customHeaderName := "Authorization" + customAuthScheme := "" + request := &http.Request{Header: http.Header{customHeaderName: {expectedToken}}} + tokenLocation := helper.BearerTokenLocation{Header: &customHeaderName, AuthScheme: &customAuthScheme} + token := helper.BearerTokenFromRequest(request, &tokenLocation) + assert.Equal(t, expectedToken, token) + }) } diff --git a/pipeline/authn/authenticator_bearer_token_test.go b/pipeline/authn/authenticator_bearer_token_test.go index f1a0cba321..11291a41c0 100644 --- a/pipeline/authn/authenticator_bearer_token_test.go +++ b/pipeline/authn/authenticator_bearer_token_test.go @@ -60,6 +60,17 @@ func TestAuthenticatorBearerToken(t *testing.T) { expectErr: true, expectExactErr: ErrAuthenticatorNotResponsible, }, + { + d: "should return error saying that authenticator is not responsible for validating the request, as the session store returns HTTP 406 Not Acceptable", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(406) + }) + }, + expectErr: true, + expectExactErr: ErrAuthenticatorNotResponsible, + }, { d: "should fail because session store returned 400", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, @@ -85,6 +96,22 @@ func TestAuthenticatorBearerToken(t *testing.T) { Extra: map[string]interface{}{"foo": "bar"}, }, }, + { + d: "should pass because session store returned 200", + r: &http.Request{Header: http.Header{"Authorization": {"AccessToken token"}}, URL: &url.URL{Path: ""}}, + config: []byte(`{"token_from": {"header": "Authorization", "auth_scheme": "AccessToken"}}`), + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(200) + w.Write([]byte(`{"sub": "123", "extra": {"foo": "bar"}}`)) + }) + }, + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + Extra: map[string]interface{}{"foo": "bar"}, + }, + }, { d: "should pass through method, path, and headers to auth server; should NOT pass through query parameters by default for backwards compatibility", r: &http.Request{Header: http.Header{"Authorization": {"bearer zyx"}}, URL: &url.URL{Path: "/users/123", RawQuery: "query=string"}, Method: "PUT"}, diff --git a/pipeline/authn/authenticator_cookie_session.go b/pipeline/authn/authenticator_cookie_session.go index 3c7a2d3a9a..0642c63315 100644 --- a/pipeline/authn/authenticator_cookie_session.go +++ b/pipeline/authn/authenticator_cookie_session.go @@ -177,6 +177,11 @@ func forwardRequestToSessionStore(r *http.Request, cf AuthenticatorForwardConfig defer res.Body.Close() + // HTTP 406 Not Acceptable + if res.StatusCode == http.StatusNotAcceptable { + return nil, errors.WithStack(ErrAuthenticatorNotResponsible) + } + if res.StatusCode == http.StatusOK { body, err := ioutil.ReadAll(res.Body) if err != nil {