From e61fc9e7a67c85c97516ba6804cd4e0e45bc2a8c Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Thu, 8 Oct 2015 09:27:00 -0400 Subject: [PATCH 1/3] Add /auth endpoint to support Nginx's auth_request Closes #152. --- README.md | 1 + oauthproxy.go | 19 ++++++++++++++++ oauthproxy_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/README.md b/README.md index 17c748d..7a5f232 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w * /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) * /oauth2/start - a URL that will redirect to start the OAuth cycle * /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. +* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) ## Logging Format diff --git a/oauthproxy.go b/oauthproxy.go index 73fd6f2..0e0896c 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -33,6 +33,7 @@ type OAuthProxy struct { SignInPath string OAuthStartPath string OAuthCallbackPath string + AuthOnlyPath string redirectURL *url.URL // the url to receive requests at provider providers.Provider @@ -156,6 +157,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), + AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix), ProxyPrefix: opts.ProxyPrefix, provider: opts.provider, @@ -390,6 +392,8 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { p.OAuthStart(rw, req) case path == p.OAuthCallbackPath: p.OAuthCallback(rw, req) + case path == p.AuthOnlyPath: + p.AuthenticateOnly(rw, req) default: p.Proxy(rw, req) } @@ -465,6 +469,21 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } } +func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { + remoteAddr := getRemoteAddr(req) + if session, _, err := p.LoadCookiedSession(req); err != nil { + log.Printf("%s %s", remoteAddr, err) + } else if session.IsExpired() { + log.Printf("%s Expired", remoteAddr, session) + } else if !p.Validator(session.Email) { + log.Printf("%s Permission Denied", remoteAddr, session) + } else { + rw.WriteHeader(http.StatusAccepted) + return + } + http.Error(rw, "unauthorized request", http.StatusUnauthorized) +} + func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { var saveSession, clearSession, revalidated bool remoteAddr := getRemoteAddr(req) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index cf3f5aa..a89f041 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -555,3 +555,58 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { t.Errorf("expected nil session %#v", session) } } + +func NewAuthOnlyEndpointTest() *ProcessCookieTest { + pc_test := NewProcessCookieTestWithDefaults() + pc_test.req, _ = http.NewRequest("GET", + pc_test.opts.ProxyPrefix + "/auth", nil) + return pc_test +} + +func TestAuthOnlyEndpointAccepted(t *testing.T) { + test := NewAuthOnlyEndpointTest() + startSession := &providers.SessionState{ + Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} + test.SaveSession(startSession, time.Now()) + + test.proxy.ServeHTTP(test.rw, test.req) + assert.Equal(t, http.StatusAccepted, test.rw.Code) + bodyBytes, _ := ioutil.ReadAll(test.rw.Body) + assert.Equal(t, "", string(bodyBytes)) +} + +func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) { + test := NewAuthOnlyEndpointTest() + + test.proxy.ServeHTTP(test.rw, test.req) + assert.Equal(t, http.StatusUnauthorized, test.rw.Code) + bodyBytes, _ := ioutil.ReadAll(test.rw.Body) + assert.Equal(t, "unauthorized request\n", string(bodyBytes)) +} + +func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) { + test := NewAuthOnlyEndpointTest() + test.proxy.CookieExpire = time.Duration(24) * time.Hour + reference := time.Now().Add(time.Duration(25) * time.Hour * -1) + startSession := &providers.SessionState{ + Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} + test.SaveSession(startSession, reference) + + test.proxy.ServeHTTP(test.rw, test.req) + assert.Equal(t, http.StatusUnauthorized, test.rw.Code) + bodyBytes, _ := ioutil.ReadAll(test.rw.Body) + assert.Equal(t, "unauthorized request\n", string(bodyBytes)) +} + +func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { + test := NewAuthOnlyEndpointTest() + startSession := &providers.SessionState{ + Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"} + test.SaveSession(startSession, time.Now()) + test.validate_user = false + + test.proxy.ServeHTTP(test.rw, test.req) + assert.Equal(t, http.StatusUnauthorized, test.rw.Code) + bodyBytes, _ := ioutil.ReadAll(test.rw.Body) + assert.Equal(t, "unauthorized request\n", string(bodyBytes)) +} From 462f6d03d233f2a1e17b9d130dd67a5e19c39725 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Thu, 8 Oct 2015 14:10:28 -0400 Subject: [PATCH 2/3] Extract Authenticate for Proxy, AuthenticateOnly --- oauthproxy.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 0e0896c..7e6d31f 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -470,21 +470,27 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { - remoteAddr := getRemoteAddr(req) - if session, _, err := p.LoadCookiedSession(req); err != nil { - log.Printf("%s %s", remoteAddr, err) - } else if session.IsExpired() { - log.Printf("%s Expired", remoteAddr, session) - } else if !p.Validator(session.Email) { - log.Printf("%s Permission Denied", remoteAddr, session) - } else { + status := p.Authenticate(rw, req) + if status == http.StatusAccepted { rw.WriteHeader(http.StatusAccepted) - return + } else { + http.Error(rw, "unauthorized request", http.StatusUnauthorized) } - http.Error(rw, "unauthorized request", http.StatusUnauthorized) } func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { + status := p.Authenticate(rw, req) + if status == http.StatusInternalServerError { + p.ErrorPage(rw, http.StatusInternalServerError, + "Internal Error", "Internal Error") + } else if status == http.StatusForbidden { + p.SignInPage(rw, req, http.StatusForbidden) + } else { + p.serveMux.ServeHTTP(rw, req) + } +} + +func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int { var saveSession, clearSession, revalidated bool remoteAddr := getRemoteAddr(req) @@ -533,8 +539,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { err := p.SaveSession(rw, req, session) if err != nil { log.Printf("%s %s", remoteAddr, err) - p.ErrorPage(rw, 500, "Internal Error", "Internal Error") - return + return http.StatusInternalServerError } } @@ -550,8 +555,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { } if session == nil { - p.SignInPage(rw, req, 403) - return + return http.StatusForbidden } // At this point, the user is authenticated. proxy normally @@ -570,8 +574,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { } else { rw.Header().Set("GAP-Auth", session.Email) } - - p.serveMux.ServeHTTP(rw, req) + return http.StatusAccepted } func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) { From d247274b0639e0e370fcdffa848ab2863c0e3de3 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Mon, 9 Nov 2015 10:58:44 -0500 Subject: [PATCH 3/3] Add nginx auth_request config to README --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7a5f232..ca248d1 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,6 @@ The command line to run `oauth2_proxy` in this configuration would look like thi --client-secret=... ``` - ## Endpoint Documentation OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. @@ -249,7 +248,7 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w * /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) * /oauth2/start - a URL that will redirect to start the OAuth cycle * /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. -* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) +* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request) ## Logging Format @@ -266,3 +265,30 @@ Follow the examples in the [`providers` package](providers/) to define a new `Provider` instance. Add a new `case` to [`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the new `Provider`. + +## Configuring for use with the Nginx `auth_request` directive + +The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2_proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example: + +```nginx +server { + listen 443 ssl spdy; + server_name ...; + include ssl/ssl.conf; + + location = /auth { + internal; + proxy_pass http://127.0.0.1:4180; + } + + location / { + auth_request /auth; + error_page 401 = ...; + + root /path/to/the/site; + default_type text/html; + charset utf-8; + charset_types application/json utf-8; + } +} +```