Error in authenticate a websocket with token authentication on django channels? - django

I face an error when calling the websocket url with passing a JWT token for authentication purpose:
my websocket request is:
ws://127.0.0.1:8000/chat/chat_2/?token=
the error is:
raise ValueError("No route found for path %r." % path)
ValueError: No route found for path 'chat/chat_2/'.
I'm using a custom authentication middleware:
middleware.py
"""
General web socket middlewares
"""
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.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication
from channels.middleware import BaseMiddleware
from channels.auth import AuthMiddlewareStack
from django.db import close_old_connections
from urllib.parse import parse_qs
from jwt import decode as jwt_decode
from django.conf import settings
from django.contrib.auth import get_user_model
User = get_user_model()
#database_sync_to_async
def get_user(validated_token):
try:
user = get_user_model().objects.get(id=validated_token["user_id"])
print(f"{user}")
return user
except User.DoesNotExist:
return AnonymousUser()
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
# Close old database connections to prevent usage of timed out connections
close_old_connections()
# Get the token
token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
# Try to authenticate the user
try:
# This will automatically validate the token and raise an error if token is invalid
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)
# Get the user using ID
scope["user"] = await get_user(validated_token=decoded_data)
return await super().__call__(scope, receive, send)
def JwtAuthMiddlewareStack(inner):
return JwtAuthMiddleware(AuthMiddlewareStack(inner))
routing.py:
from . import consumers
from django.urls.conf import path
websocket_urlpatterns = [
path("ws/chat/<str:room_name>/", consumers.ChatConsumer.as_asgi()),
path(
"ws/personal_chat/<str:room_name>/",
consumers.PersonalConsumer.as_asgi(),
),
]
asgi.py:
import os
import ChatApp.routing
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
from ChatApp.middlewares import JwtAuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Hookax.settings")
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": JwtAuthMiddlewareStack(
URLRouter(ChatApp.routing.websocket_urlpatterns)
),
}
)
The project based on:
Django 3.2.7
Channels 3.0.4
Any suggestions solution?

Related

Best practices for authenticating Django Channels

Django 4.1.4
djoser 2.1.0
channels 4.0.0
I have followed the documented recommendation for creating custom middleware to authenticate a user when using channels and I am successfully getting the user
and checking that the user is authenticated though I am sending the user ID in the querystring when connecting to the websocket to do this. The user is not automatically available in the websocket scope.
I am unsure if there are any potential security risks as the documentation mentions that their recommendation is insecure, I do check that the user.is_authenticated. So I believe I have secured it.
I do believe that using the token created by djoser would be better though I am not sure how to send headers with the websocket request unless I include the token in the querystring instead of the user's ID.
I am keen to hear what the best practices are.
I am passing the user ID to the websocket via querystring as follows at the frontend:
websocket.value = new WebSocket(`ws://127.0.0.1:8000/ws/marketwatch/? ${authStore.userId}`)
middleware.py
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
#database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return AnonymousUser()
else:
if user.is_authenticated:
return user
else:
return AnonymousUser()
class QueryAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = await get_user(int(scope["query_string"].decode()))
return await self.app(scope, receive, send)
consumers.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from api.middleware import QueryAuthMiddleware
from .routing import ws_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
QueryAuthMiddleware(
URLRouter(ws_urlpatterns)
)
)
})
After doing some extensive research I decided not to pass the id or the token via the querystring as this poses a risk due to this data being stored in the server logs.
IMO the best option with the least amount of risk was passing the token as a message to the websocket after the connection was established and then verifying the token; closing the websocket if invalid.
This meant not requiring the middleware previously implemented. In this particular project no other messages would be received from the client so I don't need to do any checking on the key of the message received. This could be changed for chat apps and other apps that will receive further messages from the client.
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
from rest_framework.authtoken.models import Token
class MarketWatchConsumer(AsyncWebsocketConsumer):
#database_sync_to_async
def verify_token(self, token_dict):
try:
token = Token.objects.get(key=token_dict['token'])
except Token.DoesNotExist:
return False
else:
if token.user.is_active:
return True
else:
return False
async def connect(self):
await self.channel_layer.group_add('group', self.channel_name)
await self.accept()
async def receive(self, text_data=None, bytes_data=None):
valid_token = await self.verify_token(json.loads(text_data))
if not valid_token:
await self.close()
async def disconnect(self, code):
await self.channel_layer.group_discard('group', self.channel_name)

How to only allow logged in users connect to websocket in Django Channels?

