Skip to content

Commit

Permalink
Implement redis-backed cookie store
Browse files Browse the repository at this point in the history
  • Loading branch information
brianv0 committed Apr 24, 2019
1 parent 11f41e4 commit 2949456
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 0 deletions.
17 changes: 17 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
branch = "v2"
name = "github.com/coreos/go-oidc"

[[constraint]]
branch = "master"
name = "github.com/go-redis/redis"

[[constraint]]
branch = "master"
name = "github.com/mreiferson/go-options"
Expand Down
145 changes: 145 additions & 0 deletions cookie/cookies_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package cookie

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/go-redis/redis"
"io"
"net/http"
"strings"
"time"
)

// ServerCookiesStore is the interface to storing cookies.
// It takes in cookies
type ServerCookiesStore interface {
Store(responseCookie *http.Cookie, requestCookie *http.Cookie) (string, error)
Clear(requestCookie *http.Cookie) error
Load(requestCookie *http.Cookie) (string, error)
}

type RedisCookieStore struct {
Client *redis.Client
Block cipher.Block
Prefix string
}

func NewRedisCookieStore(url string, cookieName string, block cipher.Block) (*RedisCookieStore, error) {
opt, err := redis.ParseURL(url)
if err != nil {
panic(err)
}

client := redis.NewClient(opt)

rs := &RedisCookieStore{
Client: client,
Prefix: cookieName,
Block: block,
}
// Create client as usually.
return rs, nil
}

// Store stores the cookie locally and returns a new response cookie value to be
// sent back to the client. That value is used to lookup the cookie later.
func (store *RedisCookieStore) Store(responseCookie *http.Cookie, requestCookie *http.Cookie) (string, error) {
var cookieHandle string
var iv []byte
if requestCookie != nil {
var err error
cookieHandle, iv, err = parseCookieTicket(store.Prefix, requestCookie.Value)
if err != nil {
return "", err
}
} else {
hasher := sha1.New()
hasher.Write([]byte(responseCookie.Value))
cookieId := fmt.Sprintf("%x", hasher.Sum(nil))
iv = make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", fmt.Errorf("failed to create initialization vector %s", err)
}
cookieHandle = fmt.Sprintf("%s-%s", store.Prefix, cookieId)
}

ciphertext := make([]byte, len(responseCookie.Value))
stream := cipher.NewCFBEncrypter(store.Block, iv)
stream.XORKeyStream(ciphertext, []byte(responseCookie.Value))

expires := responseCookie.Expires.Sub(time.Now())
err := store.Client.Set(cookieHandle, ciphertext, expires).Err()
if err != nil {
return "", err
}

cookieTicket := cookieHandle + "." + base64.RawURLEncoding.EncodeToString(iv)
return cookieTicket, nil
}

// Clear takes in the client cookie from the request and uses it to
// clear any lingering server cookies, when possible.
func (store *RedisCookieStore) Clear(requestCookie *http.Cookie) error {
var err error
cookieHandle, _, err := parseCookieTicket(store.Prefix, requestCookie.Value)
if err != nil {
return err
}

err = store.Client.Del(cookieHandle).Err()
if err != nil {
return err
}
return nil
}

// Load takes in the client cookie from the request and uses it to lookup
// the stored value.
func (store *RedisCookieStore) Load(requestCookie *http.Cookie) (string, error) {
cookieHandle, iv, err := parseCookieTicket(store.Prefix, requestCookie.Value)
if err != nil {
return "", err
}

result, err := store.Client.Get(cookieHandle).Result()
if err != nil {
return "", err
}

resultBytes := []byte(result)

stream := cipher.NewCFBDecrypter(store.Block, iv)
stream.XORKeyStream(resultBytes, resultBytes)
return string(resultBytes), nil
}

func parseCookieTicket(expectedPrefix string, ticket string) (string, []byte, error) {
cookieParts := strings.Split(ticket, ".")
if len(cookieParts) != 2 {
return "", nil, fmt.Errorf("failed to decode cookie")
}
cookieHandle, ivBase64 := cookieParts[0], cookieParts[1]
handleParts := strings.Split(cookieHandle, "-")
if len(handleParts) != 2 {
return "", nil, fmt.Errorf("failed to decode cookie handle")
}
prefix, cookieId := handleParts[0], handleParts[1]

// cookieId must be a hexadecimal string
_, err := hex.DecodeString(cookieId)
if err != nil || expectedPrefix != prefix {
return "", nil, fmt.Errorf("server cookie failed sanity checks")
// s is not a valid
}

iv, err := base64.RawURLEncoding.DecodeString(ivBase64)
if err != nil {
return "", nil, fmt.Errorf("failed to decode initialization vector %s", err)
}
return cookieHandle, iv, nil
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens")
flagSet.Var(&jwtIssuers, "extra-jwt-issuers", "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
flagSet.String("redis-connection-url", "", "connection url for redis (activates server-side cookies with redis backend)")

flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
Expand Down
49 changes: 49 additions & 0 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
b64 "encoding/base64"
"errors"
"fmt"
"github.com/go-redis/redis"
"html/template"
"log"
"net"
Expand Down Expand Up @@ -236,6 +237,17 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
}
}

