Go Basic Sessions

28 May 2019 golang

When a user logs into any applicaiton, their authorizaiton has to be persisted otherwise they will be required to re-authenticate with every request. One of the ways to accomplish this is to store a users “session”.

A session is started once a user logs in and expires either after a specific time delay or on logout. Each logged in user and device has a unique session reference, or id, that is sent with every request the user makes after they are authenticated. Sessions can store informaiton about the current state of the user, but, in our example below we’re only going to be using a session to verify authorization.

Outline

In this post we’ll be looking into how to create, persist and clear user sessions. As a simple example we’ll be using the filesystem as the backend session store, but in reality any type of database/persistent store can be used as a session store.

Our application will consist of three endpoint /login, /authenticated, and /logout.

Base HTTP server

Lets begin by defining our base HTTP server that will handle the three routes

import (
	"github.com/strattonw/go-basic-sessions/handlers"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/login", handlers.Login)
	http.HandleFunc("/authenticated", handlers.Authenticated)
	http.HandleFunc("/logout", handlers.Logout)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Constants and structs

For this example we’re going to hard code our super secure username and password combination, which, so very obviously, should never be done in the real world. We’re also going to define a struct that we will use to receive the authentication credentials from the user.

const (
	u = "admin"
	p = "root"
	path = "/tmp/"
	cookieName = "strattonDevSession"
)

type Credentials struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

Authentication handler

The first handler we’re going to define is the handler that will be behind authentication. We’re doing this one first to make sure that we get a failed authenticaion when there is no possible way for us to become authenticated.

As we’re using the filesystem as the session store our authentication handlers job is to check to make sure the file related to the session exists. If the file doesn’t exist we can assume the session is invalid or it has expired.

func Authenticated(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie(cookieName)
	if err != nil {
		if err == http.ErrNoCookie {
			log.Println("Could not find session cookie on request")
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	if _, err := os.Stat(path + c.Value); os.IsNotExist(err) {
		log.Println("Session did not exist")
		w.WriteHeader(http.StatusUnauthorized)
	} else {
		_, _ = w.Write([]byte("Welcome\n"))
	}
}

If we test this handle out we’ll see that we properly send back a Http 401 response type.

stratton.dev$ curl -i localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:38:01 GMT
Content-Length: 0

stratton.dev$ curl -i --cookie "strattonDevSession=sessionPlease" localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:39:56 GMT
Content-Length: 0

Login handler

Next lets make the handler that will allow us to create the sessions when a user logs in. The main parts of our login handler will be to decode the credentials json, verify the credential accuracy and, if the credentials are valid, create the session and add it as a cookie to the response.

func Login(w http.ResponseWriter, r *http.Request) {
	var credentials Credentials

	err := json.NewDecoder(r.Body).Decode(&credentials)
	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	if credentials.Username != u || credentials.Password != p {
		log.Println("Credentials was incorrect")
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	token, err := uuid.NewV4()

	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	_, err = os.Create(path + token.String())

	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name: cookieName,
		Value: token.String(),
		Expires: time.Now().Add(24 * time.Hour),
	})
}

First lets make sure we get a Http 401 response when passing in invalid credentials.

stratton.dev$ curl -d '{"username":"NotTheUsername","password":"root"}' -X POST -i localhost:8080/login
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:46:25 GMT
Content-Length: 0

stratton.dev$ curl -d '{"username":"admin","password":"NotThePassword"}' -X POST -i localhost:8080/login
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:45:48 GMT
Content-Length: 0

Since we know we’ll fail with incorrect credentials lets make a request with valid credentials to create our session.

stratton.dev$ curl -d '{"username":"stratton","password":".dev"}' -X POST -i localhost:8080/login
HTTP/1.1 200 OK
Set-Cookie: strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab; Expires=Thu, 30 May 2019 05:48:35 GMT
Date: Wed, 29 May 2019 05:48:35 GMT
Content-Length: 0

Awesome, we now have a session, lets copy that cookie and add it to our authenticated endpoint request. It should be noted that in a normal, non-curl, flow, the web browser would handle adding the cookie to subsequent requests for the user.

stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/authenticated
HTTP/1.1 200 OK
Date: Wed, 29 May 2019 05:51:25 GMT
Content-Length: 8
Content-Type: text/plain; charset=utf-8

Welcome

We’re now able to have users login and view authenticated pages using their session. The last step we need is to make sure that the users are able to logout and remove their session.

Logout handler

The main goal for our logout handler is to remove the session from our session store, currently the filesystem, so the session cannot be used for authorization in the future. As we have the filesystem as our session store, to remove the session the store all we have to do is delete the session file.

func Logout(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie(cookieName)
	if err != nil {
		if err == http.ErrNoCookie {
			log.Println("Returning success on logout request without session cookie")
			return
		}

		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	err = os.Remove(path + c.Value)

	if err != nil {
		if os.IsNotExist(err) {
			return
		}

		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

Lets try this handler out by calling /logout and then /authenticated both with the session we generated during login. If all goes well /logout should return Http 200 and /authenticated should return Http 401, as the session is no longer valid.

stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/logout
HTTP/1.1 200 OK
Date: Wed, 29 May 2019 05:54:35 GMT
Content-Length: 0

stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:54:38 GMT
Content-Length: 0

Awesome, we can now properly logout and remove our sessions from the session store.

Source

Github

Conclusion

Here we’ve shown a very simple way to implement session based authentication in Go. There are lots of places that we can take this basis, such as, changing the session store to redis or sql and adding the ability to expire sessions within the session store (currently sessions will live forever, or until we restart as we use /tmp for the filepath by default).