git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #153 from 18F/auth-only-endpoint

Add /auth endpoint to support Nginx's auth_request

commit7c241ec1fe047b5c4a07185f7474d455e4db057a authorJehiah Czebotar <jehiah@gmail.com> dateMon 9 Nov 2015, 11:12 (11 years ago) parente6e2dbed247274
3 files changed+111−7
README.md+28−1 oauthproxy.go+28−6 oauthproxy_test.go+55
README.md+28−1
@@ -239,7 +239,6 @@ The command line to run `oauth2_proxy` in this configuration would look like thi
239239 --client-secret=...
240240```
241241
242−
243242## Endpoint Documentation
244243
245244OAuth2 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,6 +248,7 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w
249248* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
250249* /oauth2/start - a URL that will redirect to start the OAuth cycle
251250* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
251+* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)
252252
253253## Logging Format
254254
@@ -265,3 +265,30 @@ Follow the examples in the [`providers` package](providers/) to define a new
265265`Provider` instance. Add a new `case` to
266266[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the
267267new `Provider`.
268+
269+## <a name="nginx-auth-request"></a>Configuring for use with the Nginx `auth_request` directive
270+
271+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:
272+
273+```nginx
274+server {
275+ listen 443 ssl spdy;
276+ server_name ...;
277+ include ssl/ssl.conf;
278+
279+ location = /auth {
280+ internal;
281+ proxy_pass http://127.0.0.1:4180;
282+ }
283+
284+ location / {
285+ auth_request /auth;
286+ error_page 401 = ...;
287+
288+ root /path/to/the/site;
289+ default_type text/html;
290+ charset utf-8;
291+ charset_types application/json utf-8;
292+ }
293+}
294+```
oauthproxy.go+28−6
@@ -33,6 +33,7 @@ type OAuthProxy struct {
3333 SignInPath string
3434 OAuthStartPath string
3535 OAuthCallbackPath string
36+ AuthOnlyPath string
3637
3738 redirectURL *url.URL // the url to receive requests at
3839 provider providers.Provider
@@ -156,6 +157,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
156157 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
157158 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
158159 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
160+ AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
159161
160162 ProxyPrefix: opts.ProxyPrefix,
161163 provider: opts.provider,
@@ -390,6 +392,8 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
390392 p.OAuthStart(rw, req)
391393 case path == p.OAuthCallbackPath:
392394 p.OAuthCallback(rw, req)
395+ case path == p.AuthOnlyPath:
396+ p.AuthenticateOnly(rw, req)
393397 default:
394398 p.Proxy(rw, req)
395399 }
@@ -465,7 +469,28 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
465469 }
466470}
467471
472+func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
473+ status := p.Authenticate(rw, req)
474+ if status == http.StatusAccepted {
475+ rw.WriteHeader(http.StatusAccepted)
476+ } else {
477+ http.Error(rw, "unauthorized request", http.StatusUnauthorized)
478+ }
479+}
480+
468481func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
482+ status := p.Authenticate(rw, req)
483+ if status == http.StatusInternalServerError {
484+ p.ErrorPage(rw, http.StatusInternalServerError,
485+ "Internal Error", "Internal Error")
486+ } else if status == http.StatusForbidden {
487+ p.SignInPage(rw, req, http.StatusForbidden)
488+ } else {
489+ p.serveMux.ServeHTTP(rw, req)
490+ }
491+}
492+
493+func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
469494 var saveSession, clearSession, revalidated bool
470495 remoteAddr := getRemoteAddr(req)
471496
@@ -514,8 +539,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
514539 err := p.SaveSession(rw, req, session)
515540 if err != nil {
516541 log.Printf("%s %s", remoteAddr, err)
517− p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
518− return
542+ return http.StatusInternalServerError
519543 }
520544 }
521545
@@ -531,8 +555,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
531555 }
532556
533557 if session == nil {
534− p.SignInPage(rw, req, 403)
535− return
558+ return http.StatusForbidden
536559 }
537560
538561 // At this point, the user is authenticated. proxy normally
@@ -551,8 +574,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
551574 } else {
552575 rw.Header().Set("GAP-Auth", session.Email)
553576 }
554−
555− p.serveMux.ServeHTTP(rw, req)
577+ return http.StatusAccepted
556578}
557579
558580func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {
oauthproxy_test.go+55
@@ -555,3 +555,58 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
555555 t.Errorf("expected nil session %#v", session)
556556 }
557557}
558+
559+func NewAuthOnlyEndpointTest() *ProcessCookieTest {
560+ pc_test := NewProcessCookieTestWithDefaults()
561+ pc_test.req, _ = http.NewRequest("GET",
562+ pc_test.opts.ProxyPrefix + "/auth", nil)
563+ return pc_test
564+}
565+
566+func TestAuthOnlyEndpointAccepted(t *testing.T) {
567+ test := NewAuthOnlyEndpointTest()
568+ startSession := &providers.SessionState{
569+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
570+ test.SaveSession(startSession, time.Now())
571+
572+ test.proxy.ServeHTTP(test.rw, test.req)
573+ assert.Equal(t, http.StatusAccepted, test.rw.Code)
574+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
575+ assert.Equal(t, "", string(bodyBytes))
576+}
577+
578+func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
579+ test := NewAuthOnlyEndpointTest()
580+
581+ test.proxy.ServeHTTP(test.rw, test.req)
582+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
583+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
584+ assert.Equal(t, "unauthorized request\n", string(bodyBytes))
585+}
586+
587+func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
588+ test := NewAuthOnlyEndpointTest()
589+ test.proxy.CookieExpire = time.Duration(24) * time.Hour
590+ reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
591+ startSession := &providers.SessionState{
592+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
593+ test.SaveSession(startSession, reference)
594+
595+ test.proxy.ServeHTTP(test.rw, test.req)
596+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
597+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
598+ assert.Equal(t, "unauthorized request\n", string(bodyBytes))
599+}
600+
601+func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
602+ test := NewAuthOnlyEndpointTest()
603+ startSession := &providers.SessionState{
604+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
605+ test.SaveSession(startSession, time.Now())
606+ test.validate_user = false
607+
608+ test.proxy.ServeHTTP(test.rw, test.req)
609+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
610+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
611+ assert.Equal(t, "unauthorized request\n", string(bodyBytes))
612+}