diff --git a/authenticate/handlers.go b/authenticate/handlers.go index c357f5bf8..dda857922 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -7,11 +7,14 @@ import ( "net/url" "strings" + "golang.org/x/xerrors" + "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/middleware" "github.com/pomerium/pomerium/internal/sessions" + "github.com/pomerium/pomerium/internal/urlutil" ) // CSPHeaders are the content security headers added to the service's handlers @@ -46,6 +49,8 @@ func (a *Authenticate) Handler() http.Handler { // RobotsTxt handles the /robots.txt route. func (a *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "User-agent: *\nDisallow: /") } @@ -54,93 +59,74 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request, sess if session.RefreshPeriodExpired() { session, err := a.provider.Refresh(r.Context(), session) if err != nil { - return fmt.Errorf("authenticate: session refresh failed : %v", err) + return xerrors.Errorf("session refresh failed : %w", err) } - err = a.sessionStore.SaveSession(w, r, session) - if err != nil { - return fmt.Errorf("authenticate: failed saving refreshed session : %v", err) + if err = a.sessionStore.SaveSession(w, r, session); err != nil { + return xerrors.Errorf("failed saving refreshed session : %w", err) } } else { valid, err := a.provider.Validate(r.Context(), session.IDToken) if err != nil || !valid { - return fmt.Errorf("authenticate: session valid: %v : %v", valid, err) + return xerrors.Errorf("session valid: %v : %w", valid, err) } } return nil } -// SignIn handles the sign_in endpoint. It attempts to authenticate the user, -// and if the user is not authenticated, it renders a sign in page. +// SignIn handles to authenticating a user. func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) { session, err := a.sessionStore.LoadSession(r) if err != nil { - switch err { - case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: - log.FromRequest(r).Debug().Err(err).Msg("authenticate: invalid session") - a.sessionStore.ClearSession(w, r) - a.OAuthStart(w, r) - return - default: - log.FromRequest(r).Error().Err(err).Msg("authenticate: unexpected error") - 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 { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + log.FromRequest(r).Debug().Err(err).Msg("no session loaded, restart auth") + a.sessionStore.ClearSession(w, r) + a.OAuthStart(w, r) return } - if err = r.ParseForm(); err != nil { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + // if a session already exists, authenticate it + if err := a.authenticate(w, r, session); err != nil { + httputil.ErrorResponse(w, r, err) return } - // original `state` parameter received from the proxy application. - state := r.Form.Get("state") - if state == "" { - httpErr := &httputil.Error{Message: "no state parameter supplied", Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + if err := r.ParseForm(); err != nil { + httputil.ErrorResponse(w, r, err) return } - redirectURL, err := url.Parse(r.Form.Get("redirect_uri")) + state := r.Form.Get("state") + if state == "" { + httputil.ErrorResponse(w, r, httputil.Error("sign in state empty", http.StatusBadRequest, nil)) + return + } + + redirectURL, err := urlutil.ParseAndValidateURL(r.Form.Get("redirect_uri")) if err != nil { - httpErr := &httputil.Error{Message: "malformed redirect_uri parameter passed", Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri parameter passed", http.StatusBadRequest, err)) return } // encrypt session state as json blob encrypted, err := sessions.MarshalSession(session, a.cipher) if err != nil { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("couldn't marshall session", http.StatusInternalServerError, err)) return } + http.Redirect(w, r, getAuthCodeRedirectURL(redirectURL, state, encrypted), http.StatusFound) } func getAuthCodeRedirectURL(redirectURL *url.URL, state, authCode string) string { - u, _ := url.Parse(redirectURL.String()) - params, _ := url.ParseQuery(u.RawQuery) + // error handled by go's mux stack + params, _ := url.ParseQuery(redirectURL.RawQuery) params.Set("code", authCode) params.Set("state", state) - u.RawQuery = params.Encode() - if u.Scheme == "" { - u.Scheme = "https" - } - return u.String() + redirectURL.RawQuery = params.Encode() + return redirectURL.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) { if err := r.ParseForm(); err != nil { - log.Error().Err(err).Msg("authenticate: error SignOut form") - httpErr := &httputil.Error{Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } redirectURI := r.Form.Get("redirect_uri") @@ -153,35 +139,30 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) { a.sessionStore.ClearSession(w, r) err = a.provider.Revoke(session.AccessToken) if err != nil { - log.Error().Err(err).Msg("authenticate: failed to revoke user session") - httpErr := &httputil.Error{Message: fmt.Sprintf("could not revoke session: %s ", err.Error()), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("could not revoke user session", http.StatusBadRequest, err)) return } http.Redirect(w, r, redirectURI, http.StatusFound) } // OAuthStart starts the authenticate process by redirecting to the identity provider. +// https://openid.net/specs/openid-connect-core-1_0-final.html#AuthRequest // https://tools.ietf.org/html/rfc6749#section-4.2.1 func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { authRedirectURL := a.RedirectURL.ResolveReference(r.URL) - // generate a nonce to check following authentication with the IdP + // Nonce is the opaque, cryptographically binding value used to maintain + // state between the request and the callback. + // OIDC : 3.1.2.1. Authentication Request nonce := fmt.Sprintf("%x", cryptutil.GenerateKey()) a.csrfStore.SetCSRF(w, r, nonce) - // verify redirect uri is from the root domain - if !middleware.SameDomain(authRedirectURL, a.RedirectURL) { - 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")) + // Redirection URI to which the response will be sent. This URI MUST exactly + // match one of the Redirection URI values for the Client pre-registered at + // at your identity provider + proxyRedirectURL, err := urlutil.ParseAndValidateURL(authRedirectURL.Query().Get("redirect_uri")) if err != nil || !middleware.SameDomain(proxyRedirectURL, a.RedirectURL) { - httpErr := &httputil.Error{Message: "Invalid redirect parameter: proxy url not from the root domain", Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("proxy url not from the root domain", http.StatusBadRequest, err)) return } @@ -189,12 +170,12 @@ 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) { - httpErr := &httputil.Error{Message: "Invalid redirect parameter: invalid signature", Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("invalid signature", http.StatusBadRequest, nil)) return } - // concat base64'd nonce and authenticate url to make state + // State is the opaque value used to maintain state between the request and + // the callback; contains both the nonce and redirect URI state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String()))) // build the provider sign in url @@ -204,83 +185,71 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { // OAuthCallback handles the callback from the identity provider. Displays an error page if there // was an error. If successful, the user is redirected back to the proxy-service. +// https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) { redirect, err := a.getOAuthCallback(w, r) - switch h := err.(type) { - case nil: - break - case httputil.Error: - log.Error().Err(err).Msg("authenticate: oauth callback error") - 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") - httpErr := &httputil.Error{Message: "Internal Error", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + if err != nil { + httputil.ErrorResponse(w, r, xerrors.Errorf("oauth callback : %w", err)) return } // redirect back to the proxy-service via sign_in 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 if err := r.ParseForm(); err != nil { - return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} + return "", httputil.Error("invalid signature", http.StatusBadRequest, err) } - errorString := r.Form.Get("error") - if errorString != "" { - log.FromRequest(r).Error().Str("Error", errorString).Msg("authenticate: provider returned error") - return "", httputil.Error{Code: http.StatusForbidden, Message: errorString} + // OIDC : 3.1.2.6. Authentication Error Response + // https://openid.net/specs/openid-connect-core-1_0-final.html#AuthError + if errorString := r.Form.Get("error"); errorString != "" { + return "", httputil.Error("provider returned an error", http.StatusBadRequest, fmt.Errorf("provider returned error: %v", errorString)) } + // OIDC : 3.1.2.5. Successful Authentication Response + // https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse code := r.Form.Get("code") if code == "" { - log.FromRequest(r).Error().Msg("authenticate: provider missing code") - return "", httputil.Error{Code: http.StatusBadRequest, Message: "Missing Code"} + return "", httputil.Error("provider didn't reply with code", http.StatusBadRequest, nil) } // validate the returned code with the identity provider session, err := a.provider.Authenticate(r.Context(), code) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code") - return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} + return "", xerrors.Errorf("error redeeming authenticate code: %w", err) } - // okay, time to go back to the proxy service. + // Opaque value used to maintain state between the request and the callback. + // OIDC : 3.1.2.5. Successful Authentication Response + // https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse 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.Error{Code: http.StatusBadRequest, Message: "Couldn't decode state"} + return "", xerrors.Errorf("failed decoding state: %w", err) } s := strings.SplitN(string(bytes), ":", 2) if len(s) != 2 { - return "", httputil.Error{Code: http.StatusBadRequest, Message: "Invalid State"} + return "", xerrors.Errorf("invalid state size: %v", len(s)) } + // state contains both our csrf nonce and the redirect uri nonce := s[0] redirect := s[1] c, err := a.csrfStore.GetCSRF(r) defer a.csrfStore.ClearCSRF(w, r) if err != nil || c.Value != nonce { - log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf failure") - return "", httputil.Error{Code: http.StatusForbidden, Message: "CSRF failed"} + return "", xerrors.Errorf("csrf failure: %w", err) + } - redirectURL, err := url.Parse(redirect) + redirectURL, err := urlutil.ParseAndValidateURL(redirect) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("authenticate: malformed redirect url") - return "", httputil.Error{Code: http.StatusForbidden, Message: "Malformed redirect url"} + return "", httputil.Error(fmt.Sprintf("invalid redirect uri %s", redirect), http.StatusBadRequest, err) } // sanity check, we are redirecting back to the same subdomain right? if !middleware.SameDomain(redirectURL, a.RedirectURL) { - return "", httputil.Error{Code: http.StatusBadRequest, Message: "Invalid Redirect URI domain"} + return "", httputil.Error(fmt.Sprintf("invalid redirect domain %v, %v", redirectURL, a.RedirectURL), http.StatusBadRequest, nil) } if err := a.sessionStore.SaveSession(w, r, session); err != nil { - log.Error().Err(err).Msg("authenticate: failed saving new session") - return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} + return "", xerrors.Errorf("failed saving new session: %w", err) } - return redirect, nil } @@ -289,24 +258,21 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) // audience ('aud') attribute must match Pomerium's client_id. func (a *Authenticate) ExchangeToken(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()}) + httputil.ErrorResponse(w, r, err) return } code := r.Form.Get("id_token") if code == "" { - log.FromRequest(r).Error().Msg("authenticate: provider missing id token") - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusBadRequest, Message: "missing id token"}) + httputil.ErrorResponse(w, r, httputil.Error("provider missing id token", http.StatusBadRequest, nil)) return } session, err := a.provider.IDTokenToSession(r.Context(), code) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("authenticate: error exchanging identity provider code") - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "could not exchange identity for session"}) + httputil.ErrorResponse(w, r, httputil.Error("could not exchange identity for session", http.StatusInternalServerError, err)) return } if err := a.restStore.SaveSession(w, r, session); err != nil { - log.Error().Err(err).Msg("authenticate: failed returning new session") - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "authenticate: failed returning new session"}) + httputil.ErrorResponse(w, r, httputil.Error("failed returning new session", http.StatusInternalServerError, err)) return } } diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index 5ceca1500..ae549b381 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -69,133 +69,40 @@ func TestAuthenticate_SignIn(t *testing.T) { redirectURI string session sessions.SessionStore provider identity.MockProvider + cipher cryptutil.Cipher wantCode int }{ - {"good", - "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.StatusFound}, - {"session not valid", - "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: false}, - http.StatusInternalServerError}, - {"session refresh error", - "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, - 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{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - RefreshDeadline: time.Now().Add(-10 * time.Second), - }}, - identity.MockProvider{ - ValidateResponse: true, - }, - http.StatusInternalServerError}, - {"no cookie found trying to load", - "state=example", - "redirect_uri=some.example", - &sessions.MockSessionStore{ - LoadError: http.ErrNoCookie, - Session: &sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - identity.MockProvider{ValidateResponse: true}, - http.StatusBadRequest}, - {"unexpected error trying to load session", - "state=example", - "redirect_uri=some.example", - &sessions.MockSessionStore{ - LoadError: errors.New("error"), - Session: &sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - 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}, + {"good", "state=example", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusFound}, + {"session not valid", "state=example", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: false}, &cryptutil.MockCipher{}, http.StatusInternalServerError}, + {"session refresh error", "state=example", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, identity.MockProvider{ValidateResponse: true, RefreshError: errors.New("error")}, &cryptutil.MockCipher{}, http.StatusInternalServerError}, + {"session save after refresh error", "state=example", "https://some.example", &sessions.MockSessionStore{SaveError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusInternalServerError}, + {"no cookie found trying to load", "state=example", "https://some.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie, Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusBadRequest}, + {"unexpected error trying to load session", "state=example", "https://some.example", &sessions.MockSessionStore{LoadError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusBadRequest}, + {"malformed form", "state=example", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusInternalServerError}, + {"empty state", "state=", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusBadRequest}, + {"malformed redirect uri", "state=example", "https://accounts.google.^", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusBadRequest}, + // actually caught by go's handler, but we should keep the test. + {"bad redirect uri query", "state=nonce", "%gh&%ij", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{}, http.StatusInternalServerError}, + {"marshal session failure", "state=example", "https://some.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, &cryptutil.MockCipher{MarshalError: errors.New("error")}, http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authenticate{ sessionStore: tt.session, provider: tt.provider, - RedirectURL: uriParse(tt.redirectURI), + RedirectURL: uriParse("https://some.example"), csrfStore: &sessions.MockCSRFStore{}, SharedKey: "secret", - cipher: mockCipher{}, + cipher: tt.cipher, } - uri := &url.URL{Path: "/"} + uri := &url.URL{Host: "corp.some.example", Scheme: "https", Path: "/"} if tt.name == "malformed form" { uri.RawQuery = "example=%zzzzz" } else { - uri.RawQuery = fmt.Sprintf("%s&%s", tt.state, tt.redirectURI) + uri.RawQuery = fmt.Sprintf("%s&redirect_uri=%s", tt.state, tt.redirectURI) } r := httptest.NewRequest(http.MethodGet, uri.String(), nil) + r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() a.SignIn(w, r) @@ -241,7 +148,6 @@ func Test_getAuthCodeRedirectURL(t *testing.T) { {"https", uriParse("https://www.pomerium.io"), "state", "auth-code", "https://www.pomerium.io?code=auth-code&state=state"}, {"http", uriParse("http://www.pomerium.io"), "state", "auth-code", "http://www.pomerium.io?code=auth-code&state=state"}, {"no subdomain", uriParse("http://pomerium.io"), "state", "auth-code", "http://pomerium.io?code=auth-code&state=state"}, - {"no scheme make https", uriParse("pomerium.io"), "state", "auth-code", "https://pomerium.io?code=auth-code&state=state"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -274,7 +180,7 @@ func TestAuthenticate_SignOut(t *testing.T) { }{ {"good post", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusFound, ""}, {"failed revoke", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusBadRequest, "could not revoke"}, - {"malformed form", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusBadRequest, ""}, + {"malformed form", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusInternalServerError, ""}, {"load session error", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &sessions.MockSessionStore{LoadError: errors.New("hi"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusFound, ""}, } for _, tt := range tests { @@ -316,8 +222,9 @@ func redirectURLSignature(rawRedirect string, timestamp time.Time, secret string func TestAuthenticate_OAuthStart(t *testing.T) { tests := []struct { - name string - method string + name string + method string + redirectURLSetting string redirectURL string sig string @@ -328,47 +235,16 @@ func TestAuthenticate_OAuthStart(t *testing.T) { // sessionStore sessions.SessionStore wantCode int }{ - {"good", - http.MethodGet, - "https://corp.pomerium.io/", - redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), - fmt.Sprint(time.Now().Unix()), - identity.MockProvider{}, - sessions.MockCSRFStore{}, - http.StatusFound, - }, - {"bad timestamp", - http.MethodGet, - "https://corp.pomerium.io/", - redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), - fmt.Sprint(time.Now().Add(10 * time.Hour).Unix()), - identity.MockProvider{}, - sessions.MockCSRFStore{}, - http.StatusBadRequest, - }, - {"missing redirect", - http.MethodGet, - "", - redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), - fmt.Sprint(time.Now().Unix()), - identity.MockProvider{}, - sessions.MockCSRFStore{}, - http.StatusBadRequest, - }, - {"malformed redirect", - http.MethodGet, - "https://pomerium.com%zzzzz", - redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), - fmt.Sprint(time.Now().Unix()), - identity.MockProvider{}, - sessions.MockCSRFStore{}, - http.StatusBadRequest, - }, + {"good", http.MethodGet, "https://corp.pomerium.io/", "https://corp.pomerium.io/", redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), fmt.Sprint(time.Now().Unix()), identity.MockProvider{}, sessions.MockCSRFStore{}, http.StatusFound}, + {"bad timestamp", http.MethodGet, "https://corp.pomerium.io/", "https://corp.pomerium.io/", redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), fmt.Sprint(time.Now().Add(10 * time.Hour).Unix()), identity.MockProvider{}, sessions.MockCSRFStore{}, http.StatusBadRequest}, + {"missing redirect", http.MethodGet, "https://corp.pomerium.io/", "", redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), fmt.Sprint(time.Now().Unix()), identity.MockProvider{}, sessions.MockCSRFStore{}, http.StatusBadRequest}, + {"malformed redirect", http.MethodGet, "https://corp.pomerium.io/", "https://pomerium.com%zzzzz", redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), fmt.Sprint(time.Now().Unix()), identity.MockProvider{}, sessions.MockCSRFStore{}, http.StatusBadRequest}, + {"different domains", http.MethodGet, "https://corp.notpomerium.io/", "https://corp.pomerium.io/", redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"), fmt.Sprint(time.Now().Unix()), identity.MockProvider{}, sessions.MockCSRFStore{}, http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authenticate{ - RedirectURL: uriParse("http://www.pomerium.io"), + RedirectURL: uriParse(tt.redirectURLSetting), csrfStore: tt.csrfStore, provider: tt.provider, SharedKey: "secret", @@ -383,18 +259,19 @@ func TestAuthenticate_OAuthStart(t *testing.T) { u.RawQuery = params.Encode() r := httptest.NewRequest(tt.method, u.String(), nil) + r.Header.Set("Accept", "application/json") + w := httptest.NewRecorder() a.OAuthStart(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\n%v", status, tt.wantCode, w.Body.String()) } }) } } -func TestAuthenticate_getOAuthCallback(t *testing.T) { - +func TestAuthenticate_OAuthCallback(t *testing.T) { tests := []struct { name string method string @@ -408,216 +285,22 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) { provider identity.MockProvider csrfStore sessions.MockCSRFStore - want string - wantErr bool + want string + wantCode int }{ - {"good", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "https://corp.pomerium.io", - false, - }, - {"get csrf error", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - GetError: errors.New("error"), - Cookie: &http.Cookie{Value: "not nonce"}}, - "", - true, - }, - {"csrf nonce error", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "not nonce"}}, - "", - true, - }, - {"failed authenticate", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateError: errors.New("error"), - }, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - {"failed save session", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{SaveError: errors.New("error")}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - - {"error returned", - http.MethodGet, - "idp error", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - {"empty code", - http.MethodGet, - "", - "", - base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - {"invalid state string", - http.MethodGet, - "", - "code", - "nonce:https://corp.pomerium.io", - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - {"malformed state", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, - {"invalid redirect uri", - http.MethodGet, - "", - "code", - base64.URLEncoding.EncodeToString([]byte("nonce:corp.pomerium.io")), - "https://authenticate.pomerium.io", - &sessions.MockSessionStore{}, - identity.MockProvider{ - AuthenticateResponse: sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }}, - sessions.MockCSRFStore{ - ResponseCSRF: "csrf", - Cookie: &http.Cookie{Value: "nonce"}}, - "", - true, - }, + {"good", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "https://corp.pomerium.io", http.StatusFound}, + {"get csrf error", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", GetError: errors.New("error"), Cookie: &http.Cookie{Value: "not nonce"}}, "", http.StatusInternalServerError}, + {"csrf nonce error", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "not nonce"}}, "", http.StatusInternalServerError}, + {"failed authenticate", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateError: errors.New("error")}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusInternalServerError}, + {"failed save session", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{SaveError: errors.New("error")}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusInternalServerError}, + {"provider returned error", http.MethodGet, "idp error", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusBadRequest}, + {"empty code", http.MethodGet, "", "", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusBadRequest}, + {"invalid state string", http.MethodGet, "", "code", "nonce:https://corp.pomerium.io", "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusInternalServerError}, + {"malformed state", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusInternalServerError}, + {"invalid redirect uri", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusBadRequest}, + {"malformed form", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "", http.StatusBadRequest}, + {"bad redirect uri", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:http://^^^")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "https://corp.pomerium.io", http.StatusBadRequest}, + {"different domains", http.MethodGet, "", "code", base64.URLEncoding.EncodeToString([]byte("nonce:http://some.example.notpomerium.io")), "https://authenticate.pomerium.io", &sessions.MockSessionStore{}, identity.MockProvider{AuthenticateResponse: sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, sessions.MockCSRFStore{ResponseCSRF: "csrf", Cookie: &http.Cookie{Value: "nonce"}}, "https://corp.pomerium.io", http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -636,17 +319,18 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) { u.RawQuery = params.Encode() + if tt.name == "malformed form" { + u.RawQuery = "example=%zzzzz" + } r := httptest.NewRequest(tt.method, u.String(), nil) + r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() - got, err := a.getOAuthCallback(w, r) - if (err != nil) != tt.wantErr { - t.Errorf("Authenticate.getOAuthCallback() error = %v, wantErr %v", err, tt.wantErr) + a.OAuthCallback(w, r) + if w.Result().StatusCode != tt.wantCode { + t.Errorf("Authenticate.OAuthCallback() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantCode, w.Body.String()) return } - if got != tt.want { - t.Errorf("Authenticate.getOAuthCallback() = %v, want %v", got, tt.want) - } }) } } diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index d4c85e4cb..e109d907c 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -14,6 +14,8 @@ ### Fixed +- HTTP status codes now better adhere to [RFC7235](https://tools.ietf.org/html/rfc7235). In particular, authentication failures reply with [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) while authorization failures reply with [403 Forbidden](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403). [GH-272](https://github.com/pomerium/pomerium/pull/272) + ### Changed - A policy's custom certificate authority can set as a file or a base64 encoded blob(`tls_custom_ca`/`tls_custom_ca_file`). [GH-259](https://github.com/pomerium/pomerium/pull/259) diff --git a/go.mod b/go.mod index 0dcdc8412..9e46f6584 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( golang.org/x/net v0.0.0-20190611141213-3f473d35a33a golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 google.golang.org/api v0.6.0 google.golang.org/appengine v1.6.1 // indirect google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 // indirect diff --git a/go.sum b/go.sum index e06dfd01c..117ebe2c0 100644 --- a/go.sum +++ b/go.sum @@ -257,6 +257,8 @@ golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/internal/httputil/errors.go b/internal/httputil/errors.go index 527d6d9fe..e05e6932c 100644 --- a/internal/httputil/errors.go +++ b/internal/httputil/errors.go @@ -6,27 +6,64 @@ import ( "io" "net/http" + "golang.org/x/xerrors" + "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/templates" ) -// 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 formats creates a HTTP error with code, user friendly (and safe) error +// message. If nil or empty: +// HTTP status code defaults to 500. +// Message defaults to the text of the status code. +func Error(message string, code int, err error) error { + if code == 0 { + code = http.StatusInternalServerError + } + if message == "" { + message = http.StatusText(code) + } + return &httpError{Message: message, Code: code, Err: err} } -// Error fulfills the error interface, returning a string representation of the error. -func (h Error) Error() string { - return fmt.Sprintf("%d %s: %s", h.Code, http.StatusText(h.Code), h.Message) +type httpError struct { + // Message to present to the end user. + Message string + // HTTP status codes as registered with IANA. + Code int + + Err error // the cause } -// 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, e *Error) { +func (e *httpError) Error() string { + s := fmt.Sprintf("%d %s: %s", e.Code, http.StatusText(e.Code), e.Message) + if e.Err != nil { + return s + ": " + e.Err.Error() + } + return s +} +func (e *httpError) Unwrap() error { return e.Err } + +// Timeout reports whether this error represents a user debuggable error. +func (e *httpError) Debugable() bool { return e.Code == http.StatusUnauthorized } + +// ErrorResponse renders an error page given an error. If the error is a +// http error from this package, a user friendly message is set, http status code, +// the ability to debug are also set. +func ErrorResponse(rw http.ResponseWriter, r *http.Request, e error) { + statusCode := http.StatusInternalServerError // default status code to return + errorString := e.Error() + var canDebug bool var requestID string + var httpError *httpError + // if this is an HTTPError, we can add some additional useful information + if xerrors.As(e, &httpError) { + canDebug = httpError.Debugable() + statusCode = httpError.Code + errorString = httpError.Message + } + log.FromRequest(r).Error().Err(e).Str("http-message", errorString).Int("http-code", statusCode).Msg("http-error") + if id, ok := log.IDFromRequest(r); ok { requestID = id } @@ -34,10 +71,10 @@ func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) { var response struct { Error string `json:"error"` } - response.Error = e.Message - writeJSONResponse(rw, e.Code, response) + response.Error = e.Error() + writeJSONResponse(rw, statusCode, response) } else { - rw.WriteHeader(e.Code) + rw.WriteHeader(statusCode) t := struct { Code int Title string @@ -45,11 +82,11 @@ func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) { RequestID string CanDebug bool }{ - Code: e.Code, - Title: http.StatusText(e.Code), - Message: e.Message, + Code: statusCode, + Title: http.StatusText(statusCode), + Message: errorString, RequestID: requestID, - CanDebug: e.CanDebug, + CanDebug: canDebug, } templates.New().ExecuteTemplate(rw, "error.html", t) } diff --git a/internal/httputil/errors_test.go b/internal/httputil/errors_test.go index 7134109f9..18e130b0e 100644 --- a/internal/httputil/errors_test.go +++ b/internal/httputil/errors_test.go @@ -1,9 +1,12 @@ package httputil import ( + "errors" "net/http" "net/http/httptest" "testing" + + "github.com/google/go-cmp/cmp" ) func TestErrorResponse(t *testing.T) { @@ -11,10 +14,10 @@ func TestErrorResponse(t *testing.T) { name string rw http.ResponseWriter r *http.Request - e *Error + e *httpError }{ - {"good", httptest.NewRecorder(), &http.Request{Method: http.MethodGet}, &Error{Code: http.StatusBadRequest, Message: "missing id token"}}, - {"good json", httptest.NewRecorder(), &http.Request{Method: http.MethodGet, Header: http.Header{"Accept": []string{"application/json"}}}, &Error{Code: http.StatusBadRequest, Message: "missing id token"}}, + {"good", httptest.NewRecorder(), &http.Request{Method: http.MethodGet}, &httpError{Code: http.StatusBadRequest, Message: "missing id token"}}, + {"good json", httptest.NewRecorder(), &http.Request{Method: http.MethodGet, Header: http.Header{"Accept": []string{"application/json"}}}, &httpError{Code: http.StatusBadRequest, Message: "missing id token"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -29,20 +32,44 @@ func TestError_Error(t *testing.T) { name string Message string Code int - CanDebug bool + InnerErr error want string }{ - {"good", "short and stout", http.StatusTeapot, false, "418 I'm a teapot: short and stout"}, + {"good", "short and stout", http.StatusTeapot, nil, "418 I'm a teapot: short and stout"}, + {"nested error", "short and stout", http.StatusTeapot, errors.New("another error"), "418 I'm a teapot: short and stout: another error"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := Error{ - Message: tt.Message, - Code: tt.Code, - CanDebug: tt.CanDebug, + h := httpError{ + Message: tt.Message, + Code: tt.Code, + Err: tt.InnerErr, } - if got := h.Error(); got != tt.want { - t.Errorf("Error.Error() = %v, want %v", got, tt.want) + got := h.Error() + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("Error.Error() = %s", diff) + } + }) + } +} + +func Test_httpError_Error(t *testing.T) { + tests := []struct { + name string + message string + code int + err error + want string + }{ + {"good", "foobar", 200, nil, "200 OK: foobar"}, + {"no code", "foobar", 0, nil, "500 Internal Server Error: foobar"}, + {"no message or code", "", 0, nil, "500 Internal Server Error: Internal Server Error"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := Error(tt.message, tt.code, tt.err) + if got := e.Error(); got != tt.want { + t.Errorf("httpError.Error() = %v, want %v", got, tt.want) } }) } diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 72e4bf70c..0bb4328e0 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -40,8 +40,7 @@ func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Hand defer span.End() if err := r.ParseForm(); err != nil { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err)) return } clientSecret := r.Form.Get("shared_secret") @@ -51,7 +50,7 @@ func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Hand } if clientSecret != sharedSecret { - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError}) + httputil.ErrorResponse(w, r, httputil.Error("client secret mismatch", http.StatusBadRequest, nil)) return } next.ServeHTTP(w, r.WithContext(ctx)) @@ -68,25 +67,16 @@ func ValidateRedirectURI(rootDomain *url.URL) func(next http.Handler) http.Handl defer span.End() err := r.ParseForm() if err != nil { - httpErr := &httputil.Error{ - Message: err.Error(), - Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err)) return } redirectURI, err := url.Parse(r.Form.Get("redirect_uri")) if err != nil { - httpErr := &httputil.Error{ - Message: err.Error(), - Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("bad redirect_uri", http.StatusBadRequest, err)) return } if !SameDomain(redirectURI, rootDomain) { - httpErr := &httputil.Error{ - Message: "Invalid redirect parameter", - Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("redirect uri and root domain differ", http.StatusBadRequest, nil)) return } next.ServeHTTP(w, r.WithContext(ctx)) @@ -117,18 +107,14 @@ func ValidateSignature(sharedSecret string) func(next http.Handler) http.Handler err := r.ParseForm() if err != nil { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err)) return } redirectURI := r.Form.Get("redirect_uri") sigVal := r.Form.Get("sig") timestamp := r.Form.Get("ts") if !ValidSignature(redirectURI, sigVal, timestamp, sharedSecret) { - httpErr := &httputil.Error{ - Message: "Cross service signature failed to validate", - Code: http.StatusUnauthorized} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("invalid signature", http.StatusBadRequest, nil)) return } @@ -145,7 +131,7 @@ func ValidateHost(validHost func(host string) bool) func(next http.Handler) http defer span.End() if !validHost(r.Host) { - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusNotFound}) + httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not a known route.", r.Host), http.StatusNotFound, nil)) return } next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index eb7a1ee91..b2864d908 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.StatusInternalServerError}, - {"simple bad", "bad-secret", "secret", "", http.StatusInternalServerError}, + {"missing both", "secret", "", "", http.StatusBadRequest}, + {"simple bad", "bad-secret", "secret", "", http.StatusBadRequest}, {"malformed, invalid hex digits", "secret", "%zzzzz", "", http.StatusBadRequest}, } @@ -218,7 +218,7 @@ func TestValidateSignature(t *testing.T) { status int }{ {"valid signature", secretA, goodURL, sig, now, http.StatusOK}, - {"stale signature", secretA, goodURL, sig, staleTime, http.StatusUnauthorized}, + {"stale signature", secretA, goodURL, sig, staleTime, http.StatusBadRequest}, {"malformed", secretA, goodURL, "%zzzzz", now, http.StatusBadRequest}, } diff --git a/proxy/handlers.go b/proxy/handlers.go index 7fe44a362..791770d0e 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -57,7 +57,7 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: if err := r.ParseForm(); err != nil { - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusBadRequest}) + httputil.ErrorResponse(w, r, err) return } uri, err := url.Parse(r.Form.Get("redirect_uri")) @@ -86,9 +86,7 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { // Encrypt, and save CSRF state. Will be checked on callback. localState, err := p.cipher.Marshal(state) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } p.csrfStore.SetCSRF(w, r, localState) @@ -97,9 +95,7 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { // create a different cipher text using another nonce remoteState, err := p.cipher.Marshal(state) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed to encrypt cookie") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } @@ -110,9 +106,7 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { // we panic as a failure most likely means the rands entropy source is failing? if remoteState == localState { p.sessionStore.ClearSession(w, r) - log.FromRequest(r).Error().Msg("proxy: encrypted state should not match") - httpErr := &httputil.Error{Message: http.StatusText(http.StatusBadRequest), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("encrypted state should not match", http.StatusBadRequest, nil)) return } @@ -130,15 +124,12 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { // finish the oauth cycle func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing request form") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } - if errorString := r.Form.Get("error"); errorString != "" { - httpErr := &httputil.Error{Message: errorString, Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + if callbackError := r.Form.Get("error"); callbackError != "" { + httputil.ErrorResponse(w, r, httputil.Error(callbackError, http.StatusBadRequest, nil)) return } @@ -146,18 +137,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { remoteStateEncrypted := r.Form.Get("state") remoteStatePlain := new(StateParameter) if err := p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain); err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: could not unmarshal state") - httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } // Encrypted CSRF from session storage c, err := p.csrfStore.GetCSRF(r) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } p.csrfStore.ClearCSRF(w, r) @@ -165,9 +152,7 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { localStatePlain := new(StateParameter) err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF") - httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } @@ -175,18 +160,16 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { // Likely a replay attack or nonce-reuse. if remoteStateEncrypted == localStateEncrypted { p.sessionStore.ClearSession(w, r) - log.FromRequest(r).Error().Msg("proxy: local and remote state should not match") - httpErr := &httputil.Error{Message: http.StatusText(http.StatusBadRequest), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + + httputil.ErrorResponse(w, r, httputil.Error("local and remote state should not match!", http.StatusBadRequest, nil)) + return } // Decrypted remote and local state struct (inc. nonce) must match if remoteStatePlain.SessionID != localStatePlain.SessionID { p.sessionStore.ClearSession(w, r) - log.FromRequest(r).Error().Msg("proxy: CSRF mismatch") - httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("CSRF mismatch", http.StatusBadRequest, nil)) return } @@ -225,59 +208,50 @@ func isCORSPreflight(r *http.Request) bool { // Proxy authenticates a request, either proxying the request if it is authenticated, // or starting the authenticate service for validation if not. func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { - if !p.shouldSkipAuthentication(r) { - s, err := p.restStore.LoadSession(r) - // if authorization bearer token does not exist or fails, use cookie store - if err != nil || s == nil { - s, err = p.sessionStore.LoadSession(r) - if err != nil { - switch err { - case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: - log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: invalid session, start auth process") - p.sessionStore.ClearSession(w, r) - p.OAuthStart(w, r) - return - default: - log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") - httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) - return - } - } - } - - if err = p.authenticate(w, r, s); err != nil { - p.sessionStore.ClearSession(w, r) - log.FromRequest(r).Debug().Err(err).Msg("proxy: user unauthenticated") - 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, s) - if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed authorization") - httpErr := &httputil.Error{Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) - return - } - - if !authorized { - log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized") - httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true} - httputil.ErrorResponse(w, r, httpErr) - return - } - r.Header.Set(HeaderUserID, s.User) - r.Header.Set(HeaderEmail, s.RequestEmail()) - r.Header.Set(HeaderGroups, s.RequestGroups()) - } - - // We have validated the users request and now proxy their request to the provided upstream. + // does a route exist for this request? route, ok := p.router(r) if !ok { - httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusNotFound}) + httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not a managed route.", r.Host), http.StatusNotFound, nil)) return } + + if p.shouldSkipAuthentication(r) { + log.FromRequest(r).Debug().Msg("proxy: access control skipped") + route.ServeHTTP(w, r) + return + } + + s, err := p.restStore.LoadSession(r) + // if authorization bearer token does not exist or fails, use cookie store + if err != nil || s == nil { + s, err = p.sessionStore.LoadSession(r) + if err != nil { + log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: invalid session, re-authenticating") + p.sessionStore.ClearSession(w, r) + p.OAuthStart(w, r) + return + } + } + + if err = p.authenticate(w, r, s); err != nil { + p.sessionStore.ClearSession(w, r) + httputil.ErrorResponse(w, r, httputil.Error("User unauthenticated", http.StatusUnauthorized, err)) + return + } + authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, s) + if err != nil { + httputil.ErrorResponse(w, r, err) + return + } + + if !authorized { + httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not authorized for this route", s.Email), http.StatusForbidden, nil)) + return + } + r.Header.Set(HeaderUserID, s.User) + r.Header.Set(HeaderEmail, s.RequestEmail()) + r.Header.Set(HeaderGroups, s.RequestGroups()) + route.ServeHTTP(w, r) } @@ -294,18 +268,15 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { } if err := p.authenticate(w, r, session); err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: authenticate failed") - httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true} - httputil.ErrorResponse(w, r, httpErr) + p.sessionStore.ClearSession(w, r) + httputil.ErrorResponse(w, r, httputil.Error("User unauthenticated", http.StatusUnauthorized, err)) return } redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"} isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: is admin client") - httpErr := &httputil.Error{Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } @@ -313,9 +284,7 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) { csrf := &StateParameter{SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey())} csrfCookie, err := p.cipher.Marshal(csrf) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf") - httpErr := &httputil.Error{Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } p.csrfStore.SetCSRF(w, r, csrfCookie) @@ -351,38 +320,29 @@ 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 { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } iss, err := session.IssuedAt() if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't get token's create time") - httpErr := &httputil.Error{Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } // reject a refresh if it's been less than the refresh cooldown to prevent abuse if time.Since(iss) < p.refreshCooldown { - log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown") - httpErr := &httputil.Error{ - Message: fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown), - Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, + httputil.Error(fmt.Sprintf("Session must be %s old before refreshing", p.refreshCooldown), http.StatusBadRequest, nil)) return } newSession, err := p.AuthenticateClient.Refresh(r.Context(), session) if err != nil { - log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } if err = p.sessionStore.SaveSession(w, r, newSession); err != nil { - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } http.Redirect(w, r, "/.pomerium", http.StatusFound) @@ -394,50 +354,35 @@ func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) { 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") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } session, err := p.sessionStore.LoadSession(r) if err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: load session") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) 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") - httpErr := &httputil.Error{ - Message: fmt.Sprintf("%s must be and administrator", session.Email), - Code: http.StatusForbidden, - CanDebug: true} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not an administrator", session.Email), http.StatusForbidden, err)) 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") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } p.csrfStore.ClearCSRF(w, r) encryptedCSRF := c.Value decryptedCSRF := new(StateParameter) if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF") - httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } if decryptedCSRF.SessionID != r.FormValue("csrf") { - log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate CSRF mismatch") - httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusForbidden} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, httputil.Error("CSRF mismatch", http.StatusBadRequest, nil)) return } @@ -446,9 +391,7 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) { session.ImpersonateGroups = strings.Split(r.FormValue("group"), ",") if err := p.sessionStore.SaveSession(w, r, session); err != nil { - log.FromRequest(r).Error().Err(err).Msg("proxy: save session") - httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) + httputil.ErrorResponse(w, r, err) return } } diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index e68d7ddea..4d14c42d8 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -293,21 +293,21 @@ func TestProxy_Proxy(t *testing.T) { {"good email impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateEmail: "test@user.example"}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK}, {"good group impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateGroups: []string{"group1", "group2"}}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK}, // same request as above, but with cors_allow_preflight=false in the policy - {"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, + {"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden}, // cors allowed, but the request is missing proper headers - {"invalid cors headers", optsCORS, http.MethodOptions, badCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, - {"unexpected error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError}, + {"invalid cors headers", optsCORS, http.MethodOptions, badCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden}, + {"unexpected error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest}, // redirect to start auth process {"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound}, - {"user not authorized", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, - {"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError}, + {"user not authorized", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden}, + {"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError}, // authenticate errors - {"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError}, - {"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden}, - {"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden}, - {"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden}, + {"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest}, + {"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized}, + {"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized}, + {"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized}, {"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK}, - {"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, + {"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusNotFound}, // no session, redirect to login {"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest}, {"No policies", optsNoPolicies, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound}, @@ -410,7 +410,7 @@ func TestProxy_Refresh(t *testing.T) { wantStatus int }{ {"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusFound}, - {"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest}, + {"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError}, {"bad id token", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "bad"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError}, {"issue date too soon", timeSinceError, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest}, {"refresh failure", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{RefreshError: errors.New("err")}, clients.MockAuthorize{}, http.StatusInternalServerError}, @@ -460,11 +460,11 @@ func TestProxy_Impersonate(t *testing.T) { {"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}, + {"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.StatusInternalServerError}, {"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}, + {"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.StatusBadRequest}, {"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}, + {"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.StatusInternalServerError}, {"groups", false, opts, http.MethodPost, "user@blah.com", "group1,group2", "", &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}, } for _, tt := range tests { @@ -511,7 +511,7 @@ func TestProxy_OAuthCallback(t *testing.T) { {"good", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusFound}, {"error", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"error": "some error"}, http.StatusBadRequest}, {"state err", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError}, - {"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest}, + {"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError}, {"unmarshal err", sessions.MockCSRFStore{Cookie: &http.Cookie{Name: "something_csrf", Value: "unmarshal error"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError}, {"malformed", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError}, } @@ -555,7 +555,7 @@ func TestProxy_SignOut(t *testing.T) { {"good post", http.MethodPost, "https://test.example", http.StatusFound}, {"good get", http.MethodGet, "https://test.example", http.StatusFound}, {"good empty default", http.MethodGet, "", http.StatusFound}, - {"malformed", http.MethodPost, "", http.StatusBadRequest}, + {"malformed", http.MethodPost, "", http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {