• Fri, Mar 2026

Session Management UI: List Active Sessions & Revoke — Vue + Node

Session Management UI: List Active Sessions & Revoke — Vue + Node

Hello — Being full-stack dev who has had to explain to nervous product managers why “log out from all devices” is non-negotiable. I once found a user signed in on 12 devices across the globe — giving them a per-session UI to review and revoke saved me from writing an incident report.

In this tutorial you’ll learn how to build a secure session management UI: a place where users can see active sessions (device, IP, last seen) and revoke any session — including the ability to revoke all sessions at once.

What you’re building and why it matters

A session management UI provides visibility and control. Key benefits:

  • Users can see where they’re logged in and terminate suspicious sessions.
  • Administrators can audit sessions and revoke compromised tokens quickly.
  • When combined with refresh-token rotation, revocation immediately reduces attack surface.

We’ll implement a production-minded flow using Vue 3 (frontend), Node/Express (backend), and Redis as the session store. The backend will maintain a per-user index of session IDs for fast listing and an individual session record for metadata and revocation state.

Design & data model

Session record

Each refresh-token session stored server-side contains:

Key (Redis): refresh:{sid}
Fields:
  uid: user id
  hash: hashed refresh token (HMAC/SHA256 or bcrypt)
  createdAt: ISO timestamp
  lastUsedAt: ISO timestamp
  ip: IP address (optional)
  userAgent: raw UA string
  revoked: 0|1
  device: friendly label (optional)

User → sessions index

To list sessions quickly, maintain a Redis set:

Key: userSessions:{uid}  (set of session ids like "sid-uuid")

This lets you list all session IDs for a user with SMEMBERS userSessions:{uid} then HMGET each session key. This is far better than scanning all keys.

Backend: endpoints & behaviour (Node + Express + Redis)

We assume you already have authentication & refresh token rotation. The additions below show:

  • How to store session metadata when creating refresh tokens
  • How to list sessions for the authenticated user
  • How to revoke a session (single or all)

// pseudo: issueRefreshToken(userId, token, req)
const sessionId = uuidv4()
const hash = hashToken(refreshToken) // HMAC-SHA256 with server pepper
const key = `refresh:${sessionId}`
await redis.hmset(key, {
  uid: userId,
  hash,
  createdAt: new Date().toISOString(),
  lastUsedAt: new Date().toISOString(),
  ip: req.ip || '',
  userAgent: req.headers['user-agent'] || '',
  revoked: 0,
  device: detectDeviceLabel(req.headers['user-agent']) || ''
})
await redis.expire(key, REFRESH_TTL_SECONDS)
// add to userSessions index
await redis.sadd(`userSessions:${userId}`, sessionId)

2) Endpoint: GET /auth/sessions — list active sessions for current user

This endpoint requires a valid access token. It returns a list of session objects, including whether a session is the current one (compare with access token sid).

// routes/auth.js (Express)
const express = require('express')
const router = express.Router()
const redis = require('../redis')
const { verifyAccessToken } = require('../auth-middleware') // usual JWT verify
// GET /auth/sessions
router.get('/sessions', verifyAccessToken, async (req, res) => {
  const uid = req.user.uid
  const currentSid = req.user.sid // include session id in access token on login/refresh
  const sessionIds = await redis.smembers(`userSessions:${uid}`) // array of sids
  const pipeline = redis.pipeline()
  sessionIds.forEach(sid => pipeline.hgetall(`refresh:${sid}`))
  const results = await pipeline.exec()
  // results: [[null, { uid:..., hash:..., ...}], ...]
  const sessions = results.map(([, record], i) => {
    if (!record || Object.keys(record).length === 0) return null
    return {
      sid: sessionIds[i],
      createdAt: record.createdAt,
      lastUsedAt: record.lastUsedAt,
      ip: record.ip,
      userAgent: record.userAgent,
      device: record.device,
      revoked: record.revoked === '1',
      isCurrent: sessionIds[i] === currentSid
    }
  }).filter(Boolean)
  res.json({ sessions })
})

3) Endpoint: DELETE /auth/sessions/:sid — revoke a session

Only the owner can revoke their sessions. When revoking:

  • Mark session's revoked=1 and save revokedAt
  • Remove session id from userSessions:{uid}
  • Optionally invalidate access tokens faster if you maintain an access-token revocation list
