mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-21 04:57:18 +02:00
proxy: support external access control requests (#324)
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
7abcf650e5
commit
eaa1e7a4fb
11 changed files with 730 additions and 133 deletions
|
@ -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))
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
// 'state' parameter which is encrypted and includes authenticating data
|
||||
// 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://tools.ietf.org/html/rfc6749#section-4.2.1
|
||||
func (a *Authenticate) redirectToIdentityProvider(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -4,29 +4,31 @@
|
|||
|
||||
### New
|
||||
|
||||
- Add ability to override HTTPS backend's TLS Server Name. [GH-297](https://github.com/pomerium/pomerium/pull/297)
|
||||
- Add ability to set pomerium's encrypted session in a auth bearer token, or query param.
|
||||
- Add host to the main request logger middleware. [GH-308](https://github.com/pomerium/pomerium/issues/308)
|
||||
- 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 insecure transport support. [GH-328]
|
||||
- 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
|
||||
|
||||
- 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.
|
||||
|
||||
### 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 pomerium would clean double slashes from paths.[GH-262](https://github.com/pomerium/pomerium/issues/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 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]
|
||||
- Fixed a bug where the impersonate form would persist an empty string for groups value if none set. [GH-303]
|
||||
|
||||
### 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.
|
||||
- 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)
|
||||
- Pomerium will no longer default to looking for certificates in the root directory. [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](https://github.com/pomerium/pomerium/issues/328)
|
||||
- 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]
|
||||
- Pomerium will validate that either `insecure_server`, or a valid certificate bundle is set. [GH-328]
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -52,7 +54,7 @@
|
|||
|
||||
### 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]
|
||||
- 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-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-272]: https://github.com/pomerium/pomerium/pull/272
|
||||
[gh-280]: https://github.com/pomerium/pomerium/issues/280
|
||||
[gh-284]: https://github.com/pomerium/pomerium/pull/284
|
||||
[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/
|
||||
|
|
399
docs/docs/reference/img/auth-flow-diagram.svg
Normal file
399
docs/docs/reference/img/auth-flow-diagram.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 19 KiB |
|
@ -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.
|
||||
|
||||
## GRPC Address
|
||||
### GRPC Address
|
||||
|
||||
- Environmental Variable: `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).
|
||||
|
||||
## GRPC Insecure
|
||||
### GRPC Insecure
|
||||
|
||||
- Environmental Variable: `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
|
|||
|
||||
 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
- Environmental Variable: `POLICY`
|
||||
|
@ -564,7 +640,9 @@ Certificate Authority is set when behind-the-ingress service communication uses
|
|||
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.
|
||||
|
||||
|
@ -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
|
||||
[toml]: https://en.wikipedia.org/wiki/TOML
|
||||
[yaml]: https://en.wikipedia.org/wiki/YAML
|
||||
```
|
||||
|
|
|
@ -160,6 +160,17 @@ type Options struct {
|
|||
GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout"`
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -407,6 +418,14 @@ func (o *Options) Validate() error {
|
|||
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 != "" {
|
||||
return errors.New("internal/config: policy file setting is deprecated")
|
||||
}
|
||||
|
|
|
@ -65,6 +65,15 @@ type Policy struct {
|
|||
TLSClientCertFile string `mapstructure:"tls_client_cert_file" yaml:"tls_client_cert_file"`
|
||||
TLSClientKeyFile string `mapstructure:"tls_client_key_file" yaml:"tls_client_key_file"`
|
||||
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.
|
||||
|
|
|
@ -267,7 +267,7 @@ func New() *template.Template {
|
|||
<section>
|
||||
<p class="message">
|
||||
{{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}}
|
||||
|
||||
</p>
|
||||
|
|
|
@ -19,8 +19,11 @@ import (
|
|||
// registerHelperHandlers returns the proxy service's ServeMux
|
||||
func (p *Proxy) registerHelperHandlers(r *mux.Router) *mux.Router {
|
||||
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.Path("/"),
|
||||
|
@ -32,6 +35,7 @@ func (p *Proxy) registerHelperHandlers(r *mux.Router) *mux.Router {
|
|||
h.HandleFunc("/impersonate", p.Impersonate).Methods(http.MethodPost)
|
||||
h.HandleFunc("/sign_out", p.SignOut).Methods(http.MethodGet, http.MethodPost)
|
||||
h.HandleFunc("/refresh", p.ForceRefresh).Methods(http.MethodPost)
|
||||
h.HandleFunc("/verify/{hostname}", p.Verify).Methods(http.MethodGet)
|
||||
return r
|
||||
}
|
||||
|
||||
|
@ -147,3 +151,50 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
|
@ -62,7 +64,7 @@ func TestProxy_authenticate(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodGet, "/oauth-start", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
proxy.authenticate(rr, req)
|
||||
proxy.reqNeedsAuthentication(rr, req)
|
||||
// expect oauth redirect
|
||||
if status := rr.Code; 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) {
|
||||
opts := testOptions(t)
|
||||
tests := []struct {
|
||||
|
@ -393,3 +286,119 @@ func uriParseHelper(s string) *url.URL {
|
|||
}
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ const (
|
|||
HeaderEmail = "x-pomerium-authenticated-user-email"
|
||||
// HeaderGroups is the header key containing the user's groups.
|
||||
HeaderGroups = "x-pomerium-authenticated-user-groups"
|
||||
|
||||
disableCallback = "pomerium-auth-callback"
|
||||
)
|
||||
|
||||
// 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())
|
||||
if err != nil {
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to session state error")
|
||||
p.authenticate(w, r)
|
||||
p.reqNeedsAuthentication(w, r)
|
||||
return
|
||||
}
|
||||
if err := s.Valid(); err != nil {
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: re-authenticating due to invalid session")
|
||||
p.authenticate(w, r)
|
||||
p.reqNeedsAuthentication(w, r)
|
||||
return
|
||||
}
|
||||
// add pomerium's headers to the downstream request
|
||||
r.Header.Set(HeaderUserID, s.User)
|
||||
r.Header.Set(HeaderEmail, s.RequestEmail())
|
||||
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")
|
||||
} else {
|
||||
r.Header.Set(HeaderJWT, jwt)
|
||||
w.Header().Set(HeaderJWT, jwt)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate begins the authenticate flow, encrypting the redirect url
|
||||
// in a request to the provider's sign in endpoint.
|
||||
func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request) {
|
||||
// reqNeedsAuthentication begins the authenticate flow, encrypting the
|
||||
// redirect url in a request to the provider's sign in endpoint.
|
||||
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))
|
||||
http.Redirect(w, r, uri.String(), http.StatusFound)
|
||||
}
|
||||
|
|
|
@ -173,6 +173,11 @@ func (p *Proxy) UpdatePolicies(opts *config.Options) error {
|
|||
r.HandleFunc("/robots.txt", p.RobotsTxt).Methods(http.MethodGet)
|
||||
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 {
|
||||
if err := policy.Validate(); err != nil {
|
||||
return fmt.Errorf("proxy: invalid policy %s", err)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue