Chapter 6. Sessions and Cookies
Our application is beginning to get a little more real now; in the previous chapter, we added some APIs and client-side interfaces to them.
In our application's current state, we've added /api/comments
, /api/comments/[id]
, /api/pages
, and /api/pages/[id]
, thus making it possible for us to get and update our data in JSON format and making the application better suited for Ajax and client-side access.
Though we can now add comments and edit them directly through our API, there is absolutely no restriction on who can perform these actions. In this chapter, we'll look at the ways to limit access to certain assets, establishing identities, and securely authenticating when we have them.
By the end, we should be able to enable users to register and log in and utilize sessions, cookies, and flash messages to keep user state in our application in a secure way.
Setting cookies
The most common, fundamental, and simplest way to create persistent memory across a user's session is by utilizing cookies.
Cookies provide a way to share state information across requests, URL endpoints, and even domains, and they have been used (and abused) in every possible way.
Most often, they're used to keep a track of identity. When a user logs into a service, successive requests can access some aspects of the previous request (without duplicating a lookup or the login module) by utilizing the session information stored in a cookie.
If you're familiar with cookies in any other language's implementation, the basic struct
will look familiar. Even so, the following relevant attributes are fairly lockstep with the way a cookie is presented to the client:
type Cookie struct { Name string Value string Path string Domain string Expires time.Time RawExpires string MaxAge int Secure bool HttpOnly bool Raw string Unparsed []string }
That's a lot of attributes for a very basic struct
, so let's focus on the important ones.
The Name
attribute is simply a key for the cookie. The Value
attribute represents its contents and Expires
is a Time
value for the moment when the cookie should be flushed by a browser or another headless recipient. This is all you need in order to set a valid cookie that lasts in Go.
Beyond the basics, you may find setting a Path
, Domain
, and HttpOnly
useful, if you want to lock down the accessibility of the cookie.
Capturing user information
When a user with a valid session and/
or cookie attempts to access restricted data, we need to get that from the user's browser.
A session itself is just that—a single session on the site. It doesn't naturally persist indefinitely, so we need to leave a breadcrumb, but we also want to leave one that's relatively secure.
For example, we would never want to leave critical user information in the cookie, such as name, address, email, and so on.
However, any time we have some identifying information, we leave some vector for misdeed—in this case we'll likely leave a session identifier that represents our session ID. The vector in this case allows someone, who obtains this cookie, to log in as one of our users and change information, find billing details, and so on.
These types of physical attack vectors are well outside the scope of this (and most) application and to a large degree, it's a concession that if someone loses access to their physical machine, they can also have their account compromised.
What we want to do here is ensure that we're not transmitting personal or sensitive information over clear text or without a secure connection. We'll cover setting up TLS in Chapter 9, Security, so here we want to focus on limiting the amount of information we store in our cookies.
Creating users
In the previous chapter, we allowed non-authorized requests to create new comments by hitting our REST API via a POST
. Anyone who's been on the Internet for a while knows a few truisms, such as:
- The comments section is often the most toxic part of any blog or news post
- Step 1 is true, even when users have to authenticate in non-anonymous ways
Now, let's lock down the comments section to ensure that users have registered themselves and are logged in.
We won't go deep into the authentication's security aspects now, as we'll be going deeper with that in Chapter 9, Security.
First, let's add a users
table in our database:
CREATE TABLE `users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_name` varchar(32) NOT NULL DEFAULT '', `user_guid` varchar(256) NOT NULL DEFAULT '', `user_email` varchar(128) NOT NULL DEFAULT '', `user_password` varchar(128) NOT NULL DEFAULT '', `user_salt` varchar(128) NOT NULL DEFAULT '', `user_joined_timestamp` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
We could surely go a lot deeper with user information, but this is enough to get us started. As mentioned, we won't go too deep into security, so we'll just generate a hash for the password now and not worry about the salt.
Finally, to enable sessions and users in the app, we'll make some changes to our structs:
type Page struct { Id int Title string RawContent string Content template.HTML Date string Comments []Comment Session Session } type User struct { Id int Name string } type Session struct { Id string Authenticated bool Unauthenticated bool User User }
And here are the two stub handlers for registration and logging in. Again, we're not putting our full effort into fleshing these out into something robust, we just want to open the door a bit.
Enabling sessions
In addition to storing the users themselves, we'll also want some way of persistent memory for accessing our cookie data. In other words, when a user's browser session ends and they come back, we'll validate and reconcile their cookie value against values in our database.
Use this SQL to create the sessions
table:
CREATE TABLE `sessions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `session_id` varchar(256) NOT NULL DEFAULT '', `user_id` int(11) DEFAULT NULL, `session_start` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `session_update` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', `session_active` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The most important values are the user_id
, session_id
, and the timestamps for updating and starting. We can use the latter two to decide if a session is actually valid after a certain period. This is a good security practice, just because a user has a valid cookie doesn't necessarily mean that they should remain authenticated, particularly if you're not using a secure connection.
Letting users register
To be able to allow users to create accounts themselves, we'll need a form for both registering and logging in. Now, most systems similar to this do some multi-factor authentication to allow a user backup system for retrieval as well as validation that the user is real and unique. We'll get there, but for now let's keep it as simple as possible.
We'll set up the following endpoints to allow a user to POST
both the register and login forms:
routes.HandleFunc("/register", RegisterPOST). Methods("POST"). Schemes("https") routes.HandleFunc("/login", LoginPOST). Methods("POST"). Schemes("https")
Keep in mind that these are presently set to the HTTPS scheme. If you're not using that, remove that part of the HandleFunc
register.
Since we're only showing these following views to unauthenticated users, we can put them on our blog.html
template and wrap them in {{if .Session.Unauthenticated}} … {{end}}
template snippets. We defined .Unauthenticated
and .Authenticated
in the application under the Session
struct
, as shown in the following example:
{{if .Session.Unauthenticated}}<form action="/register" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="email" name="user_email" placeholder="Your email" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="password" name="user_password2" placeholder="Password (repeat)" /></div> <div><input type="submit" value="Register" /></div> </form>{{end}}
And our /register
endpoint:
func RegisterPOST(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { log.Fatal(err.Error) } name := r.FormValue("user_name") email := r.FormValue("user_email") pass := r.FormValue("user_password") pageGUID := r.FormValue("referrer") // pass2 := r.FormValue("user_password2") gure := regexp.MustCompile("[^A-Za-z0-9]+") guid := gure.ReplaceAllString(name, "") password := weakPasswordHash(pass) res, err := database.Exec("INSERT INTO users SET user_name=?, user_guid=?, user_email=?, user_password=?", name, guid, email, password) fmt.Println(res) if err != nil { fmt.Fprintln(w, err.Error) } else { http.Redirect(w, r, "/page/"+pageGUID, 301) } }
Note that this fails inelegantly for a number of reasons. If the passwords do not match, we don't check and report to the user. If the user already exists, we don't tell them the reason for a registration failure. We'll get to that, but now our main intent is producing a session.
For reference, here's our weakPasswordHash
function, which is only intended to generate a hash for testing:
func weakPasswordHash(password string) []byte { hash := sha1.New() io.WriteString(hash, password) return hash.Sum(nil) }
Letting users log in
A user may be already registered; in which case, we'll also want to provide a login mechanism on the same page. This can obviously be subject to better design considerations, but we just want to make them both available:
<form action="/login" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="submit" value="Log in" /></div> </form>
And then we'll need receiving endpoints for each POSTed form. We're not going to do a lot of validation here either, but we're not in a position to validate a session.
Creating users
In the previous chapter, we allowed non-authorized requests to create new comments by hitting our REST API via a POST
. Anyone who's been on the Internet for a while knows a few truisms, such as:
- The comments section is often the most toxic part of any blog or news post
- Step 1 is true, even when users have to authenticate in non-anonymous ways
Now, let's lock down the comments section to ensure that users have registered themselves and are logged in.
We won't go deep into the authentication's security aspects now, as we'll be going deeper with that in Chapter 9, Security.
First, let's add a users
table in our database:
CREATE TABLE `users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_name` varchar(32) NOT NULL DEFAULT '', `user_guid` varchar(256) NOT NULL DEFAULT '', `user_email` varchar(128) NOT NULL DEFAULT '', `user_password` varchar(128) NOT NULL DEFAULT '', `user_salt` varchar(128) NOT NULL DEFAULT '', `user_joined_timestamp` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
We could surely go a lot deeper with user information, but this is enough to get us started. As mentioned, we won't go too deep into security, so we'll just generate a hash for the password now and not worry about the salt.
Finally, to enable sessions and users in the app, we'll make some changes to our structs:
type Page struct { Id int Title string RawContent string Content template.HTML Date string Comments []Comment Session Session } type User struct { Id int Name string } type Session struct { Id string Authenticated bool Unauthenticated bool User User }
And here are the two stub handlers for registration and logging in. Again, we're not putting our full effort into fleshing these out into something robust, we just want to open the door a bit.
Enabling sessions
In addition to storing the users themselves, we'll also want some way of persistent memory for accessing our cookie data. In other words, when a user's browser session ends and they come back, we'll validate and reconcile their cookie value against values in our database.
Use this SQL to create the sessions
table:
CREATE TABLE `sessions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `session_id` varchar(256) NOT NULL DEFAULT '', `user_id` int(11) DEFAULT NULL, `session_start` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `session_update` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', `session_active` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The most important values are the user_id
, session_id
, and the timestamps for updating and starting. We can use the latter two to decide if a session is actually valid after a certain period. This is a good security practice, just because a user has a valid cookie doesn't necessarily mean that they should remain authenticated, particularly if you're not using a secure connection.
Letting users register
To be able to allow users to create accounts themselves, we'll need a form for both registering and logging in. Now, most systems similar to this do some multi-factor authentication to allow a user backup system for retrieval as well as validation that the user is real and unique. We'll get there, but for now let's keep it as simple as possible.
We'll set up the following endpoints to allow a user to POST
both the register and login forms:
routes.HandleFunc("/register", RegisterPOST). Methods("POST"). Schemes("https") routes.HandleFunc("/login", LoginPOST). Methods("POST"). Schemes("https")
Keep in mind that these are presently set to the HTTPS scheme. If you're not using that, remove that part of the HandleFunc
register.
Since we're only showing these following views to unauthenticated users, we can put them on our blog.html
template and wrap them in {{if .Session.Unauthenticated}} … {{end}}
template snippets. We defined .Unauthenticated
and .Authenticated
in the application under the Session
struct
, as shown in the following example:
{{if .Session.Unauthenticated}}<form action="/register" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="email" name="user_email" placeholder="Your email" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="password" name="user_password2" placeholder="Password (repeat)" /></div> <div><input type="submit" value="Register" /></div> </form>{{end}}
And our /register
endpoint:
func RegisterPOST(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { log.Fatal(err.Error) } name := r.FormValue("user_name") email := r.FormValue("user_email") pass := r.FormValue("user_password") pageGUID := r.FormValue("referrer") // pass2 := r.FormValue("user_password2") gure := regexp.MustCompile("[^A-Za-z0-9]+") guid := gure.ReplaceAllString(name, "") password := weakPasswordHash(pass) res, err := database.Exec("INSERT INTO users SET user_name=?, user_guid=?, user_email=?, user_password=?", name, guid, email, password) fmt.Println(res) if err != nil { fmt.Fprintln(w, err.Error) } else { http.Redirect(w, r, "/page/"+pageGUID, 301) } }
Note that this fails inelegantly for a number of reasons. If the passwords do not match, we don't check and report to the user. If the user already exists, we don't tell them the reason for a registration failure. We'll get to that, but now our main intent is producing a session.
For reference, here's our weakPasswordHash
function, which is only intended to generate a hash for testing:
func weakPasswordHash(password string) []byte { hash := sha1.New() io.WriteString(hash, password) return hash.Sum(nil) }
Letting users log in
A user may be already registered; in which case, we'll also want to provide a login mechanism on the same page. This can obviously be subject to better design considerations, but we just want to make them both available:
<form action="/login" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="submit" value="Log in" /></div> </form>
And then we'll need receiving endpoints for each POSTed form. We're not going to do a lot of validation here either, but we're not in a position to validate a session.
Enabling sessions
In addition to storing the users themselves, we'll also want some way of persistent memory for accessing our cookie data. In other words, when a user's browser session ends and they come back, we'll validate and reconcile their cookie value against values in our database.
Use this SQL to create the sessions
table:
CREATE TABLE `sessions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `session_id` varchar(256) NOT NULL DEFAULT '', `user_id` int(11) DEFAULT NULL, `session_start` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `session_update` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', `session_active` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The most important values are the user_id
, session_id
, and the timestamps for updating and starting. We can use the latter two to decide if a session is actually valid after a certain period. This is a good security practice, just because a user has a valid cookie doesn't necessarily mean that they should remain authenticated, particularly if you're not using a secure connection.
Letting users register
To be able to allow users to create accounts themselves, we'll need a form for both registering and logging in. Now, most systems similar to this do some multi-factor authentication to allow a user backup system for retrieval as well as validation that the user is real and unique. We'll get there, but for now let's keep it as simple as possible.
We'll set up the following endpoints to allow a user to POST
both the register and login forms:
routes.HandleFunc("/register", RegisterPOST). Methods("POST"). Schemes("https") routes.HandleFunc("/login", LoginPOST). Methods("POST"). Schemes("https")
Keep in mind that these are presently set to the HTTPS scheme. If you're not using that, remove that part of the HandleFunc
register.
Since we're only showing these following views to unauthenticated users, we can put them on our blog.html
template and wrap them in {{if .Session.Unauthenticated}} … {{end}}
template snippets. We defined .Unauthenticated
and .Authenticated
in the application under the Session
struct
, as shown in the following example:
{{if .Session.Unauthenticated}}<form action="/register" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="email" name="user_email" placeholder="Your email" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="password" name="user_password2" placeholder="Password (repeat)" /></div> <div><input type="submit" value="Register" /></div> </form>{{end}}
And our /register
endpoint:
func RegisterPOST(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { log.Fatal(err.Error) } name := r.FormValue("user_name") email := r.FormValue("user_email") pass := r.FormValue("user_password") pageGUID := r.FormValue("referrer") // pass2 := r.FormValue("user_password2") gure := regexp.MustCompile("[^A-Za-z0-9]+") guid := gure.ReplaceAllString(name, "") password := weakPasswordHash(pass) res, err := database.Exec("INSERT INTO users SET user_name=?, user_guid=?, user_email=?, user_password=?", name, guid, email, password) fmt.Println(res) if err != nil { fmt.Fprintln(w, err.Error) } else { http.Redirect(w, r, "/page/"+pageGUID, 301) } }
Note that this fails inelegantly for a number of reasons. If the passwords do not match, we don't check and report to the user. If the user already exists, we don't tell them the reason for a registration failure. We'll get to that, but now our main intent is producing a session.
For reference, here's our weakPasswordHash
function, which is only intended to generate a hash for testing:
func weakPasswordHash(password string) []byte { hash := sha1.New() io.WriteString(hash, password) return hash.Sum(nil) }
Letting users log in
A user may be already registered; in which case, we'll also want to provide a login mechanism on the same page. This can obviously be subject to better design considerations, but we just want to make them both available:
<form action="/login" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="submit" value="Log in" /></div> </form>
And then we'll need receiving endpoints for each POSTed form. We're not going to do a lot of validation here either, but we're not in a position to validate a session.
Letting users register
To be able to allow users to create accounts themselves, we'll need a form for both registering and logging in. Now, most systems similar to this do some multi-factor authentication to allow a user backup system for retrieval as well as validation that the user is real and unique. We'll get there, but for now let's keep it as simple as possible.
We'll set up the following endpoints to allow a user to POST
both the register and login forms:
routes.HandleFunc("/register", RegisterPOST). Methods("POST"). Schemes("https") routes.HandleFunc("/login", LoginPOST). Methods("POST"). Schemes("https")
Keep in mind that these are presently set to the HTTPS scheme. If you're not using that, remove that part of the HandleFunc
register.
Since we're only showing these following views to unauthenticated users, we can put them on our blog.html
template and wrap them in {{if .Session.Unauthenticated}} … {{end}}
template snippets. We defined .Unauthenticated
and .Authenticated
in the application under the Session
struct
, as shown in the following example:
{{if .Session.Unauthenticated}}<form action="/register" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="email" name="user_email" placeholder="Your email" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="password" name="user_password2" placeholder="Password (repeat)" /></div> <div><input type="submit" value="Register" /></div> </form>{{end}}
And our /register
endpoint:
func RegisterPOST(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { log.Fatal(err.Error) } name := r.FormValue("user_name") email := r.FormValue("user_email") pass := r.FormValue("user_password") pageGUID := r.FormValue("referrer") // pass2 := r.FormValue("user_password2") gure := regexp.MustCompile("[^A-Za-z0-9]+") guid := gure.ReplaceAllString(name, "") password := weakPasswordHash(pass) res, err := database.Exec("INSERT INTO users SET user_name=?, user_guid=?, user_email=?, user_password=?", name, guid, email, password) fmt.Println(res) if err != nil { fmt.Fprintln(w, err.Error) } else { http.Redirect(w, r, "/page/"+pageGUID, 301) } }
Note that this fails inelegantly for a number of reasons. If the passwords do not match, we don't check and report to the user. If the user already exists, we don't tell them the reason for a registration failure. We'll get to that, but now our main intent is producing a session.
For reference, here's our weakPasswordHash
function, which is only intended to generate a hash for testing:
func weakPasswordHash(password string) []byte { hash := sha1.New() io.WriteString(hash, password) return hash.Sum(nil) }
Letting users log in
A user may be already registered; in which case, we'll also want to provide a login mechanism on the same page. This can obviously be subject to better design considerations, but we just want to make them both available:
<form action="/login" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="submit" value="Log in" /></div> </form>
And then we'll need receiving endpoints for each POSTed form. We're not going to do a lot of validation here either, but we're not in a position to validate a session.
Letting users log in
A user may be already registered; in which case, we'll also want to provide a login mechanism on the same page. This can obviously be subject to better design considerations, but we just want to make them both available:
<form action="/login" method="POST"> <div><input type="text" name="user_name" placeholder="User name" /></div> <div><input type="password" name="user_password" placeholder="Password" /></div> <div><input type="submit" value="Log in" /></div> </form>
And then we'll need receiving endpoints for each POSTed form. We're not going to do a lot of validation here either, but we're not in a position to validate a session.
Initiating a server-side session
One of the most common ways of authenticating a user and saving their state on the Web is through sessions. You may recall that we mentioned in the last chapter that REST is stateless, the primary reason for that is because HTTP itself is stateless.
If you think about it, to establish a consistent state with HTTP, you need to include a cookie or a URL parameter or something that is not built into the protocol itself.
Sessions are created with unique identifiers that are usually not entirely random but unique enough to avoid conflicts for most logical and plausible scenarios. This is not absolute, of course, and there are plenty of (historical) examples of session token hijacking that are not related to sniffing.
Session support as a standalone process does not exist in Go core. Given that we have a storage system on the server side, this is somewhat irrelevant. If we create a safe process for generation of server keys, we can store them in secure cookies.
But generating session tokens is not completely trivial. We can do this using a set of available cryptographic methods, but with session hijacking as a very prevalent way of getting into systems without authorization, that may be a point of insecurity in our application.
Since we're already using the Gorilla toolkit, the good news is that we don't have to reinvent the wheel, there's a robust session system in place.
Not only do we have access to a server-side session, but we get a very convenient tool for one-time messages within a session. These work somewhat similar to a message queue in the manner that once data goes into them, the flash message is no longer valid when that data is retrieved.
Creating a store
To utilize the Gorilla sessions, we first need to invoke a cookie store, which will hold all the variables that we want to keep associated with a user. You can test this out pretty easily by the following code:
package main import ( "fmt" "github.com/gorilla/sessions" "log" "net/http" ) func cookieHandler(w http.ResponseWriter, r *http.Request) { var cookieStore = sessions.NewCookieStore([]byte("ideally, some random piece of entropy")) session, _ := cookieStore.Get(r, "mystore") if value, exists := session.Values["hello"]; exists { fmt.Fprintln(w, value) } else { session.Values["hello"] = "(world)" session.Save(r, w) fmt.Fprintln(w, "We just set the value!") } } func main() { http.HandleFunc("/test", cookieHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
The first time you hit your URL and endpoint, you'll see We just set the value!, as shown in the following screenshot:
In the second request, you should see (world), as shown in the following screenshot:
A couple of notes here. First, you must set cookies before sending anything else through your io.Writer
(in this case the ResponseWriter w
). If you flip these lines:
session.Save(r, w) fmt.Fprintln(w, "We just set the value!")
You can see this in action. You'll never get the value set to your cookie store.
So now, let's apply it to our application. We will want to initiate a session store before any requests to /login
or /register
.
We'll initialize a global sessionStore
:
var database *sql.DB var sessionStore = sessions.NewCookieStore([]byte("our-social-network-application"))
Feel free to group these, as well, in a var ()
. Next, we'll want to create four simple functions that will get an active session, update a current one, generate a session ID, and evaluate an existing cookie. These will allow us to check if a user is logged in by a cookie's session ID and enable persistent logins.
First, the getSessionUID
function, which will return a user's ID if a session already exists:
func getSessionUID(sid string) int { user := User{} err := database.QueryRow("SELECT user_id FROM sessions WHERE session_id=?", sid).Scan(user.Id) if err != nil { fmt.Println(err.Error) return 0 } return user.Id }
Next, the update function, which will be called with every front-facing request, thus enabling a timestamp update or inclusion of a user ID if a new log in is attempted:
func updateSession(sid string, uid int) { const timeFmt = "2006-01-02T15:04:05.999999999" tstamp := time.Now().Format(timeFmt) _, err := database.Exec("INSERT INTO sessions SET session_id=?, user_id=?, session_update=? ON DUPLICATE KEY UPDATE user_id=?, session_update=?", sid, uid, tstamp, uid, tstamp) if err != nil { fmt.Println(err.Error) } }
An important part is the ability to generate a strongly-random byte array (cast to string) that will allow unique identifiers. We do that with the following generateSessionId()
function:
func generateSessionId() string { sid := make([]byte, 24) _, err := io.ReadFull(rand.Reader, sid) if err != nil { log.Fatal("Could not generate session id") } return base64.URLEncoding.EncodeToString(sid) }
And finally, we have the function that will be called with every request to check for a cookie's session or create one if it doesn't exist.
func validateSession(w http.ResponseWriter, r *http.Request) { session, _ := sessionStore.Get(r, "app-session") if sid, valid := session.Values["sid"]; valid { currentUID := getSessionUID(sid.(string)) updateSession(sid.(string), currentUID) UserSession.Id = string(currentUID) } else { newSID := generateSessionId() session.Values["sid"] = newSID session.Save(r, w) UserSession.Id = newSID updateSession(newSID, 0) } fmt.Println(session.ID) }
This is predicated on having a global Session struct
, in this case defined as:
var UserSession Session
This leaves us with just one piece—to call validateSession()
on our ServePage()
method and LoginPost()
method and then validate the passwords on the latter and update our session on a successful login attempt:
func LoginPOST(w http.ResponseWriter, r *http.Request) { validateSession(w, r)
In our previously defined check against the form values, if a valid user is found, we'll update the session directly:
u := User{}
name := r.FormValue("user_name")
pass := r.FormValue("user_password")
password := weakPasswordHash(pass)
err := database.QueryRow("SELECT user_id, user_name FROM users WHERE user_name=? and user_password=?", name, password).Scan(&u.Id, &u.Name)
if err != nil {
fmt.Fprintln(w, err.Error)
u.Id = 0
u.Name = ""
} else {
updateSession(UserSession.Id, u.Id)
fmt.Fprintln(w, u.Name)
}
Utilizing flash messages
As mentioned earlier in this chapter, Gorilla sessions offer a simple system to utilize a single-use and cookie-based data transfer between requests.
The idea behind a flash message is not all that different than an in-browser/server message queue. It's most frequently utilized in a process such as this:
- A form is POSTed
- The data is processed
- A header redirect is initiated
- The resulting page needs some access to information about the
POST
process (success, error)
At the end of this process, the message should be removed so that the message is not duplicated erroneously at some other point. Gorilla makes this incredibly easy, and we'll look at that shortly, but it makes sense to show a quick example of how this can be accomplished in native Go.
To start, we'll create a simple HTTP server that includes a starting point handler called startHandler
:
package main import ( "fmt" "html/template" "log" "net/http" "time" ) var ( templates = template.Must(template.ParseGlob("templates/*")) port = ":8080" ) func startHandler(w http.ResponseWriter, r *http.Request) { err := templates.ExecuteTemplate(w, "ch6-flash.html", nil) if err != nil { log.Fatal("Template ch6-flash missing") } }
We're not doing anything special here, just rendering our form:
func middleHandler(w http.ResponseWriter, r *http.Request) { cookieValue := r.PostFormValue("message") cookie := http.Cookie{Name: "message", Value: "message:" + cookieValue, Expires: time.Now().Add(60 * time.Second), HttpOnly: true} http.SetCookie(w, &cookie) http.Redirect(w, r, "/finish", 301) }
Our middleHandler
demonstrates creating cookies through a Cookie struct
, as described earlier in this chapter. There's nothing important to note here except the fact that you may want to extend the expiration out a bit, just to ensure that there's no way a cookie could expire (naturally) between requests:
func finishHandler(w http.ResponseWriter, r *http.Request) { cookieVal, _ := r.Cookie("message") if cookieVal != nil { fmt.Fprintln(w, "We found: "+string(cookieVal.Value)+", but try to refresh!") cookie := http.Cookie{Name: "message", Value: "", Expires: time.Now(), HttpOnly: true} http.SetCookie(w, &cookie) } else { fmt.Fprintln(w, "That cookie was gone in a flash") } }
The finishHandler
function does the magic of a flash message—removes the cookie if and only if a value has been found. This ensures that the cookie is a one-time retrievable value:
func main() { http.HandleFunc("/start", startHandler) http.HandleFunc("/middle", middleHandler) http.HandleFunc("/finish", finishHandler) log.Fatal(http.ListenAndServe(port, nil)) }
The following example is our HTML for POSTing our cookie value to the /middle
handler:
<html> <head><title>Flash Message</title></head> <body> <form action="/middle" method="POST"> <input type="text" name="message" /> <input type="submit" value="Send Message" /> </form> </body> </html>
If you do as the page suggests and refresh again, the cookie value would have been removed and the page will not render, as you've previously seen.
To begin the flash message, we hit our /start
endpoint and enter an intended value and then click on the Send Message button:
At this point, we'll be sent to the /middle
endpoint, which will set the cookie value and HTTP redirect to /finish
:
And now we can see our value. Since the /finish
endpoint handler also unsets the cookie, we'll be unable to retrieve that value again. Here's what happens if we do what /finish
tells us on its first appearance:
That's all for now.
Creating a store
To utilize the Gorilla sessions, we first need to invoke a cookie store, which will hold all the variables that we want to keep associated with a user. You can test this out pretty easily by the following code:
package main import ( "fmt" "github.com/gorilla/sessions" "log" "net/http" ) func cookieHandler(w http.ResponseWriter, r *http.Request) { var cookieStore = sessions.NewCookieStore([]byte("ideally, some random piece of entropy")) session, _ := cookieStore.Get(r, "mystore") if value, exists := session.Values["hello"]; exists { fmt.Fprintln(w, value) } else { session.Values["hello"] = "(world)" session.Save(r, w) fmt.Fprintln(w, "We just set the value!") } } func main() { http.HandleFunc("/test", cookieHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
The first time you hit your URL and endpoint, you'll see We just set the value!, as shown in the following screenshot:
In the second request, you should see (world), as shown in the following screenshot:
A couple of notes here. First, you must set cookies before sending anything else through your io.Writer
(in this case the ResponseWriter w
). If you flip these lines:
session.Save(r, w) fmt.Fprintln(w, "We just set the value!")
You can see this in action. You'll never get the value set to your cookie store.
So now, let's apply it to our application. We will want to initiate a session store before any requests to /login
or /register
.
We'll initialize a global sessionStore
:
var database *sql.DB var sessionStore = sessions.NewCookieStore([]byte("our-social-network-application"))
Feel free to group these, as well, in a var ()
. Next, we'll want to create four simple functions that will get an active session, update a current one, generate a session ID, and evaluate an existing cookie. These will allow us to check if a user is logged in by a cookie's session ID and enable persistent logins.
First, the getSessionUID
function, which will return a user's ID if a session already exists:
func getSessionUID(sid string) int { user := User{} err := database.QueryRow("SELECT user_id FROM sessions WHERE session_id=?", sid).Scan(user.Id) if err != nil { fmt.Println(err.Error) return 0 } return user.Id }
Next, the update function, which will be called with every front-facing request, thus enabling a timestamp update or inclusion of a user ID if a new log in is attempted:
func updateSession(sid string, uid int) { const timeFmt = "2006-01-02T15:04:05.999999999" tstamp := time.Now().Format(timeFmt) _, err := database.Exec("INSERT INTO sessions SET session_id=?, user_id=?, session_update=? ON DUPLICATE KEY UPDATE user_id=?, session_update=?", sid, uid, tstamp, uid, tstamp) if err != nil { fmt.Println(err.Error) } }
An important part is the ability to generate a strongly-random byte array (cast to string) that will allow unique identifiers. We do that with the following generateSessionId()
function:
func generateSessionId() string { sid := make([]byte, 24) _, err := io.ReadFull(rand.Reader, sid) if err != nil { log.Fatal("Could not generate session id") } return base64.URLEncoding.EncodeToString(sid) }
And finally, we have the function that will be called with every request to check for a cookie's session or create one if it doesn't exist.
func validateSession(w http.ResponseWriter, r *http.Request) { session, _ := sessionStore.Get(r, "app-session") if sid, valid := session.Values["sid"]; valid { currentUID := getSessionUID(sid.(string)) updateSession(sid.(string), currentUID) UserSession.Id = string(currentUID) } else { newSID := generateSessionId() session.Values["sid"] = newSID session.Save(r, w) UserSession.Id = newSID updateSession(newSID, 0) } fmt.Println(session.ID) }
This is predicated on having a global Session struct
, in this case defined as:
var UserSession Session
This leaves us with just one piece—to call validateSession()
on our ServePage()
method and LoginPost()
method and then validate the passwords on the latter and update our session on a successful login attempt:
func LoginPOST(w http.ResponseWriter, r *http.Request) { validateSession(w, r)
In our previously defined check against the form values, if a valid user is found, we'll update the session directly:
u := User{}
name := r.FormValue("user_name")
pass := r.FormValue("user_password")
password := weakPasswordHash(pass)
err := database.QueryRow("SELECT user_id, user_name FROM users WHERE user_name=? and user_password=?", name, password).Scan(&u.Id, &u.Name)
if err != nil {
fmt.Fprintln(w, err.Error)
u.Id = 0
u.Name = ""
} else {
updateSession(UserSession.Id, u.Id)
fmt.Fprintln(w, u.Name)
}
Utilizing flash messages
As mentioned earlier in this chapter, Gorilla sessions offer a simple system to utilize a single-use and cookie-based data transfer between requests.
The idea behind a flash message is not all that different than an in-browser/server message queue. It's most frequently utilized in a process such as this:
- A form is POSTed
- The data is processed
- A header redirect is initiated
- The resulting page needs some access to information about the
POST
process (success, error)
At the end of this process, the message should be removed so that the message is not duplicated erroneously at some other point. Gorilla makes this incredibly easy, and we'll look at that shortly, but it makes sense to show a quick example of how this can be accomplished in native Go.
To start, we'll create a simple HTTP server that includes a starting point handler called startHandler
:
package main import ( "fmt" "html/template" "log" "net/http" "time" ) var ( templates = template.Must(template.ParseGlob("templates/*")) port = ":8080" ) func startHandler(w http.ResponseWriter, r *http.Request) { err := templates.ExecuteTemplate(w, "ch6-flash.html", nil) if err != nil { log.Fatal("Template ch6-flash missing") } }
We're not doing anything special here, just rendering our form:
func middleHandler(w http.ResponseWriter, r *http.Request) { cookieValue := r.PostFormValue("message") cookie := http.Cookie{Name: "message", Value: "message:" + cookieValue, Expires: time.Now().Add(60 * time.Second), HttpOnly: true} http.SetCookie(w, &cookie) http.Redirect(w, r, "/finish", 301) }
Our middleHandler
demonstrates creating cookies through a Cookie struct
, as described earlier in this chapter. There's nothing important to note here except the fact that you may want to extend the expiration out a bit, just to ensure that there's no way a cookie could expire (naturally) between requests:
func finishHandler(w http.ResponseWriter, r *http.Request) { cookieVal, _ := r.Cookie("message") if cookieVal != nil { fmt.Fprintln(w, "We found: "+string(cookieVal.Value)+", but try to refresh!") cookie := http.Cookie{Name: "message", Value: "", Expires: time.Now(), HttpOnly: true} http.SetCookie(w, &cookie) } else { fmt.Fprintln(w, "That cookie was gone in a flash") } }
The finishHandler
function does the magic of a flash message—removes the cookie if and only if a value has been found. This ensures that the cookie is a one-time retrievable value:
func main() { http.HandleFunc("/start", startHandler) http.HandleFunc("/middle", middleHandler) http.HandleFunc("/finish", finishHandler) log.Fatal(http.ListenAndServe(port, nil)) }
The following example is our HTML for POSTing our cookie value to the /middle
handler:
<html> <head><title>Flash Message</title></head> <body> <form action="/middle" method="POST"> <input type="text" name="message" /> <input type="submit" value="Send Message" /> </form> </body> </html>
If you do as the page suggests and refresh again, the cookie value would have been removed and the page will not render, as you've previously seen.
To begin the flash message, we hit our /start
endpoint and enter an intended value and then click on the Send Message button:
At this point, we'll be sent to the /middle
endpoint, which will set the cookie value and HTTP redirect to /finish
:
And now we can see our value. Since the /finish
endpoint handler also unsets the cookie, we'll be unable to retrieve that value again. Here's what happens if we do what /finish
tells us on its first appearance:
That's all for now.
Utilizing flash messages
As mentioned earlier in this chapter, Gorilla sessions offer a simple system to utilize a single-use and cookie-based data transfer between requests.
The idea behind a flash message is not all that different than an in-browser/server message queue. It's most frequently utilized in a process such as this:
- A form is POSTed
- The data is processed
- A header redirect is initiated
- The resulting page needs some access to information about the
POST
process (success, error)
At the end of this process, the message should be removed so that the message is not duplicated erroneously at some other point. Gorilla makes this incredibly easy, and we'll look at that shortly, but it makes sense to show a quick example of how this can be accomplished in native Go.
To start, we'll create a simple HTTP server that includes a starting point handler called startHandler
:
package main import ( "fmt" "html/template" "log" "net/http" "time" ) var ( templates = template.Must(template.ParseGlob("templates/*")) port = ":8080" ) func startHandler(w http.ResponseWriter, r *http.Request) { err := templates.ExecuteTemplate(w, "ch6-flash.html", nil) if err != nil { log.Fatal("Template ch6-flash missing") } }
We're not doing anything special here, just rendering our form:
func middleHandler(w http.ResponseWriter, r *http.Request) { cookieValue := r.PostFormValue("message") cookie := http.Cookie{Name: "message", Value: "message:" + cookieValue, Expires: time.Now().Add(60 * time.Second), HttpOnly: true} http.SetCookie(w, &cookie) http.Redirect(w, r, "/finish", 301) }
Our middleHandler
demonstrates creating cookies through a Cookie struct
, as described earlier in this chapter. There's nothing important to note here except the fact that you may want to extend the expiration out a bit, just to ensure that there's no way a cookie could expire (naturally) between requests:
func finishHandler(w http.ResponseWriter, r *http.Request) { cookieVal, _ := r.Cookie("message") if cookieVal != nil { fmt.Fprintln(w, "We found: "+string(cookieVal.Value)+", but try to refresh!") cookie := http.Cookie{Name: "message", Value: "", Expires: time.Now(), HttpOnly: true} http.SetCookie(w, &cookie) } else { fmt.Fprintln(w, "That cookie was gone in a flash") } }
The finishHandler
function does the magic of a flash message—removes the cookie if and only if a value has been found. This ensures that the cookie is a one-time retrievable value:
func main() { http.HandleFunc("/start", startHandler) http.HandleFunc("/middle", middleHandler) http.HandleFunc("/finish", finishHandler) log.Fatal(http.ListenAndServe(port, nil)) }
The following example is our HTML for POSTing our cookie value to the /middle
handler:
<html> <head><title>Flash Message</title></head> <body> <form action="/middle" method="POST"> <input type="text" name="message" /> <input type="submit" value="Send Message" /> </form> </body> </html>
If you do as the page suggests and refresh again, the cookie value would have been removed and the page will not render, as you've previously seen.
To begin the flash message, we hit our /start
endpoint and enter an intended value and then click on the Send Message button:
At this point, we'll be sent to the /middle
endpoint, which will set the cookie value and HTTP redirect to /finish
:
And now we can see our value. Since the /finish
endpoint handler also unsets the cookie, we'll be unable to retrieve that value again. Here's what happens if we do what /finish
tells us on its first appearance:
That's all for now.
Summary
Hopefully by this point you have a grasp of how to utilize basic cookies and sessions in Go, either through native Go or through the use of a framework, such as Gorilla. We've tried to demonstrate the inner workings of the latter so you're able to build without additional libraries obfuscating the functionality.
We've implemented sessions into our application to enable persistent state between requests. This is the very basis of authentication for the Web. By enabling users
and sessions
table in our database, we're able to log users in, register a session, and associate that session with the proper user on subsequent requests.
By utilizing flash messages, we made use of a very specific feature that allows transfer of information between two endpoints without enabling an additional request that may look like an error to the user or generate erroneous output. Our flash messages works just once and then expire.
In Chapter 7, Microservices and Communication, we'll look at connecting disparate systems and applications across our existing and new APIs to allow event-based actions to be coordinated between those systems. This will facilitate connecting to other services within the same environment as well as those outside of our application.