Repository: pusher/oauth2_proxy
Pull request #265 (2019-09-19 07:00:07)

Add upstream with static response
## Description

This change is necessary to use `oauth2_proxy` with the envoy ext_authz module. It is not  possible to configure the envoy ext_authz module to use the `/oauth2/auth` endpoint. The module calls the original request path and need a response code `200`.

https://www.envoyproxy.io/docs/envoy/v1.11.1/api-v2/config/filter/http/ext_authz/v2/ext_authz.proto.html

## Motivation and Context

#219 

## Checklist:

- [x] My change requires a change to the documentation or CHANGELOG.
- [x] I have updated the documentation accordingly.
- [x] I have updated the CHANGELOG accordingly.
- [x] I have created a feature (non-master) branch for my PR.


cgroschupp: @JoelSpeed Can you please take a look at it?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c0b1f04..cc67d5fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
 - [#300](https://github.com/pusher/oauth2_proxy/pull/300) Added userinfo endpoint (@kbabuadze)
 - [#309](https://github.com/pusher/oauth2_proxy/pull/309) Added support for custom CA when connecting to Redis cache (@lleszczu)
 - [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored (@webnard)
+- [#265](https://github.com/pusher/oauth2_proxy/pull/265) Add upstream with static response (@cgroschupp)
 
 # v4.0.0
 
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 2f5d049a..158ad3bd 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -106,7 +106,7 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
 | `-standard-logging-format` | string | Template for standard log lines | see [Logging Configuration](#logging-configuration) |
 | `-tls-cert-file` | string | path to certificate file | |
 | `-tls-key-file` | string | path to private key file | |
-| `-upstream` | string \| list | the http url(s) of the upstream endpoint or `file://` paths for static files. Routing is based on the path | |
+| `-upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
 | `-validate-url` | string | Access token validation endpoint | |
 | `-version` | n/a | print version string | |
 | `-whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
diff --git a/main.go b/main.go
index 7cf562bd..11a28bd8 100644
--- a/main.go
+++ b/main.go
@@ -37,7 +37,7 @@ func main() {
 	flagSet.String("tls-key-file", "", "path to private key file")
 	flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
 	flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)")
-	flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path")
+	flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path")
 	flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
 	flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream")
 	flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
diff --git a/oauthproxy.go b/oauthproxy.go
index 3c665db7..d206aa64 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -13,6 +13,7 @@ import (
 	"net/http/httputil"
 	"net/url"
 	"regexp"
+	"strconv"
 	"strings"
 	"time"
 
@@ -203,12 +204,23 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
 	}
 	for _, u := range opts.proxyURLs {
 		path := u.Path
+		host := u.Host
 		switch u.Scheme {
 		case httpScheme, httpsScheme:
 			logger.Printf("mapping path %q => upstream %q", path, u)
 			proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
 			serveMux.Handle(path, proxy)
+		case "static":
+			responseCode, err := strconv.Atoi(host)
+			if err != nil {
+				logger.Printf("unable to convert %q to int, use default \"200\"", host)
+				responseCode = 200
+			}
 
+			serveMux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) {
+				rw.WriteHeader(responseCode)
+				fmt.Fprintf(rw, "Authenticated")
+			})
 		case "file":
 			if u.Fragment != "" {
 				path = u.Fragment


Test patch:
diff --git a/oauthproxy_test.go b/oauthproxy_test.go
index ce56e8a3..ee850419 100644
--- a/oauthproxy_test.go
+++ b/oauthproxy_test.go
@@ -365,6 +365,7 @@ type PassAccessTokenTest struct {
 
 type PassAccessTokenTestOptions struct {
 	PassAccessToken bool
+	ProxyUpstream   string
 }
 
 func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTest {
@@ -372,7 +373,6 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
 
 	t.providerServer = httptest.NewServer(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			logger.Printf("%#v", r)
 			var payload string
 			switch r.URL.Path {
 			case "/oauth/token":
@@ -389,6 +389,9 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
 
 	t.opts = NewOptions()
 	t.opts.Upstreams = append(t.opts.Upstreams, t.providerServer.URL)
+	if opts.ProxyUpstream != "" {
+		t.opts.Upstreams = append(t.opts.Upstreams, opts.ProxyUpstream)
+	}
 	// The CookieSecret must be 32 bytes in order to create the AES
 	// cipher.
 	t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
@@ -425,7 +428,9 @@ func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int,
 	return rw.Code, rw.HeaderMap["Set-Cookie"][1]
 }
 
-func (patTest *PassAccessTokenTest) getRootEndpoint(cookie string) (httpCode int, accessToken string) {
+// getEndpointWithCookie makes a requests againt the oauthproxy with passed requestPath
+// and cookie and returns body and status code.
+func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoint string) (httpCode int, accessToken string) {
 	cookieName := patTest.proxy.CookieName
 	var value string
 	keyPrefix := cookieName + "="
@@ -442,7 +447,7 @@ func (patTest *PassAccessTokenTest) getRootEndpoint(cookie string) (httpCode int
 		return 0, ""
 	}
 
-	req, err := http.NewRequest("GET", "/", strings.NewReader(""))
+	req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
 	if err != nil {
 		return 0, ""
 	}
@@ -475,13 +480,37 @@ func TestForwardAccessTokenUpstream(t *testing.T) {
 	// Now we make a regular request; the access_token from the cookie is
 	// forwarded as the "X-Forwarded-Access-Token" header. The token is
 	// read by the test provider server and written in the response body.
-	code, payload := patTest.getRootEndpoint(cookie)
+	code, payload := patTest.getEndpointWithCookie(cookie, "/")
 	if code != 200 {
 		t.Fatalf("expected 200; got %d", code)
 	}
 	assert.Equal(t, "my_auth_token", payload)
 }
 
+func TestStaticProxyUpstream(t *testing.T) {
+	patTest := NewPassAccessTokenTest(PassAccessTokenTestOptions{
+		PassAccessToken: true,
+		ProxyUpstream:   "static://200/static-proxy",
+	})
+
+	defer patTest.Close()
+
+	// A successful validation will redirect and set the auth cookie.
+	code, cookie := patTest.getCallbackEndpoint()
+	if code != 302 {
+		t.Fatalf("expected 302; got %d", code)
+	}
+	assert.NotEqual(t, nil, cookie)
+
+	// Now we make a regular request againts the upstream proxy; And validate
+	// the returned status code through the static proxy.
+	code, payload := patTest.getEndpointWithCookie(cookie, "/static-proxy")
+	if code != 200 {
+		t.Fatalf("expected 200; got %d", code)
+	}
+	assert.Equal(t, "Authenticated", payload)
+}
+
 func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
 	patTest := NewPassAccessTokenTest(PassAccessTokenTestOptions{
 		PassAccessToken: false,
@@ -497,7 +526,7 @@ func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
 
 	// Now we make a regular request, but the access token header should
 	// not be present.
-	code, payload := patTest.getRootEndpoint(cookie)
+	code, payload := patTest.getEndpointWithCookie(cookie, "/")
 	if code != 200 {
 		t.Fatalf("expected 200; got %d", code)
 	}