Skip to main content

Echo Authentication Security

Authentication is a critical aspect of web application security, ensuring that only authorized users can access protected resources. In this guide, we'll explore how to implement secure authentication mechanisms in Echo, a high-performance, extensible, minimalist web framework for Go.

Introduction to Authentication in Echo

Authentication verifies the identity of users attempting to access your application. Echo provides several built-in middlewares and utilities to implement various authentication methods, from simple API keys to more complex JWT (JSON Web Token) based systems.

Properly implementing authentication helps protect:

  • User data privacy
  • Application integrity
  • System resources from unauthorized access
  • Against common security vulnerabilities

Basic Authentication

HTTP Basic Authentication is one of the simplest forms of authentication, where credentials are sent in the request header.

How Basic Authentication Works

  1. Client sends credentials (username/password) in the Authorization header
  2. Server validates these credentials
  3. Server grants access or returns an unauthorized response

Implementing Basic Authentication in Echo

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

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

// Basic auth middleware
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
// Check if username and password are correct
if username == "admin" && password == "secret" {
return true, nil
}
return false, nil
}))

// Protected route
e.GET("/secure", func(c echo.Context) error {
return c.String(200, "Welcome to the secure area!")
})

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

Example request and response:

Request:

GET /secure HTTP/1.1
Host: localhost:8080
Authorization: Basic YWRtaW46c2VjcmV0

Response (Success):

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 28

Welcome to the secure area!

Response (Failed Authentication):

HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=UTF-8
WWW-Authenticate: Basic realm=Restricted
Content-Length: 12

Unauthorized
caution

Basic Authentication sends credentials in base64 encoding, which is easily decodable. Always use HTTPS when implementing this method to ensure credentials are encrypted during transmission.

JWT Authentication

JSON Web Tokens (JWT) provide a more robust authentication mechanism, allowing stateless authentication between client and server.

Benefits of JWT Authentication

  • Stateless: No need to store session data on the server
  • Portable: Can work across multiple backends
  • Secure: When implemented correctly
  • Extensible: Can contain custom claims

Implementing JWT Authentication in Echo

First, install the required JWT package:

bash
go get -u github.com/golang-jwt/jwt/v4

Then implement JWT authentication:

go
package main

import (
"net/http"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

// JWT secret key
var jwtSecret = []byte("my-secret-key")

// User represents the user model
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}

// JWTCustomClaims represents custom claims structure
type JWTCustomClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}

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

// Login route
e.POST("/login", login)

// Restricted group
r := e.Group("/restricted")

// Configure middleware with the custom claims type
config := middleware.JWTConfig{
Claims: &JWTCustomClaims{},
SigningKey: jwtSecret,
}

r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)

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

// login handler
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

// In a real app, you'd check against a database
if username != "admin" || password != "password" {
return echo.ErrUnauthorized
}

// Set custom claims
claims := &JWTCustomClaims{
UserID: 1,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}

// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Generate encoded token
t, err := token.SignedString(jwtSecret)
if err != nil {
return err
}

return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}

// restricted handler
func restricted(c echo.Context) error {
// Get the user from the token claims
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTCustomClaims)

return c.String(http.StatusOK, "Welcome "+claims.Username+"!")
}

Example login request and response:

Request:

POST /login HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

username=admin&password=password

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

Accessing a protected endpoint:

Request:

GET /restricted HTTP/1.1
Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8

Welcome admin!

OAuth2 Integration

For more complex authentication requirements, you might need to integrate with OAuth2 providers like Google, Facebook, or GitHub.

go
package main

import (
"context"
"fmt"
"net/http"
"os"

"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)

var (
githubOAuthConfig = &oauth2.Config{
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
oauthStateString = "random-state-string"
)

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

e.GET("/", handleMain)
e.GET("/login", handleGitHubLogin)
e.GET("/callback", handleGitHubCallback)

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

func handleMain(c echo.Context) error {
return c.HTML(http.StatusOK, `
<html>
<body>
<a href="/login">GitHub Login</a>
</body>
</html>
`)
}

func handleGitHubLogin(c echo.Context) error {
url := githubOAuthConfig.AuthCodeURL(oauthStateString)
return c.Redirect(http.StatusTemporaryRedirect, url)
}

func handleGitHubCallback(c echo.Context) error {
// Check state to prevent CSRF
state := c.QueryParam("state")
if state != oauthStateString {
return c.String(http.StatusBadRequest, "Invalid OAuth state")
}

code := c.QueryParam("code")
token, err := githubOAuthConfig.Exchange(context.Background(), code)
if err != nil {
return c.String(http.StatusInternalServerError, "Code exchange failed: "+err.Error())
}

// You can now use the token to access GitHub API
// For example, fetch user information
client := githubOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
return c.String(http.StatusInternalServerError, "Failed getting user info: "+err.Error())
}
defer resp.Body.Close()

// In a real application, you would parse the response
// and create a session for the authenticated user
return c.String(http.StatusOK, fmt.Sprintf("Authentication successful! Token: %s", token.AccessToken))
}
note

To run this example, you need to register an OAuth application with GitHub and set the environment variables GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.

Best Practices for Authentication Security

1. Secure Password Storage

Always hash passwords before storing them:

go
import "golang.org/x/crypto/bcrypt"

// HashPassword generates a bcrypt hash from the password
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}

