Add Two-Factor Authentication To A Golang RESTful API

When it comes to authenticating users for making use of your API, it is a good idea to add an extra step beyond standard username and password. This is called two-factor authentication (2FA) and it acts as a second layer of security for users making use of your application.

Not too long ago I had written about adding 2FA to a RESTful API created with Node.js and Express Framework, but what if we wanted to do it in Golang? The logic isn’t any different, just a new syntax for a new language.

We’re going to see how to add two-factor authentication to a Golang API that makes use of Json Web Tokens (JWT).

For simplicity, we’re going to be extending an application that we had created in a previous tutorial titled, Authenticate a Golang API with JSON Web Tokens. We’re going to be including a third-party package that will verify a time-based one-time password against a shared secret.

The scope of the application will be as follows. The user will authenticate with username and password against an API endpoint. The result will be a signed JWT that is not authorized. Using the JWT we can hit another endpoint while passing a one-time password and if valid, we’ll receive a new JWT that is authorized. Once we have an authorized JWT, we can use it against any protected API endpoint.

Getting Started with a Working Json Web Token Example

Before going any further I encourage you to view the previous tutorial on the topic of JWT in Golang. It covers a lot of topics we will not revisit here. However, just so we’re up to speed, a working version of the previous application can be found below:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/dgrijalva/jwt-go"
    "github.com/gorilla/context"
    "github.com/gorilla/mux"
    "github.com/mitchellh/mapstructure"
)

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

type JwtToken struct {
    Token string `json:"token"`
}

type Exception struct {
    Message string `json:"message"`
}

func CreateTokenEndpoint(w http.ResponseWriter, req *http.Request) {
    var user User
    _ = json.NewDecoder(req.Body).Decode(&user)
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "username": user.Username,
        "password": user.Password,
    })
    tokenString, error := token.SignedString([]byte("secret"))
    if error != nil {
        fmt.Println(error)
    }
    json.NewEncoder(w).Encode(JwtToken{Token: tokenString})
}

func ProtectedEndpoint(w http.ResponseWriter, req *http.Request) {
    params := req.URL.Query()
    token, _ := jwt.Parse(params["token"][0], func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("There was an error")
        }
        return []byte("secret"), nil
    })
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        var user User
        mapstructure.Decode(claims, &user)
        json.NewEncoder(w).Encode(user)
    } else {
        json.NewEncoder(w).Encode(Exception{Message: "Invalid authorization token"})
    }
}

func ValidateMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        authorizationHeader := req.Header.Get("authorization")
        if authorizationHeader != "" {
            bearerToken := strings.Split(authorizationHeader, " ")
            if len(bearerToken) == 2 {
                token, error := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) {
                    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                        return nil, fmt.Errorf("There was an error")
                    }
                    return []byte("secret"), nil
                })
                if error != nil {
                    json.NewEncoder(w).Encode(Exception{Message: error.Error()})
                    return
                }
                if token.Valid {
                    context.Set(req, "decoded", token.Claims)
                    next(w, req)
                } else {
                    json.NewEncoder(w).Encode(Exception{Message: "Invalid authorization token"})
                }
            }
        } else {
            json.NewEncoder(w).Encode(Exception{Message: "An authorization header is required"})
        }
    })
}

func TestEndpoint(w http.ResponseWriter, req *http.Request) {
    decoded := context.Get(req, "decoded")
    var user User
    mapstructure.Decode(decoded.(jwt.MapClaims), &user)
    json.NewEncoder(w).Encode(user)
}

func main() {
    router := mux.NewRouter()
    fmt.Println("Starting the application...")
    router.HandleFunc("/authenticate", CreateTokenEndpoint).Methods("POST")
    router.HandleFunc("/protected", ProtectedEndpoint).Methods("GET")
    router.HandleFunc("/test", ValidateMiddleware(TestEndpoint)).Methods("GET")
    log.Fatal(http.ListenAndServe(":12345", router))
}

Before we get too invested in adding new features, it makes sense for us to optimize the above code a bit. We want to optimize it to prevent too much code duplication. After all, the 2FA functions will make use of a lot of what we’ve already done.

The first logical step would be to bring the bearer token parsing, JWT signing, and JWT verification into their own functions. That may look like the following:

func SignJwt(claims jwt.MapClaims, secret string) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(secret))
}

func VerifyJwt(token string, secret string) (map[string]interface{}, error) {
    jwToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("There was an error")
        }
        return []byte(secret), nil
    })
    if err != nil {
        return nil, err
    }
    if !jwToken.Valid {
        return nil, fmt.Errorf("Invalid authorization token")
    }
    return jwToken.Claims.(jwt.MapClaims), nil
}

func GetBearerToken(header string) (string, error) {
    if header == "" {
        return "", fmt.Errorf("An authorization header is required")
    }
    token := strings.Split(header, " ")
    if len(token) != 2 {
        return "", fmt.Errorf("Malformed bearer token")
    }
    return token[1], nil
}

