Skip to main content

Overview

Manage user authentication sessions and monitor active terminal connections. Session management provides endpoints for listing active sessions, revoking access, and tracking terminal activity.

Session Types

Rexec manages two types of sessions:
  1. User Sessions: Authentication sessions (JWT/login)
  2. Terminal Sessions: Active WebSocket terminal connections

User Sessions

List User Sessions

Retrieve all authentication sessions for the current user.
GET /api/sessions

Response

{
  "sessions": [
    {
      "id": "session-uuid",
      "created_at": "2024-01-15T10:30:00Z",
      "last_seen_at": "2024-01-15T14:45:00Z",
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0...",
      "revoked_at": null,
      "revoked_reason": null,
      "is_current": true
    },
    {
      "id": "another-session-uuid",
      "created_at": "2024-01-10T08:00:00Z",
      "last_seen_at": "2024-01-14T16:20:00Z",
      "ip_address": "10.0.0.50",
      "user_agent": "Mozilla/5.0...",
      "revoked_at": null,
      "revoked_reason": null,
      "is_current": false
    }
  ]
}

Fields

id
string
Unique session identifier
created_at
timestamp
When the session was created
last_seen_at
timestamp
Last activity timestamp
ip_address
string
IP address of the session
user_agent
string
Browser/client user agent
revoked_at
timestamp
When the session was revoked (null if active)
revoked_reason
string
Reason for revocation (e.g., “revoked_by_user”)
is_current
boolean
Whether this is the current session

Revoke a Session

Revoke a specific session by ID.
DELETE /api/sessions/:id

Path Parameters

id
string
required
Session ID to revoke

Request Body

{
  "reason": "logged_out_from_app"
}
reason
string
Optional reason for revocation (defaults to “revoked_by_user”)

Response

{
  "revoked": true
}

Example

async function revokeSession(sessionId) {
  const response = await fetch(`/api/sessions/${sessionId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      reason: 'security_concern'
    })
  });
  
  if (response.ok) {
    console.log('Session revoked successfully');
  }
}

Revoke Other Sessions

Revoke all sessions except the current one.
POST /api/sessions/revoke-others

Request Body

{
  "reason": "security_review"
}
reason
string
Optional reason for revocation (defaults to “revoked_other_sessions”)

Response

{
  "revoked": true
}

Example

async function revokeOtherSessions() {
  const response = await fetch('/api/sessions/revoke-others', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      reason: 'logging_in_from_new_device'
    })
  });
  
  const data = await response.json();
  console.log('Other sessions revoked:', data.revoked);
}

Terminal Sessions

Overview

Terminal sessions represent active WebSocket connections to container terminals. These are managed automatically when WebSocket connections are established or closed.

Session Structure

From internal/api/handlers/terminal.go:
type TerminalSession struct {
    UserID          string
    ContainerID     string
    DBSessionID     string
    CreatedAt       time.Time
    ExecID          string
    Conn            *websocket.Conn
    Cols            uint
    Rows            uint
    Done            chan struct{}
    ForceNewSession bool   // Create new tmux session
    IsOwner         bool   // Container owner vs collab
    TmuxSessionName string // Tmux session name
}

Session Database Record

type SessionRecord struct {
    ID          string
    UserID      string
    ContainerID string
    CreatedAt   time.Time
    LastPingAt  time.Time
}
Terminal sessions are:
  • Created when WebSocket connects
  • Persisted to database for admin visibility
  • Updated with periodic pings
  • Removed when WebSocket closes

Session Multiplexing

Multiple terminal connections per container are supported:
sessionKey = containerID + ":" + userID + ":" + connectionID
Example:
abc123:user456:main
abc123:user456:split-1
abc123:user456:split-2

Implementation From Source

Sessions Handler

From internal/api/handlers/sessions.go:
type SessionsHandler struct {
    store *storage.PostgresStore
}

// List returns all sessions for the current user
func (h *SessionsHandler) List(c *gin.Context) {
    userID := c.GetString("userID")
    if userID == "" {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
        return
    }

    sessions, err := h.store.ListUserSessions(c.Request.Context(), userID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list sessions"})
        return
    }

    currentID := c.GetString("sessionID")
    resp := make([]gin.H, 0, len(sessions))
    for _, srec := range sessions {
        resp = append(resp, gin.H{
            "id":             srec.ID,
            "created_at":     srec.CreatedAt,
            "last_seen_at":   srec.LastSeenAt,
            "ip_address":     srec.IPAddress,
            "user_agent":     srec.UserAgent,
            "revoked_at":     srec.RevokedAt,
            "revoked_reason": srec.RevokedReason,
            "is_current":     currentID != "" && srec.ID == currentID,
        })
    }

    c.JSON(http.StatusOK, gin.H{"sessions": resp})
}

Terminal Session Management

Terminal sessions include automatic management:
// Register session
sessionKey := dockerID + ":" + userID + ":" + connectionID
h.mu.Lock()
if existingSession, exists := h.sessions[sessionKey]; exists {
    existingSession.Close()
}
h.sessions[sessionKey] = session
h.mu.Unlock()

// Persist to database
dbSession := &storage.SessionRecord{
    ID:          sessionID,
    UserID:      userID,
    ContainerID: dockerID,
    CreatedAt:   createdAt,
    LastPingAt:  createdAt,
}
store.CreateSession(ctx, dbSession)

// Cleanup on exit
defer func() {
    h.mu.Lock()
    if currentSession, exists := h.sessions[sessionKey]; exists && currentSession == session {
        delete(h.sessions, sessionKey)
    }
    h.mu.Unlock()
    
    store.DeleteSession(ctx, sessionID)
    session.Close()
}()

Admin Endpoints

Admins can monitor all terminal sessions:
GET /api/admin/terminals
Returns all active terminal sessions across all users.

Audit Logging

Session actions are logged:
store.CreateAuditLog(ctx, &models.AuditLog{
    ID:        uuid.New().String(),
    UserID:    &userID,
    Action:    "session_revoked",
    IPAddress: c.ClientIP(),
    UserAgent: c.Request.UserAgent(),
    Details:   sessionID,
    CreatedAt: time.Now(),
})
Logged actions:
  • session_revoked: Single session revoked
  • sessions_revoked_others: Bulk revocation

Session Cleanup

Automatic Cleanup

Terminal sessions are automatically cleaned up:
  1. WebSocket close → remove from memory and database
  2. Split pane sessions → kill tmux session on disconnect
  3. Collab sessions → cleanup based on collaboration mode

Control Mode Collaboration Cleanup

func (h *TerminalHandler) CleanupControlCollab(
    containerID, ownerID string, 
    participantUserIDs []string
) {
    // Close WebSocket connections for participants
    for _, session := range participantSessions {
        session.CloseWithCode(4003, "collaboration ended")
    }
    
    // Kill per-user tmux sessions
    for userID := range participants {
        if userID != ownerID {
            h.killTmuxSession(containerID, "user-"+userID)
        }
    }
}

Error Handling

Session Not Found

{
  "error": "session not found"
}
Returned when:
  • Session ID doesn’t exist
  • Session belongs to different user
  • Session already revoked

Unauthorized

{
  "error": "unauthorized"
}
Returned when authentication is missing or invalid.

Internal Error

{
  "error": "failed to list sessions"
}
Returned for database or internal errors.

Complete Example: Session Manager

class SessionManager {
  constructor(apiUrl, token) {
    this.apiUrl = apiUrl;
    this.token = token;
  }

  async listSessions() {
    const response = await fetch(`${this.apiUrl}/api/sessions`, {
      headers: {
        'Authorization': `Bearer ${this.token}`
      }
    });
    
    if (!response.ok) {
      throw new Error('Failed to list sessions');
    }
    
    const data = await response.json();
    return data.sessions;
  }

  async revokeSession(sessionId, reason = 'user_requested') {
    const response = await fetch(`${this.apiUrl}/api/sessions/${sessionId}`, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ reason })
    });
    
    if (!response.ok) {
      throw new Error('Failed to revoke session');
    }
    
    return response.json();
  }

  async revokeOtherSessions(reason = 'security_measure') {
    const response = await fetch(`${this.apiUrl}/api/sessions/revoke-others`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ reason })
    });
    
    if (!response.ok) {
      throw new Error('Failed to revoke other sessions');
    }
    
    return response.json();
  }

  async displayActiveSessions() {
    const sessions = await this.listSessions();
    
    console.log('Active Sessions:');
    sessions.forEach(session => {
      console.log(`
  ID: ${session.id}`);
      console.log(`  Created: ${session.created_at}`);
      console.log(`  Last Seen: ${session.last_seen_at}`);
      console.log(`  IP: ${session.ip_address}`);
      console.log(`  Current: ${session.is_current ? 'Yes' : 'No'}`);
      
      if (session.revoked_at) {
        console.log(`  Revoked: ${session.revoked_at}`);
        console.log(`  Reason: ${session.revoked_reason}`);
      }
    });
  }
}

// Usage
const manager = new SessionManager('https://api.rexec.io', token);

// List all sessions
const sessions = await manager.listSessions();

// Revoke a specific session
await manager.revokeSession('session-123', 'suspicious_activity');

// Revoke all other sessions
await manager.revokeOtherSessions('new_device_login');

// Display all sessions
await manager.displayActiveSessions();

Best Practices

  1. Regular Audits: Periodically list and review active sessions
  2. Revoke on Logout: Always revoke current session on logout
  3. Security Events: Revoke all other sessions when detecting suspicious activity
  4. New Device: Offer option to revoke other sessions on new device login
  5. Session Timeout: Implement client-side session timeout handling
  6. Reason Tracking: Always provide meaningful revocation reasons for audit logs

Security Considerations

  • Sessions are tied to JWT tokens
  • Revoking a session invalidates the JWT
  • Users can only manage their own sessions
  • All session actions are audit logged
  • IP addresses and user agents are tracked
  • Current session cannot accidentally revoke itself with revoke-others

Build docs developers (and LLMs) love