Strengthening Security: Implementing Two-Factor Authentication with Go

Rabieh Fashwall
8 min readDec 25, 2023

In the ever-changing digital landscape, where cybersecurity threats are constantly evolving, protecting user accounts has become paramount for developers. Two-Factor Authentication (2FA) stands out as a robust tool in the arsenal of security measures. This blog embarks on a comprehensive journey into the world of 2FA, delving into its significance, exploring the core principles that underpin it, and guiding developers through the process of implementing it within their Go applications.

Before 2FA

Prior to the advent of Two-Factor Authentication (2FA), password-based authentication was the primary method for securing online accounts. This system relied solely on a username and password combination, which could be easily compromised through various means, such as phishing, brute-force attacks, or credential leaks.

The limitations of password-based authentication became apparent as cyberattacks became more sophisticated. In 2013¹, an estimated 1.2 billion user accounts were compromised due to password breaches. This exposed individuals and organisations to significant financial losses and reputational damage.

Significance of 2FA

The primary challenges of password-based authentication include:

  • Password Reuse: Users often reuse passwords across multiple accounts, making it easier for attackers to gain access to multiple accounts if one password is compromised.
  • Weak Passwords: Many users choose weak passwords that are easy to guess or crack, such as “password” or “123456.”
  • Phishing Attacks: Phishing attacks trick users into entering their passwords on fraudulent websites, allowing attackers to steal their credentials.
  • Brute-Force Attacks: Attackers can use automated tools to repeatedly guess passwords until they find the correct one.

In the face of sophisticated cyberattacks that exploit weak or stolen passwords, 2FA emerges as a formidable shield against unauthorised access. It elevates security by adding an extra layer of verification beyond the traditional username and password combination. This additional layer, typically a one-time code sent to a user’s registered device, acts as a robust deterrent to unauthorised login attempts.

So even if a malicious actor obtains a user’s password, they would still need the second factor (something the user possesses, like a mobile device) to gain access. This significantly reduces the likelihood of unauthorised access.

The Evolution of 2FA Methods

2FA addresses these challenges by adding an extra layer of security beyond passwords. It requires users to provide two pieces of evidence to authenticate their identity:

  • Something you know: This typically involves a password or other knowledge-based authentication method, such as a security question or PIN.
  • Something you have: This refers to a device in the user’s possession, such as a smartphone or hardware token, that generates a one-time passcode (OTP) for authentication.

By requiring both a password and a second factor, 2FA makes it significantly more difficult for attackers to gain unauthorised access to accounts.

Over time, various 2FA methods have emerged, each with its own advantages and disadvantages:

  • SMS-based 2FA: This method sends an OTP to the user’s registered mobile phone number. It is convenient and widely available, but it can be vulnerable to SIM swapping attacks.
  • TOTP (Time-based One-Time Password): This method uses a cryptographically generated OTP that changes every few seconds. It is more secure than SMS-based 2FA but requires users to install and configure a dedicated app on their mobile devices.
  • U2F (Universal 2nd Factor) Tokens: These are physical security tokens that generate OTPs when plugged into a USB port or scanned by a smartphone. They offer the highest level of security but can be inconvenient to carry.

Implementation

Now we start diving in the fun part: Coding.

Full code link in the end of this blog

I made the implementation serves as a simplified introduction to 2FA functionality. It showcases the core concepts and interactions without the complexities often involved in production-ready systems. The focus remains on understanding how 2FA enhances security by adding an additional layer of protection beyond passwords, we will be implementing totp method.

Creating the User

The User struct serves as the foundation of our authentication system. It holds essential information about each user, including their username, password, 2FA secret, and a flag indicating 2FA status.

type User struct {
Username string
Password string
Secret string
TwoFAEnabled bool
}

To maintain a reliable record of users, we employ a users map, it lives in memory, once the server shuts down it will be gone. Usernames serve as keys, and corresponding User objects represent the stored values.

Don’t try this at PROD :)

var users = make(map[string]User)

Our service has 4 main endpoints:

  • The /auth/signup endpoint welcomes new users by capturing their username and password. It first verifies if the username already exists. If not, it creates a new User object, stores it in users, and sends a welcoming message.
func signUp(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if the user already exists
if _, exists := users[newUser.Username]; exists {
c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})
return
}
// Save the user in the map
users[newUser.Username] = newUser
c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"})
}
  • /auth/login endpoint facilitates user log-in and 2FA verification. It retrieves the User object for the provided username and validates the submitted password and 2FA code. Upon successful authentication, it grants access. Otherwise, it indicates an error.
func login(c *gin.Context) {
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}

if err := c.ShouldBindJSON(&credentials); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if the user exists
user, exists := users[credentials.Username]
if !exists || user.Password != credentials.Password {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
}
  • /auth/enable-2fa endpoint empowers users to adopt 2FA. It generates a new 2FA secret using the totp package, stores it in the user’s object, marks TwoFAEnabled as true, and provides the Secret to be used in 2FA App such as Google Authenticator.
func enable2FA(c *gin.Context) {
var req struct {
Username string `json:"username"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if the user exists
user, exists := users[req.Username]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Generate a secret for the user
secret, err := totp.Generate(totp.GenerateOpts{
Issuer: "MyApp",
AccountName: user.Username,
Period: 60,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating QR code"})
return
}
// Save the secret in the user object
user.Secret = secret.Secret()
user.TwoFAEnabled = true
users[req.Username] = user
c.JSON(http.StatusOK, gin.H{"secret": secret.Secret(), "url": secret.URL()})
}
2FA Verify Sequence Diagram
  • /auth/verify endpoint validates the 2FA code submitted by the user. It retrieves the User object for the specified username and checks the provided code against the user’s secret. Upon successful verification, it confirms access. Otherwise, it indicates an error.
func verify(c *gin.Context) {
var verification struct {
Username string `json:"username"`
Code string `json:"code"`
}

if err := c.ShouldBindJSON(&verification); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if the user exists
user, exists := users[verification.Username]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Verify the provided code
valid := totp.Validate(verification.Code, user.Secret)
if !valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid 2FA code"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "2FA code is valid"})
}

The totp package plays a crucial role in generating and validating Time-based One-Time Passwords (TOTP) for 2FA. TOTP is a widely used 2FA method that utilises a secret key and current time to generate a one-time code. Once logged in, the user enters this code to gain access to the system.

Seeing is Believing

Let’s test our implementation, I will guide you through the steps to test the 2FA features in our Go application.

1. Start the Go Server

Before running tests, make sure your Go server is up and running. Navigate to the directory containing your Go application and execute the following command:

go run main.go

Ensure that the server starts without any errors and is listening on the specified port (default is :8080). This step sets the stage for testing the various aspects of your 2FA implementation.

2. User Signup

To begin, let’s create a new user in your system. Open a new terminal window and execute the following curl command:

$ curl -X POST -H "Content-Type: application/json" \
-d '{"username": "user1", "password": "password123"}' \
http://localhost:8080/auth/signup

{"message":"User created successfully"}

This command sends a request to the server to create a user with the specified username and password. Verify that the response indicates the successful creation of the user.

3. User Login (Without 2FA)

Now, let’s test the login functionality without 2FA. Run the following curl command:


$ curl -X POST -H “Content-Type: application/json” \
-d ‘{“username”: “user1”, “password”: “password123”, “code”: “”}’ \
http://localhost:8080/auth/login

{"message":"Login successful"}

This command attempts to log in the user without providing a 2FA code. Confirm that the response indicates a successful login.

4. Enable 2FA

Next, let’s enable Two-Factor Authentication for the user. Execute the following curl command:

$ curl -X POST -H “Content-Type: application/json” \
-d ‘{“username”: “user1”}’ \
http://localhost:8080/auth/enable-2fa

{
"secret":"6YHCOO3YXTOPXQ3YKYTBKBLTSPFFSKDY",
"url":"otpauth://totp/MyApp:user1?algorithm=SHA1\u0026digits=6\u0026issuer=MyApp\u0026period=60\u0026secret=6YHCOO3YXTOPXQ3YKYTBKBLTSPFFSKDY"
}

This command triggers the initiation of 2FA enrolment for the user. Ensure that the response indicates successful 2FA activation.

Using an authenticator app like Google Authenticator, add a new setup key

5. Verify 2FA

With 2FA enabled, let’s now test the verify process with 2FA. Run the following curl command:

$ curl -X POST -H “Content-Type: application/json” \
-d ‘{“username”: “user1”, “code”: “450378”}’ \
http://localhost:8080/auth/verify

{"message":"2FA code is valid"}

This command attempts to verify the user while providing the correct 2FA code. Confirm that the response indicates a successful login with 2FA.

2FA vs MFA

Both two-factor authentication (2FA) and multi-factor authentication (MFA) stand as prominent safeguards against unauthorised access. While they share the common goal of enhancing security beyond passwords, they differ in their implementation and overall effectiveness.

MFA extends the same principle as 2FA by incorporating additional factors, such as a fingerprint scan, security token, or even physical verification using a security key. By requiring multiple types of authentication, MFA further strengthens the barrier against unauthorised access.

The primary distinction lies in the number of factors involved. 2FA restricts authentication to two factors, while MFA encompasses three or more, offering a heightened level of security. This added layer of protection is particularly beneficial for safeguarding sensitive accounts, such as financial and email services, where unauthorised access could have devastating consequences.

However, the increased security comes at the cost of convenience. MFA may require additional steps and devices, potentially slowing down the authentication process. This can be a deterrent for users accustomed to the simplicity of traditional password-based logins.

What’s Next

While our custom 2FA implementation is effective, it’s worth exploring the integration of third-party authentication providers to leverage their advanced features and seamless user experiences. One such provider is Okta.

Conclusion

The adoption of 2FA has been steadily increasing in recent years as businesses and individuals recognise its importance in protecting their online accounts. Major websites and services, such as Google, Facebook, and banks, now offer 2FA as an option for their users.

The adoption of 2FA has significantly reduced the number of account compromises due to password breaches. It is now considered an essential security measure for any online account that holds sensitive information.

2FA has revolutionised online security by providing an additional layer of protection beyond passwords. It has become an essential tool for protecting personal and business accounts from cyberattacks. As cyberattacks continue to evolve, 2FA will continue to play a crucial role in safeguarding our digital lives.

--

--