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:
- Knowledge - Something you know (password, PIN)
- Possession - Something you have (mobile device, hardware token)
- 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:
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:
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:
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:
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:
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:
POST /login
{
"username": "john",
"password": "password123"
}
Response:
{
"message": "Authentication successful",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": false
}
}
2. Setting Up MFA
Request:
POST /setup-mfa
{
"username": "john",
"password": "password123"
}
Response:
{
"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:
POST /verify-mfa
{
"username": "john",
"token": "123456" // The 6-digit code from the authenticator app
}
Response:
{
"message": "MFA has been successfully enabled for your account",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": true
}
}
4. Login After MFA is Enabled
Request:
POST /login
{
"username": "john",
"password": "password123"
}
Response:
{
"message": "First factor authenticated. Please provide the second factor.",
"require_mfa": true,
"username": "john"
}
5. Providing the Second Factor
Request:
POST /verify-mfa
{
"username": "john",
"token": "123456" // Current 6-digit code from the authenticator app
}
Response:
{
"message": "Two-factor authentication successful",
"user": {
"id": 1,
"username": "john",
"mfa_enabled": true
}
}
Best Practices for MFA Implementation
-
Provide Backup Codes: Generate and store recovery codes that users can use if they lose access to their authentication device.
-
Secure Storage of Secrets: Always store MFA secrets securely, preferably encrypted with a strong algorithm.
-
Rate Limiting: Implement rate limiting to prevent brute force attacks on the verification endpoint.
-
Session Management: After successful MFA verification, provide a secure session token with appropriate expiration.
-
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
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
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:
- We set up a basic Echo application with authentication endpoints
- We implemented a username/password login system (first factor)
- We added TOTP-based MFA as a second factor
- We created endpoints for setting up, verifying, and using MFA
- 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
- Modify the example to store user data in a database instead of in-memory.
- Add the ability to disable MFA for a user account.
- Implement recovery codes that users can use if they lose their second factor.
- Add support for other types of second factors, such as email verification codes.
- 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! :)