@@ -1,9 +1,12 @@
11package main
22
33import (
4+ "crypto"
45 "encoding/base64"
6+ "github.com/18F/hmacauth"
57 "github.com/bitly/oauth2_proxy/providers"
68 "github.com/bmizerany/assert"
9+ "io"
710 "io/ioutil"
811 "log"
912 "net"
@@ -88,6 +91,45 @@ func TestRobotsTxt(t *testing.T) {
8891 assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String())
8992}
9093
94+type TestProvider struct {
95+ *providers.ProviderData
96+ EmailAddress string
97+ ValidToken bool
98+}
99+
100+func NewTestProvider(provider_url *url.URL, email_address string) *TestProvider {
101+ return &TestProvider{
102+ ProviderData: &providers.ProviderData{
103+ ProviderName: "Test Provider",
104+ LoginURL: &url.URL{
105+ Scheme: "http",
106+ Host: provider_url.Host,
107+ Path: "/oauth/authorize",
108+ },
109+ RedeemURL: &url.URL{
110+ Scheme: "http",
111+ Host: provider_url.Host,
112+ Path: "/oauth/token",
113+ },
114+ ProfileURL: &url.URL{
115+ Scheme: "http",
116+ Host: provider_url.Host,
117+ Path: "/api/v1/profile",
118+ },
119+ Scope: "profile.email",
120+ },
121+ EmailAddress: email_address,
122+ }
123+}
124+
125+func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
126+ return tp.EmailAddress, nil
127+}
128+
129+func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
130+ return tp.ValidToken
131+}
132+
91133func TestBasicAuthPassword(t *testing.T) {
92134 provider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93135 log.Printf("%#v", r)
@@ -121,29 +163,7 @@ func TestBasicAuthPassword(t *testing.T) {
121163 const email_address = "michael.bland@gsa.gov"
122164 const user_name = "michael.bland"
123165
124− opts.provider = &TestProvider{
125− ProviderData: &providers.ProviderData{
126− ProviderName: "Test Provider",
127− LoginURL: &url.URL{
128− Scheme: "http",
129− Host: provider_url.Host,
130− Path: "/oauth/authorize",
131− },
132− RedeemURL: &url.URL{
133− Scheme: "http",
134− Host: provider_url.Host,
135− Path: "/oauth/token",
136− },
137− ProfileURL: &url.URL{
138− Scheme: "http",
139− Host: provider_url.Host,
140− Path: "/api/v1/profile",
141− },
142− Scope: "profile.email",
143− },
144− EmailAddress: email_address,
145− }
146−
166+ opts.provider = NewTestProvider(provider_url, email_address)
147167 proxy := NewOAuthProxy(opts, func(email string) bool {
148168 return email == email_address
149169 })
@@ -183,20 +203,6 @@ func TestBasicAuthPassword(t *testing.T) {
183203 provider_server.Close()
184204}
185205
186−type TestProvider struct {
187− *providers.ProviderData
188− EmailAddress string
189− ValidToken bool
190−}
191−
192−func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
193− return tp.EmailAddress, nil
194−}
195−
196−func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
197− return tp.ValidToken
198−}
199−
200206type PassAccessTokenTest struct {
201207 provider_server *httptest.Server
202208 proxy *OAuthProxy
@@ -242,29 +248,7 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
242248 provider_url, _ := url.Parse(t.provider_server.URL)
243249 const email_address = "michael.bland@gsa.gov"
244250
245− t.opts.provider = &TestProvider{
246− ProviderData: &providers.ProviderData{
247− ProviderName: "Test Provider",
248− LoginURL: &url.URL{
249− Scheme: "http",
250− Host: provider_url.Host,
251− Path: "/oauth/authorize",
252− },
253− RedeemURL: &url.URL{
254− Scheme: "http",
255− Host: provider_url.Host,
256− Path: "/oauth/token",
257− },
258− ProfileURL: &url.URL{
259− Scheme: "http",
260− Host: provider_url.Host,
261− Path: "/api/v1/profile",
262− },
263− Scope: "profile.email",
264− },
265− EmailAddress: email_address,
266− }
267−
251+ t.opts.provider = NewTestProvider(provider_url, email_address)
268252 t.proxy = NewOAuthProxy(t.opts, func(email string) bool {
269253 return email == email_address
270254 })
@@ -559,7 +543,7 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
559543func NewAuthOnlyEndpointTest() *ProcessCookieTest {
560544 pc_test := NewProcessCookieTestWithDefaults()
561545 pc_test.req, _ = http.NewRequest("GET",
562− pc_test.opts.ProxyPrefix + "/auth", nil)
546+ pc_test.opts.ProxyPrefix+"/auth", nil)
563547 return pc_test
564548}
565549
@@ -610,3 +594,143 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
610594 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
611595 assert.Equal(t, "unauthorized request\n", string(bodyBytes))
612596}
597+
598+type SignatureAuthenticator struct {
599+ auth hmacauth.HmacAuth
600+}
601+
602+func (v *SignatureAuthenticator) Authenticate(
603+ w http.ResponseWriter, r *http.Request) {
604+ result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
605+ if result == hmacauth.ResultNoSignature {
606+ w.Write([]byte("no signature received"))
607+ } else if result == hmacauth.ResultMatch {
608+ w.Write([]byte("signatures match"))
609+ } else if result == hmacauth.ResultMismatch {
610+ w.Write([]byte("signatures do not match:" +
611+ "\n received: " + headerSig +
612+ "\n computed: " + computedSig))
613+ } else {
614+ panic("Unknown result value: " + result.String())
615+ }
616+}
617+
618+type SignatureTest struct {
619+ opts *Options
620+ upstream *httptest.Server
621+ upstream_host string
622+ provider *httptest.Server
623+ header http.Header
624+ rw *httptest.ResponseRecorder
625+ authenticator *SignatureAuthenticator
626+}
627+
628+func NewSignatureTest() *SignatureTest {
629+ opts := NewOptions()
630+ opts.CookieSecret = "cookie secret"
631+ opts.ClientID = "client ID"
632+ opts.ClientSecret = "client secret"
633+ opts.EmailDomains = []string{"acm.org"}
634+
635+ authenticator := &SignatureAuthenticator{}
636+ upstream := httptest.NewServer(
637+ http.HandlerFunc(authenticator.Authenticate))
638+ upstream_url, _ := url.Parse(upstream.URL)
639+ opts.Upstreams = append(opts.Upstreams, upstream.URL)
640+
641+ providerHandler := func(w http.ResponseWriter, r *http.Request) {
642+ w.Write([]byte(`{"access_token": "my_auth_token"}`))
643+ }
644+ provider := httptest.NewServer(http.HandlerFunc(providerHandler))
645+ provider_url, _ := url.Parse(provider.URL)
646+ opts.provider = NewTestProvider(provider_url, "mbland@acm.org")
647+
648+ return &SignatureTest{
649+ opts,
650+ upstream,
651+ upstream_url.Host,
652+ provider,
653+ make(http.Header),
654+ httptest.NewRecorder(),
655+ authenticator,
656+ }
657+}
658+
659+func (st *SignatureTest) Close() {
660+ st.provider.Close()
661+ st.upstream.Close()
662+}
663+
664+// fakeNetConn simulates an http.Request.Body buffer that will be consumed
665+// when it is read by the hmacauth.HmacAuth if not handled properly. See:
666+// https://github.com/18F/hmacauth/pull/4
667+type fakeNetConn struct {
668+ reqBody string
669+}
670+
671+func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
672+ if bodyLen := len(fnc.reqBody); bodyLen != 0 {
673+ copy(p, fnc.reqBody)
674+ fnc.reqBody = ""
675+ return bodyLen, io.EOF
676+ }
677+ return 0, io.EOF
678+}
679+
680+func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
681+ err := st.opts.Validate()
682+ if err != nil {
683+ panic(err)
684+ }
685+ proxy := NewOAuthProxy(st.opts, func(email string) bool { return true })
686+
687+ var bodyBuf io.ReadCloser
688+ if body != "" {
689+ bodyBuf = ioutil.NopCloser(&fakeNetConn{reqBody: body})
690+ }
691+ req, err := http.NewRequest(method, "/foo/bar", bodyBuf)
692+ if err != nil {
693+ panic(err)
694+ }
695+ req.Header = st.header
696+
697+ state := &providers.SessionState{
698+ Email: "mbland@acm.org", AccessToken: "my_access_token"}
699+ value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)
700+ if err != nil {
701+ panic(err)
702+ }
703+ cookie := proxy.MakeCookie(req, value, proxy.CookieExpire, time.Now())
704+ req.AddCookie(cookie)
705+ // This is used by the upstream to validate the signature.
706+ st.authenticator.auth = hmacauth.NewHmacAuth(
707+ crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)
708+ proxy.ServeHTTP(st.rw, req)
709+}
710+
711+func TestNoRequestSignature(t *testing.T) {
712+ st := NewSignatureTest()
713+ defer st.Close()
714+ st.MakeRequestWithExpectedKey("GET", "", "")
715+ assert.Equal(t, 200, st.rw.Code)
716+ assert.Equal(t, st.rw.Body.String(), "no signature received")
717+}
718+
719+func TestRequestSignatureGetRequest(t *testing.T) {
720+ st := NewSignatureTest()
721+ defer st.Close()
722+ st.opts.SignatureKey = "sha1:foobar"
723+ st.MakeRequestWithExpectedKey("GET", "", "foobar")
724+ assert.Equal(t, 200, st.rw.Code)
725+ assert.Equal(t, st.rw.Body.String(), "signatures match")
726+}
727+
728+func TestRequestSignaturePostRequest(t *testing.T) {
729+ st := NewSignatureTest()
730+ defer st.Close()
731+ st.opts.SignatureKey = "sha1:foobar"
732+ payload := `{ "hello": "world!" }`
733+ st.MakeRequestWithExpectedKey("POST", payload, "foobar")
734+ assert.Equal(t, 200, st.rw.Code)
735+ assert.Equal(t, st.rw.Body.String(), "signatures match")
736+}