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.
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:
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.
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.
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) }
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.