Echo API Authentication
Authentication is a critical aspect of API development that ensures only authorized users can access your endpoints. In this guide, we'll explore various authentication mechanisms in the Echo framework and how to implement them in your applications.
Introduction to API Authentication
Authentication verifies the identity of users or systems attempting to access your API. Without proper authentication, your API would be open to anyone, potentially exposing sensitive data or functionality to unauthorized parties.
Echo provides several ways to implement authentication, from basic username/password schemes to more advanced token-based approaches. In this tutorial, we'll cover:
- Basic authentication
- JWT (JSON Web Token) authentication
- Custom middleware authentication
- Best practices for securing your Echo API
Basic Authentication
Basic authentication is one of the simplest forms of API authentication. It works by sending a username and password with each request.
How Basic Authentication Works
- The client combines the username and password with a colon (username:password)
- This string is base64 encoded
- The encoded string is sent in the Authorization header as
Basic {encoded-string}
Implementing Basic Authentication in Echo
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 == "password" {
return true, nil
}
return false, nil
}))
// Protected route
e.GET("/protected", func(c echo.Context) error {
return c.String(200, "You accessed a protected endpoint!")
})
e.Start(":8080")
}
Making Authenticated Requests
Using curl to access the protected endpoint:
curl -u admin:password http://localhost:8080/protected
Output:
You accessed a protected endpoint!
Without authentication or with incorrect credentials:
curl http://localhost:8080/protected
Output:
Unauthorized
JWT Authentication
JWT (JSON Web Token) is a more modern approach to authentication that uses a signed token to verify user identity. JWTs are particularly useful for stateless authentication in APIs.
How JWT Authentication Works
- User logs in with credentials
- Server validates credentials and generates a JWT
- Server returns the JWT to the client
- Client includes the JWT in subsequent requests
- Server validates the JWT signature and payload before processing the request
Implementing JWT Authentication in Echo
First, let's create a simple login endpoint that issues JWTs:
package main
import (
"net/http"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// User represents the user data structure
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}
// jwtCustomClaims are custom claims extending default ones
type jwtCustomClaims struct {
Username string `json:"username"`
Admin bool `json:"admin"`
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: []byte("your-secret-key"), // In production, use a secure key
}
r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)
e.Start(":8080")
}
// Login handler
func login(c echo.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return err
}
// Check if username and password are correct (in a real app, check against a database)
if user.Username != "admin" || user.Password != "password" {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &jwtCustomClaims{
user.Username,
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token
t, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
// Restricted handler
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Username
return c.String(http.StatusOK, "Welcome "+name+"!")
}
Using JWT Authentication
First, get a token by logging in:
curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"password"}' http://localhost:8080/login
Output:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
Then use the token to access a restricted endpoint:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/restricted
Output:
Welcome admin!
Custom Authentication Middleware
Sometimes you need more control over your authentication logic. Echo allows you to create custom middleware for specialized authentication needs.
Creating a Custom Authentication Middleware
package main
import (
"github.com/labstack/echo/v4"
"net/http"
"strings"
)
func main() {
e := echo.New()
// Apply custom auth middleware to all routes
e.Use(customAuthMiddleware)
e.GET("/api/data", func(c echo.Context) error {
return c.String(http.StatusOK, "This is protected data")
})
e.Start(":8080")
}
// Custom authentication middleware
func customAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get API key from header
apiKey := c.Request().Header.Get("X-API-Key")
// Check if API key exists and is valid
if apiKey == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing API key")
}
// In production, validate against a database or other secure storage
if apiKey != "valid-api-key-123" {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
}
// Extract user ID from token (simplified for example)
userId := "user-123"
// Store user information in context
c.Set("userId", userId)
// Proceed with the request
return next(c)
}
}
Using Custom Authentication
curl -H "X-API-Key: valid-api-key-123" http://localhost:8080/api/data
Output:
This is protected data
With invalid API key:
curl -H "X-API-Key: invalid-key" http://localhost:8080/api/data
Output:
{"message":"Invalid API key"}
Combining Authentication Methods
For more complex applications, you might want to support multiple authentication methods. Echo makes this straightforward with its middleware system:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the API")
})
// Routes using JWT authentication
jwtGroup := e.Group("/api/v1")
jwtGroup.Use(middleware.JWT([]byte("your-secret-key")))
jwtGroup.GET("/users", getUsers)
// Routes using Basic authentication
basicGroup := e.Group("/admin")
basicGroup.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
if username == "admin" && password == "adminpass" {
return true, nil
}
return false, nil
}))
basicGroup.GET("/stats", getStats)
// Routes using API key authentication
apiGroup := e.Group("/api/v2")
apiGroup.Use(apiKeyMiddleware)
apiGroup.GET("/data", getData)
e.Start(":8080")
}
func getUsers(c echo.Context) error {
return c.String(http.StatusOK, "User list")
}
func getStats(c echo.Context) error {
return c.String(http.StatusOK, "Admin statistics")
}
func getData(c echo.Context) error {
return c.String(http.StatusOK, "API data")
}
// API key middleware
func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
key := c.QueryParam("api_key")
if key != "valid-api-key" {
return echo.ErrUnauthorized
}
return next(c)
}
}
Real-World Authentication Example: OAuth 2.0 Integration
For a more comprehensive real-world example, let's implement authentication using OAuth 2.0 with a third-party provider like Google:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var (
googleOauthConfig *oauth2.Config
oauthStateString = "random-state-string" // In production, generate this randomly for each request
)
func init() {
googleOauthConfig = &oauth2.Config{
RedirectURL: "http://localhost:8080/callback",
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
}
func main() {
e := echo.New()
e.GET("/", handleMain)
e.GET("/login", handleGoogleLogin)
e.GET("/callback", handleGoogleCallback)
e.GET("/protected", protected, authMiddleware)
e.Start(":8080")
}
func handleMain(c echo.Context) error {
var html = `
<html>
<body>
<a href="/login">Log in with Google</a>
</body>
</html>
`
return c.HTML(http.StatusOK, html)
}
func handleGoogleLogin(c echo.Context) error {
url := googleOauthConfig.AuthCodeURL(oauthStateString)
return c.Redirect(http.StatusTemporaryRedirect, url)
}
func handleGoogleCallback(c echo.Context) error {
// Check state
state := c.QueryParam("state")
if state != oauthStateString {
return c.String(http.StatusBadRequest, "Invalid state parameter")
}
// Exchange authorization code for token
code := c.QueryParam("code")
token, err := googleOauthConfig.Exchange(c.Request().Context(), code)
if err != nil {
return c.String(http.StatusInternalServerError, "Code exchange failed: "+err.Error())
}
// Get user info
response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed getting user info: "+err.Error())
}
defer response.Body.Close()
// Read and parse user data
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed reading response body: "+err.Error())
}
// Store token in session or cookie for subsequent requests
// In a real application, you would use a proper session management system
c.SetCookie(&http.Cookie{
Name: "auth_token",
Value: token.AccessToken,
HttpOnly: true,
})
return c.Redirect(http.StatusTemporaryRedirect, "/protected")
}
// Authentication middleware
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("auth_token")
if err != nil || cookie.Value == "" {
return c.Redirect(http.StatusTemporaryRedirect, "/login")
}
// Validate token (in a real app, you would verify the token is still valid)
// Set user information in context
c.Set("token", cookie.Value)
return next(c)
}
}
func protected(c echo.Context) error {
token := c.Get("token").(string)
// Use token to get user info
response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed getting user info: "+err.Error())
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return c.String(http.StatusInternalServerError, "Failed reading response body: "+err.Error())
}
var userInfo map[string]interface{}
if err := json.Unmarshal(contents, &userInfo); err != nil {
return c.String(http.StatusInternalServerError, "Failed parsing user info: "+err.Error())
}
return c.String(http.StatusOK, fmt.Sprintf("Protected content! Hello, %s", userInfo["email"]))
}
Best Practices for API Authentication
-
Use HTTPS: Always serve your API over HTTPS to encrypt the communication between client and server.
-
Secure password storage: Never store passwords in plain text. Use bcrypt or another strong hashing algorithm.
import "golang.org/x/crypto/bcrypt"
// Hash a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// Check if password matches hash
func checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
-
Token expiration: Always set an expiration time on your tokens to limit the damage if they are stolen.
-
Secret key management: Never hard-code secret keys in your application. Use environment variables or a secure secret management system.
-
Rate limiting: Implement rate limiting to prevent brute force attacks.
import "github.com/labstack/echo/v4/middleware"
// Add rate limiting middleware
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) // 20 requests per second
-
Validate all input: Always validate and sanitize user input to prevent security vulnerabilities.
-
Implement proper logout: For token-based authentication, maintain a blacklist of invalidated tokens or use short-lived tokens with refresh tokens.
Summary
Authentication is a crucial aspect of API security. Echo provides built-in middleware and flexibility for implementing various authentication methods:
- Basic Authentication: Simple username/password authentication suitable for internal APIs
- JWT Authentication: Token-based authentication ideal for stateless APIs and SPAs
- Custom Authentication: Flexible approach for specialized authentication requirements
- OAuth 2.0: Industry-standard protocol for authorization
When building your API, choose the authentication method that best suits your application's requirements, keeping in mind the security implications and user experience.
Additional Resources
- Echo Framework Documentation
- JWT.io - Debugger and information about JSON Web Tokens
- OAuth 2.0 Simplified - A guide to OAuth 2.0
- OWASP Authentication Cheat Sheet
Exercises
- Implement JWT authentication with refresh tokens in an Echo application.
- Create a role-based access control system using custom middleware.
- Integrate authentication with a database to store user credentials securely.
- Implement two-factor authentication for a high-security API endpoint.
- Create a comprehensive authentication system that supports multiple methods (JWT, API key, OAuth) and allows users to choose their preferred method.
By implementing proper authentication in your Echo API, you'll ensure that your application remains secure and that only authorized users can access protected resources.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)