How do you authenticate a websocket with token authentication on django channels? - django-rest-framework

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.

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),
]),
),
})

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()),
]
)
)
)
})

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 the User model, 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.

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))

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_key in 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.

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),
])
),
})

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"]

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

Related

How to test fastapi with oracle, sql alchemy?

I have a fastapi application where I use sqlalchemy and stored procedures.
Now I want to test my endpoints like in the documentation
import pytest
from fastapi.testclient import TestClient
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from ..dependencies import get_db
import cx_Oracle
host = 'xxxx'
port = 1111
sid = 'FUU'
user = 'bar'
password = 'fuubar'
sid = cx_Oracle.makedsn(host, port, sid=sid)
database_url = 'oracle://{user}:{password}#{sid}'.format(
user=user,
password=password,
sid=sid,
)
engine = create_engine(database_url, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
app = FastAPI()
init_router(app)
#pytest.fixture()
def session():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
#pytest.fixture()
def client(session):
# Dependency override
def override_get_db():
try:
yield session
finally:
session.close()
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
def test_index(client):
res = client.get("/")
assert res.text
assert res.status_code == 200
def test_search_course_by_verid_exist():
response = client.get(
'search', params={"search_query": "1111", "semester": "S2022"})
# course exist
assert response.status_code == 200
I've tried it with creating a new app and/or importing it via getting the app from the main.py
from ..main import app
The method is in my courses router.
#router.get("/search", status_code=status.HTTP_200_OK)
async def search_course(
response: Response,
search_query: Union[str, None] = None,
semester: Union[int, None] = None,
db: Session = Depends(get_db),
):
.....
return response
The index test already failes by returning assert 400 == 200. For the 2nd (test_search_course_by_verid_exist) I'll get
AttributeError: 'function' object has no attribute 'get'
My main has some middleware settings like
app.add_middleware(
SessionMiddleware, secret_key="fastAPI"
) # , max_age=300 this should match Login action timeout in token-settings of a realm
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=settings.ALLOWED_HOSTS,
)
# MIDDLEWARE
#app.middleware("http")
async def check_route(request: Request, call_next):
....
I'm clueless what I'm missing or if things are just different with cx_Oracle
I've tried changing the testclient from fastapi to the starlette one. I've tried not overriding the db and just import the original db settings (which are basically the same). But nothing works.
I'm not sure if this is the proper way to test FastAPI application, https://fastapi.tiangolo.com/tutorial/testing/
Why you didn't declare client as :
client = TestClient(app)
?
Idk if this was the root problem. But naming my fixtures solved the problem and the db connection is working.
conftest.py
#pytest.fixture(name="db_session", scope="session")
def db_session(app: FastAPI) -> Generator[TestingSessionLocal, Any, None]:
Also created the app fixture
#pytest.fixture(name="app", scope="session")
def app() -> Generator[FastAPI, Any, None]:
"""
Create a fresh database on each test case.
"""
_app = start_application()
yield _app

How to connect VUE 3.x frontend websocket to DJANGO 4.x consumer using DJANGOCHANNELSRESTFRAMEWORK?