With the common set of functions separated from the various endpoints we can recycle them for use in our 2FA process.

Implementing Two-Factor Authentication (2FA) in the Application

Just to reiterate, the logic here will be to have the username and password authentication step provide a JWT with a property set as unauthorized.

func CreateTokenEndpoint(w http.ResponseWriter, req *http.Request) {
    mockUser := make(map[string]interface{})
    mockUser["username"] = "nraboy"
    mockUser["password"] = "password"
    mockUser["authorized"] = false
    tokenString, err := SignJwt(mockUser, jwtSecret)
    if err != nil {
        json.NewEncoder(w).Encode(err)
        return
    }
    json.NewEncoder(w).Encode(JwtToken{Token: tokenString})
}

Once we have the unauthorized JWT string, we need to visit another endpoint (the 2FA endpoint) to make it authorized. However, before we come up with that logic, it is best to install the package that we’ll be using.

From the Command Prompt or Terminal, execute the following:

go get github.com/dgryski/dgoogauth

The above command will install the Google Authenticator package for Golang. With the package installed, now we can focus on our endpoint. Again, we’re not using a database so we’ll have to mock the 2FA shared secret key.

func VerifyOtpEndpoint(w http.ResponseWriter, req *http.Request) {
    secret := "2MXGP5X3FVUEK6W4UB2PPODSP2GKYWUT"
    bearerToken, err := GetBearerToken(req.Header.Get("authorization"))
    if err != nil {
        json.NewEncoder(w).Encode(err)
        return
    }
    decodedToken, err := VerifyJwt(bearerToken, jwtSecret)
    if err != nil {
        json.NewEncoder(w).Encode(err)
        return
    }
    otpc := &dgoogauth.OTPConfig{
        Secret:      secret,
        WindowSize:  3,
        HotpCounter: 0,
    }
    var otpToken OtpToken
    _ = json.NewDecoder(req.Body).Decode(&otpToken)
    decodedToken["authorized"], _ = otpc.Authenticate(otpToken.Token)
    if decodedToken["authorized"] == false {
        json.NewEncoder(w).Encode("Invalid one-time password")
        return
    }
    jwToken, _ := SignJwt(decodedToken, jwtSecret)
    json.NewEncoder(w).Encode(jwToken)
}

The above endpoint expects a bearer token. To be specific it expects the unauthorized bearer token. If available, the token is verified and if valid, we use the two-factor authentication library to compare our passed one-time password value with the shared secret.

If the authentication succeeds, the data in the JWT becomes authorized, at which point it becomes signed again and returned back to the client.

We’re not in the clear yet though. Remember that middleware we had created for protected endpoint validation? We need to update it so it only works if the JWT is authorized rather than just having a JWT available.

func ValidateMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        bearerToken, err := GetBearerToken(req.Header.Get("authorization"))
        if err != nil {
            json.NewEncoder(w).Encode(err)
            return
        }
        decodedToken, err := VerifyJwt(bearerToken, jwtSecret)
        if err != nil {
            json.NewEncoder(w).Encode(err)
            return
        }
        if decodedToken["authorized"] == true {
            context.Set(req, "decoded", decodedToken)
            next(w, req)
        } else {
            json.NewEncoder(w).Encode("2FA is required")
        }
    })
}

After we’ve verified the JWT, we can look at the authorized property and only push the user through if it has been authorized. Any protected endpoint remains pretty basic, as previously seen in the JWT tutorial. For example, this could be a protected endpoint:

func ProtectedEndpoint(w http.ResponseWriter, req *http.Request) {
    decoded := context.Get(req, "decoded")
    json.NewEncoder(w).Encode(decoded)
}

Notice that no extra logic is in place. That is because the middleware gets attached at the HTTP mux level.

In case you’re interested to know how to generate shared secrets for 2FA, they are simply 80 bit base32 encoded strings. For example, the following endpoint could generate a random secret for your users:

func GenerateSecretEndpoint(w http.ResponseWriter, req *http.Request) {
    random := make([]byte, 10)
    rand.Read(random)
    secret := base32.StdEncoding.EncodeToString(random)
    json.NewEncoder(w).Encode(secret)
}

Once you have the shared secret, add it to your mobile Google Authenticator or Authy application to receive a time-based one-time password to be used in this example.

Conclusion

You just saw how to add two-factor authentication (2FA) to a Golang application that makes use of Json Web Tokens (JWT). This is an alternative to other examples that might use sessions to store the first factor rather than a JWT. It makes it a perfect example for RESTful APIs consumed by client applications like Angular.

If you’re interested in creating your own time-based one-time password manager, check out a NativeScript tutorial I wrote titled, Build a Time-Based One-Time Password Manager with NativeScript.

The full source code to this project can be downloaded here.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Subscribe

Subscribe to my newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

Subscribe on YouTube

Support This Site