Echo Authentication Flows
Authentication is a critical aspect of web application security. It verifies the identity of users and ensures that only authorized individuals can access protected resources. In this guide, we'll explore various authentication flows in Echo, a high-performance, minimalist Go web framework.
Introduction to Authentication Flows
Authentication flows define the process and sequence of steps a user must complete to verify their identity. Echo supports multiple authentication methods that can be tailored to your application's specific needs.
Before diving into specific flows, let's understand some key concepts:
- Credentials: Information used to verify identity (username/password, tokens, etc.)
- Authentication Middleware: Echo middleware that processes auth requests
- JWT (JSON Web Tokens): A compact, URL-safe means of representing claims securely
- Sessions: Server-side storage of user authentication state
Basic Authentication Flow
Basic authentication is one of the simplest authentication mechanisms, where credentials are sent in the HTTP header.
How Basic Authentication Works:
- Client sends a request with the
Authorization
header containing Base64-encoded credentials - Server decodes and validates the credentials
- If valid, the server grants access; otherwise, returns a 401 Unauthorized response
Implementation Example:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Configure Basic Auth middleware
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
// Check if username and password are valid
if username == "john" && password == "secret" {
return true, nil
}
return false, nil
}))
// Protected route
e.GET("/protected", func(c echo.Context) error {
return c.String(200, "You are authenticated!")
})
e.Logger.Fatal(e.Start(":8080"))
}
Testing Basic Auth:
# This will fail with 401 Unauthorized
curl http://localhost:8080/protected
# This will succeed (username:password in Base64)
curl -H "Authorization: Basic am9objpzZWNyZXQ=" http://localhost:8080/protected
# Output: You are authenticated!
JWT Authentication Flow
JWT provides a more robust authentication mechanism suitable for modern web applications, especially those with stateless APIs.
How JWT Flow Works:
- User logs in with credentials
- Server validates credentials and creates a signed JWT token
- Server sends the token to the client
- Client stores the token (typically in localStorage or cookie)
- Client includes the token in subsequent requests via Authorization header
- Server validates the token signature and extracts user info for each request
Implementation Example:
package main
import (
"net/http"
"time"
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// JWT secret key
var jwtSecret = []byte("your-secret-key")
// User represents user data
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
// JwtCustomClaims represents JWT claims
type JwtCustomClaims struct {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func main() {
e := echo.New()
// Login endpoint - generates JWT token
e.POST("/login", login)
// Configure JWT middleware for protected routes
r := e.Group("/api")
r.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: jwtSecret,
Claims: &JwtCustomClaims{},
}))
// Protected route
r.GET("/protected", protectedHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Validate user credentials (in a real app, check against database)
if username != "john" || password != "secret" {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &JwtCustomClaims{
username,
false,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
},
}
// 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, map[string]string{
"token": t,
})
}
func protectedHandler(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JwtCustomClaims)
username := claims.Username
return c.String(http.StatusOK, "Welcome "+username+"!")
}
Testing JWT Auth:
# First obtain a token
curl -X POST -d "username=john&password=secret" http://localhost:8080/login
# Output: {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# Access protected resource with token
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/api/protected
# Output: Welcome john!
OAuth2 Authentication Flow
OAuth2 allows users to grant limited access to their resources on one site to another site without sharing their credentials. Echo can be integrated with OAuth2 providers like Google, Facebook, or GitHub.
How OAuth2 Flow Works (Authorization Code Flow):
- User clicks "Login with Provider" (e.g., Google)
- Application redirects to provider's authorization URL
- User authenticates with the provider and grants permissions
- Provider redirects back to your app with an authorization code
- Your application exchanges this code for an access token
- Your application uses the token to fetch user info and create a session
Implementation Example:
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var (
googleOAuthConfig = &oauth2.Config{
ClientID: "your-client-id.apps.googleusercontent.com",
ClientSecret: "your-client-secret",
RedirectURL: "http://localhost:8080/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
// Random state to prevent CSRF
oauthStateString = "random-state"
)
func main() {
e := echo.New()
// Route to start Google OAuth flow
e.GET("/auth/google", handleGoogleLogin)
// Callback handler after Google authentication
e.GET("/auth/google/callback", handleGoogleCallback)
// Protected route
e.GET("/profile", profileHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func handleGoogleLogin(c echo.Context) error {
url := googleOAuthConfig.AuthCodeURL(oauthStateString)
return c.Redirect(http.StatusTemporaryRedirect, url)
}
func handleGoogleCallback(c echo.Context) error {
// Verify 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 := googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
return c.String(http.StatusInternalServerError, "Code exchange failed: "+err.Error())
}
// Get user info with the access token
client := googleOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to get user info: "+err.Error())
}
defer resp.Body.Close()
var userInfo struct {
Email string `json:"email"`
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return c.String(http.StatusInternalServerError, "Failed to decode user info: "+err.Error())
}
// In a real app, you would create a session or JWT token here
// For this example, we'll just set a cookie
c.SetCookie(&http.Cookie{
Name: "user_session",
Value: userInfo.Email,
Expires: time.Now().Add(24 * time.Hour),
})
return c.Redirect(http.StatusFound, "/profile")
}
func profileHandler(c echo.Context) error {
// In a real app, validate the session/token
cookie, err := c.Cookie("user_session")
if err != nil || cookie.Value == "" {
return c.Redirect(http.StatusFound, "/auth/google")
}
return c.String(http.StatusOK, fmt.Sprintf("Hello, %s!", cookie.Value))
}
Session-Based Authentication Flow
Session-based authentication creates a session on the server after the user logs in and stores a session identifier in a cookie.
How Session-Based Flow Works:
- User provides login credentials
- Server validates credentials and creates a new session
- Server sends a session ID to the client as a cookie
- Client includes this cookie in subsequent requests
- Server validates the session ID and retrieves session data
Implementation Example:
package main
import (
"net/http"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Initialize session store
store := sessions.NewCookieStore([]byte("secret-key"))
e.Use(session.Middleware(store))
// Login route
e.POST("/login", handleLogin)
// Logout route
e.GET("/logout", handleLogout)
// Protected route
e.GET("/dashboard", dashboardHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func handleLogin(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Validate credentials (in a real app, check against database)
if username != "john" || password != "secret" {
return c.String(http.StatusUnauthorized, "Invalid credentials")
}
// Create a new session
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 86400, // 1 day
HttpOnly: true,
}
// Set user data in session
sess.Values["authenticated"] = true
sess.Values["username"] = username
sess.Save(c.Request(), c.Response())
return c.Redirect(http.StatusFound, "/dashboard")
}
func handleLogout(c echo.Context) error {
sess, _ := session.Get("session", c)
// Revoke users authentication
sess.Values["authenticated"] = false
sess.Values["username"] = ""
sess.Save(c.Request(), c.Response())
return c.Redirect(http.StatusFound, "/")
}
func dashboardHandler(c echo.Context) error {
sess, _ := session.Get("session", c)
// Check if user is authenticated
if auth, ok := sess.Values["authenticated"].(bool); !ok || !auth {
return c.Redirect(http.StatusFound, "/")
}
// Get username from session
username := sess.Values["username"].(string)
return c.String(http.StatusOK, "Welcome to your dashboard, "+username+"!")
}
Custom Authentication Middleware
For more complex authentication requirements, you might need to create custom middleware:
func CustomAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get token from header, query, or cookie
token := c.Request().Header.Get("X-API-Key")
// Validate token (example)
if token != "valid-api-key" {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or missing API key")
}
// Set user information in context
c.Set("user", "api-user")
// Continue with the next middleware or handler
return next(c)
}
}
// Usage:
e.GET("/api/data", getData, CustomAuthMiddleware)
Best Practices for Authentication
- Always use HTTPS: Encrypt all authentication traffic to prevent credential theft
- Store passwords securely: Use bcrypt or Argon2 for password hashing
- Implement rate limiting: Prevent brute force attacks by limiting login attempts
- Use secure cookies: Set HTTPOnly, Secure, and SameSite flags on session cookies
- Implement proper CSRF protection: Use CSRF tokens for state-changing operations
- Rotate tokens: Implement token rotation for long-lived sessions
- Validate all inputs: Never trust user-supplied input during authentication
Real-World Example: Multi-Factor Authentication
Multi-factor authentication adds an extra layer of security by requiring users to provide additional verification.
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/pquerna/otp/totp"
)
// User with MFA enabled
type User struct {
ID int
Username string
Password string
TOTPSecret string
MFAEnabled bool
}
// Mock user database
var users = map[string]User{
"john": {
ID: 1,
Username: "john",
Password: "$2a$10$somehashedpassword", // bcrypt hash in real app
TOTPSecret: "JBSWY3DPEHPK3PXP", // In a real app, generate and store securely
MFAEnabled: true,
},
}
func main() {
e := echo.New()
e.POST("/login", handleInitialLogin)
e.POST("/login/verify", handleMFAVerification)
e.Logger.Fatal(e.Start(":8080"))
}
func handleInitialLogin(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Find user (in real app, check against database)
user, exists := users[username]
if !exists {
return echo.ErrUnauthorized
}
// In a real app: check if password matches using bcrypt.CompareHashAndPassword
if user.MFAEnabled {
// Generate a temporary session for MFA verification
// In a real app, store this securely with expiration
tempToken := "temp-session-" + username
return c.JSON(http.StatusOK, map[string]interface{}{
"require_mfa": true,
"temp_token": tempToken,
})
}
// No MFA required, generate full session token
// In a real app, generate JWT or session here
return c.JSON(http.StatusOK, map[string]interface{}{
"token": "user-session-token",
})
}
func handleMFAVerification(c echo.Context) error {
tempToken := c.FormValue("temp_token")
otpCode := c.FormValue("otp_code")
// Validate temp token (in a real app, retrieve from secure storage)
// Extract username from temp token
username := tempToken[len("temp-session-"):]
user, exists := users[username]
if !exists {
return echo.ErrUnauthorized
}
// Verify the TOTP code
valid := totp.Validate(otpCode, user.TOTPSecret)
if !valid {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid verification code",
})
}
// MFA successful, generate full session
// In a real app, create JWT or session here
return c.JSON(http.StatusOK, map[string]interface{}{
"token": "user-session-token",
})
}
Summary
We've explored various authentication flows in Echo, each with its own strengths and use cases:
- Basic Authentication: Simple but less secure, good for internal APIs
- JWT Authentication: Stateless, scalable, ideal for modern web applications
- OAuth2: Excellent for third-party authentication and single sign-on
- Session-Based: Traditional approach with server-side state
- Custom Middleware: For specialized authentication needs
- Multi-Factor Authentication: Additional security layer for sensitive applications
The best authentication approach depends on your specific requirements, including security needs, user experience, and application architecture.
Additional Resources
Practice Exercises
- Implement a JWT authentication system with token refresh functionality
- Create a password reset flow using time-limited tokens
- Build a complete OAuth2 authentication system with multiple providers
- Implement role-based access control on top of JWT authentication
- Create a secure Remember Me functionality with secure cookie storage
By mastering these authentication flows, you'll be well-equipped to build secure, user-friendly web applications with Echo.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)