Skip to main content

Echo Authentication Basics

Authentication is a critical aspect of web application security that helps verify the identity of users accessing your application. In this guide, we'll explore how to implement various authentication mechanisms in the Echo web framework for Go.

Introduction to Authentication in Echo

Authentication is the process of verifying that users are who they claim to be. Echo provides several middleware options and patterns to implement authentication in your web applications. Understanding these basics will help you secure your applications properly.

When building web applications, you'll commonly encounter these authentication types:

  1. Basic Authentication
  2. JWT (JSON Web Token) Authentication
  3. Session-based Authentication
  4. OAuth/OAuth2

We'll cover the first three methods in this guide, as they're the most commonly used in Echo applications.

Basic Authentication

Basic Authentication is the simplest form of authentication where users provide a username and password with each request.

How Basic Authentication Works

  1. The client sends a request to a protected route
  2. If no authentication credentials are provided, the server responds with a 401 status code
  3. The client then sends the request again with an Authorization header containing Basic followed by a base64-encoded string of username:password

Implementing Basic Authentication in Echo

Echo provides a built-in middleware for basic authentication:

go
package main

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

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(http.StatusOK, "You have accessed a secure resource!")
})

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

Example Request and Response:

Without authentication:

$ curl -i http://localhost:8080/secure

HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=UTF-8
Www-Authenticate: Basic realm=Restricted
Date: Mon, 10 Jul 2023 12:34:56 GMT
Content-Length: 12

Unauthorized

With authentication:

$ curl -i -u admin:secret http://localhost:8080/secure

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Date: Mon, 10 Jul 2023 12:35:30 GMT
Content-Length: 35

You have accessed a secure resource!

Limitations of Basic Authentication

While easy to implement, basic authentication has several drawbacks:

  • Credentials are sent with every request
  • Base64 encoding is not encryption (it's easily decodable)
  • No built-in expiration mechanism
  • Limited in features compared to other methods

JWT Authentication

JSON Web Tokens (JWT) provide a more robust authentication mechanism by using cryptographically signed tokens that contain claims about the user.

How JWT Authentication Works

  1. User logs in and receives a signed JWT token
  2. For subsequent requests, the client includes the JWT in the Authorization header
  3. The server verifies the JWT signature and extracts user information

Implementing JWT Authentication in Echo

First, install the JWT package:

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

Let's implement JWT authentication:

go
package main

import (
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)

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

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

// JwtCustomClaims are custom claims extending default ones
type JwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}

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

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

// Configure JWT middleware
config := middleware.JWTConfig{
Claims: &JwtCustomClaims{},
SigningKey: jwtSecret,
}

// Restricted group
r := e.Group("/restricted")
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")

// Check credentials (in a real app, check against database)
if username != "admin" || password != "password" {
return echo.ErrUnauthorized
}

// Set claims
claims := &JwtCustomClaims{
"John Doe",
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}

// 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,
})
}

// Protected handler
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}

Example Request and Response:

Login to get a token:

$ curl -X POST -d "username=admin&password=password" http://localhost:8080/login

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

Access a restricted endpoint with the token:

$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/restricted

Welcome John Doe!

Advantages of JWT Authentication

  • Stateless: No need to query the database for each request
  • Can store useful user information in the token
  • Built-in expiration mechanism
  • Widely supported across different platforms

Session-Based Authentication

Session-based authentication is another common method that maintains user state on the server.

How Session Authentication Works

  1. User logs in and receives a session ID (usually in a cookie)
  2. The server stores session data associated with the session ID
  3. On subsequent requests, the client sends the session ID, and the server looks up the associated session data

Implementing Session Authentication in Echo

For session-based authentication, we'll use the gorilla/sessions package:

go get github.com/gorilla/sessions

Then implement it:

go
package main

import (
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo-contrib/session"
"net/http"
)

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

// Initialize session store
e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))

// Routes
e.GET("/", home)
e.GET("/login", loginPage)
e.POST("/login", login)
e.GET("/dashboard", dashboard)
e.GET("/logout", logout)

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

// Home handler
func home(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the home page!")
}

// Login page handler
func loginPage(c echo.Context) error {
return c.String(http.StatusOK, "Please login")
}

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

// Check credentials
if username != "admin" || password != "password" {
return c.String(http.StatusUnauthorized, "Invalid credentials")
}

// Get the session
sess, err := session.Get("session", c)
if err != nil {
return err
}

// Set session values
sess.Values["authenticated"] = true
sess.Values["username"] = username
// Save session
sess.Save(c.Request(), c.Response())

return c.Redirect(http.StatusSeeOther, "/dashboard")
}

