mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 10:26:29 +02:00
221 lines
8.6 KiB
Go
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/templates"
|
|
"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()
|
|
// 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)
|
|
http.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
|
|
}
|
|
|
|
templates.New().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()
|
|
http.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, fmt.Sprintf("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()
|
|
|
|
http.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))
|
|
}
|