Skip to content

Commit 7c241ec

Browse files
committed
Merge pull request #153 from 18F/auth-only-endpoint
Add /auth endpoint to support Nginx's auth_request
2 parents e6e2dbe + d247274 commit 7c241ec

File tree

3 files changed

+111
-7
lines changed

3 files changed

+111
-7
lines changed

README.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -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

245244
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,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
267267
new `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
Original file line numberDiff line numberDiff line change
@@ -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+
468481
func (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

558580
func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {

oauthproxy_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -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: "[email protected]", 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: "[email protected]", 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: "[email protected]", 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+
}

0 commit comments

Comments
 (0)