I have a chat app and I want only users that are logged in to be able to connect to the websocket.
How can you achieve that?
Is there something like the #login_required decorator for Django channels?
I know from the documentation that that's how you can access the user:
class ChatConsumer(WebsocketConsumer):
def connect(self, event):
self.user = self.scope["user"]
But how do you deny the connection if the user isn't logged in?
I figured out the answer to my question:
# mysite/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.user = self.scope['user']
if self.user.is_authenticated:
# accept connection if user is logged in
self.accept()
else:
# don't accept connection if user is not logged in
self.close()
The AuthMiddlewareStack will populate the connection’s scope with a reference to the currently authenticated user, similar to how Django’s AuthenticationMiddleware populates the request object of a view function with the currently authenticated user.
Example to add AuthMiddlewareStack as below:
# mysite/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
Reference:
https://channels.readthedocs.io/en/latest/tutorial/part_2.html#write-your-first-consumer

router.register(), AttributeError: module 'rest_framework.views' has no attribute

I don't know what I am doing wrong. I have been battling with this error for hours. I have opened all the suggestions I saw and implemented what they suggested but still, the error is pending
router.register(r'^hmos/$', views.HMOList),
AttributeError: module 'rest_framework.views' has no attribute 'HMOList'
This is "core/urls.py"
from django.conf.urls import url
from .views import *
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('hmos', views.HMOList)
urlpatterns = format_suffix_patterns(urlpatterns)
This is "core/views.py"
from django.shortcuts import render_to_response
import json
from rest_framework.parsers import MultiPartParser, FileUploadParser, FormParser
from django.db.models import Q
from rest_framework import permissions
from django.contrib.auth import authenticate, login,logout
from rest_framework import generics, status, views
from rest_framework.permissions import IsAuthenticated
from .models import *
from .serializers import *
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .utils import generate_responder_serial
from rest_framework.parsers import MultiPartParser, FileUploadParser, FormParser
from django.conf import settings
import os
from django.db.models import Q
#from rest_framework.authentication import (BaseJSONWebTokenAuthentication)
from rest_framework import viewsets
def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': SystemUserSerializer(user).data
}
def create(self, request, *args, **kwargs):
new_data = {'name': request.data['name'].strip(), 'address': request.data['address'],
'state': request.data['state'], 'mobile1': request.data['mobile1'],
'mobile2': request.data['mobile2'], }
if HMO.objects.filter(name = request.data['name'].strip()):
raise serializers.ValidationError('HMO name already exists')
serializer = HMOSerializer(data=new_data)
if serializer.is_valid():
try:
serializer.save()
except Exception as e:
return Response( e)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response({
'status' : 'Bad request',
'message': 'HMO could not be created with received data.',
'errors' : serializer.errors # for example
}, status=status.HTTP_400_BAD_REQUEST)
This is promedic/urls.py
from django.conf.urls import url, include
from django.urls import path
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.staticfiles.views import serve
from rest_framework_swagger.views import get_swagger_view
schema_view = get_swagger_view(title='Pastebin API')
urlpatterns = [
url(r'^$', schema_view),
url(r'^admin/', admin.site.urls),
url('api/core/', include('core.urls')),
]
In "core/urls.py" you should have :
from .views import HMOList
router.register(r'hmos', HMOList)
urlpatterns = router.urls

Emails won't send after upgrading from Django 1.6.x to Django > 1.7.x

