pomerium/proxy/handlers.go
Bobby DeSimone ebee64b70b
internal/frontend : serve static assets (#392)
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
2019-11-22 17:46:01 -08:00

221 lines
8.6 KiB
Go

package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"github.com/gorilla/mux"
"github.com/pomerium/csrf"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/urlutil"
)
// registerDashboardHandlers returns the proxy service's ServeMux
func (p *Proxy) registerDashboardHandlers(r *mux.Router) *mux.Router {
// dashboard subrouter
h := r.PathPrefix(dashboardURL).Subrouter()
h.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy))
// 1. Retrieve the user session and add it to the request context
h.Use(sessions.RetrieveSession(p.sessionStore))
// 2. AuthN - Verify the user is authenticated. Set email, group, & id headers
h.Use(p.AuthenticateSession)
// 3. Enforce CSRF protections for any non-idempotent http method
h.Use(csrf.Protect(
p.cookieSecret,
csrf.Secure(p.cookieOptions.Secure),
csrf.CookieName(fmt.Sprintf("%s_csrf", p.cookieOptions.Name)),
csrf.ErrorHandler(http.HandlerFunc(httputil.CSRFFailureHandler)),
))
h.HandleFunc("/", p.UserDashboard).Methods(http.MethodGet)
h.HandleFunc("/impersonate", p.Impersonate).Methods(http.MethodPost)
h.HandleFunc("/sign_out", p.SignOut).Methods(http.MethodGet, http.MethodPost)
// Authenticate service callback handlers and middleware
c := r.PathPrefix(dashboardURL + "/callback").Subrouter()
// only accept payloads that have come from a trusted service (hmac)
c.Use(middleware.ValidateSignature(p.SharedKey))
c.HandleFunc("/", p.Callback).Queries("redirect_uri", "{redirect_uri}").Methods(http.MethodGet)
// Programmatic API handlers and middleware
a := r.PathPrefix(dashboardURL + "/api").Subrouter()
a.HandleFunc("/v1/login", p.ProgrammaticLogin).Queries("redirect_uri", "{redirect_uri}").Methods(http.MethodGet)
return r
}
// RobotsTxt sets the User-Agent header in the response to be "Disallow"
func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
}
// SignOut redirects the request to the sign out url. It's the responsibility
// of the authenticate service to revoke the remote session and clear
// the local session state.
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/"}
if uri, err := urlutil.ParseAndValidateURL(r.FormValue("redirect_uri")); err == nil && uri.String() != "" {
redirectURL = uri
}
uri := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSignoutURL, redirectURL)
p.sessionStore.ClearSession(w, r)
httputil.Redirect(w, r, uri.String(), http.StatusFound)
}
// UserDashboard lets users investigate, and refresh their current session.
// It also contains certain administrative actions like user impersonation.
// Nota bene: This endpoint does authentication, not authorization.
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
session, err := sessions.FromContext(r.Context())
if err != nil {
httputil.ErrorResponse(w, r, err)
return
}
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
if err != nil {
httputil.ErrorResponse(w, r, err)
return
}
p.templates.ExecuteTemplate(w, "dashboard.html", map[string]interface{}{
"Session": session,
"IsAdmin": isAdmin,
"csrfField": csrf.TemplateField(r),
})
}
// Impersonate takes the result of a form and adds user impersonation details
// to the user's current user sessions state if the user is currently an
// administrative user. Requests are redirected back to the user dashboard.
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
session, err := sessions.FromContext(r.Context())
if err != nil {
httputil.ErrorResponse(w, r, err)
return
}
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
if err != nil || !isAdmin {
errStr := fmt.Sprintf("%s is not an administrator", session.RequestEmail())
httpErr := httputil.Error(errStr, http.StatusForbidden, err)
httputil.ErrorResponse(w, r, httpErr)
return
}
// OK to impersonation
redirectURL := urlutil.GetAbsoluteURL(r)
redirectURL.Path = dashboardURL // redirect back to the dashboard
q := redirectURL.Query()
q.Add("impersonate_email", r.FormValue("email"))
q.Add("impersonate_group", r.FormValue("group"))
redirectURL.RawQuery = q.Encode()
uri := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSigninURL, redirectURL).String()
httputil.Redirect(w, r, uri, http.StatusFound)
}
func (p *Proxy) registerFwdAuthHandlers() http.Handler {
r := httputil.NewRouter()
r.StrictSlash(true)
r.Use(sessions.RetrieveSession(p.sessionStore))
r.Handle("/", p.Verify(false)).Queries("uri", "{uri}").Methods(http.MethodGet)
r.Handle("/verify", p.Verify(true)).Queries("uri", "{uri}").Methods(http.MethodGet)
return r
}
// Verify checks a user's credentials for an arbitrary host. If the user
// is properly authenticated and is authorized to access the supplied host,
// a `200` http status code is returned. If the user is not authenticated, they
// will be redirected to the authenticate service to sign in with their identity
// provider. If the user is unauthorized, a `401` error is returned.
func (p *Proxy) Verify(verifyOnly bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uri, err := urlutil.ParseAndValidateURL(r.FormValue("uri"))
if err != nil || uri.String() == "" {
httputil.ErrorResponse(w, r, httputil.Error("bad verification uri", http.StatusBadRequest, nil))
return
}
if err := p.authenticate(verifyOnly, w, r); err != nil {
return
}
if err := p.authorize(uri.Host, w, r); err != nil {
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Access to %s is allowed.", uri.Host)
})
}
// Callback takes a `redirect_uri` query param that has been hmac'd by the
// authenticate service. Embedded in the `redirect_uri` are query-params
// that tell this handler how to set the per-route user session.
// Callback is responsible for redirecting the user back to the intended
// destination URL and path, as well as to clean up any additional query params
// added by the authenticate service.
func (p *Proxy) Callback(w http.ResponseWriter, r *http.Request) {
redirectURL, err := urlutil.ParseAndValidateURL(r.FormValue("redirect_uri"))
if err != nil {
httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri", http.StatusBadRequest, err))
return
}
q := redirectURL.Query()
// 1. extract the base64 encoded and encrypted JWT from redirect_uri's query params
encryptedJWT, err := base64.URLEncoding.DecodeString(q.Get("pomerium_jwt"))
if err != nil {
httputil.ErrorResponse(w, r, httputil.Error("", http.StatusBadRequest, err))
return
}
q.Del("pomerium_jwt")
q.Del("impersonate_email")
q.Del("impersonate_group")
// 2. decrypt the JWT using the cipher using the _shared_ secret key
rawJWT, err := cryptutil.Decrypt(p.sharedCipher, encryptedJWT, nil)
if err != nil {
httputil.ErrorResponse(w, r, httputil.Error("", http.StatusBadRequest, err))
return
}
// 3. Save the decrypted JWT to the session store directly as a string, without resigning
if err = p.sessionStore.SaveSession(w, r, rawJWT); err != nil {
httputil.ErrorResponse(w, r, err)
return
}
// if this is a programmatic request, don't strip the tokens before redirect
if redirectURL.Query().Get("pomerium_programmatic_destination_url") != "" {
q.Set("pomerium_jwt", string(rawJWT))
}
redirectURL.RawQuery = q.Encode()
httputil.Redirect(w, r, redirectURL.String(), http.StatusFound)
}
// ProgrammaticLogin returns a signed url that can be used to login
// using the authenticate service.
func (p *Proxy) ProgrammaticLogin(w http.ResponseWriter, r *http.Request) {
redirectURL, err := urlutil.ParseAndValidateURL(r.FormValue("redirect_uri"))
if err != nil {
httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri", http.StatusBadRequest, err))
return
}
q := redirectURL.Query()
q.Add("pomerium_programmatic_destination_url", urlutil.GetAbsoluteURL(r).String())
redirectURL.RawQuery = q.Encode()
response := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSigninURL, redirectURL).String()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write([]byte(response))
}