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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)