InfraForge Docs

InfraNotes Module · v0

Welcome

Select a document from the sidebar to read it.

NATS Authentication Integration for InfraNotes Main Application

Deprecated – historical NATS design

InfraNotes has fully migrated away from NATS and now uses Kafka at the platform level, with HTTP APIs secured via shared JWT tokens between InfraNotes and this module. This document is kept only as a historical reference for the previous NATS-based design.

This document provides the historical implementation details for adding NATS support to the InfraNotes main application to enable authentication integration with the InfraNotes-Module.

Implementation Steps

1. Add Dependencies

Add the NATS client library to the InfraNotes main application:

# Run in the InfraNotes directory
go get github.com/nats-io/nats.go

2. Create NATS Authentication Service

Create a new file at internal/services/nats_auth_service.go:

package services

import (
	"encoding/json"
	"fmt"
	"log"
	"strconv"
	"time"

	"github.com/StackCatalyst/Infra_Notes/internal/models"
	"github.com/nats-io/nats.go"
)

// TokenValidationRequest represents a request to validate a token
type TokenValidationRequest struct {
	Token     string `json:"token"`
	RequestID string `json:"request_id"`
}

// TokenValidationResponse represents the response to a token validation
type TokenValidationResponse struct {
	Valid       bool     `json:"valid"`
	UserID      string   `json:"user_id"`
	UserRole    string   `json:"user_role"`
	Permissions []string `json:"permissions"`
	SessionID   string   `json:"session_id"`
	RequestID   string   `json:"request_id"`
	Error       string   `json:"error,omitempty"`
}

// TokenRevokedEvent represents a token revocation event
type TokenRevokedEvent struct {
	Token        string    `json:"token"`
	UserID       string    `json:"user_id"`
	RevokedAt    time.Time `json:"revoked_at"`
	Reason       string    `json:"reason,omitempty"`
	SessionID    string    `json:"session_id,omitempty"`
	RevokedByApp string    `json:"revoked_by_app"`
}

// NatsAuthService handles authentication via NATS
type NatsAuthService struct {
	nc          *nats.Conn
	authService *AuthService
	userRepo    models.UserRepository
	connected   bool
}

// NewNatsAuthService creates a new NATS authentication service
func NewNatsAuthService(
	natsURL string,
	authService *AuthService,
	userRepo models.UserRepository,
) (*NatsAuthService, error) {
	// Connect to NATS
	nc, err := nats.Connect(natsURL,
		nats.Name("infranotes-main-auth"),
		nats.ReconnectWait(5*time.Second),
		nats.MaxReconnects(10))
	if err != nil {
		return nil, fmt.Errorf("failed to connect to NATS: %w", err)
	}

	service := &NatsAuthService{
		nc:          nc,
		authService: authService,
		userRepo:    userRepo,
		connected:   true,
	}

	// Initialize subscribers
	if err := service.initSubscribers(); err != nil {
		nc.Close()
		return nil, err
	}

	return service, nil
}

// initSubscribers sets up the NATS subscribers for auth requests
func (s *NatsAuthService) initSubscribers() error {
	// Subscribe to token validation requests
	_, err := s.nc.Subscribe("auth.validate.token", s.handleTokenValidation)
	if err != nil {
		return fmt.Errorf("failed to subscribe to token validation: %w", err)
	}

	// Subscribe to user permission validation requests
	_, err = s.nc.QueueSubscribe("auth.validate.user.*", "infranotes-auth", s.handleUserValidation)
	if err != nil {
		return fmt.Errorf("failed to subscribe to user validation: %w", err)
	}

	log.Println("NATS Auth Service: Subscribed to authentication topics")
	return nil
}

// handleTokenValidation processes token validation requests
func (s *NatsAuthService) handleTokenValidation(msg *nats.Msg) {
	var req TokenValidationRequest
	if err := json.Unmarshal(msg.Data, &req); err != nil {
		s.sendErrorResponse(msg, "Invalid request format", req.RequestID)
		return
	}

	// Validate the token
	userID, role, sessionID, err := s.authService.VerifyToken(req.Token)
	
	// Prepare the response
	resp := TokenValidationResponse{
		Valid:     err == nil,
		RequestID: req.RequestID,
	}

	if err == nil {
		// Token is valid, include user info
		resp.UserID = fmt.Sprintf("%d", userID)
		resp.UserRole = role
		resp.SessionID = sessionID

		// Get permissions if needed
		if user, err := s.userRepo.GetByID(userID); err == nil {
			permissions, _ := s.getPermissionsForUser(user)
			resp.Permissions = permissions
		}
	} else {
		// Include error message
		resp.Error = err.Error()
	}

	// Send the response
	respData, _ := json.Marshal(resp)
	msg.Respond(respData)
}

// handleUserValidation processes user permission validation requests
func (s *NatsAuthService) handleUserValidation(msg *nats.Msg) {
	// TODO: Implement user permission validation logic
	// Extract user ID from the subject
	// Verify if the user has permission for the requested resource/action
}

// getPermissionsForUser returns the permissions for a user
func (s *NatsAuthService) getPermissionsForUser(user *models.User) ([]string, error) {
	// TODO: Get actual permissions based on user role and other attributes
	// This is a placeholder implementation
	permissions := []string{}

	// Add basic permissions based on role
	if user.IsAdmin() {
		permissions = append(permissions, 
			"read:analytics", 
			"write:analytics",
			"read:documents",
			"write:documents",
			"admin:system")
	} else {
		permissions = append(permissions, 
			"read:analytics", 
			"read:documents")
		
		// Add write permissions for standard users
		permissions = append(permissions, 
			"write:documents")
	}

	return permissions, nil
}

// PublishLogout publishes a logout event
func (s *NatsAuthService) PublishLogout(userID int64, sessionID, reason string) error {
	event := TokenRevokedEvent{
		UserID:       strconv.FormatInt(userID, 10),
		SessionID:    sessionID,
		RevokedAt:    time.Now().UTC(),
		Reason:       reason,
		RevokedByApp: "infranotes-main",
	}

	data, err := json.Marshal(event)
	if err != nil {
		return err
	}

	return s.nc.Publish("auth.event.logout", data)
}

// PublishTokenRevoked publishes a token revocation event
func (s *NatsAuthService) PublishTokenRevoked(token string, userID int64, sessionID, reason string) error {
	event := TokenRevokedEvent{
		Token:        token,
		UserID:       strconv.FormatInt(userID, 10),
		SessionID:    sessionID,
		RevokedAt:    time.Now().UTC(),
		Reason:       reason,
		RevokedByApp: "infranotes-main",
	}

	data, err := json.Marshal(event)
	if err != nil {
		return err
	}

	return s.nc.Publish("auth.event.token.revoked", data)
}

// sendErrorResponse sends an error response to a NATS request
func (s *NatsAuthService) sendErrorResponse(msg *nats.Msg, errMsg string, requestID string) {
	resp := TokenValidationResponse{
		Valid:     false,
		Error:     errMsg,
		RequestID: requestID,
	}
	respData, _ := json.Marshal(resp)
	msg.Respond(respData)
}

// Close closes the NATS connection
func (s *NatsAuthService) Close() {
	if s.nc != nil {
		s.nc.Close()
	}
}

3. Update Auth Service to Notify on Logout/Token Invalidation

Modify internal/services/auth_service.go to publish events when tokens are invalidated:

// Logout logs out a user and invalidates their session
func (s *AuthService) Logout(userID int64, sessionID string) error {
    // Existing logout logic...
    
    // If NATS service is available, publish a logout event
    if natsAuthService != nil {
        natsAuthService.PublishLogout(userID, sessionID, "user logout")
    }
    
    return nil
}

// InvalidateToken adds a token to the blacklist
func (s *AuthService) InvalidateToken(token string, userID int64, sessionID string, reason string) error {
    // Existing token invalidation logic...
    
    // If NATS service is available, publish a token revocation event
    if natsAuthService != nil {
        natsAuthService.PublishTokenRevoked(token, userID, sessionID, reason)
    }
    
    return nil
}

4. Update main.go to Initialize NATS Service

Add the NATS service initialization to the main application:

// Initialize NATS auth service if configured
natsURL := config.GetString("nats.url")
if natsURL != "" {
    natsAuthService, err := services.NewNatsAuthService(
        natsURL,
        authService,
        userRepo,
    )
    if err != nil {
        log.Printf("Warning: Failed to initialize NATS authentication service: %v", err)
        // Continue without NATS - will use local auth only
    } else {
        defer natsAuthService.Close()
        log.Printf("NATS authentication service initialized at %s", natsURL)
    }
}

5. Update Docker Compose

Update the Docker Compose configuration to include NATS:

services:
  # ... existing services ...
  
  nats:
    image: nats:2.10-alpine
    container_name: infranotes-nats
    restart: unless-stopped
    ports:
      - "4222:4222"  # Client connections
      - "8222:8222"  # HTTP monitoring
    volumes:
      - nats_data:/nats/data
    command: >
      --jetstream
      --http_port=8222
      --store_dir=/nats/data
    healthcheck:
      test: ["CMD", "nats-server", "check", "localhost:8222"]
      interval: 5s
      timeout: 5s
      retries: 5
      
  api:
    # ... existing api service configuration ...
    environment:
      # ... existing environment variables ...
      NATS_URL: nats://nats:4222
    depends_on:
      # ... existing dependencies ...
      nats:
        condition: service_healthy
        
volumes:
  # ... existing volumes ...
  nats_data:

6. Update Configuration

Add NATS configuration to configs/config.go:

// Define default configuration
var defaultConfig = map[string]interface{}{
    // ... existing defaults ...
    "nats": map[string]interface{}{
        "url": "nats://localhost:4222",
        "clientName": "infranotes-main",
        "reconnectWait": 5, // seconds
        "maxReconnects": 10,
    },
}

Testing the Integration

1. Unit Tests

Create unit tests for the NATS authentication service:

package services_test

import (
    "testing"
    "time"
    
    "github.com/StackCatalyst/Infra_Notes/internal/services"
    "github.com/nats-io/nats-server/v2/test"
)

func TestNatsAuthService(t *testing.T) {
    // Start a test NATS server
    opts := test.DefaultTestOptions
    opts.Port = 8368
    server := test.RunServer(&opts)
    defer server.Shutdown()
    
    // TODO: Implement tests for token validation and event publishing
}

2. Integration Testing

Test the full integration between InfraNotes and InfraNotes-Module using NATS:

  1. Start both applications with NATS enabled
  2. Generate a valid auth token in InfraNotes
  3. Use the token to access InfraNotes-Module
  4. Verify that the token is properly validated
  5. Invalidate the token and verify that it's no longer accepted

Security Considerations

  1. For production deployments, always enable TLS for NATS connections
  2. Use NATS authentication (username/password or certificates)
  3. Implement proper error handling and timeouts for NATS operations
  4. Monitor NATS connections and authentication requests
  5. Implement rate limiting for authentication requests

Performance Considerations

  1. Consider caching validation results to reduce NATS traffic
  2. Implement circuit breakers to handle NATS connection failures gracefully
  3. Use connection pooling if necessary
  4. Monitor NATS message latency and throughput