In this post, we will learn how to authenticate users using session cookies in a Golang server application.

banner

When a user logs into our application, we need to know who they are across all our HTTP methods and routes.

One way to do this is to store the users “session”. A session is started once a user logs in, and expires some time after that.

Each logged in user has some reference to the session (called a cookie), which they send with their requests. We then use this reference to look up the user that it belongs to and return information specific to them.

If you just want to see the source code for this tutorial, you can find it here

Overview

In this post, we will look at how to create and store the session of a logged in user as a cookie on the browser.

We will build an application with a /signin and a /welcome route.

  • The /signin route will accept a users username and password, and set a session cookie if successful.
  • The /welcome route will be a simple HTTP GET route which will show a personalized message to the currently logged in user.

The session information of the user will be stored in our applications local memory.

We will also assume that the users that are signing in have already created their username-password credentials on our application.

If you want to know how to implement user registration, you can read my post on password authentication and storage in Go.

Creating the HTTP Server

Let’s start by initializing the HTTP server with the required routes:

package main

import (
	"log"
	"net/http"
)

func main() {
	// "Signin" and "Welcome" are handlers that we have to implement
	http.HandleFunc("/signin", Signin)
	http.HandleFunc("/welcome", Welcome)
	// start the server on port 8080
	log.Fatal(http.ListenAndServe(":8080", nil))
}

We can now define the Signin and Welcome routes.

Creating Session Tokens

We will be creating a new session token every time a user signs in.

The /signin route will take the users credentials and log them in.

In order to make this simple, we’re storing the users information as an in-memory map in our code:

var users = map[string]string{
	"user1": "password1",
	"user2": "password2",
}

For now, there are only two valid users in our application: user1, and user2.

If you want a more production-ready method to store passwords, you can read my post on password authentication and storage in Go

We also need to define a map to store information about each users session:

// this map stores the users sessions. For larger scale applications, you can use a database or cache for this purpose
var sessions = map[string]session{}

// each session contains the username of the user and the time at which it expires
type session struct {
	username string
	expiry   time.Time
}

// we'll use this method later to determine if the session has expired
func (s session) isExpired() bool {
	return s.expiry.Before(time.Now())
}

Next, we can implement the Signin HTTP handler:

// Create a struct that models the structure of a user in the request body
type Credentials struct {
	Password string `json:"password"`
	Username string `json:"username"`
}

