git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #79 from 18F/add-myusa-provider

Add myusa provider

commit66d4d72d2ef4ea05314fd25d0e2e61e3357aa2b0 authorJehiah Czebotar <jehiah@gmail.com> dateTue 31 Mar 2015, 15:59 (12 years ago) parent243dbb7291a0b7
4 files changed+219−5
README.md+28−5 providers/myusa.go+55 providers/myusa_test.go+134 providers/providers.go+2
README.md+28−5
@@ -2,8 +2,8 @@ google_auth_proxy
22=================
33
44
5−A reverse proxy that provides authentication using Google OAuth2 to validate
6−individual accounts, or a whole google apps domain.
5+A reverse proxy that provides authentication using Google and other OAuth2
6+providers to validate individual accounts, or a whole google apps domain.
77
88[![Build Status](https://secure.travis-ci.org/bitly/google_auth_proxy.png?branch=master)](http://travis-ci.org/bitly/google_auth_proxy)
99
@@ -31,8 +31,10 @@ individual accounts, or a whole google apps domain.
3131
3232## OAuth Configuration
3333
34−You will need to register an OAuth application with google, and configure it with Redirect URI(s) for the domain you
35−intend to run `google_auth_proxy` on.
34+You will need to register an OAuth application with Google (or [another
35+provider](#providers)), and configure it with Redirect URI(s) for the domain
36+you intend to run `google_auth_proxy` on. For Google, the registration steps
37+are:
3638
37391. Create a new project: https://console.developers.google.com/project
38402. Under "APIs & Auth", choose "Credentials"
@@ -73,9 +75,15 @@ Usage of google_auth_proxy:
7375 -google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times)
7476 -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
7577 -http-address="127.0.0.1:4180": [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients
78+ -login-url="": Authentication endpoint
7679 -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
7780 -pass-host-header=true: pass the request Host Header to upstream
81+ -profile-url="": Profile access endpoint
82+ -provider="": Oauth provider (defaults to Google)
83+ -redeem-url="": Token redemption endpoint
7884 -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
85+ -request-logging=true: Log requests to stdout
86+ -scope="": Oauth scope specification
7987 -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times)
8088 -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
8189 -version=false: print version string
@@ -142,4 +150,19 @@ Google Auth Proxy logs requests to stdout in a format similar to Apache Combined
142150
143151```
144152<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
145−````
153+```
154+
155+## <a name="providers"></a>Providers other than Google
156+
157+Other providers besides Google can be specified by the `providers` flag/config
158+directive. Right now this includes:
159+
160+* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
161+ ([GitHub](https://github.com/18F/myusa))
162+
163+## Adding a new Provider
164+
165+Follow the examples in the [`providers` package](providers/) to define a new
166+`Provider` instance. Add a new `case` to
167+[`providers.New()`](providers/providers.go) to allow the auth proxy to use the
168+new `Provider`.
providers/myusa.gonew file+55
@@ -0,0 +1,55 @@
1+package providers
2+
3+import (
4+ "log"
5+ "net/http"
6+ "net/url"
7+
8+ "github.com/bitly/go-simplejson"
9+ "github.com/bitly/google_auth_proxy/api"
10+)
11+
12+type MyUsaProvider struct {
13+ *ProviderData
14+}
15+
16+func NewMyUsaProvider(p *ProviderData) *MyUsaProvider {
17+ const myUsaHost string = "alpha.my.usa.gov"
18+
19+ p.ProviderName = "MyUSA"
20+ if p.LoginUrl.String() == "" {
21+ p.LoginUrl = &url.URL{Scheme: "https",
22+ Host: myUsaHost,
23+ Path: "/oauth/authorize"}
24+ }
25+ if p.RedeemUrl.String() == "" {
26+ p.RedeemUrl = &url.URL{Scheme: "https",
27+ Host: myUsaHost,
28+ Path: "/oauth/token"}
29+ }
30+ if p.ProfileUrl.String() == "" {
31+ p.ProfileUrl = &url.URL{Scheme: "https",
32+ Host: myUsaHost,
33+ Path: "/api/v1/profile"}
34+ }
35+ if p.Scope == "" {
36+ p.Scope = "profile.email"
37+ }
38+ return &MyUsaProvider{ProviderData: p}
39+}
40+
41+func (p *MyUsaProvider) GetEmailAddress(auth_response *simplejson.Json,
42+ access_token string) (string, error) {
43+ req, err := http.NewRequest("GET",
44+ p.ProfileUrl.String()+"?access_token="+access_token, nil)
45+ if err != nil {
46+ log.Printf("failed building request %s", err)
47+ return "", err
48+ }
49+ json, err := api.Request(req)
50+ if err != nil {
51+ log.Printf("failed making request %s", err)
52+ return "", err
53+ }
54+ return json.Get("email").String()
55+}
providers/myusa_test.gonew file+134
@@ -0,0 +1,134 @@
1+package providers
2+
3+import (
4+ "github.com/bitly/go-simplejson"
5+ "github.com/bmizerany/assert"
6+ "net/http"
7+ "net/http/httptest"
8+ "net/url"
9+ "testing"
10+)
11+
12+func updateUrl(url *url.URL, hostname string) {
13+ url.Scheme = "http"
14+ url.Host = hostname
15+}
16+
17+func testMyUsaProvider(hostname string) *MyUsaProvider {
18+ p := NewMyUsaProvider(
19+ &ProviderData{
20+ ProviderName: "",
21+ LoginUrl: &url.URL{},
22+ RedeemUrl: &url.URL{},
23+ ProfileUrl: &url.URL{},
24+ Scope: ""})
25+ if hostname != "" {
26+ updateUrl(p.Data().LoginUrl, hostname)
27+ updateUrl(p.Data().RedeemUrl, hostname)
28+ updateUrl(p.Data().ProfileUrl, hostname)
29+ }
30+ return p
31+}
32+
33+func testMyUsaBackend(payload string) *httptest.Server {
34+ path := "/api/v1/profile"
35+ query := "access_token=imaginary_access_token"
36+
37+ return httptest.NewServer(http.HandlerFunc(
38+ func(w http.ResponseWriter, r *http.Request) {
39+ url := r.URL
40+ if url.Path != path || url.RawQuery != query {
41+ w.WriteHeader(404)
42+ } else {
43+ w.WriteHeader(200)
44+ w.Write([]byte(payload))
45+ }
46+ }))
47+}
48+
49+func TestMyUsaProviderDefaults(t *testing.T) {
50+ p := testMyUsaProvider("")
51+ assert.NotEqual(t, nil, p)
52+ assert.Equal(t, "MyUSA", p.Data().ProviderName)
53+ assert.Equal(t, "https://alpha.my.usa.gov/oauth/authorize",
54+ p.Data().LoginUrl.String())
55+ assert.Equal(t, "https://alpha.my.usa.gov/oauth/token",
56+ p.Data().RedeemUrl.String())
57+ assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile",
58+ p.Data().ProfileUrl.String())
59+ assert.Equal(t, "profile.email", p.Data().Scope)
60+}
61+
62+func TestMyUsaProviderOverrides(t *testing.T) {
63+ p := NewMyUsaProvider(
64+ &ProviderData{
65+ LoginUrl: &url.URL{
66+ Scheme: "https",
67+ Host: "example.com",
68+ Path: "/oauth/auth"},
69+ RedeemUrl: &url.URL{
70+ Scheme: "https",
71+ Host: "example.com",
72+ Path: "/oauth/token"},
73+ ProfileUrl: &url.URL{
74+ Scheme: "https",
75+ Host: "example.com",
76+ Path: "/oauth/profile"},
77+ Scope: "profile"})
78+ assert.NotEqual(t, nil, p)
79+ assert.Equal(t, "MyUSA", p.Data().ProviderName)
80+ assert.Equal(t, "https://example.com/oauth/auth",
81+ p.Data().LoginUrl.String())
82+ assert.Equal(t, "https://example.com/oauth/token",
83+ p.Data().RedeemUrl.String())
84+ assert.Equal(t, "https://example.com/oauth/profile",
85+ p.Data().ProfileUrl.String())
86+ assert.Equal(t, "profile", p.Data().Scope)
87+}
88+
89+func TestMyUsaProviderGetEmailAddress(t *testing.T) {
90+ b := testMyUsaBackend("{\"email\": \"michael.bland@gsa.gov\"}")
91+ defer b.Close()
92+
93+ b_url, _ := url.Parse(b.URL)
94+ p := testMyUsaProvider(b_url.Host)
95+ unused_auth_response := simplejson.New()
96+
97+ email, err := p.GetEmailAddress(unused_auth_response,
98+ "imaginary_access_token")
99+ assert.Equal(t, nil, err)
100+ assert.Equal(t, "michael.bland@gsa.gov", email)
101+}
102+
103+// Note that trying to trigger the "failed building request" case is not
104+// practical, since the only way it can fail is if the URL fails to parse.
105+func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) {
106+ b := testMyUsaBackend("unused payload")
107+ defer b.Close()
108+
109+ b_url, _ := url.Parse(b.URL)
110+ p := testMyUsaProvider(b_url.Host)
111+ unused_auth_response := simplejson.New()
112+
113+ // We'll trigger a request failure by using an unexpected access
114+ // token. Alternatively, we could allow the parsing of the payload as
115+ // JSON to fail.
116+ email, err := p.GetEmailAddress(unused_auth_response,
117+ "unexpected_access_token")
118+ assert.NotEqual(t, nil, err)
119+ assert.Equal(t, "", email)
120+}
121+
122+func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
123+ b := testMyUsaBackend("{\"foo\": \"bar\"}")
124+ defer b.Close()
125+
126+ b_url, _ := url.Parse(b.URL)
127+ p := testMyUsaProvider(b_url.Host)
128+ unused_auth_response := simplejson.New()
129+
130+ email, err := p.GetEmailAddress(unused_auth_response,
131+ "imaginary_access_token")
132+ assert.NotEqual(t, nil, err)
133+ assert.Equal(t, "", email)
134+}
providers/providers.go+2
@@ -12,6 +12,8 @@ type Provider interface {
1212
1313func New(provider string, p *ProviderData) Provider {
1414 switch provider {
15+ case "myusa":
16+ return NewMyUsaProvider(p)
1517 default:
1618 return NewGoogleProvider(p)
1719 }