Last active
August 29, 2015 13:56
-
-
Save xthexder/9026678 to your computer and use it in GitHub Desktop.
Revel SessionFilter for storing sessions in redis. Adds a config option for setting and verifying CSRF tokens.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package app | |
import ( | |
"crypto/rand" | |
"encoding/hex" | |
"fmt" | |
"net/http" | |
"strings" | |
"time" | |
"github.com/garyburd/redigo/redis" | |
"github.com/robfig/revel" | |
) | |
const ( | |
SESSION_ID_KEY = "_ID" | |
CSRF_TOKEN_KEY = "_CSRF" | |
CSRF_COOKIE = "_CSRF_TOKEN" | |
CSRF_HEADER = "X-CSRF-Token" | |
) | |
var ( | |
expireAfterDuration time.Duration | |
csrfEnabled bool | |
) | |
func init() { | |
// Set expireAfterDuration, default to 30 days if no value in config | |
revel.OnAppStart(func() { | |
var err error | |
if expiresString, ok := revel.Config.String("session.expires"); !ok { | |
expireAfterDuration = 30 * 24 * time.Hour | |
} else if expiresString == "session" { | |
expireAfterDuration = 0 | |
} else if expireAfterDuration, err = time.ParseDuration(expiresString); err != nil { | |
panic(fmt.Errorf("session.expires invalid: %s", err)) | |
} | |
csrfEnabled = revel.Config.BoolDefault("session.csrf", false) | |
}) | |
} | |
// Saves the session into redis | |
// Returns an http.Cookie containing the signed session token or nil on error | |
func saveSession(s revel.Session) *http.Cookie { | |
if len(s) == 0 { | |
return nil | |
} | |
var sessionValue string | |
sessionToken := s.Id() | |
if _, ok := s[CSRF_TOKEN_KEY]; !ok && csrfEnabled { | |
buf := make([]byte, 32) | |
_, err := rand.Read(buf) | |
if err == nil { | |
s[CSRF_TOKEN_KEY] = hex.EncodeToString(buf) | |
} else { | |
revel.ERROR.Println(err) | |
} | |
} | |
for key, value := range s { | |
if key == SESSION_ID_KEY { | |
continue | |
} | |
if strings.ContainsAny(key, ":\x00") { | |
panic("Session keys may not have colons or null bytes") | |
} | |
if strings.Contains(value, "\x00") { | |
panic("Session values may not have null bytes") | |
} | |
sessionValue += "\x00" + key + ":" + value + "\x00" | |
} | |
redisConn := Redis.Get() | |
defer redisConn.Close() | |
params := []interface{}{"session:" + sessionToken, sessionValue} | |
if expireAfterDuration != 0 { | |
params = append(params, "EX", int(expireAfterDuration/time.Second)) | |
} | |
_, err := redisConn.Do("SET", params...) | |
if err != nil { | |
revel.ERROR.Println(err) | |
return nil | |
} | |
cookie := &http.Cookie{ | |
Name: revel.CookiePrefix + "_SESSION", | |
Value: revel.Sign(sessionToken) + "-" + sessionToken, | |
Path: "/", | |
HttpOnly: revel.CookieHttpOnly, | |
Secure: revel.CookieSecure, | |
} | |
if expireAfterDuration != 0 { | |
cookie.Expires = time.Now().Add(expireAfterDuration).UTC() | |
} else { | |
cookie.Expires = time.Time{}.UTC() | |
} | |
return cookie | |
} | |
// Returns a Session from redis with the session id pulled from a signed cookie. | |
// Returns a blank session if the session has expired or there was an error | |
func restoreSession(req *http.Request) revel.Session { | |
session := make(revel.Session) | |
cookie, err := req.Cookie(revel.CookiePrefix + "_SESSION") | |
if err != nil { | |
return session | |
} | |
// Separate the token from the signature. | |
hyphen := strings.Index(cookie.Value, "-") | |
if hyphen == -1 || hyphen >= len(cookie.Value)-1 { | |
return session | |
} | |
sig, token := cookie.Value[:hyphen], cookie.Value[hyphen+1:] | |
// Verify the signature. | |
if !revel.Verify(token, sig) { | |
revel.INFO.Println("Session cookie signature failed") | |
return session | |
} | |
redisConn := Redis.Get() | |
defer redisConn.Close() | |
data, err := redis.String(redisConn.Do("GET", "session:"+token)) | |
if err != nil { | |
revel.ERROR.Println(err) | |
return session | |
} | |
revel.ParseKeyValueCookie(data, func(key, val string) { | |
session[key] = val | |
}) | |
session[SESSION_ID_KEY] = token | |
if csrfToken, ok := session[CSRF_TOKEN_KEY]; ok && csrfEnabled { | |
csrfCookie, err := req.Cookie(revel.CookiePrefix + CSRF_COOKIE) | |
if err != nil || csrfCookie.Value != csrfToken { | |
err = RevokeSession(&session) | |
if err != nil { | |
revel.ERROR.Println(err) | |
} | |
return session | |
} | |
if req.Method != "GET" && req.Method != "HEAD" { | |
csrfHeader := req.Header.Get(CSRF_HEADER) | |
if csrfHeader != csrfToken { | |
err = RevokeSession(&session) | |
if err != nil { | |
revel.ERROR.Println(err) | |
} | |
return session | |
} | |
} | |
} | |
return session | |
} | |
func RevokeSession(s *revel.Session) error { | |
token, ok := (*s)[SESSION_ID_KEY] | |
*s = make(revel.Session) | |
if !ok { | |
return nil | |
} | |
revel.INFO.Println("Revoking session:", token) | |
redisConn := Redis.Get() | |
defer redisConn.Close() | |
_, err := redis.Int(redisConn.Do("DEL", "session:"+token)) | |
return err | |
} | |
func SessionFilter(c *revel.Controller, fc []revel.Filter) { | |
c.Session = restoreSession(c.Request.Request) | |
// Make session vars available in templates as {{.session.xyz}} | |
c.RenderArgs["session"] = c.Session | |
fc[0](c, fc[1:]) | |
// Store the session (and sign it) if it isn't empty. | |
cookie := saveSession(c.Session) | |
if cookie != nil { | |
c.SetCookie(cookie) | |
if csrfEnabled { | |
c.SetCookie(&http.Cookie{ | |
Name: revel.CookiePrefix + CSRF_COOKIE, | |
Value: c.Session[CSRF_TOKEN_KEY], | |
Path: "/", | |
Expires: cookie.Expires, | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi, thanks for this gist, but how to use this?
(Where should i put it on revel and is there anything i should write after putting the file?)