Practical Golang: Writing a simple login middleware

Introduction

In this part we’ll be creating a simple middleware you can easily apply to your handlers to get authentication/authorization. Middleware like this is an awesome way to add additional functionality to your Go server. Here we will only do authorization as we will only ask for a password, not a login. Although if you want, then you can easily extend this system to any authentication/authorization you’d like.

Implementation

We will mainly use the stdlib, and will use cookies to remember who’s already logged in. To generate the cookie values we will use the go.uuid library. So remember to

    go get github.com/satori/go.uuid

We will start with a basic structure of our system with an already existing “Hello World!!!” handler:

package main

import (
    "net/http"
    "fmt"
    "github.com/satori/go.uuid"
    "sync"
)

const loginPage = "<html><head><title>Login</title></head><body><form action=\"login\" method=\"post\"> <input type=\"password\" name=\"password\" /> <input type=\"submit\" value=\"login\" /> </form> </body> </html>"

func main() {

    http.Handle("/hello", helloWorldHandler{})
    http.HandleFunc("/login", handleLogin)

    http.ListenAndServe(":3000", nil)
}

type helloWorldHandler struct {
}

func (h helloWorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello World!!!")
}

type authenticationMiddleware struct {
    wrappedHandler http.Handler
}

func (h authenticationMiddleware) ServeHTTP(w http.ResponseWriter,r *http.Request) {
}

func authenticate(h http.Handler) authenticationMiddleware {
    return authenticationMiddleware{h}
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
}

Ok, lets go over this code.

package main

import (
    "net/http"
    "fmt"
    "github.com/satori/go.uuid"
    "sync"
)

const loginPage = "<html><head><title>Login</title></head><body><form action=\"login\" method=\"post\"> <input type=\"password\" name=\"password\" /> <input type=\"submit\" value=\"login\" /> </form> </body> </html>"

func main() {

    http.Handle("/hello", helloWorldHandler{})
    http.HandleFunc("/login", handleLogin)

    http.ListenAndServe(":3000", nil)
}

type helloWorldHandler struct {
}

func (h helloWorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello World!!!")
}

First we declare the package, imports and the html code of the login page.

We also declare the basic hello world handler.

Now to get to the interesting part.

type authenticationMiddleware struct {
    wrappedHandler http.Handler
}

func (h authenticationMiddleware) ServeHTTP(w http.ResponseWriter,r *http.Request) {
}

func authenticate(h http.Handler) authenticationMiddleware {
    return authenticationMiddleware{h}
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
}

we create a handler which will supply authorization, and if authorized successfully, will let the user through to the underlying handler. We also define the authenticate method, a simple function wrapper over creating a struct, and a function to handle the login.

That means we can also define the last route, the secured hello world route.

func main() {
    http.Handle("/hello", helloWorldHandler{})
    http.Handle("/secureHello", authenticate(helloWorldHandler{}))
    http.HandleFunc("/login", handleLogin)

    http.ListenAndServe(":3000", nil)
}

Ok, we will also need a simple client struct, which will just save if the target client session is authorized, and a map containing our Clients with cookie values being the keys.

var sessionStore map[string]Client
var storageMutex sync.RWMutex

type Client struct {
    loggedIn bool
}

We also need the mutex for concurrent map access. In the main function we initialize the map:

func main() {
    sessionStore = make(map[string]Client)
    http.Handle("/hello", helloWorldHandler{})

Now we can go to the authenticationMiddleware’s ServeHTTP function:

We’ll begin with checking if the cookie is present, if it isn’t there, we’ll continue and create a new. If the error is nonstandard then we just return.

func (h authenticationMiddleware) ServeHTTP(w http.ResponseWriter,r *http.Request) {
    cookie, err := r.Cookie("session")
    if err != nil {
        if err != http.ErrNoCookie {
            fmt.Fprint(w, err)
            return
        } else {
            err = nil
        }
    }

We later check, unless the cookie exists, if it’s saved in our map. If it’s not, then we will later generate a new one.

var present bool
var client Client
if cookie != nil {
    storageMutex.RLock()
    client, present = sessionStore[cookie.Value]
    storageMutex.RUnlock()
} else {
    present = false
}

Now, if the cookie wasn’t present, then we can generate a new one!:

if present == false {
    cookie = &http.Cookie{
        Name: "session",
        Value: uuid.NewV4().String(),
    }
    client = Client{false}
    storageMutex.Lock()
    sessionStore[cookie.Value] = client
    storageMutex.Unlock()
}

We can then set the cookie to our response writer, and if the client isn’t logged in, send him the login page, however, if he is logged in, then we can send him what he wanted:

    http.SetCookie(w, cookie)
    if client.loggedIn == false {
        fmt.Fprint(w, loginPage)
        return
    }
    if client.loggedIn == true {
        h.wrappedHandler.ServeHTTP(w, r)
        return
    }
}

So far so good, that’s actually already about the main part of the middleware, now we’ll write a function just for handling the login logic. The handleLogin function:

func handleLogin(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session")
    if err != nil {
        if err != http.ErrNoCookie {
            fmt.Fprint(w, err)
            return
        } else {
            err = nil
        }
    }
    var present bool
    var client Client
    if cookie != nil {
        storageMutex.RLock()
        client, present = sessionStore[cookie.Value]
        storageMutex.RUnlock()
    } else {
        present = false
    }

    if present == false {
        cookie = &http.Cookie{
            Name: "session",
            Value: uuid.NewV4().String(),
        }
        client = Client{false}
        storageMutex.Lock()
        sessionStore[cookie.Value] = client
        storageMutex.Unlock()
    }
    http.SetCookie(w, cookie)}
}

First we created the part which is accountable for the cookie handling, as in the recent function. Now we get to the form parsing and the actual login part.

http.SetCookie(w, cookie)
err = r.ParseForm()
if err != nil {
    fmt.Fprint(w, err)
    return
}

if subtle.ConstantTimeCompare([]byte(r.FormValue("password")), []byte("password123")) == 1 {
    //login user
} else {
    fmt.Fprintln(w, "Wrong password.")
}

We parse the login form and check if the login conditions are met. Here we only need the password to be correct. If it is we log the client in:

if subtle.ConstantTimeCompare([]byte(r.FormValue("password")), []byte("password123")) == 1 {
    client.loggedIn = true
    fmt.Fprintln(w, "Thank you for logging in.")
    storageMutex.Lock()
    sessionStore[cookie.Value] = client
    storageMutex.Unlock()
}

We use subtle.ConstantTimeCompare as it protects us from time-based attacks. (Thanks for the tip in the reddit comment.)

That’s basically all we need. Now you can secure the routes you want easily.

Conclusion

Remember, that for a secure implementation you need encrypt the network traffic using SSL/TLS, otherwise, somebody can just read the cookie and impersonate your user.

Another thing to consider is redirecting the user to the page he wanted to get to after logging in.

Have fun with creating other interesting middleware!

Leave a Reply