Device & Session Management adalah fitur yang memungkinkan user melihat dan mengelola semua sesi aktif mereka — dan merevoke akses dari device yang tidak dikenal.
Mengapa ini penting:
- User kehilangan device → bisa logout dari jarak jauh
- Deteksi akses tidak sah (login dari lokasi asing)
- Compliance: beberapa regulasi mengharuskan user bisa melihat sesi aktif
Model data session:
// Tabel sessions
{
id: "sess_abc123",
userId: "user_42",
deviceName: "iPhone 16 Pro",
deviceType: "mobile", // "desktop", "mobile", "tablet"
browser: "Safari 18",
os: "iOS 18",
ip: "180.252.xxx.xxx",
location: "Jakarta, ID", // dari IP geolocation
createdAt: "2025-01-10T08:30:00Z",
lastActiveAt: "2025-04-12T14:22:00Z",
isCurrent: false, // true untuk sesi yang sedang aktif
refreshTokenId: "rt_xyz789" // untuk revokasi
}
Implementasi session tracking:
async function createSession(userId, req) {
const ua = parseUserAgent(req.headers["user-agent"]);
const geo = await geolocate(req.ip);
const session = await db.sessions.create({
userId,
deviceName: ua.device || "Unknown Device",
browser: `${ua.browser} ${ua.browserVersion}`,
os: `${ua.os} ${ua.osVersion}`,
ip: req.ip,
location: `${geo.city}, ${geo.country}`,
refreshTokenId: generateRefreshToken().id,
lastActiveAt: new Date()
});
return session;
}
// Update lastActiveAt di setiap request
async function touchSession(sessionId) {
await db.sessions.update(
{ lastActiveAt: new Date() },
{ where: { id: sessionId } }
);
}
// Revoke sesi tertentu
async function revokeSession(userId, sessionId) {
const session = await db.sessions.findOne({ where: { id: sessionId, userId } });
if (!session) throw new Error("Session tidak ditemukan");
// Invalidate refresh token
await invalidateRefreshToken(session.refreshTokenId);
await db.sessions.delete({ where: { id: sessionId } });
}
// Revoke semua sesi kecuali saat ini
async function revokeAllOtherSessions(userId, currentSessionId) {
const sessions = await db.sessions.findAll({
where: { userId, id: { [Op.ne]: currentSessionId } }
});
for (const session of sessions) {
await revokeSession(userId, session.id);
}
}
API endpoints untuk session management:
// Lihat semua sesi aktif
GET /api/user/sessions
// Response: array session info (tanpa refresh token!)
// Revoke sesi tertentu
DELETE /api/user/sessions/:sessionId
// Revoke semua sesi lain
DELETE /api/user/sessions/others
// Logout semua device
DELETE /api/user/sessions/all
Notifikasi keamanan:
// Kirim email jika login dari device/lokasi baru
async function checkNewDevice(userId, session) {
const known = await db.sessions.findOne({
where: { userId, ip: session.ip }
});
if (!known) {
await sendEmail(user.email,
`Login baru terdeteksi dari ${session.location} (${session.deviceName}). Bukan kamu? Klik di sini untuk mengamankan akun.`
);
}
}
Anomaly Detection — Deteksi Login Mencurigakan
Login baru dari Jakarta vs kota lama "asing" bukan indikator — mungkin user lagi traveling. Indikator yang lebih aktual untuk flagging:
1. Velocity check (impossible travel)
Login dari Jakarta jam 10:00, lalu login dari New York jam 10:05 — jarak fisik tidak memungkinkan. Ini strong signal akun dikompromikan.
import geoip from "geoip-lite";
import { haversineDistance } from "./geo-utils";
async function checkImpossibleTravel(userId, newIp) {
const newGeo = geoip.lookup(newIp);
if (!newGeo) return false;
// Session terakhir dalam 24 jam
const recent = await db.query(
"SELECT ip, last_active_at FROM sessions WHERE user_id = $1 AND last_active_at > NOW() - INTERVAL '24 hours' ORDER BY last_active_at DESC LIMIT 1",
[userId]
);
if (!recent.length) return false;
const lastGeo = geoip.lookup(recent[0].ip);
if (!lastGeo) return false;
const distanceKm = haversineDistance(
[lastGeo.ll[0], lastGeo.ll[1]],
[newGeo.ll[0], newGeo.ll[1]]
);
const timeDiffMin = (Date.now() - new Date(recent[0].last_active_at)) / 1000 / 60;
// Max speed commercial flight ~900 km/jam = 15 km/menit
const maxPossibleDistance = timeDiffMin * 15;
if (distanceKm > maxPossibleDistance && timeDiffMin < 60) {
return true; // IMPOSSIBLE TRAVEL
}
return false;
}
2. User-Agent change drastis
User biasa pakai "Chrome on macOS", tiba-tiba "Firefox on Linux" di jam 3 pagi — suspicious.
async function checkUserAgentAnomaly(userId, newUA) {
// Ambil 10 sesi sebelumnya
const history = await db.query(
"SELECT browser, os FROM sessions WHERE user_id = $1 ORDER BY created_at DESC LIMIT 10",
[userId]
);
const newParsed = parseUserAgent(newUA);
const isFamiliarBrowser = history.some(h => h.browser.includes(newParsed.browser));
const isFamiliarOS = history.some(h => h.os.includes(newParsed.os));
return !isFamiliarBrowser && !isFamiliarOS; // kedua-duanya baru = anomali
}
3. Velocity login attempt per akun
10 login attempt per detik = otomatis bot/attacker. Bukan user legitimate.
async function checkLoginVelocity(email) {
const recentAttempts = await redis.incr(`login_attempts:${email}`);
if (recentAttempts === 1) {
await redis.expire(`login_attempts:${email}`, 60); // 1 menit window
}
if (recentAttempts > 10) {
return "VELOCITY_EXCEEDED"; // kirim ke MFA challenge atau lockout
}
return "OK";
}
4. Score-based risk (layered signal)
Gabungkan signal untuk risk score 0-100. Threshold menentukan action:
async function calculateLoginRisk(userId, req) {
let risk = 0;
if (await checkImpossibleTravel(userId, req.ip)) risk += 50;
if (await checkUserAgentAnomaly(userId, req.headers["user-agent"])) risk += 20;
if (await isKnownVPN(req.ip)) risk += 15;
if (isUnusualTimeOfDay(userId, new Date())) risk += 10;
if (await isKnownThreatIP(req.ip)) risk += 30;
return risk;
}
// Di login endpoint
const risk = await calculateLoginRisk(user.id, req);
if (risk >= 70) {
// Force MFA re-challenge bahkan untuk user yang trust device
return { status: "mfa_required_elevated", risk };
}
if (risk >= 40) {
// Send notification email, but allow login
await sendSuspiciousLoginAlert(user.email, loginDetails);
}
// risk < 40 = normal login
Service pihak ketiga yang bisa dipakai kalau tidak mau bikin sendiri: Auth0 Adaptive MFA, AWS Cognito Risk-based, Okta WorkflowOne. Mereka expose risk score lewat API.
Tips production:
- Mulai dari rules sederhana (velocity + impossible travel), bukan ML model. Hampir semua attack terdeteksi di level ini.
- Log semua event — training data untuk tuning threshold ke depannya
- Mobile app: device fingerprint (model, OS version) lebih stabil dari browser
- False positive > false negative untuk MFA challenge — lebih baik user annoyed 1x ketik OTP daripada akun compromized