mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-21 13:07:13 +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))
|
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) {
|
||||||
|
|
|
@ -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/
|
||||||
|
|
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.
|
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
|
||||||
|
|
||||||
 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
|
 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
|
## 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
|
||||||
|
```
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue