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/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{

View file

@ -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),
)

View file

@ -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"
}
},
{

View file

@ -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)

View file

@ -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

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 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

View file

@ -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.

View file

@ -7,21 +7,15 @@ 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
}
@ -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,
}
}
// 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: <JWT>
if jwt := r.Header.Get(httputil.HeaderPomeriumAuthorization); jwt != "" {
return jwt
}
bearer := r.Header.Get(httputil.HeaderAuthorization)
// Authorization: Pomerium <JWT>
prefix := authType + " "
prefix := httputil.AuthorizationTypePomerium + " "
if strings.HasPrefix(bearer, prefix) {
return bearer[len(prefix):]
}
// Authorization: Bearer Pomerium-<JWT>
prefix = "Bearer " + authType + "-"
prefix = "Bearer " + httputil.AuthorizationTypePomerium + "-"
if strings.HasPrefix(bearer, prefix) {
return bearer[len(prefix):]
}

View file

@ -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)
})
}

View file

@ -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)
}
})

View file

@ -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