I have a DJANGO project with the following files.
Basically I am using token authentication in my settings which works well for javascript fetch/axios since I can send the token in the headers. I use djoser to manage the token authentication; however when I try to run any custom action in my consumer.py file the user in self.scope.get('user') is always AnonymousUser and then it returns with an 'Not allowed' message.
Do I need to authenticate the user over websocket protocol?
Also worth noting. I am using DJANGOCHANNELSRESTFRAMEWORK and using DEMULTIPLEXER to handle the websocket/consumer routing.
UPDATE: I tried using using the 'list' action in my json data and in consumers.py I user permissions.AllowAny and it works as expected; however I still cannot use any of my own custom actions.
UPDATE 2: When using permissions.AllowAny I can actually use all my custom actions as well; however I'm not sure if this is safe. I would like for my user to be authenticated to do this through websocket in this DJANGO decoupled project.
SETTINGS.PY
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-=1l1i2&3mw2!5^zhchewl#ziv1=ip0i$jc+8n7ikryjufwsh9e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['http://127.0.0.1:8000','127.0.0.1:8000','127.0.0.1','http://localhost:8080','ws://localhost:8080']
CORS_ALLOWED_ORIGINS = (
'http://127.0.0.1:8000',
'http://localhost:8080'
)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# 'rest_framework.authentication.BasicAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication'
],
'DEFAULT_PERMISSION_CLASSES':[
'rest_framework.permissions.IsAuthenticated'
]
}
CSRF_TRUSTED_ORIGINS=['http://127.0.0.1:8000','http://localhost:8080']
AUTH_USER_MODEL='users.User'
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'djoser',
'corsheaders',
'daphne',
'channels',
'users',
'students',
'instructors',
'groups',
'materials',
'operation',
'channels_demultiplexer',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
# 'middleware.websocket_auth.TokenAuthMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'InterAct.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR,'templates/'),
# os.path.join(BASE_DIR,'frontend/','dist/','templates/'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# WSGI_APPLICATION = 'InterAct.wsgi.application'
ASGI_APPLICATION = 'InterAct.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
MEDIA_URL='/media/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
# STATIC_ROOT= os.path.join(BASE_DIR,'publick/static/')
# STATICFILES_DIRS = [
# BASE_DIR / "static",
#
# ]
# CORS_ORIGIN_ALLOW_ALL = True
ASGI.PY
#ASGI config for InterAct project.
#It exposes the ASGI callable as a module-level variable named ``application``.
#For more information on this file, see
#https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
import os
from channels.auth import AuthMiddlewareStack
from middleware.websocket_auth import TokenAuthMiddleware
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
import operation.routing
from django.urls import re_path
from operation.consumers import DMultiplexer
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InterAct.settings')
django_application = get_asgi_application()
application = ProtocolTypeRouter(
{
"http": django_application,
"websocket": AllowedHostsOriginValidator(
# TokenAuthMiddleware(URLRouter([
AuthMiddlewareStack(URLRouter([
# operation.routing.websocket_urlpatterns,
# re_path(r"^/$", DMultiplexer.as_asgi()),
re_path(r"^ws/$", DMultiplexer.as_asgi()),
]))
),
}
)
CONSUMERS.PY
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from users.models import User
from users.serializers import UserSerializer
from channels.auth import login as channels_login
from django.contrib.auth import login
from instructors.models import Instructor
from instructors.serializers import InstructorSerializer
from groups.models import Group
from groups.serializers import GroupSerializer
from students.models import Student
from students.serializers import StudentSerializer
from djangochannelsrestframework.decorators import action
from djangochannelsrestframework.observer import model_observer
from djangochannelsrestframework import permissions
from djangochannelsrestframework.observer.generics import ObserverModelInstanceMixin
from djangochannelsrestframework.mixins import (
ListModelMixin,
RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
)
from channels.db import database_sync_to_async
import secrets
class Instructors_Consumer(
ListModelMixin,
# RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
ObserverModelInstanceMixin,
GenericAsyncAPIConsumer):
permission_classes = (permissions.AllowAny,)
queryset=Instructor.objects.all()
serializer_class=InstructorSerializer
#action()
async def subscribe_to_instructors_feed(self, request_id, **kwargs):
print('are we all getting called?')
await self.instructors_feed.subscribe(request_id=request_id)
#model_observer(Instructor,serializer_class=InstructorSerializer)
async def instructors_feed(self, data, action, subscribing_request_ids=[], **kwargs):
# print(data,action,subscribing_request_ids)
for request_id in subscribing_request_ids:
await self.reply(data=data, action=action, request_id=request_id)
#action()
async def create_model(self, *args, **kwargs):
data=kwargs.get('data')
object=await self.create_model_db(data)
return object,200
#database_sync_to_async
def create_model_db(self,data):
print(data)
email=f'{secrets.SystemRandom().randint(1,1000000)}#{secrets.SystemRandom().randint(1,1000000)}.com'
username=secrets.SystemRandom().randint(1,1000000)
new_user=User.objects.create(first_name='first_name',last_name='last_name',role="Instructor",email=email,username=username)
new_instructor=new_user.instructor_set.first()
new_instructor=InstructorSerializer(new_instructor).data
print(new_user)
print(new_instructor)
return new_instructor
class Groups_Consumer(
ListModelMixin,
# RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
ObserverModelInstanceMixin,
GenericAsyncAPIConsumer):
permission_classes = [permissions.AllowAny]
queryset=Group.objects.all()
serializer_class=GroupSerializer
#action(detail=False)
async def subscribe_to_groups_feed(self, request_id, **kwargs):
print('hello from another domain2222')
print(request_id)
print(dir(self))
# [print(thing) for thing in dir(self)]
print(self.scope.get('user'))
# login(request, user) # <- This was missing
await self.groups_feed.subscribe(request_id=request_id)
#model_observer(Group,serializer_class=GroupSerializer)
async def groups_feed(self, data, action, subscribing_request_ids=[], **kwargs):
for request_id in subscribing_request_ids:
await self.reply(data=data, action=action, request_id=request_id)
#action()
async def create_and_add_student(self, *args, **kwargs):
data=kwargs.get('data')
new_group=await self.create_and_add_student_db(data)
return new_group,200
#database_sync_to_async
def create_and_add_student_db(self,data):
pk=int(data.get('pk'))
group=Group.objects.get(pk=pk)
email=f'{secrets.SystemRandom().randint(1,1000000)}#{secrets.SystemRandom().randint(1,1000000)}.com'
username=secrets.SystemRandom().randint(1,1000000)
first_name=secrets.SystemRandom().randint(1,1000000)
new_user=User.objects.create(first_name=first_name,role="Student",email=email,username=username)
new_student=new_user.student_set.first()
group.student.add(new_student)
group.save()
new_student=StudentSerializer(new_student).data
response={
'new_student':new_student,
'parent':pk,
}
return response
class Students_Consumer(
ListModelMixin,
# RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
ObserverModelInstanceMixin,
GenericAsyncAPIConsumer):
permission_classes = (permissions.AllowAny,)
queryset=Student.objects.all()
serializer_class=StudentSerializer
#action()
async def subscribe_to_students_feed(self, request_id, **kwargs):
await self.students_feed.subscribe(request_id=request_id)
#model_observer(Student,serializer_class=StudentSerializer)
async def students_feed(self, data, action, subscribing_request_ids=[], **kwargs):
# print(data,action,subscribing_request_ids)
for request_id in subscribing_request_ids:
await self.reply(data=data, action=action, request_id=request_id)
class Users_Consumer(
ListModelMixin,
# RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
ObserverModelInstanceMixin,
GenericAsyncAPIConsumer):
queryset=User.objects.all()
serializer_class=UserSerializer
permission_classes = (permissions.AllowAny,)
#action()
async def subscribe_to_users_feed(self, request_id, **kwargs):
print('hello from another domain')
await self.users_feed.subscribe(request_id=request_id)
#model_observer(User,serializer_class=UserSerializer)
async def users_feed(self, data, action, subscribing_request_ids=[], **kwargs):
# print(data,action,subscribing_request_ids)
for request_id in subscribing_request_ids:
await self.reply(data=data, action=action, request_id=request_id)
from channels_demultiplexer.demultiplexer import WebsocketDemultiplexer
class DMultiplexer(WebsocketDemultiplexer):
consumer_classes={
"users":Users_Consumer,
"groups":Groups_Consumer,
"instructors":Instructors_Consumer,
"students":Students_Consumer,
}
in my VUEX STORE index.js I have this action:
async openEndpoint(state,params){
console.log(params);
let ws=params.ws
let endpoint=params.endpoint
ws.onopen=async ()=>{
console.log('connection established');
console.log(endpoint);
let data={
stream:endpoint,
payload:{
action:'subscribe_to_groups_feed',
request_id: new Date().getTime()
}
}
console.log(ws);
ws.onmessage=(ws_event)=>{
let response=JSON.parse(ws_event.data)
console.log(response);
console.log(response.payload.errors);
}
ws.send(JSON.stringify(data))
}
}
After hours of researching it turns out that I can write middleware to incercept the websocket requests. I had to pass the token as a query_string parameter and then decode it in the middleware so that I could then get the user from the Token.objects provided by Djoser.
However now all my normal fetch requests are failing. I provide the Middleware for anyone trying to advance in this topic.
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_token):
token = scope_token.decode('utf8')
token= token.split('=')[1]
print(token)
if not token:
return AnonymousUser()
try:
user = Token.objects.get(key=token).user
except Exception as exception:
return AnonymousUser()
if not user.is_active:
return AnonymousUser()
return user
class TokenAuthMiddleware(AuthMiddleware):
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope,receive,send,*args,**kwargs):
# 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).
print('MIDDLEWARING')
scope['user'] = await get_user(scope["query_string"])
return await self.app(scope,receive,send)
async def resolve_scope(self, scope,receive,send):
scope['user']._wrapped = await get_user(scope)
def TokenAuthMiddlewareStack(inner):
return CookieMiddleware(SessionMiddleware(TokenAuthMiddleware(inner)))
UPDATE:
On applying the above solution I stopped being able to get my axios/fetch requests to work. I suppose this has to do with the fact that I used the middleware to intercept websocket requests. When I undo all the middleware changes everything works normal. The 'trick' around this was to just disable the middleware in settings.py somehow this allows the axios/fetch requests to go through django application normally and still incercepts the websocket requests. This is how the settings look like now.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# 'middleware.websocket_auth.TokenAuthMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Http 403 Error: Details Request had insufficient authentification scopes Google Classroom Announcements

I am using Python Google Classroom API to retrieve announcements data.
Here is my code.
from fetch import Fetch
from googleapiclient.discovery import build
cred = 'catp.json'
get_credits = Fetch(cred) #fetching credential data
credit = get_credits()
service = build('Classroom', 'v1', credentials=credit)
setup = service.courses()
data = setup.list().execute()['courses']
course_names = []
course_ids = []
for i in range(len(data)):
course_names.append(data[i]['name'])
course_ids.append(data[i]['id'])
announcement_data = setup.announcements().list(courseId=course_ids[0]).execute()
But I receive the following Traceback Error:
Additional Information:
My project is registered under service account.
My role is Owner.
I have students account on Google Classroom.
To check whether the same error would be called if I tried to access announcements from a teachers account I created a Course in Classroom, using my Students account and posted some demo announcements.
The result was the same TracebackError. I also tried getting access to the data using API Explorer from Google, passing the same course ID as an argument. The data was received normally without any errors.
[Edit]
Here is the code for fetching credentials, Fetch(cred):
import os
import pickle
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
class Fetch:
def __init__ (self, credential_filename):
self.scopes = ['https://www.googleapis.com/auth/classroom.courses.readonly',
'https://www.googleapis.com/auth/classroom.announcements',
]
self.path = 'C:/frank/programs/python/google api'
self.credential_file = credential_filename
def __call__(self):
os.chdir(self.path)
token = open('token.pickle', 'rb')
creds = pickle.load(token)
if creds.valid == False:
if creds.expired == True:
creds.refresh(Request())
else:
try:
flow = InstalledAppFlow.from_client_secrets_file(self.credential_file, self.scopes)
creds = flow.run_local_server(port=0)
except FileNotFoundError:
print(f'{self.credential_file} does not exist')
token = open(self.token_file, 'wb')
pickle.dump(creds, token)
return creds

Authentication credentials were not provided - Django

I'm having trouble with this error. Tried almost all available solutions but nothing working for me. At frontend side I am using Angular 6 and I am pretty sure it's not error from it. Hoping for a response soon and thanks in advance guys.
register/url.py
from django.urls import path, include
from rest_framework import routers
from . import views
from rest_framework.authtoken.views import ObtainAuthToken
router = routers.DefaultRouter()
router.register('users', views.UserViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('', include(router.urls)),
#path('auth/', include('rest_framework.urls', namespace='rest_framework')),
path('auth/', ObtainAuthToken.as_view()),
]
serialier.py
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'password')
extra_kwargs = { 'password' : { 'write_only' : True , 'required':True } }
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
return user
view.py
from django.contrib.auth.models import User
from rest_framework import viewsets
from .serializers import UserSerializer
from rest_framework.permissions import IsAuthenticated
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
authentication_classes = (TokenAuthentication, SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
setting.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'register',
'corsheaders',
]
The following error is displayed in the browser's console:
{“detail”:“Authentication credentials were not provided.”}
Your viewset has the IsAuthenticated permission class. In other words, an user must be authenticated in order to retrieve, update or even create an instance. Make sure that the appropriate headers are included in your requests.
For example, for a token authentication, as stated by Django Rest Framework documentation
For clients to authenticate, the token key should be included in the
Authorization HTTP header. The key should be prefixed by the string literal
"Token", with whitespace separating the two strings. For example:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
In the particular case of an account creation, I'm not sure that it is intended for your application to require an user authentication.

Custom header is not added into request.META in Django Rest Framework

I implemented custom authentication, as described in docs
# custom_permissions.py
from rest_framework import authentication
from rest_framework import exceptions
class KeyAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
key = request.META.get('Authorization')
print(key)
if not key:
raise exceptions.AuthenticationFailed('Authentication failed.')
try:
key = ApiKey.objects.get(key=key)
except ApiKey.DoesNotExist:
raise exceptions.AuthenticationFailed('Authentication failed.')
return (key, None)
In my settings:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'api_server.apps.api_v1.custom_permissions.KeyAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
}
It works as expected during tests:
def test_1(self):
client = APIClient()
client.credentials(X_SECRET_KEY='INVALID_KEY')
response = client.get('/v1/resource/')
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data, {'detail': 'Authentication failed.'})
def test_2(self):
client = APIClient()
client.credentials(X_SECRET_KEY='FEJ5UI')
response = client.get('/v1/resource/')
self.assertEqual(response.status_code, 200)
However when I test with curl and locally running server, there is no X_SECRET_KEY header found in request.META. It is printing None in terminal, while received key is expected.
$ curl -X GET localhost:8080/v1/resource/ -H "X_SECRET_KEY=FEJ5UI"
{'detail': 'Authentication failed.'}
Could you give a hint, what might be a problem with that?
The headers variables are uppercase and prefixed with "HTTP_". This is general to Django, dunno about other languages / frameworks.
See https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/authentication.py#L23 for example.

Resources