Move cookie to pkg/encryption

This commit is contained in:
Joel Speed
2019-06-15 11:33:57 +02:00
parent 8027cc454e
commit d1ef14becc
14 changed files with 43 additions and 45 deletions
+2 -4
View File
@@ -1,13 +1,11 @@
package options
import (
"github.com/pusher/oauth2_proxy/cookie"
)
import "github.com/pusher/oauth2_proxy/pkg/encryption"
// SessionOptions contains configuration options for the SessionStore providers.
type SessionOptions struct {
Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
Cipher *cookie.Cipher
Cipher *encryption.Cipher
CookieStoreOptions
RedisStoreOptions
}
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/encryption"
)
// SessionState is used to store information about the currently authenticated user session
@@ -66,7 +66,7 @@ func (s *SessionState) String() string {
}
// EncodeSessionState returns string representation of the current session
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) {
var ss SessionState
if c == nil {
// Store only Email and User when cipher is unavailable
@@ -133,7 +133,7 @@ func legacyDecodeSessionStatePlain(v string) (*SessionState, error) {
// legacyDecodeSessionState attempts to decode the session state string
// generated by v3.1.0 or older
func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
func legacyDecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
chunks := strings.Split(v, "|")
if c == nil {
@@ -176,7 +176,7 @@ func legacyDecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error)
}
// DecodeSessionState decodes the session cookie string into a SessionState
func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
var ssj SessionStateJSON
var ss *SessionState
err := json.Unmarshal([]byte(v), &ssj)
+7 -7
View File
@@ -5,8 +5,8 @@ import (
"testing"
"time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/stretchr/testify/assert"
)
@@ -14,9 +14,9 @@ const secret = "0123456789abcdefghijklmnopqrstuv"
const altSecret = "0000000000abcdefghijklmnopqrstuv"
func TestSessionStateSerialization(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret))
c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret))
c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &sessions.SessionState{
Email: "user@domain.com",
@@ -54,9 +54,9 @@ func TestSessionStateSerialization(t *testing.T) {
}
func TestSessionStateSerializationWithUser(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret))
c, err := encryption.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret))
c2, err := encryption.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &sessions.SessionState{
User: "just-user",
@@ -146,7 +146,7 @@ func TestExpired(t *testing.T) {
type testCase struct {
sessions.SessionState
Encoded string
Cipher *cookie.Cipher
Cipher *encryption.Cipher
Error bool
}
@@ -203,7 +203,7 @@ func TestDecodeSessionState(t *testing.T) {
eString := string(eJSON)
eUnix := e.Unix()
c, err := cookie.NewCipher([]byte(secret))
c, err := encryption.NewCipher([]byte(secret))
assert.NoError(t, err)
testCases := []testCase{
+128
View File
@@ -0,0 +1,128 @@
package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
// additionally, the 'value' is encrypted so it's opaque to the browser
// Validate ensures a cookie is properly signed
func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
// value, timestamp, sig
parts := strings.Split(cookie.Value, "|")
if len(parts) != 3 {
return
}
sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
if checkHmac(parts[2], sig) {
ts, err := strconv.Atoi(parts[1])
if err != nil {
return
}
// The expiration timestamp set when the cookie was created
// isn't sent back by the browser. Hence, we check whether the
// creation timestamp stored in the cookie falls within the
// window defined by (Now()-expiration, Now()].
t = time.Unix(int64(ts), 0)
if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
// it's a valid cookie. now get the contents
rawValue, err := base64.URLEncoding.DecodeString(parts[0])
if err == nil {
value = string(rawValue)
ok = true
return
}
}
}
return
}
// SignedValue returns a cookie that is signed and can later be checked with Validate
func SignedValue(seed string, key string, value string, now time.Time) string {
encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
timeStr := fmt.Sprintf("%d", now.Unix())
sig := cookieSignature(seed, key, encodedValue, timeStr)
cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
return cookieVal
}
func cookieSignature(args ...string) string {
h := hmac.New(sha1.New, []byte(args[0]))
for _, arg := range args[1:] {
h.Write([]byte(arg))
}
var b []byte
b = h.Sum(b)
return base64.URLEncoding.EncodeToString(b)
}
func checkHmac(input, expected string) bool {
inputMAC, err1 := base64.URLEncoding.DecodeString(input)
if err1 == nil {
expectedMAC, err2 := base64.URLEncoding.DecodeString(expected)
if err2 == nil {
return hmac.Equal(inputMAC, expectedMAC)
}
}
return false
}
// Cipher provides methods to encrypt and decrypt cookie values
type Cipher struct {
cipher.Block
}
// NewCipher returns a new aes Cipher for encrypting cookie values
func NewCipher(secret []byte) (*Cipher, error) {
c, err := aes.NewCipher(secret)
if err != nil {
return nil, err
}
return &Cipher{Block: c}, err
}
// Encrypt a value for use in a cookie
func (c *Cipher) Encrypt(value string) (string, error) {
ciphertext := make([]byte, aes.BlockSize+len(value))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", fmt.Errorf("failed to create initialization vector %s", err)
}
stream := cipher.NewCFBEncrypter(c.Block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(value))
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt a value from a cookie to it's original string
func (c *Cipher) Decrypt(s string) (string, error) {
encrypted, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", fmt.Errorf("failed to decrypt cookie value %s", err)
}
if len(encrypted) < aes.BlockSize {
return "", fmt.Errorf("encrypted cookie value should be "+
"at least %d bytes, but is only %d bytes",
aes.BlockSize, len(encrypted))
}
iv := encrypted[:aes.BlockSize]
encrypted = encrypted[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(c.Block, iv)
stream.XORKeyStream(encrypted, encrypted)
return string(encrypted), nil
}
+43
View File
@@ -0,0 +1,43 @@
package encryption
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncodeAndDecodeAccessToken(t *testing.T) {
const secret = "0123456789abcdefghijklmnopqrstuv"
const token = "my access token"
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 secretBase64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk="
const token = "my access token"
secret, err := base64.URLEncoding.DecodeString(secretBase64)
assert.Equal(t, nil, err)
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)
}
+17
View File
@@ -0,0 +1,17 @@
package encryption
import (
"crypto/rand"
"fmt"
)
// Nonce generates a random 16 byte string to be used as a nonce
func Nonce() (nonce string, err error) {
b := make([]byte, 16)
_, err = rand.Read(b)
if err != nil {
return
}
nonce = fmt.Sprintf("%x", b)
return
}
+4 -4
View File
@@ -8,10 +8,10 @@ import (
"strings"
"time"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/sessions/utils"
)
@@ -28,7 +28,7 @@ var _ sessions.SessionStore = &SessionStore{}
// interface that stores sessions in client side cookies
type SessionStore struct {
CookieOptions *options.CookieOptions
CookieCipher *cookie.Cipher
CookieCipher *encryption.Cipher
}
// Save takes a sessions.SessionState and stores the information from it
@@ -53,7 +53,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
// always http.ErrNoCookie
return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName)
}
val, _, ok := cookie.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
if !ok {
return nil, errors.New("Cookie Signature not valid")
}
@@ -96,7 +96,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
// authentication details
func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
if value != "" {
value = cookie.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
}
c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
+6 -6
View File
@@ -13,10 +13,10 @@ import (
"time"
"github.com/go-redis/redis"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
)
// TicketData is a structure representing the ticket used in server session storage
@@ -28,7 +28,7 @@ type TicketData struct {
// SessionStore is an implementation of the sessions.SessionStore
// interface that stores sessions in redis
type SessionStore struct {
CookieCipher *cookie.Cipher
CookieCipher *encryption.Cipher
CookieOptions *options.CookieOptions
Client *redis.Client
}
@@ -106,7 +106,7 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro
return nil, fmt.Errorf("error loading session: %s", err)
}
val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
return nil, fmt.Errorf("Cookie Signature not valid")
}
@@ -166,7 +166,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
return fmt.Errorf("error retrieving cookie: %v", err)
}
val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
return fmt.Errorf("Cookie Signature not valid")
}
@@ -186,7 +186,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
// makeCookie makes a cookie, signing the value if present
func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
if value != "" {
value = cookie.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now)
value = encryption.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now)
}
return cookies.MakeCookieFromOptions(
req,
@@ -230,7 +230,7 @@ func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, e
}
// An existing cookie exists, try to retrieve the ticket
val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
if !ok {
// Cookie is invalid, create a new ticket
return newTicket()
+3 -3
View File
@@ -13,10 +13,10 @@ import (
"github.com/alicebob/miniredis"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/options"
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/cookies"
"github.com/pusher/oauth2_proxy/pkg/encryption"
"github.com/pusher/oauth2_proxy/pkg/sessions"
sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
"github.com/pusher/oauth2_proxy/pkg/sessions/redis"
@@ -158,7 +158,7 @@ var _ = Describe("NewSessionStore", func() {
BeforeEach(func() {
By("Using a valid cookie with a different providers session encoding")
broken := "BrokenSessionFromADifferentSessionImplementation"
value := cookie.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now())
value := encryption.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now())
cookie := cookies.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now())
request.AddCookie(cookie)
@@ -354,7 +354,7 @@ var _ = Describe("NewSessionStore", func() {
_, err := rand.Read(secret)
Expect(err).ToNot(HaveOccurred())
cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
cipher, err := cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
Expect(err).ToNot(HaveOccurred())
Expect(cipher).ToNot(BeNil())
opts.Cipher = cipher
+3 -3
View File
@@ -3,17 +3,17 @@ package utils
import (
"encoding/base64"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
)
// CookieForSession serializes a session state for storage in a cookie
func CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) {
func CookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) {
return s.EncodeSessionState(c)
}
// SessionFromCookie deserializes a session from a cookie value
func SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) {
func SessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
return sessions.DecodeSessionState(v, c)
}