// DELETE /auth/sessions/:sid
router.delete('/sessions/:sid', verifyAccessToken, async (req, res) => {
  const uid = req.user.uid
  const sid = req.params.sid
  // verify session belongs to user
  const rec = await redis.hgetall(`refresh:${sid}`)
  if (!rec || rec.uid !== uid) return res.status(404).json({ error: 'Session not found' })
  // mark revoked
  await redis.hmset(`refresh:${sid}`, { revoked: 1, revokedAt: new Date().toISOString() })
  // remove from userSessions
  await redis.srem(`userSessions:${uid}`, sid)
  // if revoking the current session, clear cookies / instruct client to logout
  const isCurrent = req.user.sid === sid
  res.json({ ok: true, isCurrent })
})

4) Endpoint: POST /auth/sessions/revoke-all — revoke every session except current (optional)

// POST /auth/sessions/revoke-all
router.post('/sessions/revoke-all', verifyAccessToken, async (req, res) => {
  const uid = req.user.uid
  const currentSid = req.user.sid
  const sids = await redis.smembers(`userSessions:${uid}`)
  const pipeline = redis.pipeline()
  sids.forEach(sid => {
    if (sid === currentSid) return // keep current if you want
    pipeline.hmset(`refresh:${sid}`, { revoked: 1, revokedAt: new Date().toISOString() })
    pipeline.srem(`userSessions:${uid}`, sid)
  })
  await pipeline.exec()
  res.json({ ok: true })
})

Security notes (backend)

  • Always verify that the session record belongs to the authenticated user.
  • Log revocation events for audits: who revoked which session and when.
  • If you use per-session access token sid in the JWT payload, you can identify when the client’s access token corresponds to which session.
  • If revoking the current session, clear cookies on client and force logout.

Frontend: Vue UI to list & revoke sessions

The frontend will:

  1. Call GET /auth/sessions to fetch active sessions.
  2. Display each session with device, IP, last seen, and a Revoke button.
  3. Call DELETE /auth/sessions/:sid to revoke; if current session revoked, log out user.

Helper: decode JWT payload to get current session id (sid)

We assume your access token contains sid in payload. You can decode client-side without verifying (for UI purposes only):

// utils/jwt.js
export function parseJwt(token) {
  if (!token) return null
  try {
    const payload = token.split('.')[1]
    const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
    return JSON.parse(decodeURIComponent(escape(decoded)))
  } catch (e) {
    return null
  }
}

SessionsList.vue — full component

<template>
 <section>
  <h2>Active Sessions</h2>
  <p v-if="loading">Loading sessions...</p>
  <ul v-if="sessions.length">
   <li v-for="s in sessions" :key="s.sid">
    <strong>{{ s.device || 'Unknown device' }}</strong>
    <span> — {{ s.ip || 'Unknown IP' }} · Last seen: {{ format(s.lastUsedAt) }}</span>
    <span v-if="s.isCurrent" >(This device)</span>
    <div >
     <button @click="revoke(s.sid)" :disabled="s.revoking">
      <span v-if="!s.revoking">Revoke</span>
      <span v-else>Revoking...</span>
     </button>
    </div>
   </li>
  </ul>
  <p v-else-if="!loading">No active sessions found.</p>
  <div >
   <button @click="revokeAll" :disabled="revokingAll">
    <span v-if="!revokingAll">Revoke all other sessions</span>
    <span v-else>Working...</span>
   </button>
  </div>
 </section>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/axios-client' // axios instance with Authorization header
import { useAuthStore } from '@/stores/auth'
import { parseJwt } from '@/utils/jwt'
const sessions = ref([])
const loading = ref(false)
const revokingAll = ref(false)
const auth = useAuthStore()
function format(ts) {
 if (!ts) return 'never'
 return new Date(ts).toLocaleString()
}
async function loadSessions() {
 loading.value = true
 try {
  const { data } = await api.get('/auth/sessions')
  sessions.value = data.sessions
 } catch (e) {
  console.error('Failed to load sessions', e)
 } finally {
  loading.value = false
 }
}
async function revoke(sid) {
 const idx = sessions.value.findIndex(s => s.sid === sid)
 if (idx === -1) return
 sessions.value[idx].revoking = true
 try {
  const resp = await api.delete(`/auth/sessions/${sid}`)
  if (resp.data.isCurrent) {
   // revoked current session: clear auth & redirect to login
   auth.clearAuth()
   window.location.href = '/login'
   return
  }
  // remove from list
  sessions.value.splice(idx, 1)
 } catch (e) {
  console.error('Failed to revoke', e)
  sessions.value[idx].revoking = false
 }
}
async function revokeAll() {
 revokingAll.value = true
 try {
  await api.post('/auth/sessions/revoke-all')
  // reload sessions: the server may keep current session; so reload
  await loadSessions()
 } catch (e) {
  console.error('Failed to revoke all', e)
 } finally {
  revokingAll.value = false
 }
}
onMounted(async () => {
 // if access token contains sid we can mark isCurrent server-side too,
 // but server already returns isCurrent when it can.
 await loadSessions()
})
</script>