var cookiesStore cookie.ServerCookiesStore
if opts.RedisConnectionUrl != "" {
var err error
cookiesStore, err = cookie.NewRedisCookieStore(opts.RedisConnectionUrl, opts.CookieName, cipher.Block)
if err != nil {
log.Fatal("redis-cookie-store: ", err)
}
parsed, _ := redis.ParseURL(opts.RedisConnectionUrl)
log.Printf("Redis Cookie Store enabled at %v/%v, oauth2_proxy issuing cookie tickets", parsed.Addr, parsed.DB)
}

return &OAuthProxy{
CookieName: opts.CookieName,
CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
Expand All @@ -257,6 +269,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),

ProxyPrefix: opts.ProxyPrefix,
whitelistDomains: opts.WhitelistDomains,
provider: opts.provider,
serveMux: serveMux,
redirectURL: redirectURL,
Expand All @@ -277,6 +290,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
CookieCipher: cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
Footer: opts.Footer,
CookiesStore: cookiesStore,
}
}

Expand Down Expand Up @@ -334,6 +348,26 @@ func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expirati
value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
}
c := p.makeCookie(req, p.CookieName, value, expiration, now)
if p.CookiesStore != nil {
oldCookie, err := req.Cookie(p.CookieName)
// Proactively cleanup old cookies that might be hanging around
for _, requestCookie := range req.Cookies() {
if requestCookie.Name == p.CookieName {
err := p.CookiesStore.Clear(requestCookie)
if err != nil {
log.Printf("Unable to clear cookies: %s", err)
}
}
}

// Store new cookie
newCookieValue, err := p.CookiesStore.Store(c, oldCookie)
if err != nil {
log.Printf("Unable to load cookie: %s", err)
}
responseCookie := p.makeCookie(req, p.CookieName, newCookieValue, p.CookieExpire, now)
return []*http.Cookie{responseCookie}
}
if len(c.Value) > 4096-len(p.CookieName) {
return splitCookie(c)
}
Expand Down Expand Up @@ -472,6 +506,12 @@ func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Reques
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", p.CookieName))

for _, c := range req.Cookies() {
if p.CookiesStore != nil && c.Name == p.CookieName {
err := p.CookiesStore.Clear(c)
if err != nil {
log.Printf("Unable to clear cookie: %s", err)
}
}
if cookieNameRegex.MatchString(c.Name) {
clearCookie := p.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())

Expand Down Expand Up @@ -503,6 +543,15 @@ func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionSt
// always http.ErrNoCookie
return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
}

if p.CookiesStore != nil {
// If using a cookie store, load the actual value
c.Value, err = p.CookiesStore.Load(c)
if err != nil {
return nil, age, fmt.Errorf("Unable to load cookie: %s", err)
}
}

val, timestamp, err := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
if err != nil {
return nil, age, errors.New("Cookie Signature not valid")
Expand Down
9 changes: 9 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

oidc "github.com/coreos/go-oidc"
"github.com/dgrijalva/jwt-go"
"github.com/go-redis/redis"
"github.com/mbland/hmacauth"
"github.com/pusher/oauth2_proxy/providers"
)
Expand All @@ -42,6 +43,7 @@ type Options struct {
GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
RedisConnectionUrl string `flag:"redis-connection-url" cfg:"redis_connection_url"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
Expand Down Expand Up @@ -299,6 +301,13 @@ func (o *Options) Validate() error {
}
}

if o.RedisConnectionUrl != "" {
_, err := redis.ParseURL(o.RedisConnectionUrl)
if err != nil {
msgs = append(msgs, fmt.Sprintf("unable to parse redis url: %s", err))
}
}

if o.CookieRefresh >= o.CookieExpire {
msgs = append(msgs, fmt.Sprintf(
"cookie_refresh (%s) must be less than "+
Expand Down

0 comments on commit 2949456

Please sign in to comment.