From a8b76bd623ece3a08ad896963b0cbf72d2200a72 Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Mon, 29 Nov 2021 12:19:14 -0700 Subject: [PATCH] authorize: support X-Pomerium-Authorization in addition to Authorization (#2780) * authorize: support X-Pomerium-Authorization in addition to Authorization * tangentental correction Co-authored-by: alexfornuto --- authenticate/state.go | 3 +- authorize/session.go | 3 +- config/envoyconfig/listeners_test.go | 2 +- config/envoyconfig/lua_test.go | 39 +++++++++++++++++++ .../envoyconfig/luascripts/clean-upstream.lua | 6 +-- docs/docs/topics/programmatic-access.md | 4 +- internal/httputil/headers.go | 5 +++ internal/sessions/header/header_store.go | 38 ++++++++---------- internal/sessions/header/header_store_test.go | 10 ++++- internal/sessions/header/middleware_test.go | 30 +++++++++++--- proxy/state.go | 3 +- 11 files changed, 101 insertions(+), 42 deletions(-) diff --git a/authenticate/state.go b/authenticate/state.go index b2f02968d..dfbd02a65 100644 --- a/authenticate/state.go +++ b/authenticate/state.go @@ -15,7 +15,6 @@ import ( "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/ecjson" "github.com/pomerium/pomerium/internal/encoding/jws" - "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/header" @@ -114,7 +113,7 @@ func newAuthenticateStateFromConfig(cfg *config.Config) (*authenticateState, err state.encryptedEncoder = ecjson.New(state.cookieCipher) - headerStore := header.NewStore(state.encryptedEncoder, httputil.AuthorizationTypePomerium) + headerStore := header.NewStore(state.encryptedEncoder) cookieStore, err := cookie.NewStore(func() cookie.Options { return cookie.Options{ diff --git a/authorize/session.go b/authorize/session.go index dac5aa5cb..c40b7add6 100644 --- a/authorize/session.go +++ b/authorize/session.go @@ -6,7 +6,6 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" - "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/header" @@ -22,7 +21,7 @@ func loadRawSession(req *http.Request, options *config.Options, encoder encoding } loaders = append(loaders, cookieStore, - header.NewStore(encoder, httputil.AuthorizationTypePomerium), + header.NewStore(encoder), queryparam.NewStore(encoder, urlutil.QuerySession), ) diff --git a/config/envoyconfig/listeners_test.go b/config/envoyconfig/listeners_test.go index b9b0d9c69..a49b11ae4 100644 --- a/config/envoyconfig/listeners_test.go +++ b/config/envoyconfig/listeners_test.go @@ -173,7 +173,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { "name": "envoy.filters.http.lua", "typedConfig": { "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", - "inlineCode": "function remove_pomerium_cookie(cookie_name, cookie)\n -- lua doesn't support optional capture groups\n -- so we replace twice to handle pomerium=xyz at the end of the string\n cookie = cookie:gsub(cookie_name .. \"=[^;]+; \", \"\")\n cookie = cookie:gsub(cookie_name .. \"=[^;]+\", \"\")\n return cookie\nend\n\nfunction has_prefix(str, prefix)\n return str ~= nil and str:sub(1, #prefix) == prefix\nend\n\nfunction envoy_on_request(request_handle)\n local headers = request_handle:headers()\n local metadata = request_handle:metadata()\n\n local remove_cookie_name = metadata:get(\"remove_pomerium_cookie\")\n if remove_cookie_name then\n local cookie = headers:get(\"cookie\")\n if cookie ~= nil then\n newcookie = remove_pomerium_cookie(remove_cookie_name, cookie)\n headers:replace(\"cookie\", newcookie)\n end\n end\n\n local remove_authorization = metadata:get(\"remove_pomerium_authorization\")\n if remove_authorization then\n local authorization = headers:get(\"authorization\")\n local authorization_prefix = \"Pomerium \"\n if has_prefix(authorization, authorization_prefix) then\n headers:remove(\"authorization\")\n end\n end\nend\n\nfunction envoy_on_response(response_handle)\n\nend\n" + "inlineCode": "function remove_pomerium_cookie(cookie_name, cookie)\n -- lua doesn't support optional capture groups\n -- so we replace twice to handle pomerium=xyz at the end of the string\n cookie = cookie:gsub(cookie_name .. \"=[^;]+; \", \"\")\n cookie = cookie:gsub(cookie_name .. \"=[^;]+\", \"\")\n return cookie\nend\n\nfunction has_prefix(str, prefix)\n return str ~= nil and str:sub(1, #prefix) == prefix\nend\n\nfunction envoy_on_request(request_handle)\n local headers = request_handle:headers()\n local metadata = request_handle:metadata()\n\n local remove_cookie_name = metadata:get(\"remove_pomerium_cookie\")\n if remove_cookie_name then\n local cookie = headers:get(\"cookie\")\n if cookie ~= nil then\n newcookie = remove_pomerium_cookie(remove_cookie_name, cookie)\n headers:replace(\"cookie\", newcookie)\n end\n end\n\n local remove_authorization = metadata:get(\"remove_pomerium_authorization\")\n if remove_authorization then\n local authorization = headers:get(\"authorization\")\n local authorization_prefix = \"Pomerium \"\n if has_prefix(authorization, authorization_prefix) then\n headers:remove(\"authorization\")\n end\n\n headers:remove('x-pomerium-authorization')\n end\nend\n\nfunction envoy_on_response(response_handle) end\n" } }, { diff --git a/config/envoyconfig/lua_test.go b/config/envoyconfig/lua_test.go index 20426d364..25665e6da 100644 --- a/config/envoyconfig/lua_test.go +++ b/config/envoyconfig/lua_test.go @@ -10,6 +10,39 @@ import ( lua "github.com/yuin/gopher-lua" ) +func TestLuaCleanUpstream(t *testing.T) { + L := lua.NewState() + defer L.Close() + + bs, err := luaFS.ReadFile("luascripts/clean-upstream.lua") + require.NoError(t, err) + + err = L.DoString(string(bs)) + require.NoError(t, err) + + headers := map[string]string{ + "context-type": "text/plain", + "authorization": "Pomerium JWT", + "x-pomerium-authorization": "JWT", + } + metadata := map[string]interface{}{ + "remove_pomerium_authorization": true, + } + dynamicMetadata := map[string]map[string]interface{}{} + handle := newLuaResponseHandle(L, headers, metadata, dynamicMetadata) + + err = L.CallByParam(lua.P{ + Fn: L.GetGlobal("envoy_on_request"), + NRet: 0, + Protect: true, + }, handle) + require.NoError(t, err) + + assert.Equal(t, map[string]string{ + "context-type": "text/plain", + }, headers) +} + func TestLuaFixMisdirected(t *testing.T) { t.Run("request", func(t *testing.T) { L := lua.NewState() @@ -141,6 +174,12 @@ func newLuaHeaders(L *lua.LState, headers map[string]string) lua.LValue { L.Push(lua.LString(str)) return 1 }, + "remove": func(L *lua.LState) int { + _ = L.CheckTable(1) + key := L.CheckString(2) + delete(headers, key) + return 0 + }, "replace": func(L *lua.LState) int { _ = L.CheckTable(1) key := L.CheckString(2) diff --git a/config/envoyconfig/luascripts/clean-upstream.lua b/config/envoyconfig/luascripts/clean-upstream.lua index 7aba66677..64bd60315 100644 --- a/config/envoyconfig/luascripts/clean-upstream.lua +++ b/config/envoyconfig/luascripts/clean-upstream.lua @@ -30,9 +30,9 @@ function envoy_on_request(request_handle) if has_prefix(authorization, authorization_prefix) then headers:remove("authorization") end + + headers:remove('x-pomerium-authorization') end end -function envoy_on_response(response_handle) - -end +function envoy_on_response(response_handle) end diff --git a/docs/docs/topics/programmatic-access.md b/docs/docs/topics/programmatic-access.md index 8f736df0a..572964aaf 100644 --- a/docs/docs/topics/programmatic-access.md +++ b/docs/docs/topics/programmatic-access.md @@ -94,12 +94,12 @@ The application interacting with Pomerium must manage the following workflow. Co 1. The user completes the identity providers login flow. 1. The identity provider makes a callback to pomerium's authenticate service (e.g. `authenticate.corp.domain.example`) . 1. Pomerium's authenticate service creates a user session and redirect token, then redirects back to the managed endpoint (e.g. `verify.corp.domain.example`) -1. Pomerium's proxy service makes a callback request to the original `pomerium_redirect_uri` with the user session and as an argument. +1. Pomerium's proxy service makes a callback request to the original `pomerium_redirect_uri` with the user session as an argument. 1. The script or application is responsible for handling that http callback request, and securely handling the callback session (`pomerium_jwt`) queryparam. 1. The script or application can now make any requests as normal to the upstream application by setting the `Authorization: Pomerium ${pomerium_jwt}` header. :::tip -Pomerium supports `Authorization: Bearer Pomerium-${pomerium_jwt}` in addition to `Authorization: Pomerium ${pomerium_jwt}` format. +Pomerium supports `Authorization: Bearer Pomerium-${pomerium_jwt}` or `X-Pomerium-Authorization: ${pomerium_jwt}` in addition to `Authorization: Pomerium ${pomerium_jwt}` format. ::: ## Example Code diff --git a/internal/httputil/headers.go b/internal/httputil/headers.go index 93bf2bac4..8004935b9 100644 --- a/internal/httputil/headers.go +++ b/internal/httputil/headers.go @@ -5,6 +5,7 @@ const AuthorizationTypePomerium = "Pomerium" // Standard headers const ( + HeaderAuthorization = "Authorization" HeaderReferrer = "Referer" HeaderImpersonateGroup = "Impersonate-Group" HeaderUpgrade = "Upgrade" @@ -12,6 +13,10 @@ const ( // Pomerium headers contain information added to a request. const ( + // HeaderPomeriumAuthorization is the header key for a pomerium authorization JWT. It + // can be used in place of the standard authorization header if that header is being + // used by upstream applications. + HeaderPomeriumAuthorization = "x-pomerium-authorization" // HeaderPomeriumResponse is set when pomerium itself creates a response, // as opposed to the upstream application and can be used to distinguish // between an application error, and a pomerium related error when debugging. diff --git a/internal/sessions/header/header_store.go b/internal/sessions/header/header_store.go index c0a4eda78..f04ab069c 100644 --- a/internal/sessions/header/header_store.go +++ b/internal/sessions/header/header_store.go @@ -7,22 +7,16 @@ import ( "strings" "github.com/pomerium/pomerium/internal/encoding" + "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" ) var _ sessions.SessionLoader = &Store{} -const ( - defaultAuthHeader = "Authorization" - defaultAuthType = "Bearer" -) - // Store implements the load session store interface using http // authorization headers. type Store struct { - authHeader string - authType string - encoder encoding.Unmarshaler + encoder encoding.Unmarshaler } // NewStore returns a new header store for loading sessions from @@ -30,38 +24,38 @@ type Store struct { // // NOTA BENE: While most servers do not log Authorization headers by default, // you should ensure no other services are logging or leaking your auth headers. -func NewStore(enc encoding.Unmarshaler, headerType string) *Store { - if headerType == "" { - headerType = defaultAuthType - } +func NewStore(enc encoding.Unmarshaler) *Store { return &Store{ - authHeader: defaultAuthHeader, - authType: headerType, - encoder: enc, + encoder: enc, } } // LoadSession tries to retrieve the token string from the Authorization header. func (as *Store) LoadSession(r *http.Request) (string, error) { - jwt := TokenFromHeader(r, as.authHeader, as.authType) + jwt := TokenFromHeaders(r) if jwt == "" { return "", sessions.ErrNoSessionFound } return jwt, nil } -// TokenFromHeader retrieves the value of the authorization header from a given -// request, header key, and authentication type. -func TokenFromHeader(r *http.Request, authHeader, authType string) string { - bearer := r.Header.Get(authHeader) +// TokenFromHeaders retrieves the value of the authorization header(s) from a given +// request and authentication type. +func TokenFromHeaders(r *http.Request) string { + // X-Pomerium-Authorization: + if jwt := r.Header.Get(httputil.HeaderPomeriumAuthorization); jwt != "" { + return jwt + } + + bearer := r.Header.Get(httputil.HeaderAuthorization) // Authorization: Pomerium - prefix := authType + " " + prefix := httputil.AuthorizationTypePomerium + " " if strings.HasPrefix(bearer, prefix) { return bearer[len(prefix):] } // Authorization: Bearer Pomerium- - prefix = "Bearer " + authType + "-" + prefix = "Bearer " + httputil.AuthorizationTypePomerium + "-" if strings.HasPrefix(bearer, prefix) { return bearer[len(prefix):] } diff --git a/internal/sessions/header/header_store_test.go b/internal/sessions/header/header_store_test.go index 4d92f1b8b..7acf526bf 100644 --- a/internal/sessions/header/header_store_test.go +++ b/internal/sessions/header/header_store_test.go @@ -8,16 +8,22 @@ import ( ) func TestTokenFromHeader(t *testing.T) { + t.Run("pomerium header", func(t *testing.T) { + r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) + r.Header.Set("X-Pomerium-Authorization", "JWT") + v := TokenFromHeaders(r) + assert.Equal(t, "JWT", v) + }) t.Run("pomerium type", func(t *testing.T) { r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) r.Header.Set("Authorization", "Pomerium JWT") - v := TokenFromHeader(r, "Authorization", "Pomerium") + v := TokenFromHeaders(r) assert.Equal(t, "JWT", v) }) t.Run("bearer type", func(t *testing.T) { r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) r.Header.Set("Authorization", "Bearer Pomerium-JWT") - v := TokenFromHeader(r, "Authorization", "Pomerium") + v := TokenFromHeaders(r) assert.Equal(t, "JWT", v) }) } diff --git a/internal/sessions/header/middleware_test.go b/internal/sessions/header/middleware_test.go index 92d9c6a5a..65fd9c3fd 100644 --- a/internal/sessions/header/middleware_test.go +++ b/internal/sessions/header/middleware_test.go @@ -41,9 +41,27 @@ func TestVerifier(t *testing.T) { wantBody string wantStatus int }{ - {"good auth header session", "Bearer ", sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, http.StatusText(http.StatusOK), http.StatusOK}, - {"empty auth header", "Bearer ", sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, - {"bad auth type", "bees ", sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, + { + "good auth header session", + "Pomerium ", + sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, + http.StatusText(http.StatusOK), + http.StatusOK, + }, + { + "empty auth header", + "Pomerium ", + sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, + "internal/sessions: session is not found\n", + http.StatusUnauthorized, + }, + { + "bad auth type", + "bees ", + sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, + "internal/sessions: session is not found\n", + http.StatusUnauthorized, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -60,7 +78,7 @@ func TestVerifier(t *testing.T) { // add some garbage to the end of the string encSession = append(encSession, cryptutil.NewKey()...) } - s := NewStore(encoder, "") + s := NewStore(encoder) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Set("Accept", "application/json") @@ -77,10 +95,10 @@ func TestVerifier(t *testing.T) { gotBody := w.Body.String() gotStatus := w.Result().StatusCode - if diff := cmp.Diff(gotBody, tt.wantBody); diff != "" { + if diff := cmp.Diff(tt.wantBody, gotBody); diff != "" { t.Errorf("RetrieveSession() = %v", diff) } - if diff := cmp.Diff(gotStatus, tt.wantStatus); diff != "" { + if diff := cmp.Diff(tt.wantStatus, gotStatus); diff != "" { t.Errorf("RetrieveSession() = %v", diff) } }) diff --git a/proxy/state.go b/proxy/state.go index 2a38f36f0..9f2673063 100644 --- a/proxy/state.go +++ b/proxy/state.go @@ -9,7 +9,6 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/jws" - "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/header" @@ -89,7 +88,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) { } state.sessionLoaders = []sessions.SessionLoader{ state.sessionStore, - header.NewStore(state.encoder, httputil.AuthorizationTypePomerium), + header.NewStore(state.encoder), queryparam.NewStore(state.encoder, "pomerium_session"), } state.programmaticRedirectDomainWhitelist = cfg.Options.ProgrammaticRedirectDomainWhitelist