Golang : Verify token from Google Authenticator App
One of the steps to implement 2FA(two-factor authentication) is to verify the token generated by app such as Google Authenticator App or Authy (https://www.authy.com/).
Once you have successfully scanned the QR code, an entry for SocketLoop will appear in the Google Authenticator App.
The next step is to use the https://github.com/dgryski/dgoogauth
package to verify the token. The code below will show you how to generate the QR code that you need to scan the PNG file each time you run the code.( this is because the secret string will be changed each time ). In real world application, you just have to store the secret string into database and associate the secret string with the user that enabled 2FA.
package main
import (
"bufio"
"bytes"
"code.google.com/p/rsc/qr"
"crypto/rand"
"encoding/base32"
"fmt"
"github.com/dgryski/dgoogauth"
"github.com/disintegration/imaging"
"image"
"os"
"runtime"
"strings"
)
func randStr(strSize int, randType string) string {
var dictionary string
if randType == "alphanum" {
dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "alpha" {
dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "number" {
dictionary = "0123456789"
}
var bytes = make([]byte, strSize)
rand.Read(bytes)
for k, v := range bytes {
bytes[k] = dictionary[v%byte(len(dictionary))]
}
return string(bytes)
}
func main() {
// maximize CPU usage for maximum performance
runtime.GOMAXPROCS(runtime.NumCPU())
// generate a random string - preferbly 6 or 8 characters
randomStr := randStr(6, "alphanum")
fmt.Println(randomStr)
// For Google Authenticator purpose
// for more details see
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
secret := base32.StdEncoding.EncodeToString([]byte(randomStr))
fmt.Println(secret)
// authentication link. Remember to replace SocketLoop with yours.
// for more details see
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
authLink := "otpauth://totp/SocketLoop?secret=" + secret + "&issuer=SocketLoop"
// Encode authLink to QR codes
// qr.H = 65% redundant level
// see https://godoc.org/code.google.com/p/rsc/qr#Level
code, err := qr.Encode(authLink, qr.L)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
imgByte := code.PNG()
// convert byte to image for saving to file
img, _, _ := image.Decode(bytes.NewReader(imgByte))
err = imaging.Save(img, "./QRImgGA.png")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// everything ok
fmt.Println("QR code generated and saved to QRimgGA.png. Please scan the QRImgGA.png with Google Authenticator App.")
fmt.Println("NOTE : You need to remove the old entry for SocketLoop in Google Authenticator App each time.")
tokenReader := bufio.NewReader(os.Stdin)
fmt.Print("Please enter token to verify : ")
// prompt user for input
token, err := tokenReader.ReadString('\n')
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Token : ", token)
// setup the one-time-password configuration.
otpConfig := &dgoogauth.OTPConfig{
Secret: strings.TrimSpace(secret),
WindowSize: 3,
HotpCounter: 0,
}
// get rid of the extra \n from the token string
// otherwise the validation will fail
trimmedToken := strings.TrimSpace(token)
// Validate token
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("Token string [%s] validation is : %v \n", trimmedToken, ok)
fmt.Println("IMPORTANT : Once the user token is validated. Store the secret string into")
fmt.Println("database and memory. Use the secret string associated with this")
fmt.Println("user whenever 2FA is required.")
fmt.Println("If the user decides to disable 2FA, remove the secret string from")
fmt.Println("database and memory. Generate a new secret string when user")
fmt.Println("re-enable 2FA.")
}
Sample output :
lYC4U9
NRMUGNCVHE======
QR code generated and saved to QRimgGA.png. Please scan the QRImgGA.png with Google Authenticator App.
NOTE : You need to remove the old entry for SocketLoop in Google Authenticator App each time.
Please enter token to verify : 230168
Token : 230168
Token string [230168] validation is : true
IMPORTANT : Once the user token is validated. Store the secret string into
database and memory. Use the secret string associated with this
user whenever 2FA is required.
If the user decides to disable 2FA, remove the secret string from
database and memory. Generate a new secret string when user
re-enable 2FA.
The above code is suitable for mobile app or IoT(Internet of Things) devices. If you are looking to implement 2FA for your website with Golang. Here's the code :
package main
import (
"bytes"
"code.google.com/p/rsc/qr"
"crypto/rand"
"encoding/base32"
"fmt"
"github.com/dgryski/dgoogauth"
"github.com/disintegration/imaging"
"image"
"net/http"
"os"
"strings"
)
func randStr(strSize int, randType string) string {
var dictionary string
if randType == "alphanum" {
dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "alpha" {
dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "number" {
dictionary = "0123456789"
}
var bytes = make([]byte, strSize)
rand.Read(bytes)
for k, v := range bytes {
bytes[k] = dictionary[v%byte(len(dictionary))]
}
return string(bytes)
}
// need this to be global variable
var secret string
func Verify(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
token := r.FormValue("token")
// setup the one-time-password configuration.
otpConfig := &dgoogauth.OTPConfig{
Secret: strings.TrimSpace(secret),
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
// Validate token
ok, err := otpConfig.Authenticate(trimmedToken)
// if the token is invalid or expired
if err != nil {
w.Write([]byte(fmt.Sprintf("<html><body><h1>Token [%s] verification : %v</h1></body></hmtl>", token, ok)))
}
// token validated and proceed to login, bla, bla....
w.Write([]byte(fmt.Sprintf("<html><body><h1>Token [%s] verification : %v</h1></body></hmtl>", token, ok)))
}
}
func Home(w http.ResponseWriter, r *http.Request) {
//w.Write([]byte(fmt.Sprintf("Generating QR code\n")))
// generate a random string - preferbly 6 or 8 characters
randomStr := randStr(6, "alphanum")
// For Google Authenticator purpose
// for more details see
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
secret = base32.StdEncoding.EncodeToString([]byte(randomStr))
//w.Write([]byte(fmt.Sprintf("Secret : %s !\n", secret)))
// authentication link. Remember to replace SocketLoop with yours.
// for more details see
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
authLink := "otpauth://totp/SocketLoop?secret=" + secret + "&issuer=SocketLoop"
// Encode authLink to QR codes
// qr.H = 65% redundant level
// see https://godoc.org/code.google.com/p/rsc/qr#Level
code, err := qr.Encode(authLink, qr.H)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
imgByte := code.PNG()
// convert byte to image for saving to file
img, _, _ := image.Decode(bytes.NewReader(imgByte))
err = imaging.Save(img, "./QRImgGA.png")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// in real world application, the QRImgGA.png file should
// be a temporary file with dynamic name.
// for this tutorial sake, we keep it as static name.
w.Write([]byte(fmt.Sprintf("<html><body><h1>QR code for : %s</h1><img src='http://localhost:8080/QRImgGA.png'>", authLink)))
w.Write([]byte(fmt.Sprintf("<form action='http://localhost:8080/verify' method='post'>Token : <input name='token' id='token'><input type='submit' value='Verify Token'></form></body></html>")))
}
func main() {
http.HandleFunc("/", Home)
http.HandleFunc("/verify", Verify)
// this is for displaying the QRImgGA.png from the source directory
http.Handle("/QRImgGA.png", http.FileServer(http.Dir("./")))
http.ListenAndServe(":8080", nil)
}
Hope you will find this tutorial useful in improving online security for your website or services users.
References :
https://www.socketloop.com/tutorials/golang-read-input-from-console-line
See also : Golang : Generate QR codes for Google Authenticator App and fix "Cannot interpret QR code" error
By Adam Ng
IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.
Advertisement
Tutorials
+14.8k Golang : Get timezone offset from date or timestamp
+11.6k Linux : How to install driver for 600Mbps Dual Band Wifi USB Adapter
+10.2k Golang : Interfacing with PayPal's IPN(Instant Payment Notification) example
+10.7k Golang : Fix - does not implement sort.Interface (missing Len method)
+51.6k Golang : How to get struct field and value by name
+21.6k Golang : Securing password with salt
+5k Golang : Generate Interleaved 2 inch by 5 inch barcode
+8k PHP : How to parse ElasticSearch JSON ?
+5.5k PHP : How to handle URI or URL with non-ASCII characters such as Chinese/Japanese/Korean(CJK) ?
+9.7k Golang : Edge detection with Sobel method
+23.7k Golang : How to validate URL the right way
+19.7k Golang : How to get struct tag and use field name to retrieve data?