β A practical guide to implementing single-user login session enforcement with real-time logout notifications using Django, ASGI, Channels, WebSockets, and Redis.
π Problem Statement
We want to ensure that each user can only be logged in on one device at a time.
Hear's the expected behaviour:
- When a user logs in from a new device, any existing session on other devices should get:
- β Logged out immediately, and
- π Notified in real-time about the forced logout.
Traditional Django projects (which run on WSGI) can't do this in real-time - you'd need pulling or periodic refresh. We needed an instant notification system
β Why WSGI Couldnβt Help
WSGI (Web Server Gateway Interface) is great for traditional request-response cycles but:
β WSGI Limitations
- No support for WebSockets
- No long-lived connections
- No built-in async support
- Not scalable for concurrent real-time tasks
Hence, we replaced WSGI with ASGI.
β Why We Used ASGI
ASGI (Asynchronous Server Gateway Interface) allows:
- β Real-time WebSocket support
- β Async communication
- β Push-based updates (no polling)
- β Integration with channels and channels_redis
This made ASGI a perfect fit for:
- Real-time session monitoring
- Force logout via socket
- Scalable async event-driven features
π‘ WebSockets (and Why They Matter)
A WebSocket is a protocol that enables bi-directional, full-duplex communication between the server and the client over a single TCP connection.
Unlike HTTP, which is request-response based, WebSockets stay open, so the server can push data to the client without a new request.
In our use case:
When a user logs in from a new device, we:
- Save the new session key
- Broadcast a message to the old sessionβs WebSocket connection
- The frontend (already connected via WebSocket) receives a real-time message prompting the user
π§ What We Implemented
β
1. WebSocket Connection Per User
Every logged-in user opens a WebSocket connection. We assign each user to a Redis channel group named uniquely, like user_{user.id}.
# consumers.py
class SessionConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
if self.user.is_authenticated:
self.group_name = f"user_{self.user.id}"
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
else:
await self.close()
async def disconnect(self, close_code):
if self.user.is_authenticated:
await self.channel_layer.group_discard(self.group_name, self.channel_name)
async def force_logout(self, event):
await self.send(text_data=json.dumps({
"action": "force_logout"
}))
β
2. Track Current Session ID in DB
We extended Djangoβs AbstractUser to store the current session key.
# models.py
from django.contrib.auth.models import AbstractUser
class UserMaster(AbstractUser):
current_session_key = models.CharField(max_length=255, null=True, blank=True)
β
3. Check and Destroy Old Sessions on Login
On login, we check if there is a previous session for the user. If yes:
- We delete it
- Notify the old WebSocket channel (group) to logout
# login view
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
if user.current_session_key:
old_session = Session.objects.filter(session_key=user.current_session_key).first()
if old_session:
old_session.delete()
# π Notify via WebSocket
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f"user_{user.id}",
{
"type": "force_logout",
}
)
# Save new session
user.current_session_key = request.session.session_key
user.save()
β
4. Frontend WebSocket Listener
When a logout message is sent, the frontend instantly logs the user out.
// session.js
const socket = new WebSocket('ws://' + window.location.host + '/ws/session/');
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.action === "force_logout") {
alert("You have been logged out because your account was accessed elsewhere.");
window.location.href = "/logout/";
}
};
βοΈ Supporting Infrastructure: Redis & Channels
We added these in settings.py:
INSTALLED_APPS += ["channels"]
ASGI_APPLICATION = "your_project.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis", 6379)],
},
}
}
π§± Docker Compose Setup
We added Redis and Uvicorn (ASGI server) to our docker-compose.yml:
version: "3.9"
services:
web:
build: .
command: uvicorn your_project.asgi:application --host 0.0.0.0 --port 8000 --reload
depends_on:
- redis
ports:
- "8000:8000"
redis:
image: redis:alpine
ports:
- "6379:6379"
π ASGI vs WSGI Summary Table
π§ Conclusion
With ASGI, we made our Django app:
- β Real-time capable
- β WebSocket-enabled
- β Scalable for future interactive features
- β More secure with single-session login
This pattern is now ready to be reused for:
- π Notifications
- π‘ Live dashboards
- π§Ύ Collaborative forms
- π¨βπ» Chat or support features
Top comments (0)