func Signin(w http.ResponseWriter, r *http.Request) {
	var creds Credentials
	// Get the JSON body and decode into credentials
	err := json.NewDecoder(r.Body).Decode(&creds)
	if err != nil {
		// If the structure of the body is wrong, return an HTTP error
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Get the expected password from our in memory map
	expectedPassword, ok := users[creds.Username]

	// If a password exists for the given user
	// AND, if it is the same as the password we received, the we can move ahead
	// if NOT, then we return an "Unauthorized" status
	if !ok || expectedPassword != creds.Password {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// Create a new random session token
	// we use the "github.com/google/uuid" library to generate UUIDs
	sessionToken := uuid.NewString()
	expiresAt := time.Now().Add(120 * time.Second)

	// Set the token in the session map, along with the session information
	sessions[sessionToken] = session{
		username: creds.Username,
		expiry:   expiresAt,
	}

	// Finally, we set the client cookie for "session_token" as the session token we just generated
	// we also set an expiry time of 120 seconds
	http.SetCookie(w, &http.Cookie{
		Name:    "session_token",
		Value:   sessionToken,
		Expires: expiresAt,
	})
}

sign in diagram

If a user logs in successfully, this handler will set a cookie on the client side, and inside its own local memory. Once a cookie is set on a client, it is sent along with every subsequent request.

We can use UUIDs to represent session tokens, since they are uniform in structure, and difficult to guess.

Authenticating Users Through Session Cookies

Now that we have persisted the users session information on their client (in the form of the session_token cookie) and the server, we can write our “welcome” handler to handle user specific information.

Since logged in clients have session information stored on their end as cookies, we can use it to:

  • Authenticate subsequent user requests
  • Get information about the user making the request

Let’s write our Welcome handler to do that:

func Welcome(w http.ResponseWriter, r *http.Request) {
	// We can obtain the session token from the requests cookies, which come with every request
	c, err := r.Cookie("session_token")
	if err != nil {
		if err == http.ErrNoCookie {
			// If the cookie is not set, return an unauthorized status
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		// For any other type of error, return a bad request status
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	sessionToken := c.Value

	// We then get the session from our session map
	userSession, exists := sessions[sessionToken]
	if !exists {
		// If the session token is not present in session map, return an unauthorized error
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	// If the session is present, but has expired, we can delete the session, and return
	// an unauthorized status
	if userSession.isExpired() {
		delete(sessions, sessionToken)
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// If the session is valid, return the welcome message to the user
	w.Write([]byte(fmt.Sprintf("Welcome %s!", userSession.username)))
}

From the code, we can see that our welcome handler gives us an “unauthorized” (or 401) status under certain circumstances:

  1. If there is no session_token cookie along with the request (which means that the requestor hasn’t logged in)
  2. If the session token is not present in memory (which means that the requestor is sending us an invalid session token)
  3. If the session has expired

welcome diagram

Session based authentication keeps your users sessions secure in a couple of ways:

  1. Since the session tokens are randomly generated, its near-impossible for a malicious user to brute-force their way into a users session.
  2. If a users session token is compromised somehow, it cannot be used after its expiry. This is why the expiry time is restricted to small intervals (a few seconds to a couple of minutes)

Refreshing Session Tokens

Since the expiry time of a session token is kept small, we need to issue a new token often to keep the user logged in.

Of course, we cannot expect the user to login every time their token expires. To solve this, we can create another route that accepts the users current session token, and issues a new session token with a renewed expiry time.

Lets define a Refresh HTTP handler to renew the users session token every time they hit the /refresh route in our application

func Refresh(w http.ResponseWriter, r *http.Request) {
	// (BEGIN) The code from this point is the same as the first part of the `Welcome` route
	c, err := r.Cookie("session_token")
	if err != nil {
		if err == http.ErrNoCookie {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	sessionToken := c.Value

	userSession, exists := sessions[sessionToken]
	if !exists {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	if userSession.isExpired() {
		delete(sessions, sessionToken)
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	// (END) The code until this point is the same as the first part of the `Welcome` route

	// If the previous session is valid, create a new session token for the current user
	newSessionToken := uuid.NewString()
	expiresAt := time.Now().Add(120 * time.Second)

	// Set the token in the session map, along with the user whom it represents
	sessions[newSessionToken] = session{
		username: userSession.username,
		expiry:   expiresAt,
	}

	// Delete the older session token
	delete(sessions, sessionToken)

	// Set the new token as the users `session_token` cookie
	http.SetCookie(w, &http.Cookie{
		Name:    "session_token",
		Value:   newSessionToken,
		Expires: time.Now().Add(120 * time.Second),
	})
}

refresh token diagram

We can now add this to the rest of our routes:

http.HandleFunc("/refresh", Refresh)

Logging Out Our Users

If the user decides to logout of our application, we need to remove their session token from our storage as well as the users client.

logging out a user - get the users session token and delete it from our storage, sending an empty cookie back

Let’s create a Logout handler to implement this:

func Logout(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("session_token")
	if err != nil {
		if err == http.ErrNoCookie {
			// If the cookie is not set, return an unauthorized status
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		// For any other type of error, return a bad request status
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	sessionToken := c.Value

	// remove the users session from the session map
	delete(sessions, sessionToken)

	// We need to let the client know that the cookie is expired
	// In the response, we set the session token to an empty
	// value and set its expiry as the current time
	http.SetCookie(w, &http.Cookie{
		Name:    "session_token",
		Value:   "",
		Expires: time.Now(),
	})
}

We can now add this to the rest of our routes:

http.HandleFunc("/logout", Logout)

Running Our Application

To run this application, build and run the Go binary:

go build
./go-session-auth-example

Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:

POST http://localhost:8080/signin

{"username":"user2","password":"password2"}

You can now try hitting the welcome route from the same client to get the welcome message:

GET http://localhost:8080/welcome

Hit the refresh route, and then inspect the clients cookies to see the new value of the session_token:

POST http://localhost:8080/refresh

Finally, call the logout route to clear session data:

GET http://localhost:8080/logout

Calling the welcome and refresh routes after this will result in a 401 error.

You can find the working source code for this example on Github.