Session Control in Golang with Gorilla Sessions
Session control is a fairly old concept: cookies hold some data about the user’s session. For security, we usually only keep a UUID in the cookie, as random as possible, which the server then uses to look up other data, such as which user is logged in.
The UUID needs to be as random as possible, because if someone can predict the next UUID, they can hijack a legitimate user’s session.
Assuming the UUID is secure enough, it’s always safer to keep the session data on the server side. That makes attacks like session poisoning considerably harder.
However, keeping session data on the server side creates a scalability problem. At minimum you need to query a microservice responsible for sessions, which in turn probably queries a database, and in the end you get another bottleneck and a lot more complexity, especially if you want to use any kind of load balancing.
AES-256
An alternative is to keep the session data in the cookie itself and encrypt it with a good algorithm. In our case we’ll use AES-256.
Two things matter for this strategy to work. First, use a good key: it needs to be 32 characters, as random as possible. We’ll use the gorilla/securecookie package to generate the key, which in turn uses crypto/rand. Second, rotate the key periodically to avoid brute-force attacks.
Needless to say, generating the key by hand is out of the question.
What data to store
Since we’re keeping session data in the cookie itself, we need to watch the size we use. We need to stay under the 4096-byte limit. If, for example, we store the OAuth2 access token, that alone costs 2048 bytes, and if we also store the refresh token, that’s another 512 bytes. On top of that, encryption and base64-encoding the binary data both take up extra space.
So be minimalist: store only what’s essential.
If you need more space, it’s a good idea to look at other storage options for the session, such as saving to files on disk using NewFilesystemStore instead of NewCookieStore.
Clearing sessions
There are several situations where you might want to clear the cookie holding the session data, for example when the user wants to log out. Another case is when something goes wrong with the encryption — I prefer to delete the cookie and start over.
Here’s a small utility function to delete cookies:
func clearSession(w http.ResponseWriter, session string) {
cookie := &http.Cookie{
Name: session,
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
}
Setting up storage
The first thing to do is instantiate a way to store the session. In our example we create a store variable that creates an encrypted cookie.
var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32))
As written in the example, every time the service starts it generates a new key. That can be a valid strategy, especially if you don’t want to keep that key anywhere and want it to change every time you ship a new version — with the minor inconvenience that your users lose their session every time you update the system.
Another option is to load that key from some secure system or from an environment variable.
Controlling the session
To create or load a session, we use the code below in an html handler.
session, err := store.Get(r, sessionName)
if err != nil {
clearSession(w, sessionName)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
//http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
In the first line above we instantiate a session variable, which will hold either a new session or the existing one.
Then comes the usual error handling: if something goes wrong, for example if we can’t decrypt the existing session cookie, we first clear the current cookie — it’s useless anyway — and we can redirect the user to the homepage or the login page to get a fresh session cookie. Or we could just return an error.
Saving data
The session is stored in a map of interfaces to interfaces. It’s certainly not my favorite solution, but it’s without a doubt the most flexible. We just need to be careful when reading the data back.
To save any value in the session, we do the following:
session.Values["foo"] = "bar"
session.Values[42] = "The answer to life, the universe and everything"
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
As shown above, both the key and the value can be anything — letters, numbers, etc. — but it’s a good idea to be careful and always validate that you’re reading the correct type, to avoid the system crashing with a panic.
Then just use the Save function to persist the data.
Loading the session
To read a session’s data, we do the same as in the first example and use the Get function to instantiate the session variable, then read the data directly from the Values map.
a, ok := session.Values["key_name"]
if !ok {
http.Error(w, "value not set", http.StatusInternalServerError)
return
}
This is how you read any map in Go: pass the key to the map and get the two possible return values. The first is the value stored in the map, if any, and the second is a boolean — if that second value is true, the value was found; otherwise, you can return an error indicating the value doesn’t exist.
Watch out for interfaces in Go
The values in this map are always an interface, so we need to cast them to the correct type. The problem is that if you try to cast to the wrong type, your system will crash with a panic.
Here’s a small example of how to validate the data’s type before casting.
switch a.(type) {
case string:
w.Write([]byte(a.(string)))
default:
http.Error(w, "value is not type string", http.StatusInternalServerError)
}