From cdebfd643698aa6e75d0a64865dfa5442a0a1623 Mon Sep 17 00:00:00 2001 From: Jehiah Czebotar Date: Mon, 20 Jun 2016 07:17:39 -0400 Subject: [PATCH] base64 cookie support --- cookie/cookies.go | 4 ++-- cookie/cookies_test.go | 21 +++++++++++++++++- main.go | 2 +- oauthproxy.go | 9 ++++---- oauthproxy_test.go | 2 +- options.go | 38 ++++++++++++++++++++++++++++++--- options_test.go | 27 ++++++++++++++++++++++- providers/github.go | 4 ++-- providers/session_state_test.go | 4 ++-- 9 files changed, 93 insertions(+), 18 deletions(-) diff --git a/cookie/cookies.go b/cookie/cookies.go index b9df87f6..0d354e15 100644 --- a/cookie/cookies.go +++ b/cookie/cookies.go @@ -85,8 +85,8 @@ type Cipher struct { } // NewCipher returns a new aes Cipher for encrypting cookie values -func NewCipher(secret string) (*Cipher, error) { - c, err := aes.NewCipher([]byte(secret)) +func NewCipher(secret []byte) (*Cipher, error) { + c, err := aes.NewCipher(secret) if err != nil { return nil, err } diff --git a/cookie/cookies_test.go b/cookie/cookies_test.go index d527cb8c..5c4a9434 100644 --- a/cookie/cookies_test.go +++ b/cookie/cookies_test.go @@ -1,6 +1,7 @@ package cookie import ( + "encoding/base64" "testing" "github.com/bmizerany/assert" @@ -9,7 +10,25 @@ import ( func TestEncodeAndDecodeAccessToken(t *testing.T) { const secret = "0123456789abcdefghijklmnopqrstuv" const token = "my access token" - c, err := NewCipher(secret) + c, err := NewCipher([]byte(secret)) + assert.Equal(t, nil, err) + + encoded, err := c.Encrypt(token) + assert.Equal(t, nil, err) + + decoded, err := c.Decrypt(encoded) + assert.Equal(t, nil, err) + + assert.NotEqual(t, token, encoded) + assert.Equal(t, token, decoded) +} + +func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { + const secret_b64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk=" + const token = "my access token" + + secret, err := base64.URLEncoding.DecodeString(secret_b64) + c, err := NewCipher([]byte(secret)) assert.Equal(t, nil, err) encoded, err := c.Encrypt(token) diff --git a/main.go b/main.go index f31f6629..ba336687 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ func main() { flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. //sign_in)") flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") - flagSet.String("cookie-secret", "", "the seed string for secure cookies") + flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)") flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable") diff --git a/oauthproxy.go b/oauthproxy.go index 1041d7d0..f1a6920e 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -1,7 +1,7 @@ package main import ( - "encoding/base64" + b64 "encoding/base64" "errors" "fmt" "html/template" @@ -164,10 +164,9 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { var cipher *cookie.Cipher if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) { var err error - cipher, err = cookie.NewCipher(opts.CookieSecret) + cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret)) if err != nil { - log.Fatal("error creating AES cipher with "+ - "cookie-secret ", opts.CookieSecret, ": ", err) + log.Fatal("cookie-secret error: ", err) } } @@ -626,7 +625,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, if len(s) != 2 || s[0] != "Basic" { return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization")) } - b, err := base64.StdEncoding.DecodeString(s[1]) + b, err := b64.StdEncoding.DecodeString(s[1]) if err != nil { return nil, err } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 7af1de18..59a0adae 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -427,7 +427,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest { pc_test.opts = NewOptions() pc_test.opts.ClientID = "bazquux" pc_test.opts.ClientSecret = "xyzzyplugh" - pc_test.opts.CookieSecret = "0123456789abcdef" + pc_test.opts.CookieSecret = "0123456789abcdefabcd" // First, set the CookieRefresh option so proxy.AesCipher is created, // needed to encrypt the access_token. pc_test.opts.CookieRefresh = time.Hour diff --git a/options.go b/options.go index 045e74e3..3b1366f2 100644 --- a/options.go +++ b/options.go @@ -2,6 +2,7 @@ package main import ( "crypto" + "encoding/base64" "fmt" "net/url" "os" @@ -156,17 +157,25 @@ func (o *Options) Validate() error { if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) { valid_cookie_secret_size := false for _, i := range []int{16, 24, 32} { - if len(o.CookieSecret) == i { + if len(secretBytes(o.CookieSecret)) == i { valid_cookie_secret_size = true } } + var decoded bool + if string(secretBytes(o.CookieSecret)) != o.CookieSecret { + decoded = true + } if valid_cookie_secret_size == false { + var suffix string + if decoded { + suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret) + } msgs = append(msgs, fmt.Sprintf( "cookie_secret must be 16, 24, or 32 bytes "+ "to create an AES cipher when "+ "pass_access_token == true or "+ - "cookie_refresh != 0, but is %d bytes", - len(o.CookieSecret))) + "cookie_refresh != 0, but is %d bytes.%s", + len(secretBytes(o.CookieSecret)), suffix)) } } @@ -251,3 +260,26 @@ func parseSignatureKey(o *Options, msgs []string) []string { } return msgs } + +func addPadding(secret string) string { + padding := len(secret) % 4 + switch padding { + case 1: + return secret + "===" + case 2: + return secret + "==" + case 3: + return secret + "=" + default: + return secret + } +} + +// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary +func secretBytes(secret string) []byte { + b, err := base64.URLEncoding.DecodeString(addPadding(secret)) + if err == nil { + return []byte(addPadding(string(b))) + } + return []byte(secret) +} diff --git a/options_test.go b/options_test.go index 8a8b6a77..3b50ca7f 100644 --- a/options_test.go +++ b/options_test.go @@ -160,7 +160,7 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { o := testOptions() assert.Equal(t, nil, o.Validate()) - o.CookieSecret = "0123456789abcdef" + o.CookieSecret = "0123456789abcdefabcd" o.CookieRefresh = o.CookieExpire assert.NotEqual(t, nil, o.Validate()) @@ -168,6 +168,31 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { assert.Equal(t, nil, o.Validate()) } +func TestBase64CookieSecret(t *testing.T) { + o := testOptions() + assert.Equal(t, nil, o.Validate()) + + // 32 byte, base64 (urlsafe) encoded key + o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ=" + assert.Equal(t, nil, o.Validate()) + + // 32 byte, base64 (urlsafe) encoded key, w/o padding + o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ" + assert.Equal(t, nil, o.Validate()) + + // 24 byte, base64 (urlsafe) encoded key + o.CookieSecret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3" + assert.Equal(t, nil, o.Validate()) + + // 16 byte, base64 (urlsafe) encoded key + o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA==" + assert.Equal(t, nil, o.Validate()) + + // 16 byte, base64 (urlsafe) encoded key, w/o padding + o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA" + assert.Equal(t, nil, o.Validate()) +} + func TestValidateSignatureKey(t *testing.T) { o := testOptions() o.SignatureKey = "sha1:secret" diff --git a/providers/github.go b/providers/github.go index cc5460b4..bae1f8c1 100644 --- a/providers/github.go +++ b/providers/github.go @@ -3,11 +3,11 @@ package providers import ( "encoding/json" "fmt" - "strings" "io/ioutil" "log" "net/http" "net/url" + "strings" ) type GitHubProvider struct { @@ -64,7 +64,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { "limit": {"100"}, } - endpoint := p.ValidateURL.Scheme + "://" + p.ValidateURL.Host + "/user/orgs?" + params.Encode() + endpoint := p.ValidateURL.Scheme + "://" + p.ValidateURL.Host + "/user/orgs?" + params.Encode() req, _ := http.NewRequest("GET", endpoint, nil) req.Header.Set("Accept", "application/vnd.github.v3+json") resp, err := http.DefaultClient.Do(req) diff --git a/providers/session_state_test.go b/providers/session_state_test.go index ba8de5da..6044bae1 100644 --- a/providers/session_state_test.go +++ b/providers/session_state_test.go @@ -13,9 +13,9 @@ const secret = "0123456789abcdefghijklmnopqrstuv" const altSecret = "0000000000abcdefghijklmnopqrstuv" func TestSessionStateSerialization(t *testing.T) { - c, err := cookie.NewCipher(secret) + c, err := cookie.NewCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := cookie.NewCipher(altSecret) + c2, err := cookie.NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &SessionState{ Email: "user@domain.com",