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 <alex@fornuto.com>
This commit is contained in:
Caleb Doxsey 2021-11-29 12:19:14 -07:00 committed by GitHub
parent 88c5eeba45
commit a8b76bd623
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 101 additions and 42 deletions

View file

@ -15,7 +15,6 @@ import (
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/ecjson" "github.com/pomerium/pomerium/internal/encoding/ecjson"
"github.com/pomerium/pomerium/internal/encoding/jws" "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"
"github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/cookie"
"github.com/pomerium/pomerium/internal/sessions/header" "github.com/pomerium/pomerium/internal/sessions/header"
@ -114,7 +113,7 @@ func newAuthenticateStateFromConfig(cfg *config.Config) (*authenticateState, err
state.encryptedEncoder = ecjson.New(state.cookieCipher) 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 { cookieStore, err := cookie.NewStore(func() cookie.Options {
return cookie.Options{ return cookie.Options{

View file

@ -6,7 +6,6 @@ import (
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/cookie"
"github.com/pomerium/pomerium/internal/sessions/header" "github.com/pomerium/pomerium/internal/sessions/header"
@ -22,7 +21,7 @@ func loadRawSession(req *http.Request, options *config.Options, encoder encoding
} }
loaders = append(loaders, loaders = append(loaders,
cookieStore, cookieStore,
header.NewStore(encoder, httputil.AuthorizationTypePomerium), header.NewStore(encoder),
queryparam.NewStore(encoder, urlutil.QuerySession), queryparam.NewStore(encoder, urlutil.QuerySession),
) )

View file

@ -173,7 +173,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
"name": "envoy.filters.http.lua", "name": "envoy.filters.http.lua",
"typedConfig": { "typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", "@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"
} }
}, },
{ {

View file

@ -10,6 +10,39 @@ import (
lua "github.com/yuin/gopher-lua" 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) { func TestLuaFixMisdirected(t *testing.T) {
t.Run("request", func(t *testing.T) { t.Run("request", func(t *testing.T) {
L := lua.NewState() L := lua.NewState()
@ -141,6 +174,12 @@ func newLuaHeaders(L *lua.LState, headers map[string]string) lua.LValue {
L.Push(lua.LString(str)) L.Push(lua.LString(str))
return 1 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 { "replace": func(L *lua.LState) int {
_ = L.CheckTable(1) _ = L.CheckTable(1)
key := L.CheckString(2) key := L.CheckString(2)

View file

@ -30,9 +30,9 @@ function envoy_on_request(request_handle)
if has_prefix(authorization, authorization_prefix) then if has_prefix(authorization, authorization_prefix) then
headers:remove("authorization") headers:remove("authorization")
end end
headers:remove('x-pomerium-authorization')
end end
end end
function envoy_on_response(response_handle) function envoy_on_response(response_handle) end
end

View file

@ -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 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. 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 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 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. 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 :::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 ## Example Code

View file

@ -5,6 +5,7 @@ const AuthorizationTypePomerium = "Pomerium"
// Standard headers // Standard headers
const ( const (
HeaderAuthorization = "Authorization"
HeaderReferrer = "Referer" HeaderReferrer = "Referer"
HeaderImpersonateGroup = "Impersonate-Group" HeaderImpersonateGroup = "Impersonate-Group"
HeaderUpgrade = "Upgrade" HeaderUpgrade = "Upgrade"
@ -12,6 +13,10 @@ const (
// Pomerium headers contain information added to a request. // Pomerium headers contain information added to a request.
const ( 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, // HeaderPomeriumResponse is set when pomerium itself creates a response,
// as opposed to the upstream application and can be used to distinguish // as opposed to the upstream application and can be used to distinguish
// between an application error, and a pomerium related error when debugging. // between an application error, and a pomerium related error when debugging.

View file

@ -7,22 +7,16 @@ import (
"strings" "strings"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
) )
var _ sessions.SessionLoader = &Store{} var _ sessions.SessionLoader = &Store{}
const (
defaultAuthHeader = "Authorization"
defaultAuthType = "Bearer"
)
// Store implements the load session store interface using http // Store implements the load session store interface using http
// authorization headers. // authorization headers.
type Store struct { type Store struct {
authHeader string encoder encoding.Unmarshaler
authType string
encoder encoding.Unmarshaler
} }
// NewStore returns a new header store for loading sessions from // 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, // 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. // you should ensure no other services are logging or leaking your auth headers.
func NewStore(enc encoding.Unmarshaler, headerType string) *Store { func NewStore(enc encoding.Unmarshaler) *Store {
if headerType == "" {
headerType = defaultAuthType
}
return &Store{ return &Store{
authHeader: defaultAuthHeader, encoder: enc,
authType: headerType,
encoder: enc,
} }
} }
// LoadSession tries to retrieve the token string from the Authorization header. // LoadSession tries to retrieve the token string from the Authorization header.
func (as *Store) LoadSession(r *http.Request) (string, error) { func (as *Store) LoadSession(r *http.Request) (string, error) {
jwt := TokenFromHeader(r, as.authHeader, as.authType) jwt := TokenFromHeaders(r)
if jwt == "" { if jwt == "" {
return "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
return jwt, nil return jwt, nil
} }
// TokenFromHeader retrieves the value of the authorization header from a given // TokenFromHeaders retrieves the value of the authorization header(s) from a given
// request, header key, and authentication type. // request and authentication type.
func TokenFromHeader(r *http.Request, authHeader, authType string) string { func TokenFromHeaders(r *http.Request) string {
bearer := r.Header.Get(authHeader) // X-Pomerium-Authorization: <JWT>
if jwt := r.Header.Get(httputil.HeaderPomeriumAuthorization); jwt != "" {
return jwt
}
bearer := r.Header.Get(httputil.HeaderAuthorization)
// Authorization: Pomerium <JWT> // Authorization: Pomerium <JWT>
prefix := authType + " " prefix := httputil.AuthorizationTypePomerium + " "
if strings.HasPrefix(bearer, prefix) { if strings.HasPrefix(bearer, prefix) {
return bearer[len(prefix):] return bearer[len(prefix):]
} }
// Authorization: Bearer Pomerium-<JWT> // Authorization: Bearer Pomerium-<JWT>
prefix = "Bearer " + authType + "-" prefix = "Bearer " + httputil.AuthorizationTypePomerium + "-"
if strings.HasPrefix(bearer, prefix) { if strings.HasPrefix(bearer, prefix) {
return bearer[len(prefix):] return bearer[len(prefix):]
} }

View file

@ -8,16 +8,22 @@ import (
) )
func TestTokenFromHeader(t *testing.T) { 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) { t.Run("pomerium type", func(t *testing.T) {
r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) r, _ := http.NewRequest("GET", "http://localhost/some/url", nil)
r.Header.Set("Authorization", "Pomerium JWT") r.Header.Set("Authorization", "Pomerium JWT")
v := TokenFromHeader(r, "Authorization", "Pomerium") v := TokenFromHeaders(r)
assert.Equal(t, "JWT", v) assert.Equal(t, "JWT", v)
}) })
t.Run("bearer type", func(t *testing.T) { t.Run("bearer type", func(t *testing.T) {
r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) r, _ := http.NewRequest("GET", "http://localhost/some/url", nil)
r.Header.Set("Authorization", "Bearer Pomerium-JWT") r.Header.Set("Authorization", "Bearer Pomerium-JWT")
v := TokenFromHeader(r, "Authorization", "Pomerium") v := TokenFromHeaders(r)
assert.Equal(t, "JWT", v) assert.Equal(t, "JWT", v)
}) })
} }

View file

@ -41,9 +41,27 @@ func TestVerifier(t *testing.T) {
wantBody string wantBody string
wantStatus int 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}, "good auth header session",
{"bad auth type", "bees ", sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, "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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 // add some garbage to the end of the string
encSession = append(encSession, cryptutil.NewKey()...) encSession = append(encSession, cryptutil.NewKey()...)
} }
s := NewStore(encoder, "") s := NewStore(encoder)
r := httptest.NewRequest(http.MethodGet, "/", nil) r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
@ -77,10 +95,10 @@ func TestVerifier(t *testing.T) {
gotBody := w.Body.String() gotBody := w.Body.String()
gotStatus := w.Result().StatusCode 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) 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) t.Errorf("RetrieveSession() = %v", diff)
} }
}) })

View file

@ -9,7 +9,6 @@ import (
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws" "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"
"github.com/pomerium/pomerium/internal/sessions/cookie" "github.com/pomerium/pomerium/internal/sessions/cookie"
"github.com/pomerium/pomerium/internal/sessions/header" "github.com/pomerium/pomerium/internal/sessions/header"
@ -89,7 +88,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
} }
state.sessionLoaders = []sessions.SessionLoader{ state.sessionLoaders = []sessions.SessionLoader{
state.sessionStore, state.sessionStore,
header.NewStore(state.encoder, httputil.AuthorizationTypePomerium), header.NewStore(state.encoder),
queryparam.NewStore(state.encoder, "pomerium_session"), queryparam.NewStore(state.encoder, "pomerium_session"),
} }
state.programmaticRedirectDomainWhitelist = cfg.Options.ProgrammaticRedirectDomainWhitelist state.programmaticRedirectDomainWhitelist = cfg.Options.ProgrammaticRedirectDomainWhitelist