Skip to main content

Overview

Meridian supports multi-device session management, allowing users to be logged in from multiple devices simultaneously. Each device maintains its own session with independent refresh tokens. This replaces the previous single-session model where only one device could be logged in at a time.

Key Features

  • Multi-Device Support: Users can be logged in from multiple devices simultaneously without invalidating other sessions
  • Device Tracking: Each session tracks device information, IP address, and client type for security auditing
  • Session Management: Users can view and revoke specific sessions from their account settings
  • Automatic Expiration: Sessions automatically expire after 30 days and are cleaned up automatically

How It Works

Session Creation

When a user logs in (via email/password, Google OAuth, Apple Sign In, or SAML), a new session is created:
  1. User provides credentials or completes OAuth flow
  2. System creates a new session record with:
    • Unique refresh token
    • Device information (device type, user agent)
    • IP address
    • Client type (web, mobile, ios, android)
    • Expiration timestamp (30 days)
  3. Access token (1 minute) and refresh token (30 days) are set as HTTP-only cookies
  4. Session is stored in database, allowing multiple concurrent sessions per user

Session Validation

When refreshing tokens, the system validates against the session database:
  1. Extract refresh token from HTTP-only cookie
  2. Verify JWT signature
  3. Look up session in database
  4. Check if session is expired
  5. Update last used timestamp
  6. Issue new access token

Session Deletion

When logging out, only the current session is deleted. Other devices remain logged in.

API Endpoints

List Active Sessions

Get all active sessions for the current user. Endpoint: GET /sessions Response:
{
  "success": true,
  "data": {
    "sessions": [
      {
        "id": "507f1f77bcf86cd799439011",
        "deviceInfo": "iPhone",
        "clientType": "ios",
        "ipAddress": "192.168.1.1",
        "lastUsed": "2024-01-15T10:30:00Z",
        "createdAt": "2024-01-01T08:00:00Z",
        "expiresAt": "2024-01-31T08:00:00Z",
        "isCurrent": true
      },
      {
        "id": "507f1f77bcf86cd799439012",
        "deviceInfo": "Chrome Browser",
        "clientType": "web",
        "ipAddress": "192.168.1.2",
        "lastUsed": "2024-01-14T15:20:00Z",
        "createdAt": "2023-12-20T09:00:00Z",
        "expiresAt": "2024-01-19T09:00:00Z",
        "isCurrent": false
      }
    ]
  }
}
Response Fields:
  • sessions (array): Array of active sessions for the user
    • id (string): Unique session identifier
    • deviceInfo (string): Human-readable device description (e.g., “iPhone”, “Chrome Browser”)
    • clientType (string): Client type: web, mobile, ios, or android
    • ipAddress (string): IP address where session was created
    • lastUsed (string): ISO 8601 timestamp of last token refresh
    • createdAt (string): ISO 8601 timestamp of session creation
    • expiresAt (string): ISO 8601 timestamp when session expires
    • isCurrent (boolean): Whether this is the current session making the request

Revoke Specific Session

Revoke a specific session by its ID. Only the session owner can revoke their own sessions. Endpoint: DELETE /sessions/:sessionId Path Parameters:
  • sessionId (string, required): The ID of the session to revoke
Response:
{
  "success": true,
  "message": "Session revoked successfully"
}
Error Response (404):
{
  "success": false,
  "message": "Session not found or you do not have permission to revoke it"
}

Revoke All Other Sessions

Revoke all sessions except the current one. Useful for “logout from all devices” functionality. Endpoint: POST /sessions/revoke-all-others Response:
{
  "success": true,
  "message": "All other sessions revoked successfully"
}
Warning: This will log out the user from all devices except the one making the request. Use with caution.

Frontend Integration

Displaying Sessions

Show users their active sessions in account settings: React (Web):
import { useState, useEffect } from 'react';
import apiRequest from './utils/postRequest';