UX notes: Confirm revoke actions with a dialog in real apps (I skipped it here for brevity). After revoking the current session, force logout user and redirect to login. For revoking other sessions, consider showing "Session revoked" toast and updating list optimistically.

Server: populating device label and lastUsedAt

When issuing or refreshing tokens, populate device and update lastUsedAt:

// on refresh or login success
await redis.hmset(`refresh:${sid}`, {
  ...,
  lastUsedAt: new Date().toISOString(),
  userAgent: req.headers['user-agent'] || '',
  ip: req.ip || '',
  device: friendlyDeviceName(req.headers['user-agent'])
})
// friendlyDeviceName is a helper: parse UA and return "Chrome on Mac" or "iPhone"

You can use libraries like ua-parser-js on the server to generate friendly device names. Even a raw userAgent string is useful for users to recognize devices.

Audit logging & notifications

When users revoke sessions (or when reuse is detected), record an audit event. Example audit entry:

{
  event: "session_revoked",
  userId: "user-1",
  sessionId: "sid-abc",
  actor: "self", // or "system", "admin"
  ip: "1.2.3.4",
  userAgent: "Mozilla/5.0 ...",
  timestamp: "2025-09-28T12:34:56Z"
}

Persist audit logs to ELK/CloudWatch or a DB so customer support and security teams can investigate. Optionally email the user when suspicious sessions are revoked automatically.

Edge cases and operational considerations

Scaling revocation

Do not rely on scanning Redis keys at scale. Maintain userSessions:{uid} set; this allows O(N) operations where N is the user's sessions (usually small). For admin-wide revocation across all users, maintain additional indexes or use message queues.

Session TTL & cleanup

Use Redis TTL for session keys and periodically remove stale entries from userSessions:{uid}. A background job can prune sets by checking for missing keys.

Access token invalidation

If you need immediate invalidation of access tokens after revoking a session, you must track revoked access tokens or include a session "version" claim and check it in middleware. The simplest approach: keep access tokens short-lived (minutes) and rely on refresh-token revocation for longer control.

Privacy

Be careful about exposing too much IP or device detail to other users (shouldn't happen) — only show sessions to their owner or administrators with proper authorization.

Step-by-step implementation checklist

  1. When issuing refresh tokens, create a session id (sid) and store a hashed token & metadata in Redis key refresh:{sid}.
  2. Add the sid to the per-user set userSessions:{uid}.
  3. Include the sid in the access token (JWT payload) so client can detect current session.
  4. Implement GET /auth/sessions to list sessions by reading userSessions:{uid} and HMGET for each session key.
  5. Implement DELETE /auth/sessions/:sid to mark revoked and remove from userSessions. If current session, instruct client to logout.
  6. Implement POST /auth/sessions/revoke-all to revoke other sessions for user.
  7. Build Vue UI to display sessions and call revoke endpoints. Handle revoking current session gracefully (logout & redirect).
  8. Log audit events for revocation actions and reuse detection.

Wrap up — final thoughts and tips from the trenches

Giving users control over their sessions reduces support load and improves security. A good session UI paired with token rotation / reuse detection is one of the nicest security features you can ship quickly. A few final tips from my experience:

  • Show friendly device names for clarity — users quickly understand “Chrome on MacBook” vs an opaque userAgent string.
  • Offer recovery steps if users revoke their own current session by mistake (e.g., support link or secondary factor login).
  • Make revocation fast — users expect immediate results. Use Redis or a fast DB path.
  • Test session revocation thoroughly: current session, other sessions, and replayed refresh tokens.
This website uses cookies to enhance your browsing experience. By continuing to use this site, you consent to the use of cookies. Please review our Privacy Policy for more information on how we handle your data. Cookie Policy