Echo WebSocket Authentication
Introduction
WebSockets provide full-duplex communication channels over a single TCP connection, enabling real-time data exchange between clients and servers. However, when building production applications, securing these connections becomes crucial. In this tutorial, we'll explore how to implement authentication for WebSocket connections in Echo, a high-performance web framework for Go.
Authentication ensures that only authorized users can establish WebSocket connections with your server, protecting your application from unauthorized access and potential security threats.
Why WebSocket Authentication Matters
WebSocket connections, once established, remain open for extended periods, making them potential security vulnerabilities if not properly secured. Some key reasons to implement authentication include:
- Preventing unauthorized access: Ensures only legitimate users can connect
- User-specific data protection: Guarantees users only receive data they're authorized to access
- Resource control: Limits server resources to authenticated connections
- Audit trails: Enables tracking of user activities for compliance and debugging
Authentication Approaches for WebSockets
There are several methods to authenticate WebSocket connections in Echo:
1. Token-based Authentication
Token-based authentication is the most common approach, where clients provide a token (JWT, session ID, etc.) during the WebSocket handshake.
Implementation Steps:
- Client requests a token through a standard HTTP endpoint
- Server validates credentials and issues a token
- Client includes the token in WebSocket connection request
- Server validates the token before upgrading the connection
Let's implement a simple token-based authentication system:
package main
import (
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/gorilla/websocket"
)
var (
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // For development, allow all origins
},
}
jwtSecret = []byte("your-secret-key") // Use a secure secret in production
)
// User represents a basic user for authentication
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"` // Store hashed passwords in real applications
}
// JWTClaims represents the claims in the JWT
type JWTClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.StandardClaims
}
// Sample user database (use a real database in production)
var users = []User{
{ID: 1, Username: "alice", Password: "password1"},
{ID: 2, Username: "bob", Password: "password2"},
}
func main() {
e := echo.New()
// Login endpoint to get token
e.POST("/login", login)
// WebSocket endpoint with JWT verification
e.GET("/ws", handleWebSocket, middleware.JWT(jwtSecret))
e.Logger.Fatal(e.Start(":8080"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Find user
var user *User
for _, u := range users {
if u.Username == username && u.Password == password {
user = &u
break
}
}
if user == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
// Create token
claims := &JWTClaims{
UserID: user.ID,
Username: user.Username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString(jwtSecret)
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
func handleWebSocket(c echo.Context) error {
// At this point, JWT middleware has already validated the token
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTClaims)
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer conn.Close()
// Send welcome message with username
welcomeMsg := fmt.Sprintf("Welcome, %s!", claims.Username)
if err := conn.WriteMessage(websocket.TextMessage, []byte(welcomeMsg)); err != nil {
return err
}
// WebSocket message handling loop
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
return err
}
// Echo the message with user info
response := fmt.Sprintf("User %s (ID: %d) sent: %s",
claims.Username, claims.UserID, string(p))
if err := conn.WriteMessage(messageType, []byte(response)); err != nil {
return err
}
}
}
Client-Side Implementation (JavaScript)
Here's how a client would connect using the token:
// First, get the token by logging in
async function login() {
const formData = new FormData();
formData.append('username', 'alice');
formData.append('password', 'password1');
const response = await fetch('http://localhost:8080/login', {
method: 'POST',
body: formData
});
const data = await response.json();
return data.token;
}
// Then use the token to connect to WebSocket
async function connectWebSocket() {
const token = await login();
// Connect with the token in the URL
const socket = new WebSocket(`ws://localhost:8080/ws?token=${token}`);
socket.onopen = () => {
console.log('WebSocket connected!');
socket.send('Hello, server!');
};
socket.onmessage = (event) => {
console.log('Message from server:', event.data);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
connectWebSocket();
2. Query Parameter Authentication
Another approach is to pass the authentication token as a query parameter in the WebSocket URL:
// Custom middleware for WebSocket authentication via query parameter
func websocketAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.QueryParam("token")
if token == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Missing authentication token",
})
}
// Parse and validate JWT token
claims := &JWTClaims{}
parsedToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !parsedToken.Valid {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid authentication token",
})
}
// Store user information in context
c.Set("user", parsedToken)
return next(c)
}
}
// In main():
e.GET("/ws-query", handleWebSocket, websocketAuthMiddleware)
3. Cookie-based Authentication
For web applications, you can use cookies for authentication:
// Set up a cookie-based session middleware
e.Use(middleware.Session())
// Login handler that sets a session cookie
func loginWithSession(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Validate user...
// Create session
session, _ := session.Get("session", c)
session.Values["authenticated"] = true
session.Values["user_id"] = user.ID
session.Values["username"] = user.Username
session.Save(c.Request(), c.Response())
return c.JSON(http.StatusOK, map[string]string{
"message": "Logged in successfully",
})
}
// WebSocket handler with cookie verification
func handleWebSocketWithSession(c echo.Context) error {
session, _ := session.Get("session", c)
// Check if user is authenticated
auth, ok := session.Values["authenticated"].(bool)
if !ok || !auth {
return c.String(http.StatusUnauthorized, "Unauthorized")
}
// Upgrade connection and continue as authenticated
// ...
}
Best Practices for WebSocket Authentication
- Use HTTPS/WSS: Always use secure WebSocket connections (wss://) in production
- Token Expiry: Set reasonable expiration times for authentication tokens
- Validate on Connection: Authenticate before upgrading the HTTP connection to WebSocket
- Periodic Re-authentication: For long-lived connections, consider re-authenticating periodically
- Rate Limiting: Apply connection limits per user to prevent abuse
- Handle Reconnections Gracefully: Implement client-side logic to reconnect with a valid token
Real-World Example: Chat Application with Authenticated Rooms
Here's a more comprehensive example of a chat application with authenticated rooms:
package main
import (
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/gorilla/websocket"
)
// Chat room implementation
type Room struct {
clients map[*websocket.Conn]UserInfo
broadcast chan Message
register chan *Client
unregister chan *Client
mutex sync.Mutex
}
type UserInfo struct {
ID int
Username string
}
type Client struct {
conn *websocket.Conn
room *Room
userInfo UserInfo
}
type Message struct {
Content string `json:"content"`
Username string `json:"username"`
Time time.Time `json:"time"`
}
func newRoom() *Room {
return &Room{
clients: make(map[*websocket.Conn]UserInfo),
broadcast: make(chan Message),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (r *Room) run() {
for {
select {
case client := <-r.register:
r.mutex.Lock()
r.clients[client.conn] = client.userInfo
r.mutex.Unlock()
// Notify everyone a new user joined
r.broadcast <- Message{
Content: fmt.Sprintf("%s joined the room", client.userInfo.Username),
Username: "System",
Time: time.Now(),
}
case client := <-r.unregister:
r.mutex.Lock()
if _, ok := r.clients[client.conn]; ok {
delete(r.clients, client.conn)
close(client.conn)
}
r.mutex.Unlock()
// Notify everyone a user left
r.broadcast <- Message{
Content: fmt.Sprintf("%s left the room", client.userInfo.Username),
Username: "System",
Time: time.Now(),
}
case message := <-r.broadcast:
r.mutex.Lock()
for client := range r.clients {
err := client.WriteJSON(message)
if err != nil {
log.Printf("Error sending message: %v", err)
client.Close()
delete(r.clients, client)
}
}
r.mutex.Unlock()
}
}
}
// Main application code
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Create chat room
room := newRoom()
go room.run()
// Login route
e.POST("/login", login)
// WebSocket route with JWT auth
e.GET("/chat", func(c echo.Context) error {
return handleChatWebSocket(c, room)
}, middleware.JWT(jwtSecret))
e.Logger.Fatal(e.Start(":8080"))
}
func handleChatWebSocket(c echo.Context, room *Room) error {
// Get user info from JWT
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTClaims)
// Upgrade connection
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
// Create client
client := &Client{
conn: conn,
room: room,
userInfo: UserInfo{
ID: claims.UserID,
Username: claims.Username,
},
}
// Register client
room.register <- client
// Handle messages
go func() {
defer func() {
room.unregister <- client
}()
for {
var msg Message
err := conn.ReadJSON(&msg)
if err != nil {
break
}
// Add sender info
msg.Username = claims.Username
msg.Time = time.Now()
// Broadcast to room
room.broadcast <- msg
}
}()
return nil
}
Troubleshooting WebSocket Authentication Issues
Common Issues and Solutions
-
"Unauthorized" Errors
- Check if the token is being properly sent
- Verify token hasn't expired
- Ensure token signing methods match
-
Connection Upgrades Failing
- Check CORS settings if connecting from different domains
- Ensure authentication middleware runs before the upgrade
-
Authentication Not Persisting
- Verify token storage mechanism on client
- Check token expiration time
-
CORS Issues
- Configure proper CORS headers for WebSocket connections:
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// In production, validate origin based on allowed domains
origin := r.Header.Get("Origin")
return origin == "https://your-trusted-site.com"
},
}
Summary
In this tutorial, we've covered:
- The importance of WebSocket authentication
- Different authentication methods:
- Token-based (JWT)
- Query parameter-based
- Cookie/session-based
- Best practices for securing WebSocket connections
- Real-world examples for implementing authenticated WebSockets
- Troubleshooting common authentication issues
By implementing proper authentication for your WebSocket connections, you ensure that your real-time applications remain secure while providing a seamless user experience. These techniques can be adapted for various use cases, from chat applications to real-time dashboards and collaborative tools.
Additional Resources
- Echo Framework Documentation
- Gorilla WebSocket Library
- JWT Authentication Best Practices
- WebSocket Security
Exercises
- Implement a WebSocket authentication system that uses refresh tokens
- Create a middleware that limits the number of concurrent WebSocket connections per user
- Implement role-based access control for WebSocket messages
- Build a simple chat application with private rooms that require authentication
- Add real-time notifications that are only sent to authenticated users
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)