proxy: support external access control requests (#324)

Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
Bobby DeSimone 2019-10-03 21:22:44 -07:00 committed by GitHub
parent 7abcf650e5
commit eaa1e7a4fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 730 additions and 133 deletions

View file

@ -113,6 +113,13 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri", http.StatusBadRequest, err)) httputil.ErrorResponse(w, r, httputil.Error("malformed redirect_uri", http.StatusBadRequest, err))
return return
} }
// Add query param to let downstream apps (or auth endpoints) know
// this request followed authentication. Useful for auth-forward-endpoint
// redirecting
q := redirectURL.Query()
q.Add("pomerium-auth-callback", "true")
redirectURL.RawQuery = q.Encode()
http.Redirect(w, r, redirectURL.String(), http.StatusFound) http.Redirect(w, r, redirectURL.String(), http.StatusFound)
} }
@ -142,8 +149,6 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
// user to their respective identity provider. This function also builds the // user to their respective identity provider. This function also builds the
// 'state' parameter which is encrypted and includes authenticating data // 'state' parameter which is encrypted and includes authenticating data
// for validation. // for validation.
// 'state' is : nonce|timestamp|redirect_url|encrypt(redirect_url)+mac(nonce,ts))
// https://openid.net/specs/openid-connect-core-1_0-final.html#AuthRequest // 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) redirectToIdentityProvider(w http.ResponseWriter, r *http.Request) { func (a *Authenticate) redirectToIdentityProvider(w http.ResponseWriter, r *http.Request) {

View file

@ -4,29 +4,31 @@
### New ### New
- Add ability to override HTTPS backend's TLS Server Name. [GH-297](https://github.com/pomerium/pomerium/pull/297) - Add endpoint to support "forward-auth" integration with third-party ingresses and proxies. Supports [nginx]https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/, [nginx-ingress](https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/), and [Traefik](https://docs.traefik.io/middlewares/forwardauth/). [GH-324]
- Add ability to set pomerium's encrypted session in a auth bearer token, or query param. - Add insecure transport support. [GH-328]
- Add host to the main request logger middleware. [GH-308](https://github.com/pomerium/pomerium/issues/308) - Add setting to override HTTPS backend's TLS Server Name. [GH-297]
- Add setting to set pomerium's encrypted session in a auth bearer token, or query param.
- Add host to the main request logger middleware. [GH-308]
### Security ### Security
- The user's original intended location before completing the authentication process is now encrypted and kept confidential from the identity provider. [GH-316](https://github.com/pomerium/pomerium/pull/316) - The user's original intended location before completing the authentication process is now encrypted and kept confidential from the identity provider. [GH-316]
- Under certain circumstances, where debug logging was enabled, pomerium's shared secret could be leaked to http access logs as a query param. - Under certain circumstances, where debug logging was enabled, pomerium's shared secret could be leaked to http access logs as a query param.
### Fixed ### Fixed
- Fixed an issue where CSRF would fail if multiple tabs were open. [GH-306](https://github.com/pomerium/pomerium/issues/306) - Fixed an issue where CSRF would fail if multiple tabs were open. [GH-306]
- Fixed an issue where pomerium would clean double slashes from paths.[GH-262](https://github.com/pomerium/pomerium/issues/262) - Fixed an issue where pomerium would clean double slashes from paths. [GH-262]
- Fixed a bug where the impersonate form would persist an empty string for groups value if none set.[GH-303](https://github.com/pomerium/pomerium/issues/303) - Fixed a bug where the impersonate form would persist an empty string for groups value if none set. [GH-303]
### Changed ### Changed
- The healthcheck endpoints (`/ping`) now returns the http status `405` StatusMethodNotAllowed for non-`GET` requests. [GH-319](https://github.com/pomerium/pomerium/issues/319) - The healthcheck endpoints (`/ping`) now returns the http status `405` StatusMethodNotAllowed for non-`GET` requests.
- Authenticate service no longer uses gRPC. - Authenticate service no longer uses gRPC.
- The global request logger now captures the full array of proxies from `X-Forwarded-For`, in addition to just the client IP. - The global request logger now captures the full array of proxies from `X-Forwarded-For`, in addition to just the client IP.
- Options code refactored to eliminate global Viper state. [GH-332](https://github.com/pomerium/pomerium/pull/332/files) - Options code refactored to eliminate global Viper state. [GH-332]
- Pomerium will no longer default to looking for certificates in the root directory. [GH-328](https://github.com/pomerium/pomerium/issues/328) - Pomerium will no longer default to looking for certificates in the root directory. [GH-328]
- Pomerium will validate that either `insecure_server`, or a valid certificate bundle is set. [GH-328](https://github.com/pomerium/pomerium/issues/328) - Pomerium will validate that either `insecure_server`, or a valid certificate bundle is set. [GH-328]
### Removed ### Removed
@ -52,7 +54,7 @@
### Changed ### Changed
- Pomerium will now strip `_csrf` cookies in addition to session cookies. [GG-285] - Pomerium will now strip `_csrf` cookies in addition to session cookies. [GH-285]
- Disabled gRPC service config. [GH-280] - Disabled gRPC service config. [GH-280]
- 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] - 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]
@ -272,8 +274,17 @@
[gh-259]: https://github.com/pomerium/pomerium/pull/259 [gh-259]: https://github.com/pomerium/pomerium/pull/259
[gh-259]: https://github.com/pomerium/pomerium/pull/259 [gh-259]: https://github.com/pomerium/pomerium/pull/259
[gh-261]: https://github.com/pomerium/pomerium/pull/261 [gh-261]: https://github.com/pomerium/pomerium/pull/261
[gh-262]: https://github.com/pomerium/pomerium/issues/262
[gh-266]: https://github.com/pomerium/pomerium/pull/266 [gh-266]: https://github.com/pomerium/pomerium/pull/266
[gh-272]: https://github.com/pomerium/pomerium/pull/272 [gh-272]: https://github.com/pomerium/pomerium/pull/272
[gh-280]: https://github.com/pomerium/pomerium/issues/280 [gh-280]: https://github.com/pomerium/pomerium/issues/280
[gh-284]: https://github.com/pomerium/pomerium/pull/284 [gh-284]: https://github.com/pomerium/pomerium/pull/284
[gh-285]: https://github.com/pomerium/pomerium/issues/285 [gh-285]: https://github.com/pomerium/pomerium/issues/285
[gh-297]: https://github.com/pomerium/pomerium/pull/297
[gh-303]: https://github.com/pomerium/pomerium/issues/303
[gh-306]: https://github.com/pomerium/pomerium/issues/306
[gh-308]: https://github.com/pomerium/pomerium/issues/308
[gh-316]: https://github.com/pomerium/pomerium/pull/316
[gh-319]: https://github.com/pomerium/pomerium/issues/319
[gh-328]: https://github.com/pomerium/pomerium/issues/328
[gh-332]: https://github.com/pomerium/pomerium/pull/332/

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -163,7 +163,7 @@ Timeouts set the global server timeouts. For route-specific timeouts, see [polic
These settings control upstream connections to the Authorize service. These settings control upstream connections to the Authorize service.
## GRPC Address ### GRPC Address
- Environmental Variable: `GRPC_ADDRESS` - Environmental Variable: `GRPC_ADDRESS`
- Config File Key: `grpc_address` - Config File Key: `grpc_address`
@ -173,7 +173,7 @@ These settings control upstream connections to the Authorize service.
Address specifies the host and port to serve GRPC requests from. Defaults to `:443` (or `:5443` in all in one mode). Address specifies the host and port to serve GRPC requests from. Defaults to `:443` (or `:5443` in all in one mode).
## GRPC Insecure ### GRPC Insecure
- Environmental Variable: `GRPC_INSECURE` - Environmental Variable: `GRPC_INSECURE`
- Config File Key: `grpc_insecure` - Config File Key: `grpc_insecure`
@ -284,6 +284,82 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
![jaeger example trace](./img/jaeger.png) pomerium_config_last_reload_success_timestamp | Gauge | The timestamp of the last successful configuration reload by service pomerium_build_info | Gauge | Pomerium build metadata by git revision, service, version and goversion ![jaeger example trace](./img/jaeger.png) pomerium_config_last_reload_success_timestamp | Gauge | The timestamp of the last successful configuration reload by service pomerium_build_info | Gauge | Pomerium build metadata by git revision, service, version and goversion
## Forward Auth
- Environmental Variable: `FORWARD_AUTH_URL`
- Config File Key: `forward_auth_url`
- Type: `URL` (must contain a scheme and hostname)
- Example: `https://fwdauth.corp.example.com`
- Resulting Verification URL: `https://fwdauth.corp.example.com/.pomerium/verify/{URL-TO-VERIFY}`
- Optional
Forward authentication creates an endpoint that can be used with third-party proxies that do not have rich access control capabilities ([nginx](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html), [nginx-ingress](https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/), [ambassador](https://www.getambassador.io/reference/services/auth-service/), [traefik](https://docs.traefik.io/middlewares/forwardauth/)). Forward authentication allow you to delegate authentication and authorization for each request to Pomerium.
### Request flow
![pomerium forward auth request flow](./img/auth-flow-diagram.svg)
### Examples
#### NGINX Ingress
Some reverse-proxies, such as nginx split access control flow into two parts: verification and sign-in redirection. Notice the additional the additional `?no_redirect=true` query param in `auth-rul` which tells Pomerium to return a `401` instead of redirecting and starting the sign-in process.
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: httpbin
annotations:
kubernetes.io/ingress.class: "nginx"
certmanager.k8s.io/issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/auth-url: https://fwdauth.corp.example.com/.pomerium/verify/httpbin.corp.example.com?no_redirect=true
nginx.ingress.kubernetes.io/auth-signin: https://fwdauth.corp.example.com/.pomerium/verify/httpbin.corp.example.com
spec:
tls:
- hosts:
- httpbin.corp.example.com
secretName: quickstart-example-tls
rules:
- host: httpbin.corp.example.com
http:
paths:
- path: /
backend:
serviceName: httpbin
servicePort: 80
```
### Traefik docker-compose
```yml
version: "3"
services:
traefik:
# The official v2.0 Traefik docker image
image: traefik:v2.0
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker
ports:
# The HTTP port
- "80:80"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
httpbin:
# A container that exposes an API to show its IP address
image: kennethreitz/httpbin:latest
labels:
- "traefik.http.routers.httpbin.rule=Host(`httpbin.corp.example.com`)"
# Create a middleware named `foo-add-prefix`
- "traefik.http.middlewares.test-auth.forwardauth.authResponseHeaders=X-Pomerium-Authenticated-User-Email,x-pomerium-authenticated-user-id,x-pomerium-authenticated-user-groups,x-pomerium-jwt-assertion"
- "traefik.http.middlewares.test-auth.forwardauth.address=http://fwdauth.corp.example.com/.pomerium/verify/httpbin.corp.example.com"
- "traefik.http.routers.httpbin.middlewares=test-auth@docker"
```
## Policy ## Policy
- Environmental Variable: `POLICY` - Environmental Variable: `POLICY`
@ -564,6 +640,8 @@ Certificate Authority is set when behind-the-ingress service communication uses
Strict-Transport-Security:max-age=31536000; includeSubDomains; preload, Strict-Transport-Security:max-age=31536000; includeSubDomains; preload,
``` ```
```
Headers specifies a mapping of [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be added to proxied requests. _Nota bene_ Downstream application headers will be overwritten by Pomerium's headers on conflict. Headers specifies a mapping of [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be added to proxied requests. _Nota bene_ Downstream application headers will be overwritten by Pomerium's headers on conflict.
By default, conservative [secure HTTP headers](https://www.owasp.org/index.php/OWASP_Secure_Headers_Project) are set. By default, conservative [secure HTTP headers](https://www.owasp.org/index.php/OWASP_Secure_Headers_Project) are set.
@ -599,3 +677,4 @@ Default Upstream Timeout is the default timeout applied to a proxied route when
[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh [script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh
[toml]: https://en.wikipedia.org/wiki/TOML [toml]: https://en.wikipedia.org/wiki/TOML
[yaml]: https://en.wikipedia.org/wiki/YAML [yaml]: https://en.wikipedia.org/wiki/YAML
```

View file

@ -160,6 +160,17 @@ type Options struct {
GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout"` GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout"`
GRPCClientDNSRoundRobin bool `mapstructure:"grpc_client_dns_roundrobin"` GRPCClientDNSRoundRobin bool `mapstructure:"grpc_client_dns_roundrobin"`
// ForwardAuthEndpoint allows for a given route to be used as a forward-auth
// endpoint instead of a reverse proxy. Some third-party proxies that do not
// have rich access control capabilities (nginx, envoy, ambassador, traefik)
// allow you to delegate and authenticate each request to your website
// with an external server or service. Pomerium can be configured to accept
// these requests with this switch
//
// todo(bdd): link to docs
ForwardAuthURLString string `mapstructure:"forward_auth_url"`
ForwardAuthURL *url.URL
viper *viper.Viper viper *viper.Viper
} }
@ -407,6 +418,14 @@ func (o *Options) Validate() error {
o.AuthorizeURL = u o.AuthorizeURL = u
} }
if o.ForwardAuthURLString != "" {
u, err := urlutil.ParseAndValidateURL(o.ForwardAuthURLString)
if err != nil {
return fmt.Errorf("internal/config: bad forward-auth-url %s : %w", o.ForwardAuthURLString, err)
}
o.ForwardAuthURL = u
}
if o.PolicyFile != "" { if o.PolicyFile != "" {
return errors.New("internal/config: policy file setting is deprecated") return errors.New("internal/config: policy file setting is deprecated")
} }

View file

@ -65,6 +65,15 @@ type Policy struct {
TLSClientCertFile string `mapstructure:"tls_client_cert_file" yaml:"tls_client_cert_file"` TLSClientCertFile string `mapstructure:"tls_client_cert_file" yaml:"tls_client_cert_file"`
TLSClientKeyFile string `mapstructure:"tls_client_key_file" yaml:"tls_client_key_file"` TLSClientKeyFile string `mapstructure:"tls_client_key_file" yaml:"tls_client_key_file"`
ClientCertificate *tls.Certificate ClientCertificate *tls.Certificate
// IsForwardAuthEndpoint allows for a given route to be used as a forward-auth
// endpoint instead of a reverse proxy. Some third-party proxies that do not
// have rich access control capabilities (nginx, envoy, ambassador, traefik)
// allow you to delegate and authenticate each request to your website
// with an external server or service. Pomerium can be configured to accept
// these requests with this switch
// todo(bdd): link to docs
IsForwardAuthEndpoint bool
} }
// Validate checks the validity of a policy. // Validate checks the validity of a policy.

View file

@ -267,7 +267,7 @@ func New() *template.Template {
<section> <section>
<p class="message"> <p class="message">
{{if .Message}}{{.Message}}</br>{{end}} {{if .Message}}{{.Message}}</br>{{end}}
{{if .CanDebug}}Troubleshoot your <a href="/.pomerium">session</a>.</br>{{end}} {{if .CanDebug}}Troubleshoot your <a href="/.pomerium/">session</a>.</br>{{end}}
{{if .RequestID}} Request {{.RequestID}}</br>{{end}} {{if .RequestID}} Request {{.RequestID}}</br>{{end}}
</p> </p>

View file

@ -19,8 +19,11 @@ import (
// registerHelperHandlers returns the proxy service's ServeMux // registerHelperHandlers returns the proxy service's ServeMux
func (p *Proxy) registerHelperHandlers(r *mux.Router) *mux.Router { func (p *Proxy) registerHelperHandlers(r *mux.Router) *mux.Router {
h := r.PathPrefix(dashboardURL).Subrouter() h := r.PathPrefix(dashboardURL).Subrouter()
// 1. Retrieve the user session and add it to the request context
h.Use(sessions.RetrieveSession(p.sessionStore)) h.Use(sessions.RetrieveSession(p.sessionStore))
// 2. AuthN - Verify the user is authenticated. Set email, group, & id headers
h.Use(p.AuthenticateSession) h.Use(p.AuthenticateSession)
// 3. Enforce CSRF protections for any non-idempotent http method
h.Use(csrf.Protect( h.Use(csrf.Protect(
p.cookieSecret, p.cookieSecret,
csrf.Path("/"), csrf.Path("/"),
@ -32,6 +35,7 @@ func (p *Proxy) registerHelperHandlers(r *mux.Router) *mux.Router {
h.HandleFunc("/impersonate", p.Impersonate).Methods(http.MethodPost) h.HandleFunc("/impersonate", p.Impersonate).Methods(http.MethodPost)
h.HandleFunc("/sign_out", p.SignOut).Methods(http.MethodGet, http.MethodPost) h.HandleFunc("/sign_out", p.SignOut).Methods(http.MethodGet, http.MethodPost)
h.HandleFunc("/refresh", p.ForceRefresh).Methods(http.MethodPost) h.HandleFunc("/refresh", p.ForceRefresh).Methods(http.MethodPost)
h.HandleFunc("/verify/{hostname}", p.Verify).Methods(http.MethodGet)
return r return r
} }
@ -147,3 +151,50 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, dashboardURL, http.StatusFound) http.Redirect(w, r, dashboardURL, http.StatusFound)
} }
// 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, they will be given a 403 http status.
func (p *Proxy) Verify(w http.ResponseWriter, r *http.Request) {
// retrieve the target destination hostname from the url path
hostname, ok := mux.Vars(r)["hostname"]
if !ok {
httputil.ErrorResponse(w, r, httputil.Error("no hostname set", http.StatusBadRequest, nil))
return
}
// attempt to retrieve the user session from the request context, validity
// of the identity session is asserted by the middleware chain
s, err := sessions.FromContext(r.Context())
if err != nil {
httputil.ErrorResponse(w, r, err)
return
}
// query the authorization service to see if the session's user has
// the appropriate authorization to access the given hostname
authorized, err := p.AuthorizeClient.Authorize(r.Context(), hostname, s)
if err != nil {
httputil.ErrorResponse(w, r, err)
return
} else if !authorized {
errMsg := fmt.Sprintf("%s is not authorized for this route", s.RequestEmail())
httputil.ErrorResponse(w, r, httputil.Error(errMsg, http.StatusForbidden, nil))
return
}
// check the queryparams to see if this check immediately followed
// authentication. If so, redirect back to the originally requested hostname.
if isCallback := r.URL.Query().Get(disableCallback); isCallback == "true" {
http.Redirect(w, r, "http://"+hostname, http.StatusFound)
return
}
// User is authenticated and authorized for the given host.
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set(HeaderUserID, s.User)
w.Header().Set(HeaderEmail, s.RequestEmail())
w.Header().Set(HeaderGroups, s.RequestGroups())
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s is authorized for %s.", s.Email, hostname)
}

View file

@ -11,6 +11,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/gorilla/mux"
"github.com/pomerium/pomerium/internal/config" "github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
@ -62,7 +64,7 @@ func TestProxy_authenticate(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oauth-start", nil) req := httptest.NewRequest(http.MethodGet, "/oauth-start", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
proxy.authenticate(rr, req) proxy.reqNeedsAuthentication(rr, req)
// expect oauth redirect // expect oauth redirect
if status := rr.Code; status != http.StatusFound { if status := rr.Code; status != http.StatusFound {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound) t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
@ -75,115 +77,6 @@ func TestProxy_authenticate(t *testing.T) {
} }
} }
// func TestProxy_PomeriumHandler(t *testing.T) {
// proxy, err := New(testOptions(t))
// if err != nil {
// t.Fatal(err)
// }
// h := proxy.registerHelperHandlers()
// if h == nil {
// t.Error("handler cannot be nil")
// }
// mux := http.NewServeMux()
// mux.Handle("/", h)
// req := httptest.NewRequest(http.MethodGet, "/", nil)
// rr := httptest.NewRecorder()
// mux.ServeHTTP(rr, req)
// if rr.Code != http.StatusNotFound {
// t.Errorf("expected 404 route not found for empty route")
// }
// }
// func TestProxy_Proxy(t *testing.T) {
// goodSession := &sessions.State{
// AccessToken: "AccessToken",
// RefreshToken: "RefreshToken",
// RefreshDeadline: time.Now().Add(20 * time.Second),
// }
// ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// w.Header().Set("X-Content-Type-Options", "nosniff")
// fmt.Fprintln(w, "RVSI FILIVS CAISAR")
// w.WriteHeader(http.StatusOK)
// }))
// defer ts.Close()
// opts := testOptionsTestServer(t, ts.URL)
// optsCORS := testOptionsWithCORS(t, ts.URL)
// optsPublic := testOptionsWithPublicAccess(t, ts.URL)
// optsNoPolicies := testOptionsWithEmptyPolicies(t, ts.URL)
// defaultHeaders, goodCORSHeaders, badCORSHeaders, headersWs := http.Header{}, http.Header{}, http.Header{}, http.Header{}
// goodCORSHeaders.Set("origin", "anything")
// goodCORSHeaders.Set("access-control-request-method", "anything")
// // missing "Origin"
// badCORSHeaders.Set("access-control-request-method", "anything")
// headersWs.Set("Connection", "Upgrade")
// headersWs.Set("Upgrade", "websocket")
// tests := []struct {
// name string
// options config.Options
// method string
// header http.Header
// host string
// session sessions.SessionStore
// authorizer clients.Authorizer
// wantStatus int
// }{
// {"good", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(20 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
// {"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
// {"good email impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateEmail: "test@user.example"}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
// {"good group impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateGroups: []string{"group1", "group2"}}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
// // same request as above, but with cors_allow_preflight=false in the policy
// {"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// // cors allowed, but the request is missing proper headers
// {"invalid cors headers", optsCORS, http.MethodOptions, badCORSHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// // redirect to start auth process
// {"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
// {"user not authorized", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// {"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError},
// // authenticate errors
// {"session expired,redirect to authn", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{LoadError: sessions.ErrExpired}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
// {"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
// {"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusNotFound},
// {"no http found (no session),redirect to authn", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
// {"No policies", optsNoPolicies, http.MethodGet, defaultHeaders, "https://httpbin.corp.example/", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// err := ValidateOptions(tt.options)
// if err != nil {
// t.Fatal(err)
// }
// p, err := New(tt.options)
// if err != nil {
// t.Fatal(err)
// }
// p.encoder = &cryptutil.MockEncoder{MarshalResponse: "foo"}
// p.sessionStore = tt.session
// p.AuthorizeClient = tt.authorizer
// r := httptest.NewRequest(tt.method, tt.host, nil)
// r.Header = tt.header
// r.Header.Set("Accept", "application/json")
// state, _ := tt.session.LoadSession(r)
// ctx := r.Context()
// ctx = sessions.NewContext(ctx, state, nil)
// r = r.WithContext(ctx)
// w := httptest.NewRecorder()
// p.Proxy(w, r)
// if status := w.Code; status != tt.wantStatus {
// t.Errorf("handler returned wrong status code: got %v want %v \n body %s", status, tt.wantStatus, w.Body.String())
// }
// })
// }
// }
func TestProxy_UserDashboard(t *testing.T) { func TestProxy_UserDashboard(t *testing.T) {
opts := testOptions(t) opts := testOptions(t)
tests := []struct { tests := []struct {
@ -393,3 +286,119 @@ func uriParseHelper(s string) *url.URL {
} }
return uri return uri
} }
func TestProxy_VerifyWithMiddleware(t *testing.T) {
t.Parallel()
opts := testOptions(t)
tests := []struct {
name string
options config.Options
ctxError error
method string
qp string
path string
cipher cryptutil.SecureEncoder
sessionStore sessions.SessionStore
authorizer clients.Authorizer
wantStatus int
}{
{"good", opts, nil, http.MethodGet, "false", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"good post auth redirect", opts, nil, http.MethodGet, "true", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
{"not authorized", opts, nil, http.MethodGet, "false", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
{"not authorized expired, redirect to auth", opts, nil, http.MethodGet, "false", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusFound},
{"not authorized expired, don't redirect!", opts, nil, http.MethodGet, "true", "/.pomerium/verify/some.domain.name?no_redirect=true", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized},
{"not authorized because of error", opts, nil, http.MethodGet, "false", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeError: errors.New("authz error")}, http.StatusInternalServerError},
{"bad context retrieval error", opts, errors.New("oh no"), http.MethodGet, "false", "/.pomerium/verify/some.domain.name", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
}
p.encoder = tt.cipher
p.sessionStore = tt.sessionStore
p.AuthorizeClient = tt.authorizer
p.UpdateOptions(tt.options)
uri := &url.URL{Path: tt.path}
queryString := uri.Query()
if tt.qp == "true" {
queryString.Set("pomerium-auth-callback", tt.qp)
}
uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(tt.method, uri.String(), nil)
state, err := tt.sessionStore.LoadSession(r)
if err != nil {
t.Fatal(err)
}
ctx := r.Context()
ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Authorization", "Bearer blah")
r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder()
router := mux.NewRouter()
router.StrictSlash(true)
router = p.registerHelperHandlers(router)
router.ServeHTTP(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
t.Errorf("\n%+v", w.Body.String())
}
})
}
}
func TestProxy_Verify(t *testing.T) {
t.Parallel()
opts := testOptions(t)
tests := []struct {
name string
options config.Options
ctxError error
method string
qp string
path string
cipher cryptutil.SecureEncoder
sessionStore sessions.SessionStore
authorizer clients.Authorizer
wantStatus int
}{
{"bad - no hostname in path", opts, nil, http.MethodGet, "false", "/ok", &cryptutil.MockEncoder{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
}
p.encoder = tt.cipher
p.sessionStore = tt.sessionStore
p.AuthorizeClient = tt.authorizer
uri := &url.URL{Path: tt.path}
queryString := uri.Query()
queryString.Set("pomerium-auth-callback", tt.qp)
uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(tt.method, uri.String(), nil)
state, err := tt.sessionStore.LoadSession(r)
if err != nil {
t.Fatal(err)
}
ctx := r.Context()
ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Authorization", "Bearer blah")
r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder()
p.Verify(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
t.Errorf("\n%+v", w.Body.String())
}
})
}
}

View file

@ -22,6 +22,8 @@ const (
HeaderEmail = "x-pomerium-authenticated-user-email" HeaderEmail = "x-pomerium-authenticated-user-email"
// HeaderGroups is the header key containing the user's groups. // HeaderGroups is the header key containing the user's groups.
HeaderGroups = "x-pomerium-authenticated-user-groups" HeaderGroups = "x-pomerium-authenticated-user-groups"
disableCallback = "pomerium-auth-callback"
) )
// AuthenticateSession is middleware to enforce a valid authentication // AuthenticateSession is middleware to enforce a valid authentication
@ -33,14 +35,15 @@ func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler {
s, err := sessions.FromContext(r.Context()) s, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to session state error") log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to session state error")
p.authenticate(w, r) p.reqNeedsAuthentication(w, r)
return return
} }
if err := s.Valid(); err != nil { if err := s.Valid(); err != nil {
log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to invalid session") log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to invalid session")
p.authenticate(w, r) p.reqNeedsAuthentication(w, r)
return return
} }
// add pomerium's headers to the downstream request
r.Header.Set(HeaderUserID, s.User) r.Header.Set(HeaderUserID, s.User)
r.Header.Set(HeaderEmail, s.RequestEmail()) r.Header.Set(HeaderEmail, s.RequestEmail())
r.Header.Set(HeaderGroups, s.RequestGroups()) r.Header.Set(HeaderGroups, s.RequestGroups())
@ -89,15 +92,22 @@ func (p *Proxy) SignRequest(signer cryptutil.JWTSigner) func(next http.Handler)
log.Warn().Err(err).Msg("proxy: failed signing jwt") log.Warn().Err(err).Msg("proxy: failed signing jwt")
} else { } else {
r.Header.Set(HeaderJWT, jwt) r.Header.Set(HeaderJWT, jwt)
w.Header().Set(HeaderJWT, jwt)
} }
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
} }
// Authenticate begins the authenticate flow, encrypting the redirect url // reqNeedsAuthentication begins the authenticate flow, encrypting the
// in a request to the provider's sign in endpoint. // redirect url in a request to the provider's sign in endpoint.
func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request) { func (p *Proxy) reqNeedsAuthentication(w http.ResponseWriter, r *http.Request) {
// some proxies like nginx won't follow redirects, and treat any
// non 2xx or 4xx status as an internal service error.
// https://nginx.org/en/docs/http/ngx_http_auth_request_module.html
if _, ok := r.URL.Query()[disableCallback]; ok {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
uri := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSigninURL, urlutil.GetAbsoluteURL(r)) uri := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSigninURL, urlutil.GetAbsoluteURL(r))
http.Redirect(w, r, uri.String(), http.StatusFound) http.Redirect(w, r, uri.String(), http.StatusFound)
} }

View file

@ -173,6 +173,11 @@ func (p *Proxy) UpdatePolicies(opts *config.Options) error {
r.HandleFunc("/robots.txt", p.RobotsTxt).Methods(http.MethodGet) r.HandleFunc("/robots.txt", p.RobotsTxt).Methods(http.MethodGet)
r = p.registerHelperHandlers(r) r = p.registerHelperHandlers(r)
if opts.ForwardAuthURL != nil {
// create a route to handle forward auth requests
r.Host(opts.ForwardAuthURL.Host).Subrouter().PathPrefix("/")
}
for _, policy := range opts.Policies { for _, policy := range opts.Policies {
if err := policy.Validate(); err != nil { if err := policy.Validate(); err != nil {
return fmt.Errorf("proxy: invalid policy %s", err) return fmt.Errorf("proxy: invalid policy %s", err)