I am trying to implement an Oauth2 authentication using django-oauth-toolkit
, and the key exchange works when I am using the built-in django server. However, when I am using gunicorn, I have an empty response. All the other endpoints work fine with gunicorn:
gunicorn command
gunicorn --bind 127.0.0.1:8000 api_name.wsgi
view.py
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
import requests
from .serializers import CreateUserSerializer
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
'''
Registers user to the server. Input should be in the format:
{"username": "username", "password": "1234abcd"}
'''
# Put the data from the request into the serializer
serializer = CreateUserSerializer(data=request.data)
# Validate the data
if serializer.is_valid():
# If it is valid, save the data (creates a user).
serializer.save()
# Then we get a token for the created user.
# This could be done differentley
r = requests.post('http://127.0.0.1:8000/o/token/', data={
'grant_type': 'password',
'username': request.data['username'],
'password': request.data['password'],
'client_id': get_client_id(),
'client_secret': get_client_secret(),
},
)
return Response(r.json())
return Response(serializer.errors)
@api_view(['POST'])
@permission_classes([AllowAny])
def token(request):
'''
Gets tokens with username and password. Input should be in the format:
{"username": "username", "password": "1234abcd"}
'''
r = requests.post('http://127.0.0.1:8000/o/token/', data={
'grant_type': 'password',
'username': request.data['username'],
'password': request.data['password'],
'client_id': get_client_id(),
'client_secret': get_client_secret(),
},
)
return Response(r.json())
@api_view(['POST'])
@permission_classes([AllowAny])
def refresh_token(request):
'''
Registers user to the server. Input should be in the format:
{"refresh_token": "<token>"}
'''
r = requests.post('http://127.0.0.1:8000/o/token/', data={
'grant_type': 'refresh_token',
'refresh_token': request.data['refresh_token'],
'client_id': get_client_id(),
'client_secret': get_client_secret(),
},
)
return Response(r.json())
@api_view(['POST'])
@permission_classes([AllowAny])
def revoke_token(request):
'''
Method to revoke tokens.
{"token": "<token>"}
'''
r = requests.post( 'http://127.0.0.1:8000/o/revoke_token/', data={
'token': request.data['token'],
'client_id': get_client_id(),
'client_secret': get_client_secret(),
},
)
# If it goes well return sucess message (would be empty otherwise)
if r.status_code == requests.codes.ok:
return Response({'message': 'token revoked'}, r.status_code)
# Return the error if it goes badly
return Response(r.json(), r.status_code)
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('register/', views.register),
path('token/', views.token),
path('token/refresh/', views.refresh_token),
path('token/revoke/', views.revoke_token),
]
when I launch the request:
curl -d "username=new_user&password=12345abcd" "127.0.0.1:8000/authentication/register/" -v
I have the responses:
with django manage.py runserver:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /authentication/register/ HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Length: 37
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 37 out of 37 bytes
< HTTP/1.1 200 OK
< Date: Tue, 31 Mar 2020 09:55:21 GMT
< Server: WSGIServer/0.2 CPython/3.6.2
< Content-Type: application/json
< Vary: Accept, Authorization, Cookie
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 160
<
* Connection #0 to host 127.0.0.1 left intact
{"access_token":"<access-token>","expires_in":36000,"token_type":"Bearer","scope":"read write","refresh_token":"<refresh-token>"}%
with gunicorn:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /authentication/register/ HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Length: 37
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 37 out of 37 bytes
* Empty reply from server
* Connection #0 to host 127.0.0.1 left intact
curl: (52) Empty reply from server
gunicorn log
[2020-03-31 12:11:03 +0200] [3639] [INFO] Starting gunicorn 20.0.4
[2020-03-31 12:11:03 +0200] [3639] [INFO] Listening at: http://127.0.0.1:8000 (3639)
[2020-03-31 12:11:03 +0200] [3639] [INFO] Using worker: sync
[2020-03-31 12:11:03 +0200] [3686] [INFO] Booting worker with pid: 3686
[2020-03-31 12:11:46 +0200] [3639] [CRITICAL] WORKER TIMEOUT (pid:3686)
Ah, I see what's happening.
You're using
gunicorn
with a single worker, and your view calls back to your own app ('http://127.0.0.1:8000/o/token/'
<-->--bind 127.0.0.1:8000
).While
runserver
is threaded by default, Gunicorn isn't, and while it's serving the request, you're doing another request within that request... Deadlock time!Either enable more workers for Gunicorn or refactor your app in a way that it doesn't need to make internal requests via HTTP.