// Dashboard handler
func dashboard(c echo.Context) error {
sess, err := session.Get("session", c)
if err != nil {
return err
}

// Check if authenticated
if auth, ok := sess.Values["authenticated"].(bool); !ok || !auth {
return c.Redirect(http.StatusSeeOther, "/login")
}

username := sess.Values["username"].(string)
return c.String(http.StatusOK, "Welcome to your dashboard, "+username+"!")
}

// Logout handler
func logout(c echo.Context) error {
sess, err := session.Get("session", c)
if err != nil {
return err
}

// Revoke authentication
sess.Values["authenticated"] = false
sess.Save(c.Request(), c.Response())

return c.Redirect(http.StatusSeeOther, "/")
}

Creating a Middleware to Check Authentication

We can also create a middleware to protect routes:

go
// AuthMiddleware checks if user is authenticated
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sess, err := session.Get("session", c)
if err != nil {
return err
}

// Check if authenticated
if auth, ok := sess.Values["authenticated"].(bool); !ok || !auth {
return c.Redirect(http.StatusSeeOther, "/login")
}

return next(c)
}
}

// Then use it like this:
e.GET("/dashboard", dashboard, AuthMiddleware)

Advantages of Session-Based Authentication

  • More control over authentication
  • Easy to invalidate sessions
  • Can store complex session data
  • Traditional and well-understood approach

Real-World Example: Multi-tier Authentication System

Let's combine our knowledge to create a more comprehensive authentication system that supports both JWT and session-based auth:

go
package main

import (
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo-contrib/session"
"net/http"
"time"
)

var (
jwtSecret = []byte("jwt-secret-key")
cookieStore = sessions.NewCookieStore([]byte("session-secret-key"))
)

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

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

// Public routes
e.GET("/", homePage)
e.POST("/api/login", apiLogin) // JWT authentication
e.POST("/login", webLogin) // Session authentication

// API routes (JWT protected)
apiGroup := e.Group("/api")
apiConfig := middleware.JWTConfig{
SigningKey: jwtSecret,
}
apiGroup.Use(middleware.JWTWithConfig(apiConfig))
apiGroup.GET("/profile", apiProfile)

// Web routes (Session protected)
webGroup := e.Group("/app")
webGroup.Use(sessionAuthMiddleware)
webGroup.GET("/profile", webProfile)

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

func homePage(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to our service!")
}

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

// Validate credentials
if !validateUser(username, password) {
return echo.ErrUnauthorized
}

// Create JWT token
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

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

return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}

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

// Validate credentials
if !validateUser(username, password) {
return c.String(http.StatusUnauthorized, "Invalid credentials")
}

// Create session
sess, _ := session.Get("session", c)
sess.Values["authenticated"] = true
sess.Values["username"] = username
sess.Save(c.Request(), c.Response())

return c.Redirect(http.StatusFound, "/app/profile")
}

func apiProfile(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["username"].(string)

return c.JSON(http.StatusOK, map[string]string{
"username": username,
"message": "API profile accessed",
})
}

func webProfile(c echo.Context) error {
sess, _ := session.Get("session", c)
username := sess.Values["username"].(string)

return c.String(http.StatusOK, "Welcome to your web profile, "+username+"!")
}

func sessionAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sess, _ := session.Get("session", c)
if auth, ok := sess.Values["authenticated"].(bool); !ok || !auth {
return c.Redirect(http.StatusFound, "/login")
}
return next(c)
}
}

func validateUser(username, password string) bool {
// In a real app, check against database
return username == "user" && password == "password"
}

This example demonstrates:

  1. JWT authentication for API clients
  2. Session-based authentication for web clients
  3. Route protection using middleware
  4. Proper response handling for different auth methods

Security Best Practices

When implementing authentication, follow these security best practices:

  1. Use HTTPS: Always serve your application over HTTPS to encrypt data in transit.
  2. Store passwords securely: Use bcrypt or Argon2 to hash passwords.
  3. Protect against CSRF: Use CSRF tokens for forms.
  4. Set secure cookies: Use the Secure, HttpOnly, and SameSite flags.
  5. Implement rate limiting: Prevent brute force attacks.
  6. Use strong secrets: Generate strong, random secrets for signing tokens.
  7. Implement proper logout: Clear sessions on logout.
  8. Set appropriate expiration times: Don't make tokens or sessions last forever.

Summary

In this guide, we covered:

  • Basic authentication for simple auth needs
  • JWT authentication for stateless API authorization
  • Session-based authentication for web applications
  • A real-world example combining multiple auth methods
  • Security best practices to follow

Understanding these authentication basics in Echo will help you build secure web applications that protect your users' data and resources.

Additional Resources

Exercises

  1. Implement a user registration system with password hashing
  2. Create a password reset flow using time-limited tokens
  3. Add multi-factor authentication to the login process
  4. Implement OAuth2 authentication with a provider like Google or GitHub
  5. Create a role-based access control system using authentication middleware

By completing these exercises, you'll gain practical experience implementing various authentication mechanisms in Echo applications.



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