function SessionsList() {
  const [sessions, setSessions] = useState([]);

  useEffect(() => {
    fetchSessions();
  }, []);

  const fetchSessions = async () => {
    const response = await apiRequest('/sessions', null, { method: 'GET' });
    if (response.success) {
      setSessions(response.data.sessions);
    }
  };

  const revokeSession = async (sessionId) => {
    const response = await apiRequest(`/sessions/${sessionId}`, null, { 
      method: 'DELETE' 
    });
    if (response.success) {
      fetchSessions(); // Refresh list
    }
  };

  return (
    <div>
      <h2>Active Sessions</h2>
      {sessions.map(session => (
        <div key={session.id}>
          <p>{session.deviceInfo} - {session.clientType}</p>
          <p>Last used: {new Date(session.lastUsed).toLocaleString()}</p>
          {!session.isCurrent && (
            <button onClick={() => revokeSession(session.id)}>
              Revoke
            </button>
          )}
          {session.isCurrent && <span>Current Session</span>}
        </div>
      ))}
    </div>
  );
}
React Native (Mobile):
import { useState, useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import { httpClient } from '@/services/api';

function SessionsList() {
  const [sessions, setSessions] = useState([]);

  useEffect(() => {
    fetchSessions();
  }, []);

  const fetchSessions = async () => {
    try {
      const response = await httpClient.get('/sessions');
      if (response.success) {
        setSessions(response.data.sessions);
      }
    } catch (error) {
      console.error('Failed to fetch sessions:', error);
    }
  };

  const revokeSession = async (sessionId: string) => {
    try {
      await httpClient.delete(`/sessions/${sessionId}`);
      fetchSessions(); // Refresh list
    } catch (error) {
      console.error('Failed to revoke session:', error);
    }
  };

  return (
    <View>
      <Text>Active Sessions</Text>
      {sessions.map(session => (
        <View key={session.id}>
          <Text>{session.deviceInfo} - {session.clientType}</Text>
          <Text>Last used: {new Date(session.lastUsed).toLocaleString()}</Text>
          {!session.isCurrent && (
            <Button 
              title="Revoke" 
              onPress={() => revokeSession(session.id)} 
            />
          )}
          {session.isCurrent && <Text>Current Session</Text>}
        </View>
      ))}
    </View>
  );
}

Examples

List All Sessions

// Fetch all active sessions
const response = await fetch('/sessions', {
  credentials: 'include'
});
const { sessions } = await response.json();

// Display sessions
sessions.forEach(session => {
  console.log(`${session.deviceInfo} - Last used: ${session.lastUsed}`);
});

Revoke a Session

// Revoke a specific session
await fetch(`/sessions/${sessionId}`, {
  method: 'DELETE',
  credentials: 'include'
});

// Revoke all other sessions
await fetch('/sessions/revoke-all-others', {
  method: 'POST',
  credentials: 'include'
});

Check Current Session

const response = await fetch('/sessions', {
  credentials: 'include'
});
const { sessions } = await response.json();

const currentSession = sessions.find(s => s.isCurrent);
console.log('Current session:', currentSession.deviceInfo);

Security Considerations

Session Expiration

  • Sessions expire after 30 days of inactivity
  • Expired sessions are automatically cleaned up
  • Users must re-authenticate after expiration

Device Tracking

Each session stores:
  • Device Information: Extracted from User-Agent header
  • IP Address: For security auditing
  • Client Type: web, mobile, ios, or android
  • Last Used: Timestamp of last token refresh

Best Practices

  • Session Monitoring: Regularly review active sessions and revoke suspicious sessions immediately
  • User Education: Encourage users to review sessions and provide “logout from all devices” option
  • Security Features: Use HTTP-only cookies, validate sessions on each refresh, clean up expired sessions periodically

Migration Notes

Backward Compatibility

The refreshToken field remains in the User schema for backward compatibility but is no longer used. Existing users will need to log in again to create sessions.

Database Schema

The new Session model includes:
  • userId: Reference to User
  • refreshToken: Unique refresh token for this session
  • deviceInfo: Human-readable device description
  • userAgent: Full user agent string
  • ipAddress: IP address at creation
  • clientType: web, mobile, ios, or android
  • lastUsed: Last token refresh timestamp
  • expiresAt: Session expiration timestamp

Cleanup Job

Consider adding a periodic job to clean up expired sessions:
const { cleanupExpiredSessions } = require('./utilities/sessionUtils');

// Run daily
setInterval(async () => {
    const deleted = await cleanupExpiredSessions(req);
    console.log(`Cleaned up ${deleted} expired sessions`);
}, 24 * 60 * 60 * 1000);

Troubleshooting

Session Not Found

Error: Session not found or you do not have permission to revoke it Causes:
  • Session was already revoked
  • Session expired
  • User doesn’t own the session
Solution:
  • Refresh the sessions list
  • Check session expiration dates
  • Verify user authentication

Multiple Sessions Not Working

Issue: New login invalidates previous session Causes:
  • Old code still using refreshToken field on User model
  • Session creation not implemented in login endpoint
Solution:
  • Verify all login endpoints use createSession()
  • Check that refreshToken field is no longer updated on User model
  • Ensure Session model is registered in getModelService.js

Sessions Not Expiring

Issue: Sessions remain active after expiration Causes:
  • Cleanup job not running
  • Expiration check not implemented
Solution:
  • Implement periodic cleanup job
  • Verify expiresAt is set correctly on session creation
  • Check validateSession() includes expiration check

Notes

  • Sessions work independently across devices - logging out from one device doesn’t affect others
  • Each session has its own refresh token stored securely in the database
  • Session information is automatically tracked (device, IP, client type)
  • Users can manage their sessions through the account settings page
  • Expired sessions are automatically invalidated and cleaned up