Luke Evers

Two Factor Auth in Go

Posted on 13 September 2014

I originally saw this tweet on Twitter by @thefinn93:

So I thought, "the reason so many websites still haven't implemented 2FA has to be because it's difficult." That makes sense, right? Security is very important, and everyone should support some form of 2FA.

I started searching around to see if I was right. Spoiler: I was wrong. Sure, there are various amounts of 2FA out there, and some are better--and more difficult to implement--than others, but implementing any form of 2FA at least shows that you, as a website (or service):

  • Care about the privacy of your users
  • Are actively trying to secure your data

While working on my IRC bot Kittens, written in Go, I forked master to a branch I called 2fa just to see if I could implement it. I personally use Google Authenticator with my Authy app on my iPhone, so I thought I'd try my hand at that. Not long after searching I found a QR code library, and a Google Authenticator library. Perfect!

Background on Kittens

I am using the following libraries:

Enabling 2FA on an account

From the /settings page, users have the option to enable Two Factor Auth. When the user clicks the "Enable 2FA" button, we send a GET request to /settings/2fa/generate and load the base64 encoded PNG data into the empty img tag.

Creating the QR Code
// Get random secret
sec := make([]byte, 6)  
_, err := rand.Read(sec)  
if err != nil {  
    warnf("Error creating random secret key: %s", err)
}

// Encode secret to base32 string
secret := base32.StdEncoding.EncodeToString(sec)

// Create auth string to be encoded as a QR image
auth_string := "otpauth://totp/KittensIRC?secret=" + secret + "&issuer=KittensIRC"

// Encode the QR image
code, err := qr.Encode(auth_string, qr.L)  
if err != nil {  
    warnf("Error encoding qr code: %s", err)
}

// Set temporary session values until we verify 2fa
session, _ := store.Get(req, "user")  
session.Values["secret"] = secret  
session.Save(req, w)

// Write base64 encoded QR image
w.Write([]byte(base64.StdEncoding.EncodeToString(code.PNG())))  

We don't want to add to the database that the user is definitely using 2FA if they haven't verified it yet, so we just set a session value instead. After the QR Code is loaded we bring down a window where the QR code is, and an input to verify that the user really wants to add 2FA to their account.

2FA

Verifying a token

In order to add 2FA to a user account, they need to verify their account by grabbing a token from their app and clicking "Verify."

// Get our session
session, _ := store.Get(req, "user")  
secret := session.Values["secret"]

// Parse the form input
err = req.ParseForm()  
if err != nil {  
    warnf("Error parsing form: %s", err)
}

// Get the token
token := req.Form["token"][0]

// Configure token
otpc := &dgoogauth.OTPConfig{  
    Secret:      secret.(string),
    WindowSize:  3,
    HotpCounter: 0,
}

// Validate token
val, err := otpc.Authenticate(token)  
if err != nil {  
    warnf("Error authenticating token: %s", err)
}

If the token is validated then we update our user both in memory and in the database.

Logging in

Login

When a user goes to login, they start off normally with their username and password. After they click login--assuming the details are correct--we create a session.

// Create new session
session, _ := store.New(req, "user")  
session.Values["username"] = username

// Save session
session.Save(req, w)  

Then we check to see if the user has 2FA enabled, and if they do then we set a temporary 2FA session value.

// Check if 2fa is enabled
if user.Twofa {  
    session.Values["temp"] = "true"
    session.Save(req, w)

    // Redirect, check 2fa
    http.Redirect(w, req, "/login/2fa", http.StatusSeeOther)
} else {
    // Redirect, logged in ok
    http.Redirect(w, req, "/", http.StatusSeeOther)
}

If a user tries to visit any other page, they'll be redirected to /login/2fa because they have not yet finished their login.

2FA Login

At this point the user must enter a token to continue. When the user submits the token, we check it. If the token matches, then we remove our temporary session and redirect home.

// Parse our form
err = req.ParseForm()  
if err != nil {  
    warnf("Error parsing form: %s", err)
}

// Get token from input
token := req.Form["token"][0]

// Get user
user := WhoAmI(req)

// Configure token
otpc := &dgoogauth.OTPConfig{  
    Secret:      user.TwofaSecret,
    WindowSize:  3,
    HotpCounter: 0,
}

// Validate token
val, err := otpc.Authenticate(token)  
if err != nil {  
    warnf("Error authenticating token: %s", err)
}

if val {  
    // Validated
    session, _ := store.Get(req, "user")
    session.Values["temp"] = "false"
    session.Save(req, w)

    // Redirect
    http.Redirect(w, req, "/", http.StatusSeeOther)
} else {
    // Not validated
    http.Redirect(w, req, "/login/2fa", http.StatusSeeOther)
}

Code

This is my first time implementing 2FA. If I did it wrong, or did parts of it wrong, please tell me. I want to fix anything I've done wrong, add more support, or accept your fixes! You can view all of the code on GitHub, and I always accept pull requests if anyone wants to make my code better.

comments powered by Disqus