// CheckPasswordHash compares a password with a hash
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

2. Implement Rate Limiting

Prevent brute force attacks by implementing rate limiting:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)

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

// Rate limiter middleware
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(
middleware.RateLimiterConfig{
Skipper: middleware.DefaultSkipper,
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Rate: 10, // requests per second
Burst: 30, // maximum burst
ExpiresIn: 3600, // seconds until record expires
},
),
},
)))

// Login route that will be rate limited
e.POST("/login", login)

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

func login(c echo.Context) error {
// Your login logic here
return c.String(http.StatusOK, "Login successful")
}

When using cookies for authentication:

go
func setAuthCookie(c echo.Context, token string) {
cookie := new(http.Cookie)
cookie.Name = "auth"
cookie.Value = token
cookie.Expires = time.Now().Add(24 * time.Hour)
cookie.Path = "/"
cookie.HttpOnly = true // Prevents JavaScript access
cookie.Secure = true // Only sent over HTTPS
cookie.SameSite = http.SameSiteStrictMode // CSRF protection
c.SetCookie(cookie)
}

4. Implement CSRF Protection

Cross-Site Request Forgery protection is crucial for authentication systems:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

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

// CSRF middleware
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "form:_csrf",
CookieName: "csrf",
CookieMaxAge: 3600,
CookieSecure: true,
CookieHTTPOnly: true,
}))

e.GET("/form", showForm)
e.POST("/process", processForm)

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

func showForm(c echo.Context) error {
token := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
return c.HTML(200, `
<form method="POST" action="/process">
<input type="hidden" name="_csrf" value="`+token+`">
<input type="text" name="username">
<button type="submit">Submit</button>
</form>
`)
}

func processForm(c echo.Context) error {
return c.String(200, "Form processed!")
}

Real-World Example: Multi-factor Authentication

Let's implement a simple two-factor authentication system using Echo:

go
package main

import (
"crypto/rand"
"encoding/base32"
"net/http"
"strings"

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

// User represents a user with MFA enabled
type User struct {
ID int
Username string
Password string // In production, store the hash
TOTPSecret string
MFAEnabled bool
}

// Mock database for demonstration
var users = map[string]*User{
"john": {
ID: 1,
Username: "john",
Password: "password123",
TOTPSecret: "",
MFAEnabled: false,
},
}

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

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

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

func handleLogin(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

user, exists := users[username]
if !exists || user.Password != password {
return echo.ErrUnauthorized
}

if user.MFAEnabled {
// Return session ID that will be used in the MFA verification step
sessionID := generateSessionID()
return c.JSON(http.StatusOK, map[string]interface{}{
"requireMFA": true,
"sessionID": sessionID,
})
}

// Regular login without MFA
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Login successful",
"user": user.Username,
})
}

func handleSetupMFA(c echo.Context) error {
username := c.FormValue("username")
user, exists := users[username]
if !exists {
return echo.ErrNotFound
}

// Generate a new TOTP secret
secret, err := generateTOTPSecret()
if err != nil {
return err
}

// Store the secret (in a real app, save to database)
user.TOTPSecret = secret

// Generate the TOTP key URI for QR code generation
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "YourApp",
AccountName: username,
Secret: []byte(secret),
})
if err != nil {
return err
}

return c.JSON(http.StatusOK, map[string]interface{}{
"secret": secret,
"qrCodeURL": key.URL(),
})
}

func handleVerifyMFA(c echo.Context) error {
username := c.FormValue("username")
code := c.FormValue("code")

user, exists := users[username]
if !exists {
return echo.ErrNotFound
}

// Verify the TOTP code
valid := totp.Validate(code, user.TOTPSecret)
if !valid {
return echo.ErrUnauthorized
}

// Enable MFA for the user
user.MFAEnabled = true

return c.JSON(http.StatusOK, map[string]interface{}{
"message": "MFA setup complete",
})
}

// Helper functions
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b)
return base32.StdEncoding.EncodeToString(b)
}

func generateTOTPSecret() (string, error) {
b := make([]byte, 20)
_, err := rand.Read(b)
if err != nil {
return "", err
}

return strings.TrimRight(base32.StdEncoding.EncodeToString(b), "="), nil
}

This example demonstrates how to:

  1. Set up TOTP (Time-based One-Time Password) MFA for a user
  2. Verify TOTP codes during login
  3. Handle the multi-step authentication flow

Summary

In this guide, we've covered the fundamentals of implementing secure authentication in Echo applications:

  • Basic authentication for simple use cases
  • JWT authentication for stateless, token-based systems
  • OAuth2 integration for third-party authentication
  • Security best practices like password hashing and rate limiting
  • Advanced patterns with multi-factor authentication

Remember that authentication is just one piece of the security puzzle. Always combine these techniques with other security measures like proper authorization, input validation, and regular security audits.

Additional Resources

Exercises

  1. Extend the JWT authentication example to include role-based access control.
  2. Implement a "remember me" functionality using secure cookies.
  3. Create a password reset flow with secure token generation and verification.
  4. Add session management with Redis to track and invalidate active sessions.
  5. Implement account lockout after multiple failed login attempts to protect against brute force attacks.


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