Skip to content
This repository was archived by the owner on Jan 24, 2019. It is now read-only.

Commit d49c3e1

Browse files
committed
SessionState refactoring; improve token renewal and cookie refresh
* New SessionState to consolidate email, access token and refresh token * split ServeHttp into individual methods * log on session renewal * log on access token refresh * refactor cookie encription/decription and session state serialization
1 parent b9ae5dc commit d49c3e1

21 files changed

+866
-580
lines changed

api/api.go

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"fmt"
55
"io/ioutil"
6+
"log"
67
"net/http"
78

89
"github.com/bitly/go-simplejson"
@@ -11,10 +12,12 @@ import (
1112
func 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.go

+128
Original file line numberDiff line numberDiff line change
@@ -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.go

+23
Original file line numberDiff line numberDiff line change
@@ -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.go

-140
This file was deleted.

cookies_test.go

-75
This file was deleted.

0 commit comments

Comments
 (0)