How to write custom authentication of user, that connects to chat over ws:// protocol? This user is on the other side of Django app, he is the mobile user, connecting websocket via ws:// from mobile app. I tried to test websocket with chrome extenshion, it couldn't connect to my websocket. I think it was because of authentication.
In Django channels docs it says:
If you have a custom authentication scheme, you can write a custom middleware to parse the details and put a user object (or whatever other object you need) into your scope.Middleware is written as a callable that takes an ASGI application and wraps it to return another ASGI application. Most authentication can just be done on the scope, so all you need to do is override the initial constructor that takes a scope, rather than the event-running coroutine.
Here’s a simple example of a middleware that just takes a user ID out of the query string and uses that:
The same principles can be applied to authenticate over non-HTTP protocols; for example, you might want to use someone’s chat username from a chat protocol to turn it into a user.
from django.db import close_old_connections
class QueryAuthMiddleware:
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
# Look up user from query string (you should also do things like
# check it's a valid user ID, or if scope["user"] is already populated)
user = User.objects.get(id=int(scope["query_string"]))
close_old_connections()
# Return the inner application directly and let it run everything else
return self.inner(dict(scope, user=user))
What query do I need to do? I don't know anything about that user, in fact its anonymous.
Help me, please.
In this example code, you probably have to open the websocket connection with:
ws://SERVER:PORT/PATH?1
Everything after the ? is the query string. In your example code, your query_string has to be a user id, so for example 1.
You could change the code to use different query strings. For example you could use:
from urllib.parse import parse_qs
from django.db import close_old_connections
class QueryAuthMiddleware:
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
# Look up user from query string (you should also do things like
# check it's a valid user ID, or if scope["user"] is already populated)
query_string = parse_qs(self.scope['query_string'])
if b'user_id' in query_string:
user = User.objects.get(id=int(query_string[b'user_id'][0]))
close_old_connections()
else:
user = AnonymousUser
# Return the inner application directly and let it run everything else
return self.inner(dict(scope, user=user))
Now, you can use this uri:
ws://SERVER:PORT/PATH?user_id=1
You sill have to make sure, that the user with the ID exists in the database. You also have to write actual auth code. Every user can connect to this application with a arbitrary user id. There is no password or auth-token required.
I had the same issue as well, did research a bit came across solution with the code snippet below:
let's say you have User class defined. You want authenticate user through query send query while establishing connection with ws.
I will pass channels installation and configuration let's say you have successfully installed channels and configured.
QueryAuthMiddleware class as shown below:
from channels.auth import AuthMiddlewareStack
from django.conf import LazySettings
from urllib import parse
from rest_socket.models import User
from urllib.parse import parse_qs
from urllib.parse import unquote, urlparse
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
class QueryAuthMiddleware:
"""
QueryAuthMiddleware authorization
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
query_string = parse_qs(scope['query_string']) #Used for query string token url auth
headers = dict(scope['headers']) #Used for headers token url auth
print("query", query_string)
if b'user' in query_string:
try:
user = query_string[b'user'][0].decode()
print("user", user)
existing = User.objects.filter(id=user).last()
print(existing)
if existing:
print("existinguser")
scope['user'] = existing
else:
scope["user"] ="no user found"
except User.DoesNotExist:
pass
return self.inner(scope)
QueryAuthMiddlewareStack = lambda inner: QueryAuthMiddleware(AuthMiddlewareStack(inner))
Your routing.py should be like this:
from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import OriginValidator
from django.urls import path
import your_app.routing
from project.utils.auth import QueryAuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': QueryAuthMiddlewareStack(
URLRouter(
your_app.routing.websocket_urlpatterns
)
),
})
Routing inside application:
websocket_urlpatterns = [
path("tasks/", consumers.Task)
]
and your ws connection to django-channels:
<script>
var notification;
if (location.protocol === 'https:') {
notification = new WebSocket('wss://' + "window.location.host" + "/tasks"+ "/?user=id");
console.log("with htpps")
}
else {
notification = new WebSocket('ws://' + window.location.host + "/tasks"+ "/?userr=id");
console.log("htpp")
}
notification.onopen = function open() {
console.log('notification connection created for NotificationWebsocket.');
};
notification.onmessage = function message(event) {
var data = JSON.parse(event.data);
console.log("Socket response from NotificationWebsocket => ", data);
};
if (notification.readyState === WebSocket.OPEN) {
notification.onopen();
}
</script>
Related
I'm pretty new to this, so I will show you the error first and then display my thought process.
I am attempting to use a thunk (Http request in redux) to make a post request to my personal rest api in django. (so it can run cURL commands and use a authorization code to gain an access token and get user data from an API)
when I make this request, the network returns a 201 status to stay it worked, while my console logs the dispatch as rejected, and gives the error, "message: "Unexpected end of JSON input". In this message, payload is undefined, and my payload is living in meta under arg.
After much reserach, it looks like I either need to JSON.strigify something on my front end, OR that django is receiving a null object and not knowing what to do with it. I believe it is the latter, and not sure how to proceed. Here is my code:
FRONTEND:
slice.py
import {createAsyncThunk} from '#reduxjs/toolkit'
export const postRequest= createAsyncThunk('slice/postRequest',
async postData => {
const response=
await client.post('url', {slice:postData})
return response.slice
}
)
component.py
import {useDispatch} from 'react-redux'
import {postRequest} from './slice'
const Component = () => {
const dispatch=useDispatch()
const authCode= 'samplecode'
const data={authCode:`${authCode}`, userData:{}}
dispatch(postRequest(JSON.stringify(data))
return(null)
}
export default Component
BACKEND:
models.py
from django.db import models
class UserData(models.Model):
authCode=models.CharField(max_length=100, default='')
userData= models.JSONField
serializers.py
from rest_framework import serializers
from .models import *
class UserDataSerializer(serializers.ModelSerializer):
class meta:
model=UserData
fields= ('authCode', 'userData')
views.py
from .models import *
from .serializers import *
from django.views.decorators.csrf import csrf_excempt
from rest_framework.decorators import api.view
from rest_framework.resposne import Response
from rest_framework import status
#api_view(['POST'])
#csrf_excempt
define restapi(request)
if request.method == "POST":
serializer = UserDataSerializer(data = request.data
if serializer.is_valid():
serializer.save
return Response(status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_Request
I also tried it where it would return a status of 204, and without JSON.stringify.
I noticed that when I made a successful post request to a json server, it would make the request with the payload in args, and if it succeeded, the payload would get moved to payload, but it is my guess that my current error is happening because the payload is blank at the start of the thunk dispatch. Really I am not sure though
I solved this problem, it was stupid.
I was using a homemade client and I am not experienced enough to do that. I just got axios and used that as the client and it worked fine. I also simplified the request in the component to not have JSON.stringify and not have the backticks.
I want to allow the django users to use a key in the api urls for authentication.
I do not have OAUTH set up yet so I guess the key could be a sesion key or a digest key.
I'm having 2 problems.
I've tried sending this request:
http://192.166.166.11:8000/api?task=search&term=115&csrf_token=s69WAIZqlCTur1XZQr72QhCc7fzqaRtM
First of all, I've tried using the csrf_token but it does not work.
It takes me to the login page.
Secondly, I do not know how to retrieve csrf_token of other users (the admin is trying to get their csrf_tokens).
My attempt:
x = User.objects.get(username='someone')
x.get_session_auth_hash()
gives me the user's authentication hash but it is a different value.
Can someone please guide me get these two problems sorted out?
You are creating a token-based authentication. You already mentioned OAUTH as one option, and I strongly recommend using one of the existing implementations like django-oauth-toolkit. However, you can also create your own quick solution to create a token-based authentication.
Disclaimer: This is for demo purposes only. Do not copy it in any existing project. It will make your application vulnerable.
First, we create an additional model handling the authentication tokens:
/auth_tokens/models.py
from django.db import models
from django.conf import settings
import string, random
def random_string(length = 64, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for x in range(length))
class AuthToken(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
token = models.CharField(max_length=64, default=random_string)
/auth_tokens/middleware.py
from auth_tokens.models import AuthToken
class AuthTokenMiddleware:
def process_request(self, request):
token = request.GET.get('auth', None)
if not token:
return
token = AuthToken.objects.get(token=token)
request.user = token.user
return request
Including the middleware into your settings.MIDDLEWARE_CLASSES should enable you to add ?token=<token> to your URL to login your users.
I ended up using token authentication:
http://www.django-rest-framework.org/api-guide/authentication/
so I'd like to share the workflow.
First, you need to do the set up. In settings.py, modify INSTALLED_APPS and add REST_FRAMEWORK as in documentation.
Then you need to run python manage.py syncdb because it needs to add some tables.
Then, you need to add some urls to urls.py to route the api.
You can create and retrieve tokens using this code:
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=User.objects.get(username='john'))
print token.key
Lastly, you'll have to modify your view which depends on whether you're using a function based or class based view.
Here is a function based view I used:
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import authentication_classes, permission_classes
from rest_framework.decorators import api_view
#api_view(['GET', 'POST'])
#authentication_classes((TokenAuthentication,))
#permission_classes((IsAuthenticated,))
#login_required
def mybooks(request):
entries = Book.objects.all()
return render(request, 'mybooks.html', {'entries': entries})
Lastly, to test it out:
import requests
token = '243124c52f7583e320d043c4395bd99f63344035'
headers = {'Authorization' : 'Token {}'.format(token)}
page = requests.post('http://127.0.0.1:8000/mybooks/', headers=headers)
print page.content
Note that in my case I do not need define plain serialization since I have an advanced custom serialization and that is not the topic here.
Django doesn't provide API Keys out of the box.
Use API providers such as Tastypie to have this feature
I am a newbie at Django. Using django-allauth I have set up single click sign in. I obtained my domain credentials ( client_id and secret_key) from google api console. But the problem is django-allauth is letting me login from any google account while I want the email addresses to be restricted to my domain ( #example.com instead of #gmail.com)
django-social-auth has the white listed domains parameter for this, how do I include this information in allauth?
I found django-allauth much easier to set up after spending hours on django-social-auth
Any help would be much appreciated.
Answering my own question-
What you want to do is stall the login after a user has been authenticated by a social account provider and before they can proceed to their profile page. You can do this with the
pre_social_login method of the DefaultSocialAccountAdapter class in allauth/socialaccount/adaptor.py
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
You can use this hook to intervene, e.g. abort the login by
raising an ImmediateHttpResponse
Why both an adapter hook and the signal? Intervening in
e.g. the flow from within a signal handler is bad -- multiple
handlers may be active and are executed in undetermined order.
Do something like
from allauth.socialaccount.adaptor import DefaultSocialAccountAdapter
class MySocialAccount(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
u = sociallogin.account.user
if not u.email.split('#')[1] == "example.com"
raise ImmediateHttpResponse(render_to_response('error.html'))
This is not an exact implementation but something like this works.
Here's an alternate solution:
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False # No email/password signups allowed
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
u = sociallogin.user
# Optionally, set as staff now as well.
# This is useful if you are using this for the Django Admin login.
# Be careful with the staff setting, as some providers don't verify
# email address, so that could be considered a security flaw.
#u.is_staff = u.email.split('#')[1] == "customdomain.com"
return u.email.split('#')[1] == "customdomain.com"
This code can live anywhere, but assuming it's in mysite/adapters.py, you'll also need the following in your settings.py:
ACCOUNT_ADAPTER = 'mysite.adapters.CustomAccountAdapter'
SOCIALACCOUNT_ADAPTER = 'mysite.adapters.CustomSocialAccountAdapter'
You could do something in the line of overriding allauth's allauth.socialaccount.forms.SignupForm and checking the domain during the signup process.
Discalmer: this is all written without testing, but something in the line of that should work.
# settings.py
# not necesarry, but it would be a smart way to go instead of hardcoding it
ALLOWED_DOMAIN = 'example.com'
.
# forms.py
from django.conf import settings
from allauth.socialaccount.forms import SignupForm
class MySignupForm(SignupForm):
def clean_email(self):
data = self.cleaned_data['email']
if data.split('#')[1].lower() == settings.ALLOWED_DOMAIN:
raise forms.ValidationError(_(u'domena!'))
return data
in your urls override allauth defaults (put this before the include of django-allauth)
# urls.py
from allauth.socialaccount.views import SignupView
from .forms import MySignupForm
urlpatterns = patterns('',
# ...
url(r"^social/signup/$", SignupView.as_view(form_class=MySignupForm), name="account_signup"),
# ...
)
I'm not sure for the "^social/signup/$", recheck that.
I am trying to write chat application, here are some desing thoughts:
Django doing main website serving.
Sockjs-tornado serves chat and between them I would like to setup redis. When sockjs-tornado receives message in chat, it processes it and sends to other connected clients and also puts it in redis, so Django can save this message in persistent database. I know I should use pubsub functionallity of redis. I know how to setup it in tornado (brukva library), but how can I subscribe to redis' channel in django? So I can receive message in django and save it to database? Do you have any ideas?
i'm not know how sockjs use, but this example illustrate how to save in django model
#in tornado
import brukva
import tornado.web
import tornado.websocket
c = brukva.Client()
c.connect()
class MessagesHandler(tornado.websoket.WebsocketHandler):
def open(self):
#....
def handle_request(self, response):
pass
def on_message(self, message):
#....
c.publish(self.channel, json.dumps({
"sender": self.sender_name,
"text": message,
}))
http_client = tornado.httpclient.AsyncHTTPClient()
request = tornado.httpclient.HTTPRequest(
'/to/django_project/url/chat_save_base/',
method="POST",
body=urllib.urlencode({
"message": message.encode("utf-8"),
"sender": self.sender.name,
})
http_client.fetch(request, self.handle_request)
#in django_url
url(r'/to/django_project/url/chat_save_base/','app.my_view')
#my_view
from django.views.decorators.csrf import csrf_exempt
from messages.models import Message
#csrf_exempt
def my_view(request):
message_text = request.POST.get("message")
sender = User.objects.get(id=request.POST.get("sender_id"))
message = Message()
message.text = message_text
message.sender_id = sender_id
message.save()
source for additional info:
http://habrahabr.ru/post/160123/
I'm trying to write a functional test that uses Selenium to test a Django view. When the user comes to a page ("page2"), the view that renders that page expects to find a session variable "uid" (user ID). I've read a half dozen articles on how this is supposed to be done but none of them have worked for me. The code below shows how the Django documentation says it should be done but it doesn't work for me either. When I run the test, the view never completes executing and I get a "server error occurred" message. Could someone please tell me what I'm doing wrong? Thank you.
views.py:
from django.shortcuts import render_to_response
def page2(request):
uid = request.session['uid']
return render_to_response('session_tests/page2.html', {'uid': uid})
test.py:
from django.test import LiveServerTestCase
from selenium import webdriver
from django.test.client import Client
class SessionTest(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
self.client = Client()
self.session = self.client.session
self.session['uid'] = 1
def tearDown(self):
self.browser.implicitly_wait(3)
self.browser.quit()
def test_session(self):
self.browser.get(self.live_server_url + '/session_tests/page2/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Page 2', body.text)
Here's how to solve this problem. James Aylett hinted at the solution when he mentioned the session ID above. jscn showed how to set up a session but he didn't mention the importance of the session key to a solution and he also didn't discuss how to link the session state to Selenium's browser object.
First, you have to understand that when you create a session key/value pair (e.g. 'uid'=1), Django's middleware will create a session key/data/expiration date record in your backend of choice (database, file, etc.). The response object will then send that session key in a cookie back to the client's browser. When the browser sends a subsequent request, it will send a cookie back that contains that key which is then used by the middleware to lookup the user's session items.
Thus, the solution required 1.) finding a way to obtain the session key that is generated when you create a session item and then; 2.) finding a way to pass that key back in a cookie via Selenium's Firefox webdriver browser object. Here's the code that does that:
selenium_test.py:
-----------------
from django.conf import settings
from django.test import LiveServerTestCase
from selenium import webdriver
from django.test.client import Client
import pdb
def create_session_store():
""" Creates a session storage object. """
from django.utils.importlib import import_module
engine = import_module(settings.SESSION_ENGINE)
# Implement a database session store object that will contain the session key.
store = engine.SessionStore()
store.save()
return store
class SeleniumTestCase(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
self.client = Client()
def tearDown(self):
self.browser.implicitly_wait(3)
self.browser.quit()
def test_welcome_page(self):
#pdb.set_trace()
# Create a session storage object.
session_store = create_session_store()
# In pdb, you can do 'session_store.session_key' to view the session key just created.
# Create a session object from the session store object.
session_items = session_store
# Add a session key/value pair.
session_items['uid'] = 1
session_items.save()
# Go to the correct domain.
self.browser.get(self.live_server_url)
# Add the session key to the cookie that will be sent back to the server.
self.browser.add_cookie({'name': settings.SESSION_COOKIE_NAME, 'value': session_store.session_key})
# In pdb, do 'self.browser.get_cookies() to verify that it's there.'
# The client sends a request to the view that's expecting the session item.
self.browser.get(self.live_server_url + '/signup/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Welcome', body.text)
There are a couple of tickets in Django's bug tracker around this kind of problem, the main one seems to be: https://code.djangoproject.com/ticket/10899 which hasn't had any movement on it for a few months. Basically, you need to do some extra set up to get the session to work properly. Here's what worked for me (may not work as is with your particular set up, as I wasn't using Selenium):
def setUp(self):
from django.conf import settings
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
store.save()
self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
Now you should be able to access self.client.session and it should remember any changes you make to it.
Here is my solution for django==2.2.
from importlib import import_module
from django.conf import settings
from django.contrib.auth import get_user_model
# create the session database instance
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore()
# create the user and instantly login
User = get_user_model()
temp_user = User.objects.create(username='admin')
temp_user.set_password('password')
self.client.login(username='admin', password='password')
# get session object and insert data
session = self.client.session
session[key] = value
session.save()
# update selenium instance with sessionID
selenium.add_cookie({'name': 'sessionid', 'value': session._SessionBase__session_key,
'secure': False, 'path': '/'})