Skip to content

Add WebSocket with Authentication Middleware example (Fixes issue#1699) #ieeesoc #1715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions examples/using-web-socket-with-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# WebSocket with Authentication Example

This GoFr example demonstrates how to implement WebSocket connections with authentication middleware. It shows how to:

1. Set up Basic Authentication for your GoFr application with a custom validator
2. Use WebSockets with authenticated connections
3. Handle messages from authenticated clients
4. Track active WebSocket connections
5. Extract username from authentication credentials

## Features

- **Authenticated WebSocket Connections**: Only authenticated users can establish WebSocket connections
- **User Tracking**: The example keeps track of connected users and provides an endpoint to list them
- **Custom Authentication**: Uses a custom validator function to authenticate users
- **Chat-like Functionality**: Demonstrates a simple chat application where messages include usernames

## How Authentication Works with WebSockets

WebSockets start as HTTP connections that are then upgraded to WebSocket protocol. In GoFr, authentication middleware is applied during the initial HTTP handshake, before the connection is upgraded to a WebSocket.

The authentication flow works as follows:

1. Client sends an HTTP request with authentication credentials (e.g., Basic Auth header)
2. GoFr's authentication middleware validates the credentials using the custom validator
3. If authentication succeeds, the connection is upgraded to WebSocket
4. If authentication fails, a 401 Unauthorized response is returned, and the WebSocket connection is not established

This example uses Basic Authentication for simplicity, but the same principle applies to other authentication methods like API Key, OAuth, or custom authentication.

## Running the Example

To run the example, use the following command:

```console
go run main.go
```

## Testing the WebSocket Connection

You can test the WebSocket connection using tools like [websocat](https://github.com/vi/websocat) or browser-based WebSocket clients.

### Using websocat with Basic Auth

```console
websocat ws://localhost:8000/ws -H="Authorization: Basic dXNlcjE6cGFzc3dvcmQx"
```

The Basic Auth header `dXNlcjE6cGFzc3dvcmQx` is the base64-encoded string of `user1:password1`.

### Using curl to check active users

You can use curl to check the list of active users:

```console
curl -u user1:password1 http://localhost:8000/users
```

This will return a JSON response with the list of currently connected users.

### Using JavaScript in a Browser

```javascript
// Function to create a WebSocket with authentication
function createAuthenticatedWebSocket(url, username, password) {
// Create a custom WebSocket object that includes authentication
return new Promise((resolve, reject) => {
// Create the WebSocket connection
const socket = new WebSocket(url);

// Add authentication headers to the connection
// Note: This is a workaround as browsers don't allow setting headers directly
// In a real application, you would use a token-based approach

// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
resolve(socket);
});

// Connection error
socket.addEventListener('error', (event) => {
console.error('WebSocket connection error:', event);
reject(event);
});
});
}

// Usage example
async function connectToChat() {
try {
// Connect to the WebSocket server
// Note: In a real application, you would need to handle authentication differently
// as browsers don't allow setting custom headers for WebSockets
const socket = await createAuthenticatedWebSocket('ws://localhost:8000/ws', 'user1', 'password1');

// Send a message
socket.send(JSON.stringify({content: 'Hello from browser!'}));

// Listen for messages
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
// Display the message in the UI
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML += `<div>${event.data}</div>`;
});

// Set up UI for sending messages
document.getElementById('send-button').addEventListener('click', () => {
const messageInput = document.getElementById('message-input');
const message = messageInput.value;
if (message) {
socket.send(JSON.stringify({content: message}));
messageInput.value = '';
}
});
} catch (error) {
console.error('Failed to connect:', error);
}
}

// Start the connection
connectToChat();
```

**Note**: Browser WebSocket API doesn't allow setting custom headers directly. In a real application, you would typically use a token-based approach where the token is obtained via a separate authenticated HTTP request and then included in the WebSocket URL or in the first message sent after connection.

## Security Considerations

In a production environment, consider these security best practices:

1. Use HTTPS/WSS instead of HTTP/WS to encrypt the connection
2. Implement token-based authentication (JWT) instead of Basic Auth
3. Validate user permissions for specific WebSocket actions
4. Implement rate limiting to prevent abuse
5. Sanitize and validate all incoming WebSocket messages
6. Store user credentials securely (e.g., hashed passwords in a database)
7. Implement proper session management and token expiration

## Implementation Details

This example demonstrates several key concepts:

1. **Authentication Middleware**: GoFr's authentication middleware is applied before the WebSocket connection is established, ensuring only authenticated users can connect.

2. **Custom Validator**: The example uses a custom validator function to authenticate users, which could be extended to validate against a database.

3. **Username Extraction**: The example extracts the username from the Basic Auth header to identify the user in the WebSocket connection.

4. **Connection Tracking**: The example keeps track of active connections and provides an endpoint to list them.

5. **Continuous Message Handling**: The WebSocket handler uses a loop to continuously process incoming messages until the connection is closed.

## Additional Resources

- [GoFr Documentation](https://gofr.dev)
- [WebSocket Protocol](https://tools.ietf.org/html/rfc6455)
- [HTTP Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)
162 changes: 162 additions & 0 deletions examples/using-web-socket-with-auth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package main

import (
"encoding/base64"
"fmt"
"strings"
"sync"
"time"

"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
)

// Message represents a chat message
type Message struct {
Username string `json:"username"`
Content string `json:"content"`
Time time.Time `json:"time"`
}

// ActiveUsers keeps track of connected users
var (
activeUsers = make(map[string]bool)
usersMutex sync.RWMutex
)

// validateCredentials is a custom validator function for basic auth
// In a real application, you would validate against a database
func validateCredentials(_ *container.Container, username, password string) bool {
validUsers := map[string]string{
"user1": "password1",
"user2": "password2",
"admin": "admin123",
}

storedPassword, exists := validUsers[username]
return exists && storedPassword == password
}

// extractUsernameFromAuthHeader extracts the username from the Authorization header
func extractUsernameFromAuthHeader(authHeader string) string {
if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") {
return ""
}

// Remove "Basic " prefix
encodedCreds := strings.TrimPrefix(authHeader, "Basic ")

// Decode base64
decodedCreds, err := base64.StdEncoding.DecodeString(encodedCreds)
if err != nil {
return ""
}

// Split username:password
creds := strings.SplitN(string(decodedCreds), ":", 2)
if len(creds) != 2 {
return ""
}

return creds[0]
}

func main() {
app := gofr.New()

// Enable Basic Authentication with custom validator
app.EnableBasicAuthWithValidator(validateCredentials)

// Register WebSocket handler with authentication
app.WebSocket("/ws", WSHandler)

// Add a simple HTTP endpoint to list active users
app.GET("/users", listActiveUsers)

app.Run()
}

// listActiveUsers returns a list of currently connected users
func listActiveUsers(ctx *gofr.Context) (any, error) {
usersMutex.RLock()
defer usersMutex.RUnlock()

users := make([]string, 0, len(activeUsers))
for user := range activeUsers {
users = append(users, user)
}

// Return a simple response with the active users
return struct {
ActiveUsers []string `json:"active_users"`
Count int `json:"count"`
}{
ActiveUsers: users,
Count: len(users),
}, nil
}

// WSHandler handles WebSocket connections
// Since authentication middleware is applied at the HTTP level before upgrading to WebSocket,
// only authenticated users will reach this handler
func WSHandler(ctx *gofr.Context) (any, error) {
// Get username from the authentication info
// The username is set by the basic auth middleware
username := ctx.GetAuthInfo().GetUsername()
if username == "" {
username = "anonymous" // Fallback, though this shouldn't happen due to auth middleware
}

// Add user to active users
usersMutex.Lock()
activeUsers[username] = true
usersMutex.Unlock()

// Remove user when connection closes
defer func() {
usersMutex.Lock()
delete(activeUsers, username)
usersMutex.Unlock()

ctx.Logger.Infof("User %s disconnected", username)
}()

ctx.Logger.Infof("User %s connected", username)

// Send welcome message
welcomeMsg := fmt.Sprintf("Welcome, %s! You are now connected to the chat.", username)
err := ctx.WriteMessageToSocket(welcomeMsg)
if err != nil {
return nil, err
}

// Handle incoming messages
for {
var message Message

// Bind the incoming message
err := ctx.Bind(&message)
if err != nil {
// If there's an error binding, the connection might be closed
ctx.Logger.Errorf("Error binding message: %v", err)
return nil, err
}

// Set the username and timestamp
message.Username = username
message.Time = time.Now()

ctx.Logger.Infof("Received message from %s: %s", message.Username, message.Content)

// Echo the message back to the client
response := fmt.Sprintf("[%s] %s: %s",
message.Time.Format("15:04:05"),
message.Username,
message.Content)

err = ctx.WriteMessageToSocket(response)
if err != nil {
return nil, err
}
}
}
Loading
Loading