1. Use Refresh Tokens with Access Tokens
-
Access tokens should be short-lived (e.g., 15 minutes).
-
Refresh tokens should be long-lived and tied to a specific device.
-
This prevents all sessions from being compromised if one token leaks.
2. Store Tokens Securely Per Device
-
Mobile apps: Use Secure Storage (e.g., Keychain on iOS, Keystore on Android).
-
Web apps: Prefer HTTP-only cookies for refresh tokens (avoid localStorage).
3. Track Devices with Unique Identifiers
-
Assign a device ID for each device upon login/registration.
-
Store it alongside the refresh token in your DB (e.g.,
{ userId, deviceId, refreshToken, lastSeen, ipAddress }
).
4. Allow Session Management
-
Provide users a “manage devices” interface.
-
Let them revoke sessions from specific devices.
5. Token Revocation Strategy
-
Use a server-side token store or blacklist mechanism to invalidate compromised tokens.
-
Maintain a list of valid refresh tokens per user/device.
6. Notify on New Device Logins
-
Alert users when a new device logs in or an existing session is reused from a different IP or location.
7. Rate Limiting & Device Limits
-
Optionally limit how many devices a user can log in to simultaneously.
-
Prevent abuse by rate-limiting login attempts.
Here's a practical example of handling multiple devices using token-based authentication (with access/refresh tokens and device tracking) using Node.js + Express + JWT + MongoDB.
1. Database Schema (MongoDB + Mongoose)
js
const mongoose = require('mongoose');
const RefreshTokenSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
deviceId: { type: String, required: true },
refreshToken: { type: String, required: true },
userAgent: String,
ip: String,
lastUsed: { type: Date, default: Date.now },
});
module.exports = mongoose.model('RefreshToken', RefreshTokenSchema);
2. Login Route (Issuing Tokens Per Device)
js
const jwt = require('jsonwebtoken');
const RefreshToken = require('./models/RefreshToken');
app.post('/login', async (req, res) => {
const { email, password, deviceId } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).send('Invalid credentials');
}
const accessToken = jwt.sign({ userId: user._id }, 'ACCESS_SECRET', { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user._id, deviceId }, 'REFRESH_SECRET', { expiresIn: '7d' });
await RefreshToken.findOneAndUpdate(
{ userId: user._id, deviceId },
{ refreshToken, ip: req.ip, userAgent: req.headers['user-agent'], lastUsed: new Date() },
{ upsert: true }
);
res.json({ accessToken, refreshToken });
});
3. Refresh Token Route (Per Device)
js
app.post('/token', async (req, res) => {
const { refreshToken, deviceId } = req.body;
if (!refreshToken) return res.status(401).send('Missing token');
try {
const payload = jwt.verify(refreshToken, 'REFRESH_SECRET');
const storedToken = await RefreshToken.findOne({ userId: payload.userId, deviceId, refreshToken });
if (!storedToken) return res.status(403).send('Invalid session');
const newAccessToken = jwt.sign({ userId: payload.userId }, 'ACCESS_SECRET', { expiresIn: '15m' });
storedToken.lastUsed = new Date();
await storedToken.save();
res.json({ accessToken: newAccessToken });
} catch (err) {
return res.status(403).send('Token expired or invalid');
}
});
4. Logout from a Specific Device
js
app.post('/logout', async (req, res) => {
const { deviceId } = req.body;
const userId = req.user.userId; // from middleware
await RefreshToken.deleteOne({ userId, deviceId });
res.send('Logged out from device');
});
5. Optional: Get All Logged-in Devices
js
app.get('/devices', async (req, res) => {
const userId = req.user.userId;
const devices = await RefreshToken.find({ userId }).select('-refreshToken');
res.json(devices);
});
This structure supports:
-
Multiple devices per user.
-
Secure refresh token handling per device.
-
Manual session management (logout specific device, list devices).
-
Optional device fingerprinting or IP tracking.
No comments:
Post a Comment