We want to use django-channels for our websockets but we need to authenticate as well. We have a rest api running with django-rest-framework and there we use tokens to authenticate a user, but the same functionality does not seem to be built into django-channels.
12 Answers
For Django-Channels 2 you can write custom authentication middleware https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a
token_auth.py:
from channels.auth import AuthMiddlewareStack
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
headers = dict(scope['headers'])
if b'authorization' in headers:
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
token = Token.objects.get(key=token_key)
scope['user'] = token.user
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
routing.py:
from django.urls import path
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from yourapp.consumers import SocketCostumer
from yourapp.token_auth import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
"websocket": TokenAuthMiddlewareStack(
URLRouter([
path("socket/", SocketCostumer),
]),
),
})
12 Comments
If you are using Django Channels 3 you can use this code: https://gist.github.com/AliRn76/1fb99688315bedb2bf32fc4af0e50157
middleware.py
from django.contrib.auth.models import AnonymousUser
from rest_framework.authtoken.models import Token
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
@database_sync_to_async
def get_user(token_key):
try:
token = Token.objects.get(key=token_key)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
super().__init__(inner)
async def __call__(self, scope, receive, send):
try:
token_key = (dict((x.split('=') for x in scope['query_string'].decode().split("&")))).get('token', None)
except ValueError:
token_key = None
scope['user'] = AnonymousUser() if token_key is None else await get_user(token_key)
return await super().__call__(scope, receive, send)
routing.py
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from .middleware import TokenAuthMiddleware
from main.consumers import MainConsumer
from django.conf.urls import url
application = ProtocolTypeRouter({
'websocket': AllowedHostsOriginValidator(
TokenAuthMiddleware(
URLRouter(
[
url(r"^main/$", MainConsumer.as_asgi()),
]
)
)
)
})
14 Comments
AllowedHostsOriginValidator. Django3.1 and channel3.close_code on connect() if self.scope['user'] is Anonymoustoken key@database_sync_to_async by using the Async ORM interface in Django 4.1+?This answer is valid for channels 1.
You can find all information in this github issue: https://github.com/django/channels/issues/510#issuecomment-288677354
I will summarise the discussion here.
copy this mixin into your project: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4
apply the decorator to
ws_connect
the token is received in the app via an earlier authentication request to the /auth-token view in django-rest-framework. We use a querystring to send the token back to django-channels. If you're not using django-rest-framework you can consume the querystring in your own way. Read the mixin for how to get to it.
- After using the mixin, and the correct token is used with the upgrade / connect request, the message will have a user like in the example below.
As you can see, we have
has_permission()implemented on theUsermodel, so it can just check its instance. If there is no token or the token is invalid, there will be no user on the message.
# get_group, get_group_category and get_id are specific to the way we named
# things in our implementation but I've included them for completeness.
# We use the URL `wss://www.website.com/ws/app_1234?token=3a5s4er34srd32`
def get_group(message):
return message.content['path'].strip('/').replace('ws/', '', 1)
def get_group_category(group):
partition = group.rpartition('_')
if partition[0]:
return partition[0]
else:
return group
def get_id(group):
return group.rpartition('_')[2]
def accept_connection(message, group):
message.reply_channel.send({'accept': True})
Group(group).add(message.reply_channel)
# here in connect_app we access the user on message
# that has been set by @rest_token_user
def connect_app(message, group):
if message.user.has_permission(pk=get_id(group)):
accept_connection(message, group)
@rest_token_user
def ws_connect(message):
group = get_group(message) # returns 'app_1234'
category = get_group_category(group) # returns 'app'
if category == 'app':
connect_app(message, group)
# sends the message contents to everyone in the same group
def ws_message(message):
Group(get_group(message)).send({'text': message.content['text']})
# removes this connection from its group. In this setup a
# connection wil only ever have one group.
def ws_disconnect(message):
Group(get_group(message)).discard(message.reply_channel)
thanks to github user leonardoo for sharing his mixin.
11 Comments
get_group function doing? Could you show a sample of your models if that would elp. Thanksget_id was not in there yet so I added it as well. It just returns the group id from group name. Are you able to use those same tokens to authenticate in other ways? (not through websockets) What is your problem specifically?The following Django-Channels 2 middleware authenticates JWTs generated by djangorestframework-jwt .
The token can be set via the djangorestframework-jwt http APIs, and it will also be sent for WebSocket connections if JWT_AUTH_COOKIE is defined.
settings.py
JWT_AUTH = {
'JWT_AUTH_COOKIE': 'JWT', # the cookie will also be sent on WebSocket connections
}
routing.py:
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path
from json_token_auth import JsonTokenAuthMiddlewareStack
from yourapp.consumers import SocketCostumer
application = ProtocolTypeRouter({
"websocket": JsonTokenAuthMiddlewareStack(
URLRouter([
path("socket/", SocketCostumer),
]),
),
})
json_token_auth.py
from http import cookies
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
"""
Extracts the JWT from a channel scope (instead of an http request)
"""
def get_jwt_value(self, scope):
try:
cookie = next(x for x in scope['headers'] if x[0].decode('utf-8') == 'cookie')[1].decode('utf-8')
return cookies.SimpleCookie(cookie)['JWT'].value
except:
return None
class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
try:
# Close old database connections to prevent usage of timed out connections
close_old_connections()
user, jwt_value = JsonWebTokenAuthenticationFromScope().authenticate(scope)
scope['user'] = user
except:
scope['user'] = AnonymousUser()
return self.inner(scope)
def JsonTokenAuthMiddlewareStack(inner):
return JsonTokenAuthMiddleware(AuthMiddlewareStack(inner))
1 Comment
self.scope['user'] in the Consumer is still returning AnonymousUser any help would be highly appreciated.channels-auth-token-middlewares provides a QueryStringSimpleJWTAuthTokenMiddleware to support token authentication out of the box when using Simple JWT with Django REST Framework.
Update INSTALLED_APPS:
INSTALLED_APPS = [
# base django apps (django.contrib.auth is required)
# other apps this one depends on (like rest_framework if it's necessary)
'channels_auth_token_middlewares',
# custom apps
]
Insert QueryStringSimpleJWTAuthTokenMiddleware into your ASGI application stack:
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
QueryStringSimpleJWTAuthTokenMiddleware(
URLRouter(...),
),
),
}
)
Clients pass their JWT token into the token query parameter:
from websocket import create_connection
token = "EXAMPLE_TOKEN"
ws = create_connection(f"ws://127.0.0.1/ws/?token={token}")
The authenticated User (or AnonymousUser if the JWT is invalid) will be populated into the "user" key of the scope passed to the Consumer
class MyAsyncCommunicator(AsyncWebsocketConsumer):
async def connect(self) -> None:
user = self.scope["user"]
# Validate user before accepting the Websocket Connection
# For example:
if not user.is_authenticated or user.is_anonymous:
# Handle unauthorized.
Comments
I believe sending token in query string can expose token even inside HTTPS protocols. To come around such issue I have used the following steps:
Create a token based REST API endpoint which creates temporary session and respond back with this
session_key(This session is set to expire in 2 minutes)login(request,request.user)#Create session with this user request.session.set_expiry(2*60)#Make this session expire in 2Mins return Response({'session_key':request.session.session_key})Use this
session_keyin query parameter in channels parameter
I understand there is one extra API call but I believe it's much more secure than sending token in URL string.
Edit: This is just another approach to this problem, as discussed in comments, get parameters are exposed only in urls of http protocols, which should be avoided in anyhow.
6 Comments
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from jwt import decode as jwt_decode
from urllib.parse import parse_qs
from django.contrib.auth import get_user_model
from channels.db import database_sync_to_async
from django.conf import settings
@database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return 'AnonymousUser'
class TokenAuthMiddleware:
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
# Look up user from query string (you should also do things like
# checking if it is a valid user ID, or if scope["user"] is already
# populated).
token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
print(token)
try:
# This will automatically validate the token and raise an error if token is invalid
is_valid = UntypedToken(token)
except (InvalidToken, TokenError) as e:
# Token is invalid
print(e)
return None
else:
# Then token is valid, decode it
decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=["HS256"])
print(decoded_data)
scope['user'] = await get_user(int(decoded_data.get('user_id', None)))
# Return the inner application directly and let it run everything else
return await self.app(scope, receive, send)
Asgi like this
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import path
from channelsAPI.routing import websocket_urlpatterns
from channelsAPI.token_auth import TokenAuthMiddleware
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VirtualCurruncy.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": TokenAuthMiddleware(
URLRouter([
path("virtualcoin/", websocket_urlpatterns),
])
),
})
Comments
ovveride custom AuthMiddleware
from urllib.parse import parse_qs
from channels.auth import AuthMiddleware
from channels.db import database_sync_to_async
from channels.sessions import CookieMiddleware, SessionMiddleware
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
@database_sync_to_async
def get_user(scope):
query_string = parse_qs(scope['query_string'].decode())
token = query_string.get('token')
if not token:
return AnonymousUser()
try:
user = Token.objects.get(key=token[0]).user
except Exception as exception:
return AnonymousUser()
if not user.is_active:
return AnonymousUser()
return user
class TokenAuthMiddleware(AuthMiddleware):
async def resolve_scope(self, scope):
scope['user']._wrapped = await get_user(scope)
def TokenAuthMiddlewareStack(inner):
return CookieMiddleware(SessionMiddleware(TokenAuthMiddleware(inner)))
import the TokenAuthMiddlewareStack middleware in asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from chat.api.router_ws import urlpatterns_websocket
from .middleware import TokenAuthMiddlewareStack
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
TokenAuthMiddlewareStack(
URLRouter(urlpatterns_websocket)
)
),
})
In frontend:new WebSocket(ws://8000/{your_path}?token=${localStorage.getItem('token')})
In Consumer: you can access the requested user as self.scope["user"]
Comments
Regarding Channels 1.x
As already pointed out here the mixin by leonardoo is the easiest way: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4
I think, however, it is somewhat confusing to figure out what the mixin is doing and what not, so I will try to make that clear:
When looking for a way to access message.user using the native django channels decorators you would have to implement it like this:
@channel_session_user_from_http
def ws_connect(message):
print(message.user)
pass
@channel_session_user
def ws_receive(message):
print(message.user)
pass
@channel_session_user
def ws_disconnect(message):
print(message.user)
pass
Channels does that by authenticating the user, creating a http_session and then converting the http_session in a channel_session, which uses the reply channel instead of cookies to identify the client. All this is done in channel_session_user_from_http. Have a look at the channels source code for more detail: https://github.com/django/channels/blob/1.x/channels/sessions.py
leonardoo's decorator rest_token_user does, however, not create a channel session it simply stores the user in the message object in ws_connect. As the token is not sent again in ws_receive and the message object is not available either, in order to get the user in ws_receive and ws_disconnect as well, you would have to store it in the session yourself. This would be a easy way to do this:
@rest_token_user #Set message.user
@channel_session #Create a channel session
def ws_connect(message):
message.channel_session['userId'] = message.user.id
message.channel_session.save()
pass
@channel_session
def ws_receive(message):
message.user = User.objects.get(id = message.channel_session['userId'])
pass
@channel_session
def ws_disconnect(message):
message.user = User.objects.get(id = message.channel_session['userId'])
pass
Comments
Just wrote a middleware, same secure as http(using the JWT token from the header instead of url):
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken
@database_sync_to_async
def get_user_from_token(token_str):
User = get_user_model()
try:
untyped_token = UntypedToken(token_str)
print("untyped_token: ", untyped_token)
user = JWTAuthentication().get_user(untyped_token)
if user is not None:
return user
else:
# No user associated with the JWT
return AnonymousUser()
except (InvalidToken, TokenError, User.DoesNotExist) as e:
print("Invalid token: ", e)
return AnonymousUser()
class TokenAuthMiddleware:
"""
Custom middleware (insecure) that takes user IDs from the query string.
"""
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
# Look up user from query string (you should also do things like
# checking if it is a valid user ID, or if scope["user"] is already
# populated).
headers_dict = dict((k.decode('utf-8'), v.decode('utf-8'))
for k, v in scope['headers'])
jwtToken = headers_dict.get('authorization', None).split(" ")[1]
scope['user'] = await get_user_from_token(jwtToken)
return await self.app(scope, receive, send)
Comments
If you're seeing this no (2024 and beyond), probably dropping it for someone that has been hitting their head to the wall like I was.
Check this out if you're using recent versions and JWT
[Check this post out][1]
Also, don't forget to include
'rest_framework.authtoken',
'rest_framework_simplejwt',
In your settings.py [1]: https://dev.to/buurak/django-rest-framework-websocket-3pb6
Comments
A friend wrote an article on this; it leverages off of a database cluster. Also included is a full working git, with both synchronous and asynchronous implementations.
Authentication for Django Channels using a Database Cluster
He is going to be writing a subsequent article soon that implements it using a database connection pool, allowing for scalability. Following that, he said he is going to implement elective pluggable authentication inside the socket (as opposed to with query parameters...), but with a shortened timeout to prevent DOS attacks (but the socket timeout gets extended once authentication is accomplished...)