httputil: use http error wrapper

Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
Bobby DeSimone 2019-08-22 17:33:37 -07:00
parent d26f935cbb
commit 6e6ab3baa0
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
11 changed files with 325 additions and 677 deletions

View file

@ -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")
log.FromRequest(r).Debug().Err(err).Msg("no session loaded, restart auth")
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)
}
// if a session already exists, authenticate it
if err := a.authenticate(w, r, session); err != nil {
httputil.ErrorResponse(w, r, err)
return
}
}
err = a.authenticate(w, r, session)
if err != nil {
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
httputil.ErrorResponse(w, r, httpErr)
return
}
if err = r.ParseForm(); err != nil {
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
httputil.ErrorResponse(w, r, httpErr)
return
}
// original `state` parameter received from the proxy application.
state := r.Form.Get("state")
if state == "" {
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
}
}

View file

@ -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 {
@ -318,6 +224,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
tests := []struct {
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
@ -409,215 +286,21 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
csrfStore sessions.MockCSRFStore
want string
wantErr bool
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)
}
})
}
}

View file

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

1
go.mod
View file

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

2
go.sum
View file

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

View file

@ -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 {
// 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}
}
type httpError struct {
// Message to present to the end user.
Message string
// HTTP status codes as registered with IANA.
Code int
CanDebug bool
Err error // the cause
}
// 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)
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 }
// 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) {
// 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)
}

View file

@ -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{
h := httpError{
Message: tt.Message,
Code: tt.Code,
CanDebug: tt.CanDebug,
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)
}
})
}

View file

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

View file

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

View file

@ -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) {
// does a route exist for this request?
route, ok := p.router(r)
if !ok {
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 {
switch err {
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: invalid session, start auth process")
log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: invalid session, re-authenticating")
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)
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 {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed authorization")
httpErr := &httputil.Error{Code: http.StatusInternalServerError}
httputil.ErrorResponse(w, r, httpErr)
httputil.ErrorResponse(w, r, err)
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)
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())
}
// We have validated the users request and now proxy their request to the provided upstream.
route, ok := p.router(r)
if !ok {
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusNotFound})
return
}
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
}
}

View file

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