I am currently using Django Allauth and a modified version of Django Invitations (https://github.com/bee-keeper/django-invitations). The only thing added is a field for which group to add the user to, and the application works perfectly when Django 1.6.x is being used. I would like to upgrade to Django 1.7.x or 1.8 but this somehow breaks the emailing feature.
The specific piece of code is here:
'import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.encoding import python_2_unicode_compatible
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.adapter import get_adapter
from .managers import InvitationManager
from . import app_settings
from . import signals
...(other code)
def send_invitation(self, request, **kwargs):
current_site = (kwargs['site'] if 'site' in kwargs
else Site.objects.get_current())
invite_url = reverse('invitations:accept-invite',
args=[self.key])
invite_url = request.build_absolute_uri(invite_url)
ctx = {
'invite_url': invite_url,
'current_site': current_site,
'email': self.email,
'key': self.key,
}
email_template = 'invitations/email/email_invite'
get_adapter().send_mail(email_template,
self.email,
ctx)
self.sent = timezone.now()
self.save()
signals.invite_url_sent.send(
sender=self.__class__,
instance=self,
invite_url_sent=invite_url)'
found here (https://github.com/bee-keeper/django-invitations/blob/master/invitations/models.py)
This also references the code from allauth here:
from __future__ import unicode_literals import re
import warnings
import json
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.template import TemplateDoesNotExist
from django.contrib.sites.models import Site
from django.core.mail import EmailMultiAlternatives, EmailMessage
from django.utils.translation import ugettext_lazy as _
from django import forms
from django.contrib import messages
try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text
from ..utils import (import_attribute, get_user_model,
generate_unique_username,
resolve_url)
from . import app_settings
USERNAME_REGEX = re.compile(r'^[\w.#+-]+$', re.UNICODE)
........ (other code)
def render_mail(self, template_prefix, email, context):
"""
Renders an e-mail to `email`. `template_prefix` identifies the
e-mail that is to be sent, e.g. "account/email/email_confirmation"
"""
subject = render_to_string('{0}_subject.txt'.format(template_prefix),
context)
# remove superfluous line breaks
subject = " ".join(subject.splitlines()).strip()
subject = self.format_email_subject(subject)
bodies = {}
for ext in ['html', 'txt']:
try:
template_name = '{0}_message.{1}'.format(template_prefix, ext)
bodies[ext] = render_to_string(template_name,
context).strip()
except TemplateDoesNotExist:
if ext == 'txt' and not bodies:
# We need at least one body
raise
if 'txt' in bodies:
msg = EmailMultiAlternatives(subject,
bodies['txt'],
settings.DEFAULT_FROM_EMAIL,
[email])
if 'html' in bodies:
msg.attach_alternative(bodies['html'], 'text/html')
else:
msg = EmailMessage(subject,
bodies['html'],
settings.DEFAULT_FROM_EMAIL,
[email])
msg.content_subtype = 'html' # Main content is now text/html
return msg
def send_mail(self, template_prefix, email, context):
msg = self.render_mail(template_prefix, email, context)
msg.send()'
found at (allauth/account/adapter.py)
The form always saves an invitation element into the database but breaks at the sending email line. (all infor stored is correct, so that isn't breaking it). If the email is removed, all code afterwards runs fine. I have even tried to just send a basic email like such in place:
from django.core.mail import EmailMessage
msg = EmailMessage("TEST", "HELLO", my_email, [some_email])
msg.send()
but this, too does not send emails.
I am hoping this is super simple, but any help would be appreciated.
I had the same problem, the execution just hung when running this code in a django shell (Django 1.7):
from django.core.mail import send_mail
send_mail('Subject here', 'Here is the message.', 'from#example.com',
['to#example.com'], fail_silently=False)
Following the Django docs on email settings, I used in settings.py:
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
EMAIL_PORT = 465
This worked.

How to create a django-allauth regular and social account for testing purposes?

I managed to create a regular user as happens when signing in with Django-allauth.
I've been trying to do the same for a social account (Github) but I am really struggling. I assume there must be people out here that had to make a social account for testing purposes. Could anyone show how they did that?
Also, if you know a better way to create a regular user this is highly appreciated.
The following snippet from the django-allauth tests shows how to do this:
from allauth.account import app_settings as account_settings
from allauth.account.models import EmailAddress
from allauth.account.utils import user_email
from allauth.socialaccount.helpers import complete_social_login
from allauth.socialaccount.models import SocialApp, SocialAccount, SocialLogin
from allauth.utils import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import User
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import Client
from django.test.client import RequestFactory
from django.test.utils import override_settings
class SocialAccountTests(TestCase):
#override_settings(
SOCIALACCOUNT_AUTO_SIGNUP=True,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.NONE # noqa
)
def test_email_address_created(self):
factory = RequestFactory()
request = factory.get('/accounts/login/callback/')
request.user = AnonymousUser()
SessionMiddleware().process_request(request)
MessageMiddleware().process_request(request)
User = get_user_model()
user = User()
setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, 'test')
setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, 'test#test.com')
account = SocialAccount(user=user, provider='openid', uid='123')
sociallogin = SocialLogin(account)
complete_social_login(request, sociallogin)
user = User.objects.get(
**{account_settings.USER_MODEL_USERNAME_FIELD: 'test'}
)
self.assertTrue(
SocialAccount.objects.filter(user=user, uid=account.uid).exists()
)
self.assertTrue(
EmailAddress.objects.filter(user=user,
email=user_email(user)).exists()
)