diff --git a/README.md b/README.md index 524de2d..83fc5fb 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ google_auth_proxy ================= -A reverse proxy that provides authentication using Google OAuth2 to validate -individual accounts, or a whole google apps domain. +A reverse proxy that provides authentication using Google and other OAuth2 +providers to validate individual accounts, or a whole google apps domain. [![Build Status](https://secure.travis-ci.org/bitly/google_auth_proxy.png?branch=master)](http://travis-ci.org/bitly/google_auth_proxy) @@ -31,8 +31,10 @@ individual accounts, or a whole google apps domain. ## OAuth Configuration -You will need to register an OAuth application with google, and configure it with Redirect URI(s) for the domain you -intend to run `google_auth_proxy` on. +You will need to register an OAuth application with Google (or [another +provider](#providers)), and configure it with Redirect URI(s) for the domain +you intend to run `google_auth_proxy` on. For Google, the registration steps +are: 1. Create a new project: https://console.developers.google.com/project 2. Under "APIs & Auth", choose "Credentials" @@ -73,9 +75,15 @@ Usage of google_auth_proxy: -google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times) -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption -http-address="127.0.0.1:4180": [http://]: or unix:// to listen on for HTTP clients + -login-url="": Authentication endpoint -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream -pass-host-header=true: pass the request Host Header to upstream + -profile-url="": Profile access endpoint + -provider="": Oauth provider (defaults to Google) + -redeem-url="": Token redemption endpoint -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" + -request-logging=true: Log requests to stdout + -scope="": Oauth scope specification -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path -version=false: print version string @@ -142,4 +150,19 @@ Google Auth Proxy logs requests to stdout in a format similar to Apache Combined ``` - [19/Mar/2015:17:20:19 -0400] GET "/path/" HTTP/1.1 "" -```` +``` + +## Providers other than Google + +Other providers besides Google can be specified by the `providers` flag/config +directive. Right now this includes: + +* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service + ([GitHub](https://github.com/18F/myusa)) + +## Adding a new Provider + +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 the auth proxy to use the +new `Provider`. diff --git a/providers/myusa.go b/providers/myusa.go new file mode 100644 index 0000000..2c9119a --- /dev/null +++ b/providers/myusa.go @@ -0,0 +1,55 @@ +package providers + +import ( + "log" + "net/http" + "net/url" + + "github.com/bitly/go-simplejson" + "github.com/bitly/google_auth_proxy/api" +) + +type MyUsaProvider struct { + *ProviderData +} + +func NewMyUsaProvider(p *ProviderData) *MyUsaProvider { + const myUsaHost string = "alpha.my.usa.gov" + + p.ProviderName = "MyUSA" + if p.LoginUrl.String() == "" { + p.LoginUrl = &url.URL{Scheme: "https", + Host: myUsaHost, + Path: "/oauth/authorize"} + } + if p.RedeemUrl.String() == "" { + p.RedeemUrl = &url.URL{Scheme: "https", + Host: myUsaHost, + Path: "/oauth/token"} + } + if p.ProfileUrl.String() == "" { + p.ProfileUrl = &url.URL{Scheme: "https", + Host: myUsaHost, + Path: "/api/v1/profile"} + } + if p.Scope == "" { + p.Scope = "profile.email" + } + return &MyUsaProvider{ProviderData: p} +} + +func (p *MyUsaProvider) GetEmailAddress(auth_response *simplejson.Json, + access_token string) (string, error) { + req, err := http.NewRequest("GET", + p.ProfileUrl.String()+"?access_token="+access_token, nil) + if err != nil { + log.Printf("failed building request %s", err) + return "", err + } + json, err := api.Request(req) + if err != nil { + log.Printf("failed making request %s", err) + return "", err + } + return json.Get("email").String() +} diff --git a/providers/myusa_test.go b/providers/myusa_test.go new file mode 100644 index 0000000..74bb1a9 --- /dev/null +++ b/providers/myusa_test.go @@ -0,0 +1,134 @@ +package providers + +import ( + "github.com/bitly/go-simplejson" + "github.com/bmizerany/assert" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func updateUrl(url *url.URL, hostname string) { + url.Scheme = "http" + url.Host = hostname +} + +func testMyUsaProvider(hostname string) *MyUsaProvider { + p := NewMyUsaProvider( + &ProviderData{ + ProviderName: "", + LoginUrl: &url.URL{}, + RedeemUrl: &url.URL{}, + ProfileUrl: &url.URL{}, + Scope: ""}) + if hostname != "" { + updateUrl(p.Data().LoginUrl, hostname) + updateUrl(p.Data().RedeemUrl, hostname) + updateUrl(p.Data().ProfileUrl, hostname) + } + return p +} + +func testMyUsaBackend(payload string) *httptest.Server { + path := "/api/v1/profile" + query := "access_token=imaginary_access_token" + + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + url := r.URL + if url.Path != path || url.RawQuery != query { + w.WriteHeader(404) + } else { + w.WriteHeader(200) + w.Write([]byte(payload)) + } + })) +} + +func TestMyUsaProviderDefaults(t *testing.T) { + p := testMyUsaProvider("") + assert.NotEqual(t, nil, p) + assert.Equal(t, "MyUSA", p.Data().ProviderName) + assert.Equal(t, "https://alpha.my.usa.gov/oauth/authorize", + p.Data().LoginUrl.String()) + assert.Equal(t, "https://alpha.my.usa.gov/oauth/token", + p.Data().RedeemUrl.String()) + assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile", + p.Data().ProfileUrl.String()) + assert.Equal(t, "profile.email", p.Data().Scope) +} + +func TestMyUsaProviderOverrides(t *testing.T) { + p := NewMyUsaProvider( + &ProviderData{ + LoginUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/auth"}, + RedeemUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/token"}, + ProfileUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/profile"}, + Scope: "profile"}) + assert.NotEqual(t, nil, p) + assert.Equal(t, "MyUSA", p.Data().ProviderName) + assert.Equal(t, "https://example.com/oauth/auth", + p.Data().LoginUrl.String()) + assert.Equal(t, "https://example.com/oauth/token", + p.Data().RedeemUrl.String()) + assert.Equal(t, "https://example.com/oauth/profile", + p.Data().ProfileUrl.String()) + assert.Equal(t, "profile", p.Data().Scope) +} + +func TestMyUsaProviderGetEmailAddress(t *testing.T) { + b := testMyUsaBackend("{\"email\": \"michael.bland@gsa.gov\"}") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testMyUsaProvider(b_url.Host) + unused_auth_response := simplejson.New() + + email, err := p.GetEmailAddress(unused_auth_response, + "imaginary_access_token") + assert.Equal(t, nil, err) + assert.Equal(t, "michael.bland@gsa.gov", email) +} + +// Note that trying to trigger the "failed building request" case is not +// practical, since the only way it can fail is if the URL fails to parse. +func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) { + b := testMyUsaBackend("unused payload") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testMyUsaProvider(b_url.Host) + unused_auth_response := simplejson.New() + + // We'll trigger a request failure by using an unexpected access + // token. Alternatively, we could allow the parsing of the payload as + // JSON to fail. + email, err := p.GetEmailAddress(unused_auth_response, + "unexpected_access_token") + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} + +func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { + b := testMyUsaBackend("{\"foo\": \"bar\"}") + defer b.Close() + + b_url, _ := url.Parse(b.URL) + p := testMyUsaProvider(b_url.Host) + unused_auth_response := simplejson.New() + + email, err := p.GetEmailAddress(unused_auth_response, + "imaginary_access_token") + assert.NotEqual(t, nil, err) + assert.Equal(t, "", email) +} diff --git a/providers/providers.go b/providers/providers.go index 8076497..eb43b26 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -12,6 +12,8 @@ type Provider interface { func New(provider string, p *ProviderData) Provider { switch provider { + case "myusa": + return NewMyUsaProvider(p) default: return NewGoogleProvider(p) }