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:
- User Sessions: Authentication sessions (JWT/login)
- Terminal Sessions: Active WebSocket terminal connections
User Sessions
List User Sessions
Retrieve all authentication sessions for the current user.
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
Unique session identifier
When the session was created
IP address of the session
Browser/client user agent
When the session was revoked (null if active)
Reason for revocation (e.g., “revoked_by_user”)
Whether this is the current session
Revoke a Session
Revoke a specific session by ID.
Path Parameters
Request Body
{
"reason": "logged_out_from_app"
}
Optional reason for revocation (defaults to “revoked_by_user”)
Response
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"
}
Optional reason for revocation (defaults to “revoked_other_sessions”)
Response
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:
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:
- WebSocket close → remove from memory and database
- Split pane sessions → kill tmux session on disconnect
- 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
- Regular Audits: Periodically list and review active sessions
- Revoke on Logout: Always revoke current session on logout
- Security Events: Revoke all other sessions when detecting suspicious activity
- New Device: Offer option to revoke other sessions on new device login
- Session Timeout: Implement client-side session timeout handling
- 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