From bade9f50e6c32112c546c605c89a0813efbb2884 Mon Sep 17 00:00:00 2001 From: Bobby DeSimone Date: Mon, 3 Jun 2019 20:00:37 -0700 Subject: [PATCH] internal/httputil: use error structs for http errors (#159) The existing implementation used a ErrorResponse method to propogate and create http error messages. Since we added functionality to troubleshoot, signout, and do other tasks following an http error it's useful to use Error struct in place of method arguments. This fixes #157 where a troubleshooting links were appearing on pages that it didn't make sense on (e.g. pages without valid sessions). --- authenticate/handlers.go | 76 ++++++++++------- authenticate/handlers_test.go | 78 +++++++++++++---- internal/config/options.go | 2 +- internal/httputil/errors.go | 38 ++++----- internal/middleware/middleware.go | 33 ++++--- internal/middleware/middleware_test.go | 4 +- internal/templates/templates.go | 8 +- proxy/handlers.go | 114 ++++++++++++++++--------- proxy/handlers_test.go | 24 ++++-- 9 files changed, 244 insertions(+), 133 deletions(-) diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 182dc347e..659a8a477 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -14,7 +14,7 @@ import ( "github.com/pomerium/pomerium/internal/sessions" ) -// CSPHeaders adds content security headers for authenticate's handlers +// CSPHeaders are the content security headers added to the service's handlers var CSPHeaders = map[string]string{ "Content-Security-Policy": "default-src 'none'; style-src 'self'" + " 'sha256-z9MsgkMbQjRSLxzAfN55jB3a9pP0PQ4OHFH8b4iDP6s=' " + @@ -80,36 +80,41 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) { return default: log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") - httputil.ErrorResponse(w, r, "An unexpected error occurred", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } } err = a.authenticate(w, r, session) if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } - err = r.ParseForm() - if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + if err = r.ParseForm(); err != nil { + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } // original `state` parameter received from the proxy application. state := r.Form.Get("state") if state == "" { - httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "no state parameter supplied", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } redirectURL, err := url.Parse(r.Form.Get("redirect_uri")) if err != nil { - httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "malformed redirect_uri parameter passed", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } // encrypt session state as json blob encrypted, err := sessions.MarshalSession(session, a.cipher) if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } http.Redirect(w, r, getAuthCodeRedirectURL(redirectURL, state, string(encrypted)), http.StatusFound) @@ -130,9 +135,10 @@ func getAuthCodeRedirectURL(redirectURL *url.URL, state, authCode string) string // SignOut signs the user out by trying to revoke the user's remote identity session along with // the associated local session state. Handles both GET and POST. func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + if err := r.ParseForm(); err != nil { + log.Error().Err(err).Msg("authenticate: error SignOut form") + httpErr := &httputil.Error{Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } redirectURI := r.Form.Get("redirect_uri") @@ -146,7 +152,8 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) { err = a.provider.Revoke(session.AccessToken) if err != nil { log.Error().Err(err).Msg("authenticate: failed to revoke user session") - httputil.ErrorResponse(w, r, fmt.Sprintf("could not revoke session: %s ", err.Error()), http.StatusBadRequest) + httpErr := &httputil.Error{Message: fmt.Sprintf("could not revoke session: %s ", err.Error()), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } http.Redirect(w, r, redirectURI, http.StatusFound) @@ -163,14 +170,16 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { // verify redirect uri is from the root domain if !middleware.SameDomain(authRedirectURL, a.RedirectURL) { - httputil.ErrorResponse(w, r, "Invalid redirect parameter: redirect uri not from the root domain", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "Invalid redirect parameter: redirect uri not from the root domain", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } // verify proxy url is from the root domain proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri")) if err != nil || !middleware.SameDomain(proxyRedirectURL, a.RedirectURL) { - httputil.ErrorResponse(w, r, "Invalid redirect parameter: proxy url not from the root domain", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "Invalid redirect parameter: proxy url not from the root domain", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } @@ -178,7 +187,8 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { proxyRedirectSig := authRedirectURL.Query().Get("sig") ts := authRedirectURL.Query().Get("ts") if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, a.SharedKey) { - httputil.ErrorResponse(w, r, "Invalid redirect parameter: invalid signature", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "Invalid redirect parameter: invalid signature", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } @@ -197,36 +207,36 @@ func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) { switch h := err.(type) { case nil: break - case httputil.HTTPError: + case httputil.Error: log.Error().Err(err).Msg("authenticate: oauth callback error") - httputil.ErrorResponse(w, r, h.Message, h.Code) + httpErr := &httputil.Error{Message: h.Message, Code: h.Code} + httputil.ErrorResponse(w, r, httpErr) return default: log.Error().Err(err).Msg("authenticate: unexpected oauth callback error") - httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "Internal Error", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } // redirect back to the proxy-service via sign_in - log.Info().Interface("redirect", redirect).Msg("proxy: OAuthCallback") http.Redirect(w, r, redirect, http.StatusFound) } // getOAuthCallback completes the oauth cycle from an identity provider's callback func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) { // handle the callback response from the identity provider - err := r.ParseForm() - if err != nil { - return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()} + if err := r.ParseForm(); err != nil { + return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} } errorString := r.Form.Get("error") if errorString != "" { log.FromRequest(r).Error().Str("Error", errorString).Msg("authenticate: provider returned error") - return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString} + return "", httputil.Error{Code: http.StatusForbidden, Message: errorString} } code := r.Form.Get("code") if code == "" { - log.FromRequest(r).Error().Err(err).Msg("authenticate: provider missing code") - return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Missing Code"} + log.FromRequest(r).Error().Msg("authenticate: provider missing code") + return "", httputil.Error{Code: http.StatusBadRequest, Message: "Missing Code"} } @@ -234,18 +244,18 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) session, err := a.provider.Authenticate(code) if err != nil { log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code") - return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()} + return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} } // okay, time to go back to the proxy service. bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state")) if err != nil { log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state") - return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Couldn't decode state"} + return "", httputil.Error{Code: http.StatusBadRequest, Message: "Couldn't decode state"} } s := strings.SplitN(string(bytes), ":", 2) if len(s) != 2 { - return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Invalid State"} + return "", httputil.Error{Code: http.StatusBadRequest, Message: "Invalid State"} } nonce := s[0] redirect := s[1] @@ -253,22 +263,22 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) defer a.csrfStore.ClearCSRF(w, r) if err != nil || c.Value != nonce { log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf failure") - return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "CSRF failed"} + return "", httputil.Error{Code: http.StatusForbidden, Message: "CSRF failed"} } redirectURL, err := url.Parse(redirect) if err != nil { log.FromRequest(r).Error().Err(err).Msg("authenticate: malformed redirect url") - return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Malformed redirect url"} + return "", httputil.Error{Code: http.StatusForbidden, Message: "Malformed redirect url"} } // sanity check, we are redirecting back to the same subdomain right? if !middleware.SameDomain(redirectURL, a.RedirectURL) { - return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Invalid Redirect URI domain"} + return "", httputil.Error{Code: http.StatusBadRequest, Message: "Invalid Redirect URI domain"} } err = a.sessionStore.SaveSession(w, r, session) if err != nil { log.Error().Err(err).Msg("authenticate: failed saving new session") - return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: "Internal Error"} + return "", httputil.Error{Code: http.StatusInternalServerError, Message: "Internal Error"} } return redirect, nil diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index 8bb8f32f1..f4f0e9cfc 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -64,12 +64,16 @@ func TestAuthenticate_Handler(t *testing.T) { func TestAuthenticate_SignIn(t *testing.T) { tests := []struct { - name string - session sessions.SessionStore - provider identity.MockProvider - wantCode int + name string + state string + redirectURI string + session sessions.SessionStore + provider identity.MockProvider + wantCode int }{ {"good", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ Session: &sessions.SessionState{ AccessToken: "AccessToken", @@ -77,8 +81,10 @@ func TestAuthenticate_SignIn(t *testing.T) { RefreshDeadline: time.Now().Add(10 * time.Second), }}, identity.MockProvider{ValidateResponse: true}, - http.StatusBadRequest}, + http.StatusFound}, {"session not valid", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ Session: &sessions.SessionState{ AccessToken: "AccessToken", @@ -87,15 +93,9 @@ func TestAuthenticate_SignIn(t *testing.T) { }}, identity.MockProvider{ValidateResponse: false}, http.StatusInternalServerError}, - {"session fails fails to save", &sessions.MockSessionStore{ - SaveError: errors.New("error"), - Session: &sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, identity.MockProvider{ValidateResponse: true}, - http.StatusBadRequest}, {"session refresh error", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ Session: &sessions.SessionState{ AccessToken: "AccessToken", @@ -107,6 +107,8 @@ func TestAuthenticate_SignIn(t *testing.T) { RefreshError: errors.New("error")}, http.StatusInternalServerError}, {"session save after refresh error", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ SaveError: errors.New("error"), Session: &sessions.SessionState{ @@ -119,6 +121,8 @@ func TestAuthenticate_SignIn(t *testing.T) { }, http.StatusInternalServerError}, {"no cookie found trying to load", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ LoadError: http.ErrNoCookie, Session: &sessions.SessionState{ @@ -129,6 +133,8 @@ func TestAuthenticate_SignIn(t *testing.T) { identity.MockProvider{ValidateResponse: true}, http.StatusBadRequest}, {"unexpected error trying to load session", + "state=example", + "redirect_uri=some.example", &sessions.MockSessionStore{ LoadError: errors.New("unexpeted"), Session: &sessions.SessionState{ @@ -138,23 +144,63 @@ func TestAuthenticate_SignIn(t *testing.T) { }}, identity.MockProvider{ValidateResponse: true}, http.StatusInternalServerError}, + {"malformed form", + "state=example", + "redirect_uri=some.example", + &sessions.MockSessionStore{ + Session: &sessions.SessionState{ + AccessToken: "AccessToken", + RefreshToken: "RefreshToken", + RefreshDeadline: time.Now().Add(10 * time.Second), + }}, + identity.MockProvider{ValidateResponse: true}, + http.StatusInternalServerError}, + {"empty state", + "state=", + "redirect_uri=some.example", + &sessions.MockSessionStore{ + Session: &sessions.SessionState{ + AccessToken: "AccessToken", + RefreshToken: "RefreshToken", + RefreshDeadline: time.Now().Add(10 * time.Second), + }}, + identity.MockProvider{ValidateResponse: true}, + http.StatusBadRequest}, + + {"malformed redirect uri", + "state=example", + "redirect_uri=https://accounts.google.^", + &sessions.MockSessionStore{ + Session: &sessions.SessionState{ + AccessToken: "AccessToken", + RefreshToken: "RefreshToken", + RefreshDeadline: time.Now().Add(10 * time.Second), + }}, + identity.MockProvider{ValidateResponse: true}, + http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authenticate{ sessionStore: tt.session, provider: tt.provider, - RedirectURL: uriParse("http://www.pomerium.io"), + RedirectURL: uriParse(tt.redirectURI), csrfStore: &sessions.MockCSRFStore{}, SharedKey: "secret", cipher: mockCipher{}, } - r := httptest.NewRequest("GET", "/sign-in", nil) + uri := &url.URL{Path: "/"} + if tt.name == "malformed form" { + uri.RawQuery = "example=%zzzzz" + } else { + uri.RawQuery = fmt.Sprintf("%s&%s", tt.state, tt.redirectURI) + } + r := httptest.NewRequest(http.MethodGet, uri.String(), nil) w := httptest.NewRecorder() a.SignIn(w, r) if status := w.Code; status != tt.wantCode { - t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode) + t.Errorf("handler returned wrong status code: got %v want %v %s", status, tt.wantCode, uri) t.Errorf("\n%+v", w.Body) } }) diff --git a/internal/config/options.go b/internal/config/options.go index 3c9db9983..ad62fbd5e 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -164,7 +164,7 @@ func NewOptions() *Options { AuthenticateInternalAddr: new(url.URL), AuthorizeURL: new(url.URL), RefreshCooldown: time.Duration(5 * time.Minute), - AllowWebsockets: false, + AllowWebsockets: false, } return o } diff --git a/internal/httputil/errors.go b/internal/httputil/errors.go index 87ab97b03..607ad7e21 100644 --- a/internal/httputil/errors.go +++ b/internal/httputil/errors.go @@ -10,14 +10,16 @@ import ( "github.com/pomerium/pomerium/internal/templates" ) -// HTTPError stores the status code and a message for a given HTTP error. -type HTTPError struct { - Code int - Message string +// Error reports an http error, its http status code, a custom message, and +// whether it is CanDebug. +type Error struct { + Message string + Code int + CanDebug bool } // Error fulfills the error interface, returning a string representation of the error. -func (h HTTPError) Error() string { +func (h Error) Error() string { return fmt.Sprintf("%d %s: %s", h.Code, http.StatusText(h.Code), h.Message) } @@ -32,34 +34,32 @@ func CodeForError(err error) int { // ErrorResponse renders an error page for errors given a message and a status code. // If no message is passed, defaults to the text of the status code. -func ErrorResponse(rw http.ResponseWriter, r *http.Request, message string, code int) { - if message == "" { - message = http.StatusText(code) - } - reqID := "" +func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) { + requestID := "" id, ok := log.IDFromRequest(r) if ok { - reqID = id + requestID = id } if r.Header.Get("Accept") == "application/json" { var response struct { Error string `json:"error"` } - response.Error = message - writeJSONResponse(rw, code, response) + response.Error = e.Message + writeJSONResponse(rw, e.Code, response) } else { - title := http.StatusText(code) - rw.WriteHeader(code) + rw.WriteHeader(e.Code) t := struct { Code int Title string Message string RequestID string + CanDebug bool }{ - Code: code, - Title: title, - Message: message, - RequestID: reqID, + Code: e.Code, + Title: http.StatusText(e.Code), + Message: e.Message, + RequestID: requestID, + CanDebug: e.CanDebug, } templates.New().ExecuteTemplate(rw, "error.html", t) } diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 84475f380..18e65b1ab 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -32,9 +32,9 @@ func SetHeaders(securityHeaders map[string]string) func(next http.Handler) http. func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + if err := r.ParseForm(); err != nil { + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } clientSecret := r.Form.Get("shared_secret") @@ -44,7 +44,7 @@ func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Hand } if clientSecret != sharedSecret { - httputil.ErrorResponse(w, r, "Invalid client secret", http.StatusUnauthorized) + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError}) return } next.ServeHTTP(w, r) @@ -59,16 +59,25 @@ func ValidateRedirectURI(rootDomain *url.URL) func(next http.Handler) http.Handl return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{ + Message: err.Error(), + Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } redirectURI, err := url.Parse(r.Form.Get("redirect_uri")) if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{ + Message: err.Error(), + Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } if !SameDomain(redirectURI, rootDomain) { - httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) + httpErr := &httputil.Error{ + Message: "Invalid redirect parameter", + Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } next.ServeHTTP(w, r) @@ -96,14 +105,18 @@ func ValidateSignature(sharedSecret string) func(next http.Handler) http.Handler return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } redirectURI := r.Form.Get("redirect_uri") sigVal := r.Form.Get("sig") timestamp := r.Form.Get("ts") if !ValidSignature(redirectURI, sigVal, timestamp, sharedSecret) { - httputil.ErrorResponse(w, r, "Cross service signature failed to validate", http.StatusUnauthorized) + httpErr := &httputil.Error{ + Message: "Cross service signature failed to validate", + Code: http.StatusUnauthorized} + httputil.ErrorResponse(w, r, httpErr) return } @@ -117,7 +130,7 @@ func ValidateHost(validHost func(host string) bool) func(next http.Handler) http return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !validHost(r.Host) { - httputil.ErrorResponse(w, r, "Unknown route", http.StatusNotFound) + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusNotFound}) return } next.ServeHTTP(w, r) diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index de7b15c6b..e817b53c8 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -175,8 +175,8 @@ func TestValidateClientSecret(t *testing.T) { }{ {"simple", "secret", "secret", "secret", http.StatusOK}, {"missing get param, valid header", "secret", "", "secret", http.StatusOK}, - {"missing both", "secret", "", "", http.StatusUnauthorized}, - {"simple bad", "bad-secret", "secret", "", http.StatusUnauthorized}, + {"missing both", "secret", "", "", http.StatusInternalServerError}, + {"simple bad", "bad-secret", "secret", "", http.StatusInternalServerError}, {"malformed, invalid hex digits", "secret", "%zzzzz", "", http.StatusBadRequest}, } diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 5c0a35175..f5989fb21 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -265,9 +265,11 @@ func New() *template.Template {

{{.Title}}

-

{{.Message}}.

-

Troubleshoot your session.
- {{if .RequestID}} Request {{.RequestID}} {{end}} +

+ {{if .Message}}{{.Message}}
{{end}} + {{if .CanDebug}}Troubleshoot your session.
{{end}} + {{if .RequestID}} Request {{.RequestID}}
{{end}} +

diff --git a/proxy/handlers.go b/proxy/handlers.go index 8beb31928..82e9ae582 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -3,12 +3,12 @@ package proxy // import "github.com/pomerium/pomerium/proxy" import ( "encoding/base64" "fmt" - "github.com/pomerium/pomerium/internal/config" "net/http" "net/url" "strings" "time" + "github.com/pomerium/pomerium/internal/config" "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" @@ -74,7 +74,8 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { localState, err := p.cipher.Marshal(state) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } p.csrfStore.SetCSRF(w, r, localState) @@ -84,7 +85,8 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { remoteState, err := p.cipher.Marshal(state) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed to encrypt cookie") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } @@ -96,7 +98,8 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { if remoteState == localState { p.sessionStore.ClearSession(w, r) log.FromRequest(r).Error().Msg("proxy: encrypted state should not match") - httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + httpErr := &httputil.Error{Message: http.StatusText(http.StatusBadRequest), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } @@ -113,25 +116,26 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { // other metadata, from the authenticator. // finish the oauth cycle func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { + if err := r.ParseForm(); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing request form") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } - errorString := r.Form.Get("error") - if errorString != "" { - httputil.ErrorResponse(w, r, errorString, http.StatusBadRequest) + + if errorString := r.Form.Get("error"); errorString != "" { + httpErr := &httputil.Error{Message: errorString, Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } // Encrypted CSRF passed from authenticate service remoteStateEncrypted := r.Form.Get("state") remoteStatePlain := new(StateParameter) - err = p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain) - if err != nil { + if err := p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: could not unmarshal state") - httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } @@ -139,7 +143,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { c, err := p.csrfStore.GetCSRF(r) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie") - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } p.csrfStore.ClearCSRF(w, r) @@ -148,7 +153,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF") - httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } @@ -157,7 +163,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { if remoteStateEncrypted == localStateEncrypted { p.sessionStore.ClearSession(w, r) log.FromRequest(r).Error().Msg("proxy: local and remote state should not match") - httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + httpErr := &httputil.Error{Message: http.StatusText(http.StatusBadRequest), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } @@ -165,7 +172,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { if remoteStatePlain.SessionID != localStatePlain.SessionID { p.sessionStore.ClearSession(w, r) log.FromRequest(r).Error().Msg("proxy: CSRF mismatch") - httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } @@ -215,7 +223,8 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { return default: log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") - httputil.ErrorResponse(w, r, "An unexpected error occurred", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } } @@ -223,13 +232,18 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { if err = p.authenticate(w, r, session); err != nil { p.sessionStore.ClearSession(w, r) log.Debug().Err(err).Msg("proxy: user unauthenticated") - httputil.ErrorResponse(w, r, "User unauthenticated", http.StatusForbidden) + httpErr := &httputil.Error{ + Message: "User unauthenticated", + Code: http.StatusForbidden, + CanDebug: true} + httputil.ErrorResponse(w, r, httpErr) return } authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, session) if err != nil || !authorized { log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized") - httputil.ErrorResponse(w, r, "Access unauthorized", http.StatusUnauthorized) + httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true} + httputil.ErrorResponse(w, r, httpErr) return } } @@ -237,7 +251,7 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { // We have validated the users request and now proxy their request to the provided upstream. route, ok := p.router(r) if !ok { - httputil.ErrorResponse(w, r, "unknown route to proxy", http.StatusNotFound) + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusNotFound}) return } route.ServeHTTP(w, r) @@ -250,13 +264,15 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { session, err := p.sessionStore.LoadSession(r) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: load session failed") - httputil.ErrorResponse(w, r, "", http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } if err := p.authenticate(w, r, session); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: authenticate failed") - httputil.ErrorResponse(w, r, "", http.StatusUnauthorized) + httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true} + httputil.ErrorResponse(w, r, httpErr) return } @@ -264,7 +280,8 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: is admin client") - httputil.ErrorResponse(w, r, "", http.StatusInternalServerError) + httpErr := &httputil.Error{Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } @@ -273,7 +290,8 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { csrfCookie, err := p.cipher.Marshal(csrf) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } p.csrfStore.SetCSRF(w, r, csrfCookie) @@ -310,13 +328,15 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) { session, err := p.sessionStore.LoadSession(r) if err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } iss, err := session.IssuedAt() if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't get token's create time") - httputil.ErrorResponse(w, r, "", http.StatusInternalServerError) + httpErr := &httputil.Error{Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } @@ -324,20 +344,23 @@ func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) { // trying to DOS the identity provider. if time.Since(iss) < p.refreshCooldown { log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown") - httputil.ErrorResponse(w, r, - fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown), - http.StatusBadRequest) + httpErr := &httputil.Error{ + Message: fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown), + Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } newSession, err := p.AuthenticateClient.Refresh(r.Context(), session) if err != nil { log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } if err = p.sessionStore.SaveSession(w, r, newSession); err != nil { - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } http.Redirect(w, r, "/.pomerium", http.StatusFound) @@ -350,27 +373,34 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { if err := r.ParseForm(); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate form") - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } session, err := p.sessionStore.LoadSession(r) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: load session") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } // authorization check -- is this user an admin? isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session) if err != nil || !isAdmin { log.FromRequest(r).Error().Err(err).Msg("proxy: user must be admin to impersonate") - httputil.ErrorResponse(w, r, "user must be admin to impersonate", http.StatusForbidden) + httpErr := &httputil.Error{ + Message: fmt.Sprintf("%s must be and administrator", session.Email), + Code: http.StatusForbidden, + CanDebug: true} + httputil.ErrorResponse(w, r, httpErr) return } // CSRF check -- did this request originate from our form? c, err := p.csrfStore.GetCSRF(r) if err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie") - httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } p.csrfStore.ClearCSRF(w, r) @@ -378,12 +408,14 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) { decryptedCSRF := new(StateParameter) if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF") - httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError) + httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } if decryptedCSRF.SessionID != r.FormValue("csrf") { log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate CSRF mismatch") - httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusForbidden) + httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusForbidden} + httputil.ErrorResponse(w, r, httpErr) return } @@ -393,7 +425,8 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) { if err := p.sessionStore.SaveSession(w, r, session); err != nil { log.FromRequest(r).Error().Err(err).Msg("proxy: save session") - httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) + httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) return } } @@ -510,7 +543,8 @@ func websocketHandlerFunc(baseHandler http.Handler, timeoutHandler http.Handler, } log.FromRequest(r).Warn().Msg("proxy: attempt to proxy a websocket connection, but websocket support is disabled in the configuration") - httputil.ErrorResponse(w, r, "websockets not supported by proxy", http.StatusBadRequest) + httpErr := &httputil.Error{Message: "websockets not supported by proxy", Code: http.StatusBadRequest} + httputil.ErrorResponse(w, r, httpErr) return } diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index 2fbca421b..7545e93d9 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -451,6 +451,7 @@ func TestProxy_Impersonate(t *testing.T) { tests := []struct { name string + malformed bool options *config.Options method string email string @@ -463,14 +464,15 @@ func TestProxy_Impersonate(t *testing.T) { authorizer clients.Authorizer wantStatus int }{ - {"good", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, - {"session load error", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, - {"non admin users rejected", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden}, - {"non admin users rejected on error", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusForbidden}, - {"csrf from store retrieve failure", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}, GetError: errors.New("err")}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest}, - {"can't decrypt csrf value", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, - {"decrypted csrf mismatch", opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusForbidden}, - {"save session failure", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, + {"good", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, + {"session load error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, + {"non admin users rejected", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden}, + {"non admin users rejected on error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusForbidden}, + {"csrf from store retrieve failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}, GetError: errors.New("err")}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest}, + {"can't decrypt csrf value", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, + {"decrypted csrf mismatch", false, opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusForbidden}, + {"save session failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, + {"malformed", true, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -487,8 +489,12 @@ func TestProxy_Impersonate(t *testing.T) { postForm.Add("email", tt.email) postForm.Add("group", tt.groups) postForm.Set("csrf", tt.csrf) + uri := &url.URL{Path: "/"} + if tt.malformed { + uri.RawQuery = "email=%zzzzz" + } + r := httptest.NewRequest(tt.method, uri.String(), bytes.NewBufferString(postForm.Encode())) - r := httptest.NewRequest(tt.method, "/", bytes.NewBufferString(postForm.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") w := httptest.NewRecorder() p.Impersonate(w, r)