git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #117 from jehiah/always_refresh_117

Google - continually use refresh token

commit01c9d04feb043fbfabaf7b0d659eb139a2b7abd6 authorJehiah Czebotar <jehiah@gmail.com> dateThu 2 Jul 2015, 23:14 (11 years ago) parentb9ae5dcd49c3e1
21 files changed+866−580
api/api.go+3 cookie/cookies.go+128 cookie/cookies_test.go+23 cookies.go−140 cookies_test.go−75 oauthproxy.go+201−136 oauthproxy_test.go+47−102 providers/github.go+11−10 providers/google.go+47−21 providers/google_test.go+73−37 providers/internal_util.go+4 providers/internal_util_test.go+26−24 providers/linkedin.go+6−8 providers/linkedin_test.go+6−4 providers/myusa.go+2−6 providers/myusa_test.go+8−4 providers/provider_default.go+51−10 providers/provider_default_test.go+17 providers/providers.go+10−3 providers/session_state.go+115 providers/session_state_test.go+88
api/api.go+3
@@ -3,6 +3,7 @@ package api
33import (
44 "fmt"
55 "io/ioutil"
6+ "log"
67 "net/http"
78
89 "github.com/bitly/go-simplejson"
@@ -11,10 +12,12 @@ import (
1112func Request(req *http.Request) (*simplejson.Json, error) {
1213 resp, err := http.DefaultClient.Do(req)
1314 if err != nil {
15+ log.Printf("%s %s %s", req.Method, req.URL, err)
1416 return nil, err
1517 }
1618 body, err := ioutil.ReadAll(resp.Body)
1719 resp.Body.Close()
20+ log.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body)
1821 if err != nil {
1922 return nil, err
2023 }
cookie/cookies.gonew file+128
@@ -0,0 +1,128 @@
1+package cookie
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/hmac"
7+ "crypto/rand"
8+ "crypto/sha1"
9+ "encoding/base64"
10+ "fmt"
11+ "io"
12+ "net/http"
13+ "strconv"
14+ "strings"
15+ "time"
16+)
17+
18+// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
19+// additionally, the 'value' is encrypted so it's opaque to the browser
20+
21+// Validate ensures a cookie is properly signed
22+func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
23+ // value, timestamp, sig
24+ parts := strings.Split(cookie.Value, "|")
25+ if len(parts) != 3 {
26+ return
27+ }
28+ sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
29+ if checkHmac(parts[2], sig) {
30+ ts, err := strconv.Atoi(parts[1])
31+ if err != nil {
32+ return
33+ }
34+ // The expiration timestamp set when the cookie was created
35+ // isn't sent back by the browser. Hence, we check whether the
36+ // creation timestamp stored in the cookie falls within the
37+ // window defined by (Now()-expiration, Now()].
38+ t = time.Unix(int64(ts), 0)
39+ if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
40+ // it's a valid cookie. now get the contents
41+ rawValue, err := base64.URLEncoding.DecodeString(parts[0])
42+ if err == nil {
43+ value = string(rawValue)
44+ ok = true
45+ return
46+ }
47+ }
48+ }
49+ return
50+}
51+
52+// SignedValue returns a cookie that is signed and can later be checked with Validate
53+func SignedValue(seed string, key string, value string, now time.Time) string {
54+ encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
55+ timeStr := fmt.Sprintf("%d", now.Unix())
56+ sig := cookieSignature(seed, key, encodedValue, timeStr)
57+ cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
58+ return cookieVal
59+}
60+
61+func cookieSignature(args ...string) string {
62+ h := hmac.New(sha1.New, []byte(args[0]))
63+ for _, arg := range args[1:] {
64+ h.Write([]byte(arg))
65+ }
66+ var b []byte
67+ b = h.Sum(b)
68+ return base64.URLEncoding.EncodeToString(b)
69+}
70+
71+func checkHmac(input, expected string) bool {
72+ inputMAC, err1 := base64.URLEncoding.DecodeString(input)
73+ if err1 == nil {
74+ expectedMAC, err2 := base64.URLEncoding.DecodeString(expected)
75+ if err2 == nil {
76+ return hmac.Equal(inputMAC, expectedMAC)
77+ }
78+ }
79+ return false
80+}
81+
82+// Cipher provides methods to encrypt and decrypt cookie values
83+type Cipher struct {
84+ cipher.Block
85+}
86+
87+// NewCipher returns a new aes Cipher for encrypting cookie values
88+func NewCipher(secret string) (*Cipher, error) {
89+ c, err := aes.NewCipher([]byte(secret))
90+ if err != nil {
91+ return nil, err
92+ }
93+ return &Cipher{Block: c}, err
94+}
95+
96+// Encrypt a value for use in a cookie
97+func (c *Cipher) Encrypt(value string) (string, error) {
98+ ciphertext := make([]byte, aes.BlockSize+len(value))
99+ iv := ciphertext[:aes.BlockSize]
100+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
101+ return "", fmt.Errorf("failed to create initialization vector %s", err)
102+ }
103+
104+ stream := cipher.NewCFBEncrypter(c.Block, iv)
105+ stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(value))
106+ return base64.StdEncoding.EncodeToString(ciphertext), nil
107+}
108+
109+// Decrypt a value from a cookie to it's original string
110+func (c *Cipher) Decrypt(s string) (string, error) {
111+ encrypted, err := base64.StdEncoding.DecodeString(s)
112+ if err != nil {
113+ return "", fmt.Errorf("failed to decrypt cookie value %s", err)
114+ }
115+
116+ if len(encrypted) < aes.BlockSize {
117+ return "", fmt.Errorf("encrypted cookie value should be "+
118+ "at least %d bytes, but is only %d bytes",
119+ aes.BlockSize, len(encrypted))
120+ }
121+
122+ iv := encrypted[:aes.BlockSize]
123+ encrypted = encrypted[aes.BlockSize:]
124+ stream := cipher.NewCFBDecrypter(c.Block, iv)
125+ stream.XORKeyStream(encrypted, encrypted)
126+
127+ return string(encrypted), nil
128+}
cookie/cookies_test.gonew file+23
@@ -0,0 +1,23 @@
1+package cookie
2+
3+import (
4+ "testing"
5+
6+ "github.com/bmizerany/assert"
7+)
8+
9+func TestEncodeAndDecodeAccessToken(t *testing.T) {
10+ const secret = "0123456789abcdefghijklmnopqrstuv"
11+ const token = "my access token"
12+ c, err := NewCipher(secret)
13+ assert.Equal(t, nil, err)
14+
15+ encoded, err := c.Encrypt(token)
16+ assert.Equal(t, nil, err)
17+
18+ decoded, err := c.Decrypt(encoded)
19+ assert.Equal(t, nil, err)
20+
21+ assert.NotEqual(t, token, encoded)
22+ assert.Equal(t, token, decoded)
23+}
cookies.godeleted−140
@@ -1,140 +0,0 @@
1−package main
2−
3−import (
4− "crypto/aes"
5− "crypto/cipher"
6− "crypto/hmac"
7− "crypto/rand"
8− "crypto/sha1"
9− "encoding/base64"
10− "fmt"
11− "io"
12− "net/http"
13− "strconv"
14− "strings"
15− "time"
16−)
17−
18−func validateCookie(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
19− // value, timestamp, sig
20− parts := strings.Split(cookie.Value, "|")
21− if len(parts) != 3 {
22− return
23− }
24− sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
25− if checkHmac(parts[2], sig) {
26− ts, err := strconv.Atoi(parts[1])
27− if err != nil {
28− return
29− }
30− // The expiration timestamp set when the cookie was created
31− // isn't sent back by the browser. Hence, we check whether the
32− // creation timestamp stored in the cookie falls within the
33− // window defined by (Now()-expiration, Now()].
34− t = time.Unix(int64(ts), 0)
35− if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
36− // it's a valid cookie. now get the contents
37− rawValue, err := base64.URLEncoding.DecodeString(parts[0])
38− if err == nil {
39− value = string(rawValue)
40− ok = true
41− return
42− }
43− }
44− }
45− return
46−}
47−
48−func signedCookieValue(seed string, key string, value string, now time.Time) string {
49− encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
50− timeStr := fmt.Sprintf("%d", now.Unix())
51− sig := cookieSignature(seed, key, encodedValue, timeStr)
52− cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
53− return cookieVal
54−}
55−
56−func cookieSignature(args ...string) string {
57− h := hmac.New(sha1.New, []byte(args[0]))
58− for _, arg := range args[1:] {
59− h.Write([]byte(arg))
60− }
61− var b []byte
62− b = h.Sum(b)
63− return base64.URLEncoding.EncodeToString(b)
64−}
65−
66−func checkHmac(input, expected string) bool {
67− inputMAC, err1 := base64.URLEncoding.DecodeString(input)
68− if err1 == nil {
69− expectedMAC, err2 := base64.URLEncoding.DecodeString(expected)
70− if err2 == nil {
71− return hmac.Equal(inputMAC, expectedMAC)
72− }
73− }
74− return false
75−}
76−
77−func encodeAccessToken(aes_cipher cipher.Block, access_token string) (string, error) {
78− ciphertext := make([]byte, aes.BlockSize+len(access_token))
79− iv := ciphertext[:aes.BlockSize]
80− if _, err := io.ReadFull(rand.Reader, iv); err != nil {
81− return "", fmt.Errorf("failed to create access code initialization vector")
82− }
83−
84− stream := cipher.NewCFBEncrypter(aes_cipher, iv)
85− stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(access_token))
86− return base64.StdEncoding.EncodeToString(ciphertext), nil
87−}
88−
89−func decodeAccessToken(aes_cipher cipher.Block, encoded_access_token string) (string, error) {
90− encrypted_access_token, err := base64.StdEncoding.DecodeString(
91− encoded_access_token)
92−
93− if err != nil {
94− return "", fmt.Errorf("failed to decode access token")
95− }
96−
97− if len(encrypted_access_token) < aes.BlockSize {
98− return "", fmt.Errorf("encrypted access token should be "+
99− "at least %d bytes, but is only %d bytes",
100− aes.BlockSize, len(encrypted_access_token))
101− }
102−
103− iv := encrypted_access_token[:aes.BlockSize]
104− encrypted_access_token = encrypted_access_token[aes.BlockSize:]
105− stream := cipher.NewCFBDecrypter(aes_cipher, iv)
106− stream.XORKeyStream(encrypted_access_token, encrypted_access_token)
107−
108− return string(encrypted_access_token), nil
109−}
110−
111−func buildCookieValue(email string, aes_cipher cipher.Block,
112− access_token string) (string, error) {
113− if aes_cipher == nil {
114− return email, nil
115− }
116−
117− encoded_token, err := encodeAccessToken(aes_cipher, access_token)
118− if err != nil {
119− return email, fmt.Errorf(
120− "error encoding access token for %s: %s", email, err)
121− }
122− return email + "|" + encoded_token, nil
123−}
124−
125−func parseCookieValue(value string, aes_cipher cipher.Block) (email, user,
126− access_token string, err error) {
127− components := strings.Split(value, "|")
128− email = components[0]
129− user = strings.Split(email, "@")[0]
130−
131− if aes_cipher != nil && len(components) == 2 {
132− access_token, err = decodeAccessToken(aes_cipher, components[1])
133− if err != nil {
134− err = fmt.Errorf(
135− "error decoding access token for %s: %s",
136− email, err)
137− }
138− }
139− return email, user, access_token, err
140−}
cookies_test.godeleted−75
@@ -1,75 +0,0 @@
1−package main
2−
3−import (
4− "crypto/aes"
5− "github.com/bmizerany/assert"
6− "strings"
7− "testing"
8−)
9−
10−func TestEncodeAndDecodeAccessToken(t *testing.T) {
11− const key = "0123456789abcdefghijklmnopqrstuv"
12− const access_token = "my access token"
13− c, err := aes.NewCipher([]byte(key))
14− assert.Equal(t, nil, err)
15−
16− encoded_token, err := encodeAccessToken(c, access_token)
17− assert.Equal(t, nil, err)
18−
19− decoded_token, err := decodeAccessToken(c, encoded_token)
20− assert.Equal(t, nil, err)
21−
22− assert.NotEqual(t, access_token, encoded_token)
23− assert.Equal(t, access_token, decoded_token)
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+201−136
@@ -1,8 +1,6 @@
11package main
22
33import (
4− "crypto/aes"
5− "crypto/cipher"
64 "encoding/base64"
75 "errors"
86 "fmt"
@@ -16,6 +14,7 @@ import (
1614 "strings"
1715 "time"
1816
17+ "github.com/bitly/oauth2_proxy/cookie"
1918 "github.com/bitly/oauth2_proxy/providers"
2019)
2120
@@ -44,7 +43,7 @@ type OauthProxy struct {
4443 serveMux http.Handler
4544 PassBasicAuth bool
4645 PassAccessToken bool
47− AesCipher cipher.Block
46+ CookieCipher *cookie.Cipher
4847 skipAuthRegex []string
4948 compiledRegex []*regexp.Regexp
5049 templates *template.Template
@@ -116,10 +115,10 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
116115
117116 log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain, refresh)
118117
119− var aes_cipher cipher.Block
118+ var cipher *cookie.Cipher
120119 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
121120 var err error
122− aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret))
121+ cipher, err = cookie.NewCipher(opts.CookieSecret)
123122 if err != nil {
124123 log.Fatal("error creating AES cipher with "+
125124 "cookie-secret ", opts.CookieSecret, ": ", err)
@@ -150,7 +149,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
150149 compiledRegex: opts.CompiledRegex,
151150 PassBasicAuth: opts.PassBasicAuth,
152151 PassAccessToken: opts.PassAccessToken,
153− AesCipher: aes_cipher,
152+ CookieCipher: cipher,
154153 templates: loadTemplates(opts.CustomTemplatesDir),
155154 }
156155}
@@ -177,22 +176,20 @@ func (p *OauthProxy) displayCustomLoginForm() bool {
177176 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
178177}
179178
180−func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
179+func (p *OauthProxy) redeemCode(host, code string) (s *providers.SessionState, err error) {
181180 if code == "" {
182− return "", "", errors.New("missing code")
181+ return nil, errors.New("missing code")
183182 }
184183 redirectUri := p.GetRedirectURI(host)
185− body, access_token, err := p.provider.Redeem(redirectUri, code)
184+ s, err = p.provider.Redeem(redirectUri, code)
186185 if err != nil {
187− return "", "", err
186+ return
188187 }
189188
190− email, err := p.provider.GetEmailAddress(body, access_token)
191− if err != nil {
192− return "", "", err
189+ if s.Email == "" {
190+ s.Email, err = p.provider.GetEmailAddress(s)
193191 }
194−
195− return access_token, email, nil
192+ return
196193}
197194
198195func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
@@ -208,9 +205,8 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
208205 }
209206
210207 if value != "" {
211− value = signedCookieValue(p.CookieSeed, p.CookieName, value, now)
208+ value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
212209 }
213−
214210 return &http.Cookie{
215211 Name: p.CookieName,
216212 Value: value,
@@ -230,35 +226,34 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st
230226 http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire, time.Now()))
231227}
232228
233−func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) {
234− var value string
235− var timestamp time.Time
236− cookie, err := req.Cookie(p.CookieName)
237− if err == nil {
238− value, timestamp, ok = validateCookie(cookie, p.CookieSeed, p.CookieExpire)
239− if ok {
240− email, user, access_token, err = parseCookieValue(value, p.AesCipher)
241− }
229+func (p *OauthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
230+ var age time.Duration
231+ c, err := req.Cookie(p.CookieName)
232+ if err != nil {
233+ // always http.ErrNoCookie
234+ return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
235+ }
236+ val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
237+ if !ok {
238+ return nil, age, errors.New("Cookie Signature not valid")
242239 }
240+
241+ session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
243242 if err != nil {
244− log.Printf(err.Error())
245− ok = false
246− } else if ok && p.CookieRefresh != time.Duration(0) {
247− refresh := timestamp.Add(p.CookieRefresh)
248− if refresh.Before(time.Now()) {
249− log.Printf("refreshing %s old session for %s (refresh after %s)", time.Now().Sub(timestamp), email, p.CookieRefresh)
250− ok = p.Validator(email)
251− log.Printf("re-validating %s valid:%v", email, ok)
252− if ok {
253− ok = p.provider.ValidateToken(access_token)
254− log.Printf("re-validating access token. valid:%v", ok)
255− }
256− if ok {
257− p.SetCookie(rw, req, value)
258− }
259− }
243+ return nil, age, err
260244 }
261− return
245+
246+ age = time.Now().Truncate(time.Second).Sub(timestamp)
247+ return session, age, nil
248+}
249+
250+func (p *OauthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error {
251+ value, err := p.provider.CookieForSession(s, p.CookieCipher)
252+ if err != nil {
253+ return err
254+ }
255+ p.SetCookie(rw, req, value)
256+ return nil
262257}
263258
264259func (p *OauthProxy) RobotsTxt(rw http.ResponseWriter) {
@@ -344,156 +339,226 @@ func (p *OauthProxy) GetRedirect(req *http.Request) (string, error) {
344339 return redirect, err
345340}
346341
347−func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
348− // check if this is a redirect back at the end of oauth
349− remoteAddr := req.RemoteAddr
350− if req.Header.Get("X-Real-IP") != "" {
351− remoteAddr += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
342+func (p *OauthProxy) IsWhitelistedPath(path string) (ok bool) {
343+ for _, u := range p.compiledRegex {
344+ ok = u.MatchString(path)
345+ if ok {
346+ return
347+ }
352348 }
349+ return
350+}
353351
354− var ok bool
355− var user string
356− var email string
357− var access_token string
352+func getRemoteAddr(req *http.Request) (s string) {
353+ s = req.RemoteAddr
354+ if req.Header.Get("X-Real-IP") != "" {
355+ s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
356+ }
357+ return
358+}
358359
359− if req.URL.Path == p.RobotsPath {
360+func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
361+ switch path := req.URL.Path; {
362+ case path == p.RobotsPath:
360363 p.RobotsTxt(rw)
361− return
364+ case path == p.PingPath:
365+ p.PingPage(rw)
366+ case p.IsWhitelistedPath(path):
367+ p.serveMux.ServeHTTP(rw, req)
368+ case path == p.SignInPath:
369+ p.SignIn(rw, req)
370+ case path == p.OauthStartPath:
371+ p.OauthStart(rw, req)
372+ case path == p.OauthCallbackPath:
373+ p.OauthCallback(rw, req)
374+ default:
375+ p.Proxy(rw, req)
362376 }
377+}
363378
364− if req.URL.Path == p.PingPath {
365− p.PingPage(rw)
379+func (p *OauthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
380+ redirect, err := p.GetRedirect(req)
381+ if err != nil {
382+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
366383 return
367384 }
368385
369− for _, u := range p.compiledRegex {
370− match := u.MatchString(req.URL.Path)
371− if match {
372− p.serveMux.ServeHTTP(rw, req)
373− return
374− }
386+ user, ok := p.ManualSignIn(rw, req)
387+ if ok {
388+ session := &providers.SessionState{User: user}
389+ p.SaveSession(rw, req, session)
390+ http.Redirect(rw, req, redirect, 302)
391+ } else {
392+ p.SignInPage(rw, req, 200)
393+ }
394+}
375395
396+func (p *OauthProxy) OauthStart(rw http.ResponseWriter, req *http.Request) {
397+ redirect, err := p.GetRedirect(req)
398+ if err != nil {
399+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
400+ return
376401 }
402+ redirectURI := p.GetRedirectURI(req.Host)
403+ http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, redirect), 302)
404+}
377405
378− if req.URL.Path == p.SignInPath {
379− redirect, err := p.GetRedirect(req)
380− if err != nil {
381− p.ErrorPage(rw, 500, "Internal Error", err.Error())
382− return
383− }
406+func (p *OauthProxy) OauthCallback(rw http.ResponseWriter, req *http.Request) {
407+ remoteAddr := getRemoteAddr(req)
384408
385− user, ok = p.ManualSignIn(rw, req)
386− if ok {
387− p.SetCookie(rw, req, user)
388− http.Redirect(rw, req, redirect, 302)
389− } else {
390− p.SignInPage(rw, req, 200)
391− }
409+ // finish the oauth cycle
410+ err := req.ParseForm()
411+ if err != nil {
412+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
392413 return
393414 }
394− if req.URL.Path == p.OauthStartPath {
395− redirect, err := p.GetRedirect(req)
396− if err != nil {
397− p.ErrorPage(rw, 500, "Internal Error", err.Error())
398− return
399− }
400− redirectURI := p.GetRedirectURI(req.Host)
401− http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, redirect), 302)
415+ errorString := req.Form.Get("error")
416+ if errorString != "" {
417+ p.ErrorPage(rw, 403, "Permission Denied", errorString)
402418 return
403419 }
404− if req.URL.Path == p.OauthCallbackPath {
405− // finish the oauth cycle
406− err := req.ParseForm()
407− if err != nil {
408− p.ErrorPage(rw, 500, "Internal Error", err.Error())
409− return
410− }
411− errorString := req.Form.Get("error")
412− if errorString != "" {
413− p.ErrorPage(rw, 403, "Permission Denied", errorString)
414− return
415− }
416420
417− access_token, email, err = p.redeemCode(req.Host, req.Form.Get("code"))
421+ session, err := p.redeemCode(req.Host, req.Form.Get("code"))
422+ if err != nil {
423+ log.Printf("%s error redeeming code %s", remoteAddr, err)
424+ p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
425+ return
426+ }
427+
428+ redirect := req.Form.Get("state")
429+ if redirect == "" {
430+ redirect = "/"
431+ }
432+
433+ // set cookie, or deny
434+ if p.Validator(session.Email) {
435+ log.Printf("%s authentication complete %s", remoteAddr, session)
436+ err := p.SaveSession(rw, req, session)
418437 if err != nil {
419− log.Printf("%s error redeeming code %s", remoteAddr, err)
420− p.ErrorPage(rw, 500, "Internal Error", err.Error())
438+ log.Printf("%s %s", remoteAddr, err)
439+ p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
421440 return
422441 }
442+ http.Redirect(rw, req, redirect, 302)
443+ } else {
444+ log.Printf("%s Permission Denied: %q is unauthorized", remoteAddr, session.Email)
445+ p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
446+ }
447+}
423448
424− redirect := req.Form.Get("state")
425− if redirect == "" {
426− redirect = "/"
449+func (p *OauthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
450+ var saveSession, clearSession, revalidated bool
451+ remoteAddr := getRemoteAddr(req)
452+
453+ session, sessionAge, err := p.LoadCookiedSession(req)
454+ if err != nil {
455+ log.Printf("%s %s", remoteAddr, err)
456+ }
457+ if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
458+ log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
459+ saveSession = true
460+ }
461+
462+ if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
463+ log.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
464+ clearSession = true
465+ session = nil
466+ } else if ok {
467+ saveSession = true
468+ revalidated = true
469+ }
470+
471+ if session != nil && session.IsExpired() {
472+ log.Printf("%s removing session. token expired %s", remoteAddr, session)
473+ session = nil
474+ saveSession = false
475+ clearSession = true
476+ }
477+
478+ if saveSession && !revalidated && session.AccessToken != "" {
479+ if !p.provider.ValidateSessionState(session) {
480+ log.Printf("%s removing session. error validating %s", remoteAddr, session)
481+ saveSession = false
482+ session = nil
483+ clearSession = true
427484 }
485+ }
428486
429− // set cookie, or deny
430− if p.Validator(email) {
431− log.Printf("%s authenticating %s completed", remoteAddr, email)
432− value, err := buildCookieValue(
433− email, p.AesCipher, access_token)
434− if err != nil {
435− log.Printf("%s", err)
436− }
437− p.SetCookie(rw, req, value)
438− http.Redirect(rw, req, redirect, 302)
439− return
440− } else {
441− log.Printf("validating: %s is unauthorized")
442− p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
487+ if saveSession && session.Email != "" && !p.Validator(session.Email) {
488+ log.Printf("%s Permission Denied: removing session %s", remoteAddr, session)
489+ session = nil
490+ saveSession = false
491+ clearSession = true
492+ }
493+
494+ if saveSession {
495+ err := p.SaveSession(rw, req, session)
496+ if err != nil {
497+ log.Printf("%s %s", remoteAddr, err)
498+ p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
443499 return
444500 }
445501 }
446502
447− if !ok {
448− email, user, access_token, ok = p.ProcessCookie(rw, req)
503+ if clearSession {
504+ p.ClearCookie(rw, req)
449505 }
450506
451− if !ok {
452− user, ok = p.CheckBasicAuth(req)
507+ if session == nil {
508+ session, err = p.CheckBasicAuth(req)
509+ if err != nil {
510+ log.Printf("%s %s", remoteAddr, err)
511+ }
453512 }
454513
455− if !ok {
514+ if session == nil {
456515 p.SignInPage(rw, req, 403)
457516 return
458517 }
459518
460519 // At this point, the user is authenticated. proxy normally
461520 if p.PassBasicAuth {
462− req.SetBasicAuth(user, "")
463− req.Header["X-Forwarded-User"] = []string{user}
464− req.Header["X-Forwarded-Email"] = []string{email}
521+ req.SetBasicAuth(session.User, "")
522+ req.Header["X-Forwarded-User"] = []string{session.User}
523+ if session.Email != "" {
524+ req.Header["X-Forwarded-Email"] = []string{session.Email}
525+ }
465526 }
466− if p.PassAccessToken {
467− req.Header["X-Forwarded-Access-Token"] = []string{access_token}
527+ if p.PassAccessToken && session.AccessToken != "" {
528+ req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
468529 }
469− if email == "" {
470− rw.Header().Set("GAP-Auth", user)
530+ if session.Email == "" {
531+ rw.Header().Set("GAP-Auth", session.User)
471532 } else {
472− rw.Header().Set("GAP-Auth", email)
533+ rw.Header().Set("GAP-Auth", session.Email)
473534 }
474535
475536 p.serveMux.ServeHTTP(rw, req)
476537}
477538
478−func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) {
539+func (p *OauthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {
479540 if p.HtpasswdFile == nil {
480− return "", false
541+ return nil, nil
542+ }
543+ auth := req.Header.Get("Authorization")
544+ if auth == "" {
545+ return nil, nil
481546 }
482− s := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
547+ s := strings.SplitN(auth, " ", 2)
483548 if len(s) != 2 || s[0] != "Basic" {
484− return "", false
549+ return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
485550 }
486551 b, err := base64.StdEncoding.DecodeString(s[1])
487552 if err != nil {
488− return "", false
553+ return nil, err
489554 }
490555 pair := strings.SplitN(string(b), ":", 2)
491556 if len(pair) != 2 {
492− return "", false
557+ return nil, fmt.Errorf("invalid format %s", b)
493558 }
494559 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
495560 log.Printf("authenticated %q via basic auth", pair[0])
496− return pair[0], true
561+ return &providers.SessionState{User: pair[0]}, nil
497562 }
498− return "", false
563+ return nil, fmt.Errorf("%s not in HtpasswdFile", pair[0])
499564}
oauthproxy_test.go+47−102
@@ -94,11 +94,11 @@ type TestProvider struct {
9494 ValidToken bool
9595}
9696
97−func (tp *TestProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
97+func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
9898 return tp.EmailAddress, nil
9999}
100100
101−func (tp *TestProvider) ValidateToken(access_token string) bool {
101+func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
102102 return tp.ValidToken
103103}
104104
@@ -378,97 +378,73 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
378378 })
379379}
380380
381−func (p *ProcessCookieTest) MakeCookie(value, access_token string, ref time.Time) *http.Cookie {
382− cookie_value, _ := buildCookieValue(value, p.proxy.AesCipher, access_token)
383− return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire, ref)
381+func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) *http.Cookie {
382+ return p.proxy.MakeCookie(p.req, value, p.opts.CookieExpire, ref)
384383}
385384
386−func (p *ProcessCookieTest) AddCookie(value, access_token string) {
387− p.req.AddCookie(p.MakeCookie(value, access_token, time.Now()))
385+func (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time) error {
386+ value, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher)
387+ if err != nil {
388+ return err
389+ }
390+ p.req.AddCookie(p.proxy.MakeCookie(p.req, value, p.proxy.CookieExpire, ref))
391+ return nil
388392}
389393
390−func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
391− return p.proxy.ProcessCookie(p.rw, p.req)
394+func (p *ProcessCookieTest) LoadCookiedSession() (*providers.SessionState, time.Duration, error) {
395+ return p.proxy.LoadCookiedSession(p.req)
392396}
393397
394−func TestProcessCookie(t *testing.T) {
398+func TestLoadCookiedSession(t *testing.T) {
395399 pc_test := NewProcessCookieTestWithDefaults()
396400
397− pc_test.AddCookie("michael.bland@gsa.gov", "my_access_token")
398− email, user, access_token, ok := pc_test.ProcessCookie()
399− assert.Equal(t, true, ok)
400− assert.Equal(t, "michael.bland@gsa.gov", email)
401− assert.Equal(t, "michael.bland", user)
402− assert.Equal(t, "my_access_token", access_token)
401+ startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
402+ pc_test.SaveSession(startSession, time.Now())
403+
404+ session, _, err := pc_test.LoadCookiedSession()
405+ assert.Equal(t, nil, err)
406+ assert.Equal(t, startSession.Email, session.Email)
407+ assert.Equal(t, "michael.bland", session.User)
408+ assert.Equal(t, startSession.AccessToken, session.AccessToken)
403409}
404410
405411func TestProcessCookieNoCookieError(t *testing.T) {
406412 pc_test := NewProcessCookieTestWithDefaults()
407− _, _, _, ok := pc_test.ProcessCookie()
408− assert.Equal(t, false, ok)
409−}
410413
411−func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
412− pc_test := NewProcessCookieTestWithDefaults()
413− value, _ := buildCookieValue("michael.bland@gsa.gov",
414− pc_test.proxy.AesCipher, "my_access_token")
415− pc_test.req.AddCookie(pc_test.proxy.MakeCookie(
416− pc_test.req, value+"some bogus bytes",
417− pc_test.opts.CookieExpire, time.Now()))
418− _, _, _, ok := pc_test.ProcessCookie()
419− assert.Equal(t, false, ok)
414+ session, _, err := pc_test.LoadCookiedSession()
415+ assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error())
416+ if session != nil {
417+ t.Errorf("expected nil session. got %#v", session)
418+ }
420419}
421420
422421func TestProcessCookieRefreshNotSet(t *testing.T) {
423422 pc_test := NewProcessCookieTestWithDefaults()
424423 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
425424 reference := time.Now().Add(time.Duration(-2) * time.Hour)
426− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "", reference)
427− pc_test.req.AddCookie(cookie)
428−
429− _, _, _, ok := pc_test.ProcessCookie()
430− assert.Equal(t, true, ok)
431− assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
432−}
433−
434−func TestProcessCookieRefresh(t *testing.T) {
435− pc_test := NewProcessCookieTestWithDefaults()
436− pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
437− reference := time.Now().Add(time.Duration(-2) * time.Hour)
438− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
439− pc_test.req.AddCookie(cookie)
440425
441− pc_test.proxy.CookieRefresh = time.Hour
442− _, _, _, ok := pc_test.ProcessCookie()
443− assert.Equal(t, true, ok)
444− assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
445−}
426+ startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
427+ pc_test.SaveSession(startSession, reference)
446428
447−func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
448− pc_test := NewProcessCookieTestWithDefaults()
449− pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
450− reference := time.Now().Add(time.Duration(-30) * time.Minute)
451− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
452− pc_test.req.AddCookie(cookie)
453−
454− pc_test.proxy.CookieRefresh = time.Hour
455− _, _, _, ok := pc_test.ProcessCookie()
456− assert.Equal(t, true, ok)
457− assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
429+ session, age, err := pc_test.LoadCookiedSession()
430+ assert.Equal(t, nil, err)
431+ if age < time.Duration(-2)*time.Hour {
432+ t.Errorf("cookie too young %v", age)
433+ }
434+ assert.Equal(t, startSession.Email, session.Email)
458435}
459436
460437func TestProcessCookieFailIfCookieExpired(t *testing.T) {
461438 pc_test := NewProcessCookieTestWithDefaults()
462439 pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
463440 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
464− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
465− pc_test.req.AddCookie(cookie)
441+ startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
442+ pc_test.SaveSession(startSession, reference)
466443
467− if _, _, _, ok := pc_test.ProcessCookie(); ok {
468− t.Error("ProcessCookie() should have failed")
469− }
470− if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
471− t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
444+ session, _, err := pc_test.LoadCookiedSession()
445+ assert.NotEqual(t, nil, err)
446+ if session != nil {
447+ t.Errorf("expected nil session %#v", session)
472448 }
473449}
474450
@@ -476,44 +452,13 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
476452 pc_test := NewProcessCookieTestWithDefaults()
477453 pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
478454 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
479− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
480− pc_test.req.AddCookie(cookie)
455+ startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
456+ pc_test.SaveSession(startSession, reference)
481457
482458 pc_test.proxy.CookieRefresh = time.Hour
483− if _, _, _, ok := pc_test.ProcessCookie(); ok {
484− t.Error("ProcessCookie() should have failed")
485− }
486− if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
487− t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
459+ session, _, err := pc_test.LoadCookiedSession()
460+ assert.NotEqual(t, nil, err)
461+ if session != nil {
462+ t.Errorf("expected nil session %#v", session)
488463 }
489464}
490−
491−func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
492− pc_test := NewProcessCookieTest(ProcessCookieTestOpts{
493− provider_validate_cookie_response: false,
494− })
495− pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
496− reference := time.Now().Add(time.Duration(-24) * time.Hour)
497− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
498− pc_test.req.AddCookie(cookie)
499−
500− pc_test.proxy.CookieRefresh = time.Hour
501− _, _, _, ok := pc_test.ProcessCookie()
502− assert.Equal(t, false, ok)
503− assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
504−}
505−
506−func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) {
507− pc_test := NewProcessCookieTestWithDefaults()
508− pc_test.validate_user = false
509−
510− pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
511− reference := time.Now().Add(time.Duration(-2) * time.Hour)
512− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
513− pc_test.req.AddCookie(cookie)
514−
515− pc_test.proxy.CookieRefresh = time.Hour
516− _, _, _, ok := pc_test.ProcessCookie()
517− assert.Equal(t, false, ok)
518− assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
519−}
providers/github.go+11−10
@@ -2,8 +2,10 @@ package providers
22
33import (
44 "encoding/json"
5+ "errors"
56 "fmt"
67 "io/ioutil"
8+ "log"
79 "net/http"
810 "net/url"
911)
@@ -138,7 +140,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
138140 return false, nil
139141}
140142
141−func (p *GitHubProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
143+func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
142144
143145 var emails []struct {
144146 Email string `json:"email"`
@@ -148,31 +150,34 @@ func (p *GitHubProvider) GetEmailAddress(body []byte, access_token string) (stri
148150 // if we require an Org or Team, check that first
149151 if p.Org != "" {
150152 if p.Team != "" {
151− if ok, err := p.hasOrgAndTeam(access_token); err != nil || !ok {
153+ if ok, err := p.hasOrgAndTeam(s.AccessToken); err != nil || !ok {
152154 return "", err
153155 }
154156 } else {
155− if ok, err := p.hasOrg(access_token); err != nil || !ok {
157+ if ok, err := p.hasOrg(s.AccessToken); err != nil || !ok {
156158 return "", err
157159 }
158160 }
159161 }
160162
161163 params := url.Values{
162− "access_token": {access_token},
164+ "access_token": {s.AccessToken},
163165 }
164166 endpoint := "https://api.github.com/user/emails?" + params.Encode()
165167 resp, err := http.DefaultClient.Get(endpoint)
166168 if err != nil {
167169 return "", err
168170 }
169− body, err = ioutil.ReadAll(resp.Body)
171+ body, err := ioutil.ReadAll(resp.Body)
170172 resp.Body.Close()
171173 if err != nil {
172174 return "", err
173175 }
176+
174177 if resp.StatusCode != 200 {
175178 return "", fmt.Errorf("got %d from %q %s", resp.StatusCode, endpoint, body)
179+ } else {
180+ log.Printf("got %d from %q %s", resp.StatusCode, endpoint, body)
176181 }
177182
178183 if err := json.Unmarshal(body, &emails); err != nil {
@@ -185,9 +190,5 @@ func (p *GitHubProvider) GetEmailAddress(body []byte, access_token string) (stri
185190 }
186191 }
187192
188− return "", nil
189−}
190−
191−func (p *GitHubProvider) ValidateToken(access_token string) bool {
192− return validateToken(p, access_token, nil)
193+ return "", errors.New("no email address found")
193194}
providers/google.go+47−21
@@ -7,9 +7,11 @@ import (
77 "errors"
88 "fmt"
99 "io/ioutil"
10+ "log"
1011 "net/http"
1112 "net/url"
1213 "strings"
14+ "time"
1315)
1416
1517type GoogleProvider struct {
@@ -43,25 +45,19 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
4345 return &GoogleProvider{ProviderData: p}
4446}
4547
46−func (s *GoogleProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
47− var response struct {
48− IdToken string `json:"id_token"`
49− }
50−
51− if err := json.Unmarshal(body, &response); err != nil {
52− return "", err
53− }
48+func emailFromIdToken(idToken string) (string, error) {
5449
5550 // id_token is a base64 encode ID token payload
5651 // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
57− jwt := strings.Split(response.IdToken, ".")
52+ jwt := strings.Split(idToken, ".")
5853 b, err := jwtDecodeSegment(jwt[1])
5954 if err != nil {
6055 return "", err
6156 }
6257
6358 var email struct {
64− Email string `json:"email"`
59+ Email string `json:"email"`
60+ EmailVerified bool `json:"email_verified"`
6561 }
6662 err = json.Unmarshal(b, &email)
6763 if err != nil {
@@ -70,6 +66,9 @@ func (s *GoogleProvider) GetEmailAddress(body []byte, access_token string) (stri
7066 if email.Email == "" {
7167 return "", errors.New("missing email")
7268 }
69+ if !email.EmailVerified {
70+ return "", fmt.Errorf("email %s not listed as verified", email.Email)
71+ }
7372 return email.Email, nil
7473}
7574
@@ -81,11 +80,7 @@ func jwtDecodeSegment(seg string) ([]byte, error) {
8180 return base64.URLEncoding.DecodeString(seg)
8281}
8382
84−func (p *GoogleProvider) ValidateToken(access_token string) bool {
85− return validateToken(p, access_token, nil)
86−}
87−
88−func (p *GoogleProvider) Redeem(redirectUrl, code string) (body []byte, token string, err error) {
83+func (p *GoogleProvider) Redeem(redirectUrl, code string) (s *SessionState, err error) {
8984 if code == "" {
9085 err = errors.New("missing code")
9186 return
@@ -108,6 +103,7 @@ func (p *GoogleProvider) Redeem(redirectUrl, code string) (body []byte, token st
108103 if err != nil {
109104 return
110105 }
106+ var body []byte
111107 body, err = ioutil.ReadAll(resp.Body)
112108 resp.Body.Close()
113109 if err != nil {
@@ -122,17 +118,44 @@ func (p *GoogleProvider) Redeem(redirectUrl, code string) (body []byte, token st
122118 var jsonResponse struct {
123119 AccessToken string `json:"access_token"`
124120 RefreshToken string `json:"refresh_token"`
121+ ExpiresIn int64 `json:"expires_in"`
122+ IdToken string `json:"id_token"`
125123 }
126124 err = json.Unmarshal(body, &jsonResponse)
127125 if err != nil {
128126 return
129127 }
130−
131− token, err = p.redeemRefreshToken(jsonResponse.RefreshToken)
128+ var email string
129+ email, err = emailFromIdToken(jsonResponse.IdToken)
130+ if err != nil {
131+ return
132+ }
133+ s = &SessionState{
134+ AccessToken: jsonResponse.AccessToken,
135+ ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
136+ RefreshToken: jsonResponse.RefreshToken,
137+ Email: email,
138+ }
132139 return
133140}
134141
135−func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, err error) {
142+func (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
143+ if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
144+ return false, nil
145+ }
146+
147+ newToken, duration, err := p.redeemRefreshToken(s.RefreshToken)
148+ if err != nil {
149+ return false, err
150+ }
151+ origExpiration := s.ExpiresOn
152+ s.AccessToken = newToken
153+ s.ExpiresOn = time.Now().Add(duration).Truncate(time.Second)
154+ log.Printf("refreshed access token %s (expired on %s)", s, origExpiration)
155+ return true, nil
156+}
157+
158+func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {
136159 // https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
137160 params := url.Values{}
138161 params.Add("client_id", p.ClientID)
@@ -162,12 +185,15 @@ func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string,
162185 return
163186 }
164187
165− var jsonResponse struct {
188+ var data struct {
166189 AccessToken string `json:"access_token"`
190+ ExpiresIn int64 `json:"expires_in"`
167191 }
168− err = json.Unmarshal(body, &jsonResponse)
192+ err = json.Unmarshal(body, &data)
169193 if err != nil {
170194 return
171195 }
172− return jsonResponse.AccessToken, nil
196+ token = data.AccessToken
197+ expires = time.Duration(data.ExpiresIn) * time.Second
198+ return
173199}
providers/google_test.go+73−37
@@ -3,11 +3,22 @@ package providers
33import (
44 "encoding/base64"
55 "encoding/json"
6− "github.com/bmizerany/assert"
6+ "net/http"
7+ "net/http/httptest"
78 "net/url"
89 "testing"
10+
11+ "github.com/bmizerany/assert"
912)
1013
14+func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
15+ s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
16+ rw.Write(body)
17+ }))
18+ u, _ := url.Parse(s.URL)
19+ return u, s
20+}
21+
1122func newGoogleProvider() *GoogleProvider {
1223 return NewGoogleProvider(
1324 &ProviderData{
@@ -66,63 +77,88 @@ func TestGoogleProviderOverrides(t *testing.T) {
6677 assert.Equal(t, "profile", p.Data().Scope)
6778}
6879
80+type redeemResponse struct {
81+ AccessToken string `json:"access_token"`
82+ RefreshToken string `json:"refresh_token"`
83+ ExpiresIn int64 `json:"expires_in"`
84+ IdToken string `json:"id_token"`
85+}
86+
6987func TestGoogleProviderGetEmailAddress(t *testing.T) {
7088 p := newGoogleProvider()
71− body, err := json.Marshal(
72− struct {
73− IdToken string `json:"id_token"`
74− }{
75− IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov"}`)),
76− },
77− )
89+ body, err := json.Marshal(redeemResponse{
90+ AccessToken: "a1234",
91+ ExpiresIn: 10,
92+ RefreshToken: "refresh12345",
93+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov", "email_verified":true}`)),
94+ })
7895 assert.Equal(t, nil, err)
79− email, err := p.GetEmailAddress(body, "ignored access_token")
80− assert.Equal(t, "michael.bland@gsa.gov", email)
96+ var server *httptest.Server
97+ p.RedeemUrl, server = newRedeemServer(body)
98+ defer server.Close()
99+
100+ session, err := p.Redeem("http://redirect/", "code1234")
81101 assert.Equal(t, nil, err)
102+ assert.NotEqual(t, session, nil)
103+ assert.Equal(t, "michael.bland@gsa.gov", session.Email)
104+ assert.Equal(t, "a1234", session.AccessToken)
105+ assert.Equal(t, "refresh12345", session.RefreshToken)
82106}
83107
108+//
84109func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
85110 p := newGoogleProvider()
86− body, err := json.Marshal(
87− struct {
88− IdToken string `json:"id_token"`
89− }{
90− IdToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
91− },
92− )
111+ body, err := json.Marshal(redeemResponse{
112+ AccessToken: "a1234",
113+ IdToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
114+ })
93115 assert.Equal(t, nil, err)
94− email, err := p.GetEmailAddress(body, "ignored access_token")
95− assert.Equal(t, "", email)
116+ var server *httptest.Server
117+ p.RedeemUrl, server = newRedeemServer(body)
118+ defer server.Close()
119+
120+ session, err := p.Redeem("http://redirect/", "code1234")
96121 assert.NotEqual(t, nil, err)
122+ if session != nil {
123+ t.Errorf("expect nill session %#v", session)
124+ }
97125}
98126
99127func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
100128 p := newGoogleProvider()
101129
102− body, err := json.Marshal(
103− struct {
104− IdToken string `json:"id_token"`
105− }{
106− IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)),
107− },
108− )
130+ body, err := json.Marshal(redeemResponse{
131+ AccessToken: "a1234",
132+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)),
133+ })
109134 assert.Equal(t, nil, err)
110− email, err := p.GetEmailAddress(body, "ignored access_token")
111− assert.Equal(t, "", email)
135+ var server *httptest.Server
136+ p.RedeemUrl, server = newRedeemServer(body)
137+ defer server.Close()
138+
139+ session, err := p.Redeem("http://redirect/", "code1234")
112140 assert.NotEqual(t, nil, err)
141+ if session != nil {
142+ t.Errorf("expect nill session %#v", session)
143+ }
144+
113145}
114146
115147func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
116148 p := newGoogleProvider()
117− body, err := json.Marshal(
118− struct {
119− IdToken string `json:"id_token"`
120− }{
121− IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
122− },
123− )
149+ body, err := json.Marshal(redeemResponse{
150+ AccessToken: "a1234",
151+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
152+ })
124153 assert.Equal(t, nil, err)
125− email, err := p.GetEmailAddress(body, "ignored access_token")
126− assert.Equal(t, "", email)
154+ var server *httptest.Server
155+ p.RedeemUrl, server = newRedeemServer(body)
156+ defer server.Close()
157+
158+ session, err := p.Redeem("http://redirect/", "code1234")
127159 assert.NotEqual(t, nil, err)
160+ if session != nil {
161+ t.Errorf("expect nill session %#v", session)
162+ }
163+
128164}
providers/internal_util.go+4
@@ -9,6 +9,7 @@ import (
99 "github.com/bitly/oauth2_proxy/api"
1010)
1111
12+// validateToken returns true if token is valid
1213func validateToken(p Provider, access_token string, header http.Header) bool {
1314 if access_token == "" || p.Data().ValidateUrl == nil {
1415 return false
@@ -20,12 +21,15 @@ func validateToken(p Provider, access_token string, header http.Header) bool {
2021 }
2122 resp, err := api.RequestUnparsedResponse(endpoint, header)
2223 if err != nil {
24+ log.Printf("GET %s", endpoint)
2325 log.Printf("token validation request failed: %s", err)
2426 return false
2527 }
2628
2729 body, _ := ioutil.ReadAll(resp.Body)
2830 resp.Body.Close()
31+ log.Printf("%d GET %s %s", resp.StatusCode, endpoint, body)
32+
2933 if resp.StatusCode == 200 {
3034 return true
3135 }
providers/internal_util_test.go+26−24
@@ -1,36 +1,38 @@
11package providers
22
33import (
4− "github.com/bmizerany/assert"
4+ "errors"
55 "net/http"
66 "net/http/httptest"
77 "net/url"
88 "testing"
9+
10+ "github.com/bmizerany/assert"
911)
1012
11−type ValidateTokenTestProvider struct {
13+type ValidateSessionStateTestProvider struct {
1214 *ProviderData
1315}
1416
15−func (tp *ValidateTokenTestProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
16− return "", nil
17+func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *SessionState) (string, error) {
18+ return "", errors.New("not implemented")
1719}
1820
1921// Note that we're testing the internal validateToken() used to implement
20−// several Provider's ValidateToken() implementations
21−func (tp *ValidateTokenTestProvider) ValidateToken(access_token string) bool {
22+// several Provider's ValidateSessionState() implementations
23+func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *SessionState) bool {
2224 return false
2325}
2426
25−type ValidateTokenTest struct {
27+type ValidateSessionStateTest struct {
2628 backend *httptest.Server
2729 response_code int
28− provider *ValidateTokenTestProvider
30+ provider *ValidateSessionStateTestProvider
2931 header http.Header
3032}
3133
32−func NewValidateTokenTest() *ValidateTokenTest {
33− var vt_test ValidateTokenTest
34+func NewValidateSessionStateTest() *ValidateSessionStateTest {
35+ var vt_test ValidateSessionStateTest
3436
3537 vt_test.backend = httptest.NewServer(
3638 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -59,7 +61,7 @@ func NewValidateTokenTest() *ValidateTokenTest {
5961
6062 }))
6163 backend_url, _ := url.Parse(vt_test.backend.URL)
62− vt_test.provider = &ValidateTokenTestProvider{
64+ vt_test.provider = &ValidateSessionStateTestProvider{
6365 ProviderData: &ProviderData{
6466 ValidateUrl: &url.URL{
6567 Scheme: "http",
@@ -72,18 +74,18 @@ func NewValidateTokenTest() *ValidateTokenTest {
7274 return &vt_test
7375}
7476
75−func (vt_test *ValidateTokenTest) Close() {
77+func (vt_test *ValidateSessionStateTest) Close() {
7678 vt_test.backend.Close()
7779}
7880
79−func TestValidateTokenValidToken(t *testing.T) {
80− vt_test := NewValidateTokenTest()
81+func TestValidateSessionStateValidToken(t *testing.T) {
82+ vt_test := NewValidateSessionStateTest()
8183 defer vt_test.Close()
8284 assert.Equal(t, true, validateToken(vt_test.provider, "foobar", nil))
8385}
8486
85−func TestValidateTokenValidTokenWithHeaders(t *testing.T) {
86− vt_test := NewValidateTokenTest()
87+func TestValidateSessionStateValidTokenWithHeaders(t *testing.T) {
88+ vt_test := NewValidateSessionStateTest()
8789 defer vt_test.Close()
8890 vt_test.header = make(http.Header)
8991 vt_test.header.Set("Authorization", "Bearer foobar")
@@ -91,28 +93,28 @@ func TestValidateTokenValidTokenWithHeaders(t *testing.T) {
9193 validateToken(vt_test.provider, "foobar", vt_test.header))
9294}
9395
94−func TestValidateTokenEmptyToken(t *testing.T) {
95− vt_test := NewValidateTokenTest()
96+func TestValidateSessionStateEmptyToken(t *testing.T) {
97+ vt_test := NewValidateSessionStateTest()
9698 defer vt_test.Close()
9799 assert.Equal(t, false, validateToken(vt_test.provider, "", nil))
98100}
99101
100−func TestValidateTokenEmptyValidateUrl(t *testing.T) {
101− vt_test := NewValidateTokenTest()
102+func TestValidateSessionStateEmptyValidateUrl(t *testing.T) {
103+ vt_test := NewValidateSessionStateTest()
102104 defer vt_test.Close()
103105 vt_test.provider.Data().ValidateUrl = nil
104106 assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
105107}
106108
107−func TestValidateTokenRequestNetworkFailure(t *testing.T) {
108− vt_test := NewValidateTokenTest()
109+func TestValidateSessionStateRequestNetworkFailure(t *testing.T) {
110+ vt_test := NewValidateSessionStateTest()
109111 // Close immediately to simulate a network failure
110112 vt_test.Close()
111113 assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
112114}
113115
114−func TestValidateTokenExpiredToken(t *testing.T) {
115− vt_test := NewValidateTokenTest()
116+func TestValidateSessionStateExpiredToken(t *testing.T) {
117+ vt_test := NewValidateSessionStateTest()
116118 defer vt_test.Close()
117119 vt_test.response_code = 401
118120 assert.Equal(t, false, validateToken(vt_test.provider, "foobar", nil))
providers/linkedin.go+6−8
@@ -1,7 +1,6 @@
11package providers
22
33import (
4− "bytes"
54 "errors"
65 "fmt"
76 "log"
@@ -49,16 +48,15 @@ func getLinkedInHeader(access_token string) http.Header {
4948 return header
5049}
5150
52−func (p *LinkedInProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
53− if access_token == "" {
51+func (p *LinkedInProvider) GetEmailAddress(s *SessionState) (string, error) {
52+ if s.AccessToken == "" {
5453 return "", errors.New("missing access token")
5554 }
56− params := url.Values{}
57− req, err := http.NewRequest("GET", p.ProfileUrl.String()+"?format=json", bytes.NewBufferString(params.Encode()))
55+ req, err := http.NewRequest("GET", p.ProfileUrl.String()+"?format=json", nil)
5856 if err != nil {
5957 return "", err
6058 }
61− req.Header = getLinkedInHeader(access_token)
59+ req.Header = getLinkedInHeader(s.AccessToken)
6260
6361 json, err := api.Request(req)
6462 if err != nil {
@@ -74,6 +72,6 @@ func (p *LinkedInProvider) GetEmailAddress(body []byte, access_token string) (st
7472 return email, nil
7573}
7674
77−func (p *LinkedInProvider) ValidateToken(access_token string) bool {
78− return validateToken(p, access_token, getLinkedInHeader(access_token))
75+func (p *LinkedInProvider) ValidateSessionState(s *SessionState) bool {
76+ return validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken))
7977}
providers/linkedin_test.go+6−4
@@ -97,8 +97,8 @@ func TestLinkedInProviderGetEmailAddress(t *testing.T) {
9797 b_url, _ := url.Parse(b.URL)
9898 p := testLinkedInProvider(b_url.Host)
9999
100− email, err := p.GetEmailAddress([]byte{},
101− "imaginary_access_token")
100+ session := &SessionState{AccessToken: "imaginary_access_token"}
101+ email, err := p.GetEmailAddress(session)
102102 assert.Equal(t, nil, err)
103103 assert.Equal(t, "user@linkedin.com", email)
104104}
@@ -113,7 +113,8 @@ func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {
113113 // We'll trigger a request failure by using an unexpected access
114114 // token. Alternatively, we could allow the parsing of the payload as
115115 // JSON to fail.
116− email, err := p.GetEmailAddress([]byte{}, "unexpected_access_token")
116+ session := &SessionState{AccessToken: "unexpected_access_token"}
117+ email, err := p.GetEmailAddress(session)
117118 assert.NotEqual(t, nil, err)
118119 assert.Equal(t, "", email)
119120}
@@ -125,7 +126,8 @@ func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
125126 b_url, _ := url.Parse(b.URL)
126127 p := testLinkedInProvider(b_url.Host)
127128
128− email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
129+ session := &SessionState{AccessToken: "imaginary_access_token"}
130+ email, err := p.GetEmailAddress(session)
129131 assert.NotEqual(t, nil, err)
130132 assert.Equal(t, "", email)
131133}
providers/myusa.go+2−6
@@ -42,9 +42,9 @@ func NewMyUsaProvider(p *ProviderData) *MyUsaProvider {
4242 return &MyUsaProvider{ProviderData: p}
4343}
4444
45−func (p *MyUsaProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
45+func (p *MyUsaProvider) GetEmailAddress(s *SessionState) (string, error) {
4646 req, err := http.NewRequest("GET",
47− p.ProfileUrl.String()+"?access_token="+access_token, nil)
47+ p.ProfileUrl.String()+"?access_token="+s.AccessToken, nil)
4848 if err != nil {
4949 log.Printf("failed building request %s", err)
5050 return "", err
@@ -56,7 +56,3 @@ func (p *MyUsaProvider) GetEmailAddress(body []byte, access_token string) (strin
5656 }
5757 return json.Get("email").String()
5858}
59−
60−func (p *MyUsaProvider) ValidateToken(access_token string) bool {
61− return validateToken(p, access_token, nil)
62−}
providers/myusa_test.go+8−4
@@ -1,11 +1,12 @@
11package providers
22
33import (
4− "github.com/bmizerany/assert"
54 "net/http"
65 "net/http/httptest"
76 "net/url"
87 "testing"
8+
9+ "github.com/bmizerany/assert"
910)
1011
1112func updateUrl(url *url.URL, hostname string) {
@@ -102,7 +103,8 @@ func TestMyUsaProviderGetEmailAddress(t *testing.T) {
102103 b_url, _ := url.Parse(b.URL)
103104 p := testMyUsaProvider(b_url.Host)
104105
105− email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
106+ session := &SessionState{AccessToken: "imaginary_access_token"}
107+ email, err := p.GetEmailAddress(session)
106108 assert.Equal(t, nil, err)
107109 assert.Equal(t, "michael.bland@gsa.gov", email)
108110}
@@ -119,7 +121,8 @@ func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) {
119121 // We'll trigger a request failure by using an unexpected access
120122 // token. Alternatively, we could allow the parsing of the payload as
121123 // JSON to fail.
122− email, err := p.GetEmailAddress([]byte{}, "unexpected_access_token")
124+ session := &SessionState{AccessToken: "unexpected_access_token"}
125+ email, err := p.GetEmailAddress(session)
123126 assert.NotEqual(t, nil, err)
124127 assert.Equal(t, "", email)
125128}
@@ -131,7 +134,8 @@ func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
131134 b_url, _ := url.Parse(b.URL)
132135 p := testMyUsaProvider(b_url.Host)
133136
134− email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
137+ session := &SessionState{AccessToken: "imaginary_access_token"}
138+ email, err := p.GetEmailAddress(session)
135139 assert.NotEqual(t, nil, err)
136140 assert.Equal(t, "", email)
137141}
providers/provider_default.go+51−10
@@ -9,9 +9,11 @@ import (
99 "net/http"
1010 "net/url"
1111 "strings"
12+
13+ "github.com/bitly/oauth2_proxy/cookie"
1214)
1315
14−func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token string, err error) {
16+func (p *ProviderData) Redeem(redirectUrl, code string) (s *SessionState, err error) {
1517 if code == "" {
1618 err = errors.New("missing code")
1719 return
@@ -23,24 +25,28 @@ func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token stri
2325 params.Add("client_secret", p.ClientSecret)
2426 params.Add("code", code)
2527 params.Add("grant_type", "authorization_code")
26− req, err := http.NewRequest("POST", p.RedeemUrl.String(), bytes.NewBufferString(params.Encode()))
28+ var req *http.Request
29+ req, err = http.NewRequest("POST", p.RedeemUrl.String(), bytes.NewBufferString(params.Encode()))
2730 if err != nil {
28− return nil, "", err
31+ return
2932 }
3033 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
3134
32− resp, err := http.DefaultClient.Do(req)
35+ var resp *http.Response
36+ resp, err = http.DefaultClient.Do(req)
3337 if err != nil {
34− return nil, "", err
38+ return nil, err
3539 }
40+ var body []byte
3641 body, err = ioutil.ReadAll(resp.Body)
3742 resp.Body.Close()
3843 if err != nil {
39− return nil, "", err
44+ return
4045 }
4146
4247 if resp.StatusCode != 200 {
43− return body, "", fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemUrl.String(), body)
48+ err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemUrl.String(), body)
49+ return
4450 }
4551
4652 // blindly try json and x-www-form-urlencoded
@@ -49,11 +55,23 @@ func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token stri
4955 }
5056 err = json.Unmarshal(body, &jsonResponse)
5157 if err == nil {
52− return body, jsonResponse.AccessToken, nil
58+ s = &SessionState{
59+ AccessToken: jsonResponse.AccessToken,
60+ }
61+ return
5362 }
5463
55− v, err := url.ParseQuery(string(body))
56− return body, v.Get("access_token"), err
64+ var v url.Values
65+ v, err = url.ParseQuery(string(body))
66+ if err != nil {
67+ return
68+ }
69+ if a := v.Get("access_token"); a != "" {
70+ s = &SessionState{AccessToken: a}
71+ } else {
72+ err = fmt.Errorf("no access token found %s", body)
73+ }
74+ return
5775}
5876
5977// GetLoginURL with typical oauth parameters
@@ -72,3 +90,26 @@ func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string {
7290 a.RawQuery = params.Encode()
7391 return a.String()
7492}
93+
94+// CookieForSession serializes a session state for storage in a cookie
95+func (p *ProviderData) CookieForSession(s *SessionState, c *cookie.Cipher) (string, error) {
96+ return s.EncodeSessionState(c)
97+}
98+
99+// SessionFromCookie deserializes a session from a cookie value
100+func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *SessionState, err error) {
101+ return DecodeSessionState(v, c)
102+}
103+
104+func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {
105+ return "", errors.New("not implemented")
106+}
107+
108+func (p *ProviderData) ValidateSessionState(s *SessionState) bool {
109+ return validateToken(p, s.AccessToken, nil)
110+}
111+
112+// RefreshSessionIfNeeded
113+func (p *ProviderData) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
114+ return false, nil
115+}
providers/provider_default_test.gonew file+17
@@ -0,0 +1,17 @@
1+package providers
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bmizerany/assert"
8+)
9+
10+func TestRefresh(t *testing.T) {
11+ p := &ProviderData{}
12+ refreshed, err := p.RefreshSessionIfNeeded(&SessionState{
13+ ExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute),
14+ })
15+ assert.Equal(t, false, refreshed)
16+ assert.Equal(t, nil, err)
17+}
providers/providers.go+10−3
@@ -1,11 +1,18 @@
11package providers
22
3+import (
4+ "github.com/bitly/oauth2_proxy/cookie"
5+)
6+
37type Provider interface {
48 Data() *ProviderData
5− GetEmailAddress(body []byte, access_token string) (string, error)
6− Redeem(string, string) ([]byte, string, error)
7− ValidateToken(access_token string) bool
9+ GetEmailAddress(*SessionState) (string, error)
10+ Redeem(string, string) (*SessionState, error)
11+ ValidateSessionState(*SessionState) bool
812 GetLoginURL(redirectURI, finalRedirect string) string
13+ RefreshSessionIfNeeded(*SessionState) (bool, error)
14+ SessionFromCookie(string, *cookie.Cipher) (*SessionState, error)
15+ CookieForSession(*SessionState, *cookie.Cipher) (string, error)
916}
1017
1118func New(provider string, p *ProviderData) Provider {
providers/session_state.gonew file+115
@@ -0,0 +1,115 @@
1+package providers
2+
3+import (
4+ "fmt"
5+ "strconv"
6+ "strings"
7+ "time"
8+
9+ "github.com/bitly/oauth2_proxy/cookie"
10+)
11+
12+type SessionState struct {
13+ AccessToken string
14+ ExpiresOn time.Time
15+ RefreshToken string
16+ Email string
17+ User string
18+}
19+
20+func (s *SessionState) IsExpired() bool {
21+ if !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(time.Now()) {
22+ return true
23+ }
24+ return false
25+}
26+
27+func (s *SessionState) String() string {
28+ o := fmt.Sprintf("Session{%s", s.userOrEmail())
29+ if s.AccessToken != "" {
30+ o += " token:true"
31+ }
32+ if !s.ExpiresOn.IsZero() {
33+ o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
34+ }
35+ if s.RefreshToken != "" {
36+ o += " refresh_token:true"
37+ }
38+ return o + "}"
39+}
40+
41+func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
42+ if c == nil || s.AccessToken == "" {
43+ return s.userOrEmail(), nil
44+ }
45+ return s.EncryptedString(c)
46+}
47+
48+func (s *SessionState) userOrEmail() string {
49+ u := s.User
50+ if s.Email != "" {
51+ u = s.Email
52+ }
53+ return u
54+}
55+
56+func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
57+ var err error
58+ if c == nil {
59+ panic("error. missing cipher")
60+ }
61+ a := s.AccessToken
62+ if a != "" {
63+ a, err = c.Encrypt(a)
64+ if err != nil {
65+ return "", err
66+ }
67+ }
68+ r := s.RefreshToken
69+ if r != "" {
70+ r, err = c.Encrypt(r)
71+ if err != nil {
72+ return "", err
73+ }
74+ }
75+ return fmt.Sprintf("%s|%s|%d|%s", s.userOrEmail(), a, s.ExpiresOn.Unix(), r), nil
76+}
77+
78+func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error) {
79+ chunks := strings.Split(v, "|")
80+ if len(chunks) == 1 {
81+ if strings.Contains(chunks[0], "@") {
82+ u := strings.Split(v, "@")[0]
83+ return &SessionState{Email: v, User: u}, nil
84+ }
85+ return &SessionState{User: v}, nil
86+ }
87+
88+ if len(chunks) != 4 {
89+ err = fmt.Errorf("invalid number of fields (got %d expected 4)", len(chunks))
90+ return
91+ }
92+
93+ s = &SessionState{}
94+ if c != nil && chunks[1] != "" {
95+ s.AccessToken, err = c.Decrypt(chunks[1])
96+ if err != nil {
97+ return nil, err
98+ }
99+ }
100+ if c != nil && chunks[3] != "" {
101+ s.RefreshToken, err = c.Decrypt(chunks[3])
102+ if err != nil {
103+ return nil, err
104+ }
105+ }
106+ if u := chunks[0]; strings.Contains(u, "@") {
107+ s.Email = u
108+ s.User = strings.Split(u, "@")[0]
109+ } else {
110+ s.User = u
111+ }
112+ ts, _ := strconv.Atoi(chunks[2])
113+ s.ExpiresOn = time.Unix(int64(ts), 0)
114+ return
115+}
providers/session_state_test.gonew file+88
@@ -0,0 +1,88 @@
1+package providers
2+
3+import (
4+ "strings"
5+ "testing"
6+ "time"
7+
8+ "github.com/bitly/oauth2_proxy/cookie"
9+ "github.com/bmizerany/assert"
10+)
11+
12+const secret = "0123456789abcdefghijklmnopqrstuv"
13+const altSecret = "0000000000abcdefghijklmnopqrstuv"
14+
15+func TestSessionStateSerialization(t *testing.T) {
16+ c, err := cookie.NewCipher(secret)
17+ assert.Equal(t, nil, err)
18+ c2, err := cookie.NewCipher(altSecret)
19+ assert.Equal(t, nil, err)
20+ s := &SessionState{
21+ Email: "user@domain.com",
22+ AccessToken: "token1234",
23+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
24+ RefreshToken: "refresh4321",
25+ }
26+ encoded, err := s.EncodeSessionState(c)
27+ assert.Equal(t, nil, err)
28+ assert.Equal(t, 3, strings.Count(encoded, "|"))
29+
30+ ss, err := DecodeSessionState(encoded, c)
31+ t.Logf("%#v", ss)
32+ assert.Equal(t, nil, err)
33+ assert.Equal(t, s.Email, ss.Email)
34+ assert.Equal(t, s.AccessToken, ss.AccessToken)
35+ assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
36+ assert.Equal(t, s.RefreshToken, ss.RefreshToken)
37+
38+ // ensure a different cipher can't decode properly (ie: it gets gibberish)
39+ ss, err = DecodeSessionState(encoded, c2)
40+ t.Logf("%#v", ss)
41+ assert.Equal(t, nil, err)
42+ assert.Equal(t, s.Email, ss.Email)
43+ assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
44+ assert.NotEqual(t, s.AccessToken, ss.AccessToken)
45+ assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
46+}
47+
48+func TestSessionStateSerializationNoCipher(t *testing.T) {
49+
50+ s := &SessionState{
51+ Email: "user@domain.com",
52+ AccessToken: "token1234",
53+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
54+ RefreshToken: "refresh4321",
55+ }
56+ encoded, err := s.EncodeSessionState(nil)
57+ assert.Equal(t, nil, err)
58+ assert.Equal(t, s.Email, encoded)
59+
60+ // only email should have been serialized
61+ ss, err := DecodeSessionState(encoded, nil)
62+ assert.Equal(t, nil, err)
63+ assert.Equal(t, s.Email, ss.Email)
64+ assert.Equal(t, "", ss.AccessToken)
65+ assert.Equal(t, "", ss.RefreshToken)
66+}
67+
68+func TestSessionStateUserOrEmail(t *testing.T) {
69+
70+ s := &SessionState{
71+ Email: "user@domain.com",
72+ User: "just-user",
73+ }
74+ assert.Equal(t, "user@domain.com", s.userOrEmail())
75+ s.Email = ""
76+ assert.Equal(t, "just-user", s.userOrEmail())
77+}
78+
79+func TestExpired(t *testing.T) {
80+ s := &SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}
81+ assert.Equal(t, true, s.IsExpired())
82+
83+ s = &SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}
84+ assert.Equal(t, false, s.IsExpired())
85+
86+ s = &SessionState{}
87+ assert.Equal(t, false, s.IsExpired())
88+}