mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 15:47:36 +02:00
httputil: use http error wrapper
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
d26f935cbb
commit
6e6ab3baa0
11 changed files with 325 additions and 677 deletions
|
@ -7,11 +7,14 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/internal/middleware"
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSPHeaders are the content security headers added to the service's handlers
|
// 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.
|
// RobotsTxt handles the /robots.txt route.
|
||||||
func (a *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
|
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)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
||||||
}
|
}
|
||||||
|
@ -54,93 +59,74 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request, sess
|
||||||
if session.RefreshPeriodExpired() {
|
if session.RefreshPeriodExpired() {
|
||||||
session, err := a.provider.Refresh(r.Context(), session)
|
session, err := a.provider.Refresh(r.Context(), session)
|
||||||
if err != nil {
|
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 = a.sessionStore.SaveSession(w, r, session); err != nil {
|
||||||
if err != nil {
|
return xerrors.Errorf("failed saving refreshed session : %w", err)
|
||||||
return fmt.Errorf("authenticate: failed saving refreshed session : %v", err)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
valid, err := a.provider.Validate(r.Context(), session.IDToken)
|
valid, err := a.provider.Validate(r.Context(), session.IDToken)
|
||||||
if err != nil || !valid {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignIn handles the sign_in endpoint. It attempts to authenticate the user,
|
// SignIn handles to authenticating a user.
|
||||||
// and if the user is not authenticated, it renders a sign in page.
|
|
||||||
func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
|
func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := a.sessionStore.LoadSession(r)
|
session, err := a.sessionStore.LoadSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
log.FromRequest(r).Debug().Err(err).Msg("no session loaded, restart auth")
|
||||||
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
|
a.sessionStore.ClearSession(w, r)
|
||||||
log.FromRequest(r).Debug().Err(err).Msg("authenticate: invalid session")
|
a.OAuthStart(w, r)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = r.ParseForm(); err != nil {
|
// if a session already exists, authenticate it
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
if err := a.authenticate(w, r, session); err != nil {
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
httputil.ErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// original `state` parameter received from the proxy application.
|
if err := r.ParseForm(); err != nil {
|
||||||
state := r.Form.Get("state")
|
httputil.ErrorResponse(w, r, err)
|
||||||
if state == "" {
|
|
||||||
httpErr := &httputil.Error{Message: "no state parameter supplied", Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{Message: "malformed redirect_uri parameter passed", Code: http.StatusBadRequest}
|
httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri parameter passed", http.StatusBadRequest, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// encrypt session state as json blob
|
// encrypt session state as json blob
|
||||||
encrypted, err := sessions.MarshalSession(session, a.cipher)
|
encrypted, err := sessions.MarshalSession(session, a.cipher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
httputil.ErrorResponse(w, r, httputil.Error("couldn't marshall session", http.StatusInternalServerError, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, getAuthCodeRedirectURL(redirectURL, state, encrypted), http.StatusFound)
|
http.Redirect(w, r, getAuthCodeRedirectURL(redirectURL, state, encrypted), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthCodeRedirectURL(redirectURL *url.URL, state, authCode string) string {
|
func getAuthCodeRedirectURL(redirectURL *url.URL, state, authCode string) string {
|
||||||
u, _ := url.Parse(redirectURL.String())
|
// error handled by go's mux stack
|
||||||
params, _ := url.ParseQuery(u.RawQuery)
|
params, _ := url.ParseQuery(redirectURL.RawQuery)
|
||||||
params.Set("code", authCode)
|
params.Set("code", authCode)
|
||||||
params.Set("state", state)
|
params.Set("state", state)
|
||||||
u.RawQuery = params.Encode()
|
redirectURL.RawQuery = params.Encode()
|
||||||
if u.Scheme == "" {
|
return redirectURL.String()
|
||||||
u.Scheme = "https"
|
|
||||||
}
|
|
||||||
return u.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignOut signs the user out by trying to revoke the user's remote identity session along with
|
// 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.
|
// the associated local session state. Handles both GET and POST.
|
||||||
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
log.Error().Err(err).Msg("authenticate: error SignOut form")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectURI := r.Form.Get("redirect_uri")
|
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)
|
a.sessionStore.ClearSession(w, r)
|
||||||
err = a.provider.Revoke(session.AccessToken)
|
err = a.provider.Revoke(session.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("authenticate: failed to revoke user session")
|
httputil.ErrorResponse(w, r, httputil.Error("could not revoke user session", http.StatusBadRequest, err))
|
||||||
httpErr := &httputil.Error{Message: fmt.Sprintf("could not revoke session: %s ", err.Error()), Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, redirectURI, http.StatusFound)
|
http.Redirect(w, r, redirectURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthStart starts the authenticate process by redirecting to the identity provider.
|
// 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
|
// https://tools.ietf.org/html/rfc6749#section-4.2.1
|
||||||
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||||
authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
|
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())
|
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
|
||||||
a.csrfStore.SetCSRF(w, r, nonce)
|
a.csrfStore.SetCSRF(w, r, nonce)
|
||||||
|
|
||||||
// verify redirect uri is from the root domain
|
// Redirection URI to which the response will be sent. This URI MUST exactly
|
||||||
if !middleware.SameDomain(authRedirectURL, a.RedirectURL) {
|
// match one of the Redirection URI values for the Client pre-registered at
|
||||||
httpErr := &httputil.Error{Message: "Invalid redirect parameter: redirect uri not from the root domain", Code: http.StatusBadRequest}
|
// at your identity provider
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
proxyRedirectURL, err := urlutil.ParseAndValidateURL(authRedirectURL.Query().Get("redirect_uri"))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify proxy url is from the root domain
|
|
||||||
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
|
|
||||||
if err != nil || !middleware.SameDomain(proxyRedirectURL, a.RedirectURL) {
|
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, httputil.Error("proxy url not from the root domain", http.StatusBadRequest, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,12 +170,12 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||||
proxyRedirectSig := authRedirectURL.Query().Get("sig")
|
proxyRedirectSig := authRedirectURL.Query().Get("sig")
|
||||||
ts := authRedirectURL.Query().Get("ts")
|
ts := authRedirectURL.Query().Get("ts")
|
||||||
if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, a.SharedKey) {
|
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, httputil.Error("invalid signature", http.StatusBadRequest, nil))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
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())))
|
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
|
||||||
|
|
||||||
// build the provider sign in url
|
// 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
|
// 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.
|
// 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) {
|
func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
redirect, err := a.getOAuthCallback(w, r)
|
redirect, err := a.getOAuthCallback(w, r)
|
||||||
switch h := err.(type) {
|
if err != nil {
|
||||||
case nil:
|
httputil.ErrorResponse(w, r, xerrors.Errorf("oauth callback : %w", err))
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// redirect back to the proxy-service via sign_in
|
// redirect back to the proxy-service via sign_in
|
||||||
http.Redirect(w, r, redirect, http.StatusFound)
|
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) {
|
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 {
|
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")
|
// OIDC : 3.1.2.6. Authentication Error Response
|
||||||
if errorString != "" {
|
// https://openid.net/specs/openid-connect-core-1_0-final.html#AuthError
|
||||||
log.FromRequest(r).Error().Str("Error", errorString).Msg("authenticate: provider returned error")
|
if errorString := r.Form.Get("error"); errorString != "" {
|
||||||
return "", httputil.Error{Code: http.StatusForbidden, Message: 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")
|
code := r.Form.Get("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
log.FromRequest(r).Error().Msg("authenticate: provider missing code")
|
return "", httputil.Error("provider didn't reply with code", http.StatusBadRequest, nil)
|
||||||
return "", httputil.Error{Code: http.StatusBadRequest, Message: "Missing Code"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the returned code with the identity provider
|
// validate the returned code with the identity provider
|
||||||
session, err := a.provider.Authenticate(r.Context(), code)
|
session, err := a.provider.Authenticate(r.Context(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code")
|
return "", xerrors.Errorf("error redeeming authenticate code: %w", err)
|
||||||
return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"))
|
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state")
|
return "", xerrors.Errorf("failed decoding state: %w", err)
|
||||||
return "", httputil.Error{Code: http.StatusBadRequest, Message: "Couldn't decode state"}
|
|
||||||
}
|
}
|
||||||
s := strings.SplitN(string(bytes), ":", 2)
|
s := strings.SplitN(string(bytes), ":", 2)
|
||||||
if len(s) != 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]
|
nonce := s[0]
|
||||||
redirect := s[1]
|
redirect := s[1]
|
||||||
c, err := a.csrfStore.GetCSRF(r)
|
c, err := a.csrfStore.GetCSRF(r)
|
||||||
defer a.csrfStore.ClearCSRF(w, r)
|
defer a.csrfStore.ClearCSRF(w, r)
|
||||||
if err != nil || c.Value != nonce {
|
if err != nil || c.Value != nonce {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf failure")
|
return "", xerrors.Errorf("csrf failure: %w", err)
|
||||||
return "", httputil.Error{Code: http.StatusForbidden, Message: "CSRF failed"}
|
|
||||||
}
|
}
|
||||||
redirectURL, err := url.Parse(redirect)
|
redirectURL, err := urlutil.ParseAndValidateURL(redirect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: malformed redirect url")
|
return "", httputil.Error(fmt.Sprintf("invalid redirect uri %s", redirect), http.StatusBadRequest, err)
|
||||||
return "", httputil.Error{Code: http.StatusForbidden, Message: "Malformed redirect url"}
|
|
||||||
}
|
}
|
||||||
// sanity check, we are redirecting back to the same subdomain right?
|
// sanity check, we are redirecting back to the same subdomain right?
|
||||||
if !middleware.SameDomain(redirectURL, a.RedirectURL) {
|
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 {
|
if err := a.sessionStore.SaveSession(w, r, session); err != nil {
|
||||||
log.Error().Err(err).Msg("authenticate: failed saving new session")
|
return "", xerrors.Errorf("failed saving new session: %w", err)
|
||||||
return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect, nil
|
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.
|
// audience ('aud') attribute must match Pomerium's client_id.
|
||||||
func (a *Authenticate) ExchangeToken(w http.ResponseWriter, r *http.Request) {
|
func (a *Authenticate) ExchangeToken(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
httputil.ErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := r.Form.Get("id_token")
|
code := r.Form.Get("id_token")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
log.FromRequest(r).Error().Msg("authenticate: provider missing id token")
|
httputil.ErrorResponse(w, r, httputil.Error("provider missing id token", http.StatusBadRequest, nil))
|
||||||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusBadRequest, Message: "missing id token"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, err := a.provider.IDTokenToSession(r.Context(), code)
|
session, err := a.provider.IDTokenToSession(r.Context(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: error exchanging identity provider code")
|
httputil.ErrorResponse(w, r, httputil.Error("could not exchange identity for session", http.StatusInternalServerError, err))
|
||||||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "could not exchange identity for session"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := a.restStore.SaveSession(w, r, session); err != nil {
|
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("failed returning new session", http.StatusInternalServerError, err))
|
||||||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "authenticate: failed returning new session"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,133 +69,40 @@ func TestAuthenticate_SignIn(t *testing.T) {
|
||||||
redirectURI string
|
redirectURI string
|
||||||
session sessions.SessionStore
|
session sessions.SessionStore
|
||||||
provider identity.MockProvider
|
provider identity.MockProvider
|
||||||
|
cipher cryptutil.Cipher
|
||||||
wantCode int
|
wantCode int
|
||||||
}{
|
}{
|
||||||
{"good",
|
{"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},
|
||||||
"state=example",
|
{"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},
|
||||||
"redirect_uri=some.example",
|
{"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},
|
||||||
&sessions.MockSessionStore{
|
{"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},
|
||||||
Session: &sessions.SessionState{
|
{"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},
|
||||||
AccessToken: "AccessToken",
|
{"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},
|
||||||
RefreshToken: "RefreshToken",
|
{"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},
|
||||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
{"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},
|
||||||
identity.MockProvider{ValidateResponse: true},
|
// actually caught by go's handler, but we should keep the test.
|
||||||
http.StatusFound},
|
{"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},
|
||||||
{"session not valid",
|
{"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},
|
||||||
"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},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
a := &Authenticate{
|
a := &Authenticate{
|
||||||
sessionStore: tt.session,
|
sessionStore: tt.session,
|
||||||
provider: tt.provider,
|
provider: tt.provider,
|
||||||
RedirectURL: uriParse(tt.redirectURI),
|
RedirectURL: uriParse("https://some.example"),
|
||||||
csrfStore: &sessions.MockCSRFStore{},
|
csrfStore: &sessions.MockCSRFStore{},
|
||||||
SharedKey: "secret",
|
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" {
|
if tt.name == "malformed form" {
|
||||||
uri.RawQuery = "example=%zzzzz"
|
uri.RawQuery = "example=%zzzzz"
|
||||||
} else {
|
} 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 := httptest.NewRequest(http.MethodGet, uri.String(), nil)
|
||||||
|
r.Header.Set("Accept", "application/json")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
a.SignIn(w, r)
|
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"},
|
{"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"},
|
{"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 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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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, ""},
|
{"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"},
|
{"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, ""},
|
{"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 {
|
for _, tt := range tests {
|
||||||
|
@ -316,8 +222,9 @@ func redirectURLSignature(rawRedirect string, timestamp time.Time, secret string
|
||||||
|
|
||||||
func TestAuthenticate_OAuthStart(t *testing.T) {
|
func TestAuthenticate_OAuthStart(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
method string
|
method string
|
||||||
|
redirectURLSetting string
|
||||||
|
|
||||||
redirectURL string
|
redirectURL string
|
||||||
sig string
|
sig string
|
||||||
|
@ -328,47 +235,16 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
|
||||||
// sessionStore sessions.SessionStore
|
// sessionStore sessions.SessionStore
|
||||||
wantCode int
|
wantCode int
|
||||||
}{
|
}{
|
||||||
{"good",
|
{"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},
|
||||||
http.MethodGet,
|
{"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},
|
||||||
"https://corp.pomerium.io/",
|
{"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},
|
||||||
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
|
{"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},
|
||||||
fmt.Sprint(time.Now().Unix()),
|
{"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},
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
a := &Authenticate{
|
a := &Authenticate{
|
||||||
RedirectURL: uriParse("http://www.pomerium.io"),
|
RedirectURL: uriParse(tt.redirectURLSetting),
|
||||||
csrfStore: tt.csrfStore,
|
csrfStore: tt.csrfStore,
|
||||||
provider: tt.provider,
|
provider: tt.provider,
|
||||||
SharedKey: "secret",
|
SharedKey: "secret",
|
||||||
|
@ -383,18 +259,19 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
|
||||||
u.RawQuery = params.Encode()
|
u.RawQuery = params.Encode()
|
||||||
|
|
||||||
r := httptest.NewRequest(tt.method, u.String(), nil)
|
r := httptest.NewRequest(tt.method, u.String(), nil)
|
||||||
|
r.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
a.OAuthStart(w, r)
|
a.OAuthStart(w, r)
|
||||||
if status := w.Code; status != tt.wantCode {
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
method string
|
method string
|
||||||
|
@ -408,216 +285,22 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
|
||||||
provider identity.MockProvider
|
provider identity.MockProvider
|
||||||
csrfStore sessions.MockCSRFStore
|
csrfStore sessions.MockCSRFStore
|
||||||
|
|
||||||
want string
|
want string
|
||||||
wantErr bool
|
wantCode int
|
||||||
}{
|
}{
|
||||||
{"good",
|
{"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},
|
||||||
http.MethodGet,
|
{"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},
|
||||||
"code",
|
{"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},
|
||||||
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
|
{"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},
|
||||||
"https://authenticate.pomerium.io",
|
{"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},
|
||||||
&sessions.MockSessionStore{},
|
{"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},
|
||||||
identity.MockProvider{
|
{"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},
|
||||||
AuthenticateResponse: sessions.SessionState{
|
{"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},
|
||||||
AccessToken: "AccessToken",
|
{"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},
|
||||||
RefreshToken: "RefreshToken",
|
{"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},
|
||||||
Email: "blah@blah.com",
|
{"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},
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -636,17 +319,18 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
|
||||||
|
|
||||||
u.RawQuery = params.Encode()
|
u.RawQuery = params.Encode()
|
||||||
|
|
||||||
|
if tt.name == "malformed form" {
|
||||||
|
u.RawQuery = "example=%zzzzz"
|
||||||
|
}
|
||||||
r := httptest.NewRequest(tt.method, u.String(), nil)
|
r := httptest.NewRequest(tt.method, u.String(), nil)
|
||||||
|
r.Header.Set("Accept", "application/json")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
got, err := a.getOAuthCallback(w, r)
|
a.OAuthCallback(w, r)
|
||||||
if (err != nil) != tt.wantErr {
|
if w.Result().StatusCode != tt.wantCode {
|
||||||
t.Errorf("Authenticate.getOAuthCallback() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Authenticate.OAuthCallback() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantCode, w.Body.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("Authenticate.getOAuthCallback() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
### Fixed
|
### 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
|
### 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)
|
- 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
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
||||||
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a
|
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae // indirect
|
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/api v0.6.0
|
||||||
google.golang.org/appengine v1.6.1 // indirect
|
google.golang.org/appengine v1.6.1 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 // indirect
|
google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 // indirect
|
||||||
|
|
2
go.sum
2
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-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
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.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.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
|
|
@ -6,27 +6,64 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/internal/templates"
|
"github.com/pomerium/pomerium/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error reports an http error, its http status code, a custom message, and
|
// Error formats creates a HTTP error with code, user friendly (and safe) error
|
||||||
// whether it is CanDebug.
|
// message. If nil or empty:
|
||||||
type Error struct {
|
// HTTP status code defaults to 500.
|
||||||
Message string
|
// Message defaults to the text of the status code.
|
||||||
Code int
|
func Error(message string, code int, err error) error {
|
||||||
CanDebug bool
|
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.
|
type httpError struct {
|
||||||
func (h Error) Error() string {
|
// Message to present to the end user.
|
||||||
return fmt.Sprintf("%d %s: %s", h.Code, http.StatusText(h.Code), h.Message)
|
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.
|
func (e *httpError) Error() string {
|
||||||
// If no message is passed, defaults to the text of the status code.
|
s := fmt.Sprintf("%d %s: %s", e.Code, http.StatusText(e.Code), e.Message)
|
||||||
func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) {
|
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 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 {
|
if id, ok := log.IDFromRequest(r); ok {
|
||||||
requestID = id
|
requestID = id
|
||||||
}
|
}
|
||||||
|
@ -34,10 +71,10 @@ func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) {
|
||||||
var response struct {
|
var response struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
response.Error = e.Message
|
response.Error = e.Error()
|
||||||
writeJSONResponse(rw, e.Code, response)
|
writeJSONResponse(rw, statusCode, response)
|
||||||
} else {
|
} else {
|
||||||
rw.WriteHeader(e.Code)
|
rw.WriteHeader(statusCode)
|
||||||
t := struct {
|
t := struct {
|
||||||
Code int
|
Code int
|
||||||
Title string
|
Title string
|
||||||
|
@ -45,11 +82,11 @@ func ErrorResponse(rw http.ResponseWriter, r *http.Request, e *Error) {
|
||||||
RequestID string
|
RequestID string
|
||||||
CanDebug bool
|
CanDebug bool
|
||||||
}{
|
}{
|
||||||
Code: e.Code,
|
Code: statusCode,
|
||||||
Title: http.StatusText(e.Code),
|
Title: http.StatusText(statusCode),
|
||||||
Message: e.Message,
|
Message: errorString,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
CanDebug: e.CanDebug,
|
CanDebug: canDebug,
|
||||||
}
|
}
|
||||||
templates.New().ExecuteTemplate(rw, "error.html", t)
|
templates.New().ExecuteTemplate(rw, "error.html", t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package httputil
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestErrorResponse(t *testing.T) {
|
func TestErrorResponse(t *testing.T) {
|
||||||
|
@ -11,10 +14,10 @@ func TestErrorResponse(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
rw http.ResponseWriter
|
rw http.ResponseWriter
|
||||||
r *http.Request
|
r *http.Request
|
||||||
e *Error
|
e *httpError
|
||||||
}{
|
}{
|
||||||
{"good", httptest.NewRecorder(), &http.Request{Method: http.MethodGet}, &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"}}}, &Error{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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -29,20 +32,44 @@ func TestError_Error(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
Message string
|
Message string
|
||||||
Code int
|
Code int
|
||||||
CanDebug bool
|
InnerErr error
|
||||||
want string
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
h := Error{
|
h := httpError{
|
||||||
Message: tt.Message,
|
Message: tt.Message,
|
||||||
Code: tt.Code,
|
Code: tt.Code,
|
||||||
CanDebug: tt.CanDebug,
|
Err: tt.InnerErr,
|
||||||
}
|
}
|
||||||
if got := h.Error(); got != tt.want {
|
got := h.Error()
|
||||||
t.Errorf("Error.Error() = %v, want %v", got, tt.want)
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,7 @@ func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Hand
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clientSecret := r.Form.Get("shared_secret")
|
clientSecret := r.Form.Get("shared_secret")
|
||||||
|
@ -51,7 +50,7 @@ func ValidateClientSecret(sharedSecret string) func(next http.Handler) http.Hand
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientSecret != sharedSecret {
|
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
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
@ -68,25 +67,16 @@ func ValidateRedirectURI(rootDomain *url.URL) func(next http.Handler) http.Handl
|
||||||
defer span.End()
|
defer span.End()
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{
|
httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err))
|
||||||
Message: err.Error(),
|
|
||||||
Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectURI, err := url.Parse(r.Form.Get("redirect_uri"))
|
redirectURI, err := url.Parse(r.Form.Get("redirect_uri"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{
|
httputil.ErrorResponse(w, r, httputil.Error("bad redirect_uri", http.StatusBadRequest, err))
|
||||||
Message: err.Error(),
|
|
||||||
Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !SameDomain(redirectURI, rootDomain) {
|
if !SameDomain(redirectURI, rootDomain) {
|
||||||
httpErr := &httputil.Error{
|
httputil.ErrorResponse(w, r, httputil.Error("redirect uri and root domain differ", http.StatusBadRequest, nil))
|
||||||
Message: "Invalid redirect parameter",
|
|
||||||
Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
@ -117,18 +107,14 @@ func ValidateSignature(sharedSecret string) func(next http.Handler) http.Handler
|
||||||
|
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
httputil.ErrorResponse(w, r, httputil.Error("couldn't parse form", http.StatusBadRequest, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectURI := r.Form.Get("redirect_uri")
|
redirectURI := r.Form.Get("redirect_uri")
|
||||||
sigVal := r.Form.Get("sig")
|
sigVal := r.Form.Get("sig")
|
||||||
timestamp := r.Form.Get("ts")
|
timestamp := r.Form.Get("ts")
|
||||||
if !ValidSignature(redirectURI, sigVal, timestamp, sharedSecret) {
|
if !ValidSignature(redirectURI, sigVal, timestamp, sharedSecret) {
|
||||||
httpErr := &httputil.Error{
|
httputil.ErrorResponse(w, r, httputil.Error("invalid signature", http.StatusBadRequest, nil))
|
||||||
Message: "Cross service signature failed to validate",
|
|
||||||
Code: http.StatusUnauthorized}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +131,7 @@ func ValidateHost(validHost func(host string) bool) func(next http.Handler) http
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if !validHost(r.Host) {
|
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
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
|
|
@ -175,8 +175,8 @@ func TestValidateClientSecret(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"simple", "secret", "secret", "secret", http.StatusOK},
|
{"simple", "secret", "secret", "secret", http.StatusOK},
|
||||||
{"missing get param, valid header", "secret", "", "secret", http.StatusOK},
|
{"missing get param, valid header", "secret", "", "secret", http.StatusOK},
|
||||||
{"missing both", "secret", "", "", http.StatusInternalServerError},
|
{"missing both", "secret", "", "", http.StatusBadRequest},
|
||||||
{"simple bad", "bad-secret", "secret", "", http.StatusInternalServerError},
|
{"simple bad", "bad-secret", "secret", "", http.StatusBadRequest},
|
||||||
{"malformed, invalid hex digits", "secret", "%zzzzz", "", http.StatusBadRequest},
|
{"malformed, invalid hex digits", "secret", "%zzzzz", "", http.StatusBadRequest},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ func TestValidateSignature(t *testing.T) {
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{"valid signature", secretA, goodURL, sig, now, http.StatusOK},
|
{"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},
|
{"malformed", secretA, goodURL, "%zzzzz", now, http.StatusBadRequest},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusBadRequest})
|
httputil.ErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uri, err := url.Parse(r.Form.Get("redirect_uri"))
|
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.
|
// Encrypt, and save CSRF state. Will be checked on callback.
|
||||||
localState, err := p.cipher.Marshal(state)
|
localState, err := p.cipher.Marshal(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.csrfStore.SetCSRF(w, r, localState)
|
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
|
// create a different cipher text using another nonce
|
||||||
remoteState, err := p.cipher.Marshal(state)
|
remoteState, err := p.cipher.Marshal(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to encrypt cookie")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
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?
|
// we panic as a failure most likely means the rands entropy source is failing?
|
||||||
if remoteState == localState {
|
if remoteState == localState {
|
||||||
p.sessionStore.ClearSession(w, r)
|
p.sessionStore.ClearSession(w, r)
|
||||||
log.FromRequest(r).Error().Msg("proxy: encrypted state should not match")
|
httputil.ErrorResponse(w, r, httputil.Error("encrypted state should not match", http.StatusBadRequest, nil))
|
||||||
httpErr := &httputil.Error{Message: http.StatusText(http.StatusBadRequest), Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,15 +124,12 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||||
// finish the oauth cycle
|
// finish the oauth cycle
|
||||||
func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing request form")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorString := r.Form.Get("error"); errorString != "" {
|
if callbackError := r.Form.Get("error"); callbackError != "" {
|
||||||
httpErr := &httputil.Error{Message: errorString, Code: http.StatusBadRequest}
|
httputil.ErrorResponse(w, r, httputil.Error(callbackError, http.StatusBadRequest, nil))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,18 +137,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
remoteStateEncrypted := r.Form.Get("state")
|
remoteStateEncrypted := r.Form.Get("state")
|
||||||
remoteStatePlain := new(StateParameter)
|
remoteStatePlain := new(StateParameter)
|
||||||
if err := p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain); err != nil {
|
if err := p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: could not unmarshal state")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypted CSRF from session storage
|
// Encrypted CSRF from session storage
|
||||||
c, err := p.csrfStore.GetCSRF(r)
|
c, err := p.csrfStore.GetCSRF(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.csrfStore.ClearCSRF(w, r)
|
p.csrfStore.ClearCSRF(w, r)
|
||||||
|
@ -165,9 +152,7 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
localStatePlain := new(StateParameter)
|
localStatePlain := new(StateParameter)
|
||||||
err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain)
|
err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,18 +160,16 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
// Likely a replay attack or nonce-reuse.
|
// Likely a replay attack or nonce-reuse.
|
||||||
if remoteStateEncrypted == localStateEncrypted {
|
if remoteStateEncrypted == localStateEncrypted {
|
||||||
p.sessionStore.ClearSession(w, r)
|
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, httputil.Error("local and remote state should not match!", http.StatusBadRequest, nil))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypted remote and local state struct (inc. nonce) must match
|
// Decrypted remote and local state struct (inc. nonce) must match
|
||||||
if remoteStatePlain.SessionID != localStatePlain.SessionID {
|
if remoteStatePlain.SessionID != localStatePlain.SessionID {
|
||||||
p.sessionStore.ClearSession(w, r)
|
p.sessionStore.ClearSession(w, r)
|
||||||
log.FromRequest(r).Error().Msg("proxy: CSRF mismatch")
|
httputil.ErrorResponse(w, r, httputil.Error("CSRF mismatch", http.StatusBadRequest, nil))
|
||||||
httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,59 +208,50 @@ func isCORSPreflight(r *http.Request) bool {
|
||||||
// Proxy authenticates a request, either proxying the request if it is authenticated,
|
// Proxy authenticates a request, either proxying the request if it is authenticated,
|
||||||
// or starting the authenticate service for validation if not.
|
// or starting the authenticate service for validation if not.
|
||||||
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
||||||
if !p.shouldSkipAuthentication(r) {
|
// does a route exist for this request?
|
||||||
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.
|
|
||||||
route, ok := p.router(r)
|
route, ok := p.router(r)
|
||||||
if !ok {
|
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
|
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)
|
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 {
|
if err := p.authenticate(w, r, session); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: authenticate failed")
|
p.sessionStore.ClearSession(w, r)
|
||||||
httpErr := &httputil.Error{Code: http.StatusUnauthorized, CanDebug: true}
|
httputil.ErrorResponse(w, r, httputil.Error("User unauthenticated", http.StatusUnauthorized, err))
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"}
|
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"}
|
||||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: is admin client")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,9 +284,7 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
csrf := &StateParameter{SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey())}
|
csrf := &StateParameter{SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey())}
|
||||||
csrfCookie, err := p.cipher.Marshal(csrf)
|
csrfCookie, err := p.cipher.Marshal(csrf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.csrfStore.SetCSRF(w, r, csrfCookie)
|
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) {
|
func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := p.sessionStore.LoadSession(r)
|
session, err := p.sessionStore.LoadSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
httputil.ErrorResponse(w, r, err)
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iss, err := session.IssuedAt()
|
iss, err := session.IssuedAt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't get token's create time")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject a refresh if it's been less than the refresh cooldown to prevent abuse
|
// reject a refresh if it's been less than the refresh cooldown to prevent abuse
|
||||||
if time.Since(iss) < p.refreshCooldown {
|
if time.Since(iss) < p.refreshCooldown {
|
||||||
log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown")
|
httputil.ErrorResponse(w, r,
|
||||||
httpErr := &httputil.Error{
|
httputil.Error(fmt.Sprintf("Session must be %s old before refreshing", p.refreshCooldown), http.StatusBadRequest, nil))
|
||||||
Message: fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown),
|
|
||||||
Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newSession, err := p.AuthenticateClient.Refresh(r.Context(), session)
|
newSession, err := p.AuthenticateClient.Refresh(r.Context(), session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = p.sessionStore.SaveSession(w, r, newSession); err != nil {
|
if err = p.sessionStore.SaveSession(w, r, newSession); err != nil {
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
httputil.ErrorResponse(w, r, err)
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/.pomerium", http.StatusFound)
|
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) {
|
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate form")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, err := p.sessionStore.LoadSession(r)
|
session, err := p.sessionStore.LoadSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: load session")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// authorization check -- is this user an admin?
|
// authorization check -- is this user an admin?
|
||||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||||
if err != nil || !isAdmin {
|
if err != nil || !isAdmin {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: user must be admin to impersonate")
|
httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not an administrator", session.Email), http.StatusForbidden, err))
|
||||||
httpErr := &httputil.Error{
|
|
||||||
Message: fmt.Sprintf("%s must be and administrator", session.Email),
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
CanDebug: true}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// CSRF check -- did this request originate from our form?
|
// CSRF check -- did this request originate from our form?
|
||||||
c, err := p.csrfStore.GetCSRF(r)
|
c, err := p.csrfStore.GetCSRF(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusBadRequest}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.csrfStore.ClearCSRF(w, r)
|
p.csrfStore.ClearCSRF(w, r)
|
||||||
encryptedCSRF := c.Value
|
encryptedCSRF := c.Value
|
||||||
decryptedCSRF := new(StateParameter)
|
decryptedCSRF := new(StateParameter)
|
||||||
if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil {
|
if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: "Internal error", Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if decryptedCSRF.SessionID != r.FormValue("csrf") {
|
if decryptedCSRF.SessionID != r.FormValue("csrf") {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate CSRF mismatch")
|
httputil.ErrorResponse(w, r, httputil.Error("CSRF mismatch", http.StatusBadRequest, nil))
|
||||||
httpErr := &httputil.Error{Message: "CSRF mismatch", Code: http.StatusForbidden}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,9 +391,7 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
||||||
session.ImpersonateGroups = strings.Split(r.FormValue("group"), ",")
|
session.ImpersonateGroups = strings.Split(r.FormValue("group"), ",")
|
||||||
|
|
||||||
if err := p.sessionStore.SaveSession(w, r, session); err != nil {
|
if err := p.sessionStore.SaveSession(w, r, session); err != nil {
|
||||||
log.FromRequest(r).Error().Err(err).Msg("proxy: save session")
|
httputil.ErrorResponse(w, r, err)
|
||||||
httpErr := &httputil.Error{Message: err.Error(), Code: http.StatusInternalServerError}
|
|
||||||
httputil.ErrorResponse(w, r, httpErr)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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},
|
{"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
|
// 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
|
// 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},
|
{"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.StatusInternalServerError},
|
{"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
|
// 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},
|
{"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},
|
{"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://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError},
|
{"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
|
// 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},
|
{"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.StatusForbidden},
|
{"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.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.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.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.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", 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 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 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},
|
{"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
|
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},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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", 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},
|
{"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},
|
{"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},
|
{"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},
|
{"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 {
|
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},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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 post", http.MethodPost, "https://test.example", http.StatusFound},
|
||||||
{"good get", http.MethodGet, "https://test.example", http.StatusFound},
|
{"good get", http.MethodGet, "https://test.example", http.StatusFound},
|
||||||
{"good empty default", http.MethodGet, "", http.StatusFound},
|
{"good empty default", http.MethodGet, "", http.StatusFound},
|
||||||
{"malformed", http.MethodPost, "", http.StatusBadRequest},
|
{"malformed", http.MethodPost, "", http.StatusInternalServerError},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue