git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #81 from 18F/access-token-refactor
9534808Jehiah Czebotar
.gitignore+3
@@ -24,3 +24,6 @@ _testmain.go
24*.exe
24*.exe
25dist
25dist
26.godeps
26.godeps
27+
28+# Editor swap/temp files
29+.*.swp
cookies.go+31
@@ -97,3 +97,34 @@ func decodeAccessToken(aes_cipher cipher.Block, encoded_access_token string) (st
97
97
98 return string(encrypted_access_token), nil
98 return string(encrypted_access_token), nil
99}
99}
100+
101+func buildCookieValue(email string, aes_cipher cipher.Block,
102+ access_token string) (string, error) {
103+ if aes_cipher == nil {
104+ return email, nil
105+ }
106+
107+ encoded_token, err := encodeAccessToken(aes_cipher, access_token)
108+ if err != nil {
109+ return email, fmt.Errorf(
110+ "error encoding access token for %s: %s", email, err)
111+ }
112+ return email + "|" + encoded_token, nil
113+}
114+
115+func parseCookieValue(value string, aes_cipher cipher.Block) (email, user,
116+ access_token string, err error) {
117+ components := strings.Split(value, "|")
118+ email = components[0]
119+ user = strings.Split(email, "@")[0]
120+
121+ if aes_cipher != nil && len(components) == 2 {
122+ access_token, err = decodeAccessToken(aes_cipher, components[1])
123+ if err != nil {
124+ err = fmt.Errorf(
125+ "error decoding access token for %s: %s",
126+ email, err)
127+ }
128+ }
129+ return email, user, access_token, err
130+}
cookies_test.go+52
@@ -3,6 +3,7 @@ package main
3import (
3import (
4 "crypto/aes"
4 "crypto/aes"
5 "github.com/bmizerany/assert"
5 "github.com/bmizerany/assert"
6+ "strings"
6 "testing"
7 "testing"
7)
8)
8
9
@@ -21,3 +22,54 @@ func TestEncodeAndDecodeAccessToken(t *testing.T) {
21 assert.NotEqual(t, access_token, encoded_token)
22 assert.NotEqual(t, access_token, encoded_token)
22 assert.Equal(t, access_token, decoded_token)
23 assert.Equal(t, access_token, decoded_token)
23}
24}
25+
26+func TestBuildCookieValueWithoutAccessToken(t *testing.T) {
27+ value, err := buildCookieValue("michael.bland@gsa.gov", nil, "")
28+ assert.Equal(t, nil, err)
29+ assert.Equal(t, "michael.bland@gsa.gov", value)
30+}
31+
32+func TestBuildCookieValueWithAccessTokenAndNilCipher(t *testing.T) {
33+ value, err := buildCookieValue("michael.bland@gsa.gov", nil,
34+ "access token")
35+ assert.Equal(t, nil, err)
36+ assert.Equal(t, "michael.bland@gsa.gov", value)
37+}
38+
39+func TestParseCookieValueWithoutAccessToken(t *testing.T) {
40+ email, user, access_token, err := parseCookieValue(
41+ "michael.bland@gsa.gov", nil)
42+ assert.Equal(t, nil, err)
43+ assert.Equal(t, "michael.bland@gsa.gov", email)
44+ assert.Equal(t, "michael.bland", user)
45+ assert.Equal(t, "", access_token)
46+}
47+
48+func TestParseCookieValueWithAccessTokenAndNilCipher(t *testing.T) {
49+ email, user, access_token, err := parseCookieValue(
50+ "michael.bland@gsa.gov|access_token", nil)
51+ assert.Equal(t, nil, err)
52+ assert.Equal(t, "michael.bland@gsa.gov", email)
53+ assert.Equal(t, "michael.bland", user)
54+ assert.Equal(t, "", access_token)
55+}
56+
57+func TestBuildAndParseCookieValueWithAccessToken(t *testing.T) {
58+ aes_cipher, err := aes.NewCipher([]byte("0123456789abcdef"))
59+ assert.Equal(t, nil, err)
60+ value, err := buildCookieValue("michael.bland@gsa.gov", aes_cipher,
61+ "access_token")
62+ assert.Equal(t, nil, err)
63+
64+ prefix := "michael.bland@gsa.gov|"
65+ if !strings.HasPrefix(value, prefix) {
66+ t.Fatal("cookie value does not start with \"%s\": %s",
67+ prefix, value)
68+ }
69+
70+ email, user, access_token, err := parseCookieValue(value, aes_cipher)
71+ assert.Equal(t, nil, err)
72+ assert.Equal(t, "michael.bland@gsa.gov", email)
73+ assert.Equal(t, "michael.bland", user)
74+ assert.Equal(t, "access_token", access_token)
75+}
oauthproxy.go+10−35
@@ -47,7 +47,6 @@ type OauthProxy struct {
47 DisplayHtpasswdForm bool
47 DisplayHtpasswdForm bool
48 serveMux http.Handler
48 serveMux http.Handler
49 PassBasicAuth bool
49 PassBasicAuth bool
50− PassAccessToken bool
51 AesCipher cipher.Block
50 AesCipher cipher.Block
52 skipAuthRegex []string
51 skipAuthRegex []string
53 compiledRegex []*regexp.Regexp
52 compiledRegex []*regexp.Regexp
@@ -121,20 +120,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
121 log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)
120 log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)
122
121
123 var aes_cipher cipher.Block
122 var aes_cipher cipher.Block
124−
125− if opts.PassAccessToken == true {
123+ if opts.PassAccessToken {
126− valid_cookie_secret_size := false
127− for _, i := range []int{16, 24, 32} {
128− if len(opts.CookieSecret) == i {
129− valid_cookie_secret_size = true
130− }
131− }
132− if valid_cookie_secret_size == false {
133− log.Fatal("cookie_secret must be 16, 24, or 32 bytes " +
134− "to create an AES cipher when " +
135− "pass_access_token == true")
136− }
137−
138 var err error
124 var err error
139 aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret))
125 aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret))
140 if err != nil {
126 if err != nil {
@@ -163,7 +149,6 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
163 skipAuthRegex: opts.SkipAuthRegex,
149 skipAuthRegex: opts.SkipAuthRegex,
164 compiledRegex: opts.CompiledRegex,
150 compiledRegex: opts.CompiledRegex,
165 PassBasicAuth: opts.PassBasicAuth,
151 PassBasicAuth: opts.PassBasicAuth,
166− PassAccessToken: opts.PassAccessToken,
167 AesCipher: aes_cipher,
152 AesCipher: aes_cipher,
168 templates: loadTemplates(opts.CustomTemplatesDir),
153 templates: loadTemplates(opts.CustomTemplatesDir),
169 }
154 }
@@ -440,20 +425,12 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
440 // set cookie, or deny
425 // set cookie, or deny
441 if p.Validator(email) {
426 if p.Validator(email) {
442 log.Printf("%s authenticating %s completed", remoteAddr, email)
427 log.Printf("%s authenticating %s completed", remoteAddr, email)
443− encoded_token := ""
428+ value, err := buildCookieValue(
444− if p.PassAccessToken {
445− encoded_token, err = encodeAccessToken(p.AesCipher, access_token)
429+ email, p.AesCipher, access_token)
446− if err != nil {
430+ if err != nil {
447− log.Printf("error encoding access token: %s", err)
431+ log.Printf(err.Error())
448− }
449− }
450− access_token = ""
451−
452− if encoded_token != "" {
453− p.SetCookie(rw, req, email+"|"+encoded_token)
454− } else {
455− p.SetCookie(rw, req, email)
456 }
432 }
433+ p.SetCookie(rw, req, value)
457 http.Redirect(rw, req, redirect, 302)
434 http.Redirect(rw, req, redirect, 302)
458 return
435 return
459 } else {
436 } else {
@@ -467,15 +444,13 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
467 if err == nil {
444 if err == nil {
468 var value string
445 var value string
469 value, ok = validateCookie(cookie, p.CookieSeed)
446 value, ok = validateCookie(cookie, p.CookieSeed)
470− components := strings.Split(value, "|")
447+ if ok {
471− email = components[0]
448+ email, user, access_token, err = parseCookieValue(
472− if len(components) == 2 {
449+ value, p.AesCipher)
473− access_token, err = decodeAccessToken(p.AesCipher, components[1])
474 if err != nil {
450 if err != nil {
475− log.Printf("error decoding access token: %s", err)
451+ log.Printf(err.Error())
476 }
452 }
477 }
453 }
478− user = strings.Split(email, "@")[0]
479 }
454 }
480 }
455 }
481
456
oauthproxy_test.go+17−16
@@ -152,24 +152,25 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
152 return t
152 return t
153}
153}
154
154
155−func Close(t *PassAccessTokenTest) {
155+func (pat_test *PassAccessTokenTest) Close() {
156− t.provider_server.Close()
156+ pat_test.provider_server.Close()
157}
157}
158
158
159−func getCallbackEndpoint(pac_test *PassAccessTokenTest) (http_code int, cookie string) {
159+func (pat_test *PassAccessTokenTest) getCallbackEndpoint() (http_code int,
160+ cookie string) {
160 rw := httptest.NewRecorder()
161 rw := httptest.NewRecorder()
161 req, err := http.NewRequest("GET", "/oauth2/callback?code=callback_code",
162 req, err := http.NewRequest("GET", "/oauth2/callback?code=callback_code",
162 strings.NewReader(""))
163 strings.NewReader(""))
163 if err != nil {
164 if err != nil {
164 return 0, ""
165 return 0, ""
165 }
166 }
166− pac_test.proxy.ServeHTTP(rw, req)
167+ pat_test.proxy.ServeHTTP(rw, req)
167 return rw.Code, rw.HeaderMap["Set-Cookie"][0]
168 return rw.Code, rw.HeaderMap["Set-Cookie"][0]
168}
169}
169
170
170−func getRootEndpoint(pac_test *PassAccessTokenTest, cookie string) (http_code int,
171+func (pat_test *PassAccessTokenTest) getRootEndpoint(
171− access_token string) {
172+ cookie string) (http_code int, access_token string) {
172− cookie_key := pac_test.proxy.CookieKey
173+ cookie_key := pat_test.proxy.CookieKey
173 var value string
174 var value string
174 key_prefix := cookie_key + "="
175 key_prefix := cookie_key + "="
175
176
@@ -198,43 +199,43 @@ func getRootEndpoint(pac_test *PassAccessTokenTest, cookie string) (http_code in
198 })
199 })
199
200
200 rw := httptest.NewRecorder()
201 rw := httptest.NewRecorder()
201− pac_test.proxy.ServeHTTP(rw, req)
202+ pat_test.proxy.ServeHTTP(rw, req)
202 return rw.Code, rw.Body.String()
203 return rw.Code, rw.Body.String()
203}
204}
204
205
205func TestForwardAccessTokenUpstream(t *testing.T) {
206func TestForwardAccessTokenUpstream(t *testing.T) {
206− pac_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{
207+ pat_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{
207 PassAccessToken: true,
208 PassAccessToken: true,
208 })
209 })
209− defer Close(pac_test)
210+ defer pat_test.Close()
210
211
211 // A successful validation will redirect and set the auth cookie.
212 // A successful validation will redirect and set the auth cookie.
212− code, cookie := getCallbackEndpoint(pac_test)
213+ code, cookie := pat_test.getCallbackEndpoint()
213 assert.Equal(t, 302, code)
214 assert.Equal(t, 302, code)
214 assert.NotEqual(t, nil, cookie)
215 assert.NotEqual(t, nil, cookie)
215
216
216 // Now we make a regular request; the access_token from the cookie is
217 // Now we make a regular request; the access_token from the cookie is
217 // forwarded as the "X-Forwarded-Access-Token" header. The token is
218 // forwarded as the "X-Forwarded-Access-Token" header. The token is
218 // read by the test provider server and written in the response body.
219 // read by the test provider server and written in the response body.
219− code, payload := getRootEndpoint(pac_test, cookie)
220+ code, payload := pat_test.getRootEndpoint(cookie)
220 assert.Equal(t, 200, code)
221 assert.Equal(t, 200, code)
221 assert.Equal(t, "my_auth_token", payload)
222 assert.Equal(t, "my_auth_token", payload)
222}
223}
223
224
224func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
225func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
225− pac_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{
226+ pat_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{
226 PassAccessToken: false,
227 PassAccessToken: false,
227 })
228 })
228− defer Close(pac_test)
229+ defer pat_test.Close()
229
230
230 // A successful validation will redirect and set the auth cookie.
231 // A successful validation will redirect and set the auth cookie.
231− code, cookie := getCallbackEndpoint(pac_test)
232+ code, cookie := pat_test.getCallbackEndpoint()
232 assert.Equal(t, 302, code)
233 assert.Equal(t, 302, code)
233 assert.NotEqual(t, nil, cookie)
234 assert.NotEqual(t, nil, cookie)
234
235
235 // Now we make a regular request, but the access token header should
236 // Now we make a regular request, but the access token header should
236 // not be present.
237 // not be present.
237− code, payload := getRootEndpoint(pac_test, cookie)
238+ code, payload := pat_test.getRootEndpoint(cookie)
238 assert.Equal(t, 200, code)
239 assert.Equal(t, 200, code)
239 assert.Equal(t, "No access token found.", payload)
240 assert.Equal(t, "No access token found.", payload)
240}
241}
options.go+17
@@ -117,6 +117,23 @@ func (o *Options) Validate() error {
117 }
117 }
118 msgs = parseProviderInfo(o, msgs)
118 msgs = parseProviderInfo(o, msgs)
119
119
120+ if o.PassAccessToken {
121+ valid_cookie_secret_size := false
122+ for _, i := range []int{16, 24, 32} {
123+ if len(o.CookieSecret) == i {
124+ valid_cookie_secret_size = true
125+ }
126+ }
127+ if valid_cookie_secret_size == false {
128+ msgs = append(msgs, fmt.Sprintf(
129+ "cookie_secret must be 16, 24, or 32 bytes "+
130+ "to create an AES cipher when "+
131+ "pass_access_token == true, "+
132+ "but is %d bytes",
133+ len(o.CookieSecret)))
134+ }
135+ }
136+
120 if len(msgs) != 0 {
137 if len(msgs) != 0 {
121 return fmt.Errorf("Invalid configuration:\n %s",
138 return fmt.Errorf("Invalid configuration:\n %s",
122 strings.Join(msgs, "\n "))
139 strings.Join(msgs, "\n "))
options_test.go+19
@@ -102,3 +102,22 @@ func TestDefaultProviderApiSettings(t *testing.T) {
102 assert.Equal(t, "", p.ProfileUrl.String())
102 assert.Equal(t, "", p.ProfileUrl.String())
103 assert.Equal(t, "profile email", p.Scope)
103 assert.Equal(t, "profile email", p.Scope)
104}
104}
105+
106+func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {
107+ o := testOptions()
108+ assert.Equal(t, nil, o.Validate())
109+
110+ assert.Equal(t, false, o.PassAccessToken)
111+ o.PassAccessToken = true
112+ o.CookieSecret = "cookie of invalid length-"
113+ assert.NotEqual(t, nil, o.Validate())
114+
115+ o.CookieSecret = "16 bytes AES-128"
116+ assert.Equal(t, nil, o.Validate())
117+
118+ o.CookieSecret = "24 byte secret AES-192--"
119+ assert.Equal(t, nil, o.Validate())
120+
121+ o.CookieSecret = "32 byte secret for AES-256------"
122+ assert.Equal(t, nil, o.Validate())
123+}