Skip to main content

Echo Multi-factor Authentication

Introduction

Multi-factor Authentication (MFA) is a security mechanism that requires users to provide two or more verification factors to gain access to a resource such as an application or online account. In today's security landscape, relying solely on passwords is insufficient. MFA significantly enhances security by adding additional layers of verification.

In this guide, we'll explore how to implement Multi-factor Authentication in the Echo framework. Echo is a high-performance, extensible, and minimalist Go web framework that makes it easy to build robust web applications.

Understanding Multi-factor Authentication

MFA typically consists of verification from different categories:

  1. Knowledge - Something you know (password, PIN)
  2. Possession - Something you have (mobile device, hardware token)
  3. Inherence - Something you are (fingerprint, face recognition)

By requiring multiple types of verification, MFA protects against various attack vectors. Even if an attacker obtains a user's password, they would still need the second factor to gain access.

Setting Up MFA in Echo

Let's implement a basic MFA system in Echo using Time-based One-Time Passwords (TOTP), which is commonly used for generating authentication codes in mobile authenticator apps.

Step 1: Set Up the Project and Dependencies

First, let's create a new project and install the necessary dependencies:

bash
mkdir echo-mfa-demo
cd echo-mfa-demo
go mod init echo-mfa-demo
go get github.com/labstack/echo/v4
go get github.com/pquerna/otp/totp

Step 2: Creating the Base Echo Application

Let's create our main application file with basic structure:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/pquerna/otp/totp"
)

// User represents a user in our system
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
TOTPSecret string `json:"totp_secret"`
MFAEnabled bool `json:"mfa_enabled"`
}

// Simple in-memory user store
var users = map[string]User{
"john": {
ID: 1,
Username: "john",
Password: "password123", // In production, store hashed passwords!
TOTPSecret: "",
MFAEnabled: false,
},
}

func main() {
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Routes
e.POST("/login", handleLogin)
e.POST("/setup-mfa", handleSetupMFA)
e.POST("/verify-mfa", handleVerifyMFA)

// Start server
e.Logger.Fatal(e.Start(":8080"))
}

Step 3: Implementing the Login Handler

Now, let's implement the login handler that will check the username and password:

go
func handleLogin(c echo.Context) error {
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

req := new(LoginRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}

// Find the user
user, exists := users[req.Username]
if !exists || user.Password != req.Password {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}

// Check if MFA is enabled
if user.MFAEnabled {
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "First factor authenticated. Please provide the second factor.",
"require_mfa": true,
"username": user.Username,
})
}

// If MFA is not enabled, the user is fully authenticated
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Authentication successful",
"user": map[string]interface{}{
"id": user.ID,
"username": user.Username,
"mfa_enabled": user.MFAEnabled,
},
})
}

Step 4: Implementing MFA Setup

Now, let's add the capability for users to set up MFA:

go
func handleSetupMFA(c echo.Context) error {
type SetupRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

req := new(SetupRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}

// Authenticate user first
user, exists := users[req.Username]
if !exists || user.Password != req.Password {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}

// Generate a new TOTP key
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "EchoMFADemo",
AccountName: req.Username,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate MFA secret",
})
}

// Update user with new TOTP secret (not enabled yet)
user.TOTPSecret = key.Secret()
users[req.Username] = user

return c.JSON(http.StatusOK, map[string]interface{}{
"message": "MFA setup initiated. Please use the secret to configure your authenticator app.",
"secret": key.Secret(),
"qr_code_url": key.URL(),
})
}

Step 5: Implementing MFA Verification

Finally, let's add the capability to verify the MFA code:

go
func handleVerifyMFA(c echo.Context) error {
type VerifyRequest struct {
Username string `json:"username"`
Token string `json:"token"` // The TOTP code from the authenticator app
}

req := new(VerifyRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}

// Get the user
user, exists := users[req.Username]
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "User not found",
})
}

// During setup phase, validate the token and enable MFA
if !user.MFAEnabled && user.TOTPSecret != "" {
valid := totp.Validate(req.Token, user.TOTPSecret)
if valid {
user.MFAEnabled = true
users[req.Username] = user
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "MFA has been successfully enabled for your account",
"user": map[string]interface{}{
"id": user.ID,
"username": user.Username,
"mfa_enabled": user.MFAEnabled,
},
})
}

return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid token. Please try again",
})
}

// For subsequent logins, validate the token
if user.MFAEnabled {
valid := totp.Validate(req.Token, user.TOTPSecret)
if valid {
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Two-factor authentication successful",
"user": map[string]interface{}{
"id": user.ID,
"username": user.Username,
"mfa_enabled": user.MFAEnabled,
},
})
}

return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid token. Please try again",
})
}

return c.JSON(http.StatusBadRequest, map[string]string{
"error": "MFA is not configured for this user",
})
}

Example Flow: Setting Up and Using MFA

Let's go through a typical flow of setting up and using MFA in our application:

1. Regular Login (Before MFA is enabled)

Request:

json
POST /login
{
"username": "john",
"password": "password123"
}

Response:

json
{
"message": "Authentication successful",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": false
}
}

2. Setting Up MFA

Request:

json
POST /setup-mfa
{
"username": "john",
"password": "password123"
}

Response:

json
{
"message": "MFA setup initiated. Please use the secret to configure your authenticator app.",
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_url": "otpauth://totp/EchoMFADemo:john?secret=JBSWY3DPEHPK3PXP&issuer=EchoMFADemo"
}

The user would now use an authenticator app like Google Authenticator or Authy to scan the QR code or manually enter the secret.

3. Verifying and Enabling MFA

Request:

json
POST /verify-mfa
{
"username": "john",
"token": "123456" // The 6-digit code from the authenticator app
}

Response:

json
{
"message": "MFA has been successfully enabled for your account",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": true
}
}

4. Login After MFA is Enabled

Request:

json
POST /login
{
"username": "john",
"password": "password123"
}

Response:

json
{
"message": "First factor authenticated. Please provide the second factor.",
"require_mfa": true,
"username": "john"
}

5. Providing the Second Factor

Request:

json
POST /verify-mfa
{
"username": "john",
"token": "123456" // Current 6-digit code from the authenticator app
}

Response:

json
{
"message": "Two-factor authentication successful",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": true
}
}

Best Practices for MFA Implementation

  1. Provide Backup Codes: Generate and store recovery codes that users can use if they lose access to their authentication device.

  2. Secure Storage of Secrets: Always store MFA secrets securely, preferably encrypted with a strong algorithm.

  3. Rate Limiting: Implement rate limiting to prevent brute force attacks on the verification endpoint.

  4. Session Management: After successful MFA verification, provide a secure session token with appropriate expiration.

  5. Remember Me Functionality: Consider allowing users to "remember" a trusted device for a certain period to balance security and convenience.

Enhancing the Implementation

Here's how you could enhance the basic implementation:

Adding Recovery Codes

go
func generateRecoveryCodes() []string {
codes := make([]string, 8)
for i := 0; i < 8; i++ {
// Generate a random 10-character code
// This is a simplified example - use a cryptographically secure method in production
codes[i] = fmt.Sprintf("%010d", rand.Intn(9999999999))
}
return codes
}

Adding Rate Limiting Middleware

go
func rateLimitMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
// A simple in-memory rate limiter
rateLimits := make(map[string]int)
mutex := &sync.Mutex{}

return func(c echo.Context) error {
ip := c.RealIP()

mutex.Lock()
count := rateLimits[ip]
if count > 5 { // 5 attempts max
mutex.Unlock()
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Please try again later.",
})
}

rateLimits[ip]++
mutex.Unlock()

// Reset the counter after 1 minute
time.AfterFunc(time.Minute, func() {
mutex.Lock()
delete(rateLimits, ip)
mutex.Unlock()
})

return next(c)
}
}

Summary

In this guide, we've explored how to implement Multi-factor Authentication in an Echo framework application:

  1. We set up a basic Echo application with authentication endpoints
  2. We implemented a username/password login system (first factor)
  3. We added TOTP-based MFA as a second factor
  4. We created endpoints for setting up, verifying, and using MFA
  5. We discussed best practices and potential enhancements

MFA provides a significant security improvement over single-factor authentication. While our example used TOTP as the second factor, many other options exist such as SMS codes, email verification, push notifications, or hardware security keys like YubiKey.

Remember that security is a continuous process. Always keep your dependencies updated, follow security best practices, and be prepared to adapt to evolving security threats.

Additional Resources

Exercises

  1. Modify the example to store user data in a database instead of in-memory.
  2. Add the ability to disable MFA for a user account.
  3. Implement recovery codes that users can use if they lose their second factor.
  4. Add support for other types of second factors, such as email verification codes.
  5. Implement a "remember this device" feature that allows users to skip MFA on trusted devices.

Happy coding and stay secure!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)