django-rest-framework makes use of django.contrib.auth for authentication and authorization (as stated in the django-rest-framework authentication api guide)
However, no-where in the documentation does it talk about how users are actually authenticated using the rest-framework
By default the django.contrib.auth views will respond with a server-side rendered login form.
However, if using a client-side framework such as AngularJs this is not desired - you simply want an api endpoint against which you can authenticate.
Questions:
Is there django-rest-framework documentation I am somehow missing which explains how user authentication is done-out-of-the-box?
Does an out-of-the-box solution even exist?
If not, what is the recommended way of achieving this with minimal reinvention of the wheel?
lets say that you have login view:
Note: with this method you have to assure SSL/TLS because username and password are sending as plain text.
import json
import requests
def login(request):
if request.method == "POST":
username = request.POST['username']
password = request.POST['password']
login_url = 'http://your_url:port/rest-api/login/'
response = requests.post(login_url, data={'username': username, 'password': password})
response = json.loads(response.text)
if response.status_code == 200:
return render_to_response("login.html", {"success": True}, RequestContext(request))
your view in rest-api:
from django.contrib.auth.backends import ModelBackend as DjangoModelBackend
def login(request):
response = base_response.copy()
username = request.DATA.get('username', '')
password = request.DATA.get('password', '')
user = DjangoModelBackend().authenticate(username=email, password=password)
if user is not None:
response["message"] = "Authenticated"
else:
response["message"] = "Login Failed"
return Response(response)
and here is the part of ModelBackend
from django.contrib.auth import get_user_model
class ModelBackend(object):
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
if user.check_password(password):
return user
except UserModel.DoesNotExist:
return None
You don't usually go through login forms when authenticating yourself at an API endpoint - you either use an API token or send the authentication credentials through a header, see How to use Basic Auth with jQuery and AJAX? on how to do that.
Related
My setup: Django-3.0, Python-3.8, django_auth_ldap
I have LDAP Server (Active Directory Server) in my organization.
I am building a Django Application which serves some operations for all the users.
I know Django has built-in User Authentication mechanism, But it authenticates if users are present in User Model Database.
But my requirement is.
All user entries are in LDAP Server(Active Directory). Using proper user credentials LDAP server authenticates me.
I Created a Login Page in Django 'accounts' app,
1. whenever I enter username and password from Login Page, it should Authenticate using My Organization LDAP Server.
2. After Login I have to hold the session for the logged in user for 5 active minutes. (Django auth session)
I saw django_auth_ldap package gives some insight for my purpose.
I have these contents in settings.py.
import ldap
##Ldap settings
AUTH_LDAP_SERVER_URI = "ldap://myldapserver.com"
AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS : 0}
AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s, OU=USERS,dc=myldapserver, dc=com"
AUTH_LDAP_START_TLS = True
#Register authentication backend
AUTHENTICATION_BACKENDS = [
"django_auth_ldap.backend.LDAPBackend",
]
Calling authenticate in views.py.
from django_auth_ldap.backend import LDAPBackend
def accounts_login(request):
username = ""
password = ""
if request.method == "POST":
username = request.POST.get('username')
password = request.POST.get('password')
auth = LDAPBackend()
user = auth.authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect("/")
else:
error = "Authentication Failed"
return render(request, "accounts/login.html", 'error':error)
return render(request, "accounts/login.html")
But using above method always authenticate fails with the LDAP Server.
If I call using normal python simple_bind_s(), authentication is working fine to same LDAP server.
import ldap
def ldap_auth(username, password):
conn = ldap.initialize(myproj.settings.LDAP_AUTH_URI)
try:
ldap.set_option(ldap.OPT_REFERRALS, 0)
#ldap.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
conn.simple_bind_s(username, password)
except ldap.LDAPError as e:
return f'failed to authenticate'
conn.unbind_s()
return "Success"
Can anybody suggest me to make LDAPBackend Authentication work as per my requirement ?
Note: I do not have admin permission of LDAP Server.
This is how I would do it with ldap3 and without django_auth_ldap packages.
1 - Create a custom AuthenticationBackend in your_app/backends.py :
import logging
from ldap3 import Server, Connection
from ldap3.core.exceptions import LDAPBindError
from django.conf import settings
from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
UserModel = get_user_model()
class LDAPBackend:
def authenticate(self, request, username=None, password=None, **kwargs):
# set username to lowercase for consistency
username = username.lower()
# get the bind client to resolve DN
logger.info('authenticating %s' % username)
# set your server
server = Server(settings.LDAP_HOST, get_info=ALL)
try:
conn = Connection(server, f"{username}#{settings.LDAP_DOMAIN}", password=password, auto_bind=True)
except LDAPBindError as e:
logger.info('LDAP authentication failed')
logger.info(e)
return None
user = UserModel.objects.update_or_create(username=username)
return user
def get_user(self, user_id):
try:
return UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
2 - Declare LDAPBackend as your authentication backend in settings.py
AUTHENTICATION_BACKENDS = [
'your_app.backends.LDAPBackend',
'django.contrib.auth.backends.ModelBackend'
]
This allows you to use django's native function for authentication, and it work with the admin.
To have session only work for 5 minutes, add this setting to your settings.py file :
SESSION_COOKIE_AGE = 5 * 60
Let me know if it works for your.
I'm using Django JWT authentication with the Django Rest Framework.
How can I get user info of the logged in user after I retrieve the token?
just check your app settings file, whether you have specified the jwt authentication backend or not.
if it mentioned there and if you are using User model ( in otherwords django.contrib.auth.models.User) request.user will work
If you are using your own custom User model
from django.conf import settings
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework.authentication import get_authorization_header
import CustomUser # just import your model here
import jwt
class JWTAuthentication(authentication.BaseAuthentication):
def authenticate(self, request): # it will return user object
try:
token = get_authorization_header(request).decode('utf-8')
if token is None or token == "null" or token.strip() == "":
raise exceptions.AuthenticationFailed('Authorization Header or Token is missing on Request Headers')
print(token)
decoded = jwt.decode(token, settings.SECRET_KEY)
username = decoded['username']
user_obj = CustomUser.objects.get(username=username)
except jwt.ExpiredSignature :
raise exceptions.AuthenticationFailed('Token Expired, Please Login')
except jwt.DecodeError :
raise exceptions.AuthenticationFailed('Token Modified by thirdparty')
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed('Invalid Token')
except Exception as e:
raise exceptions.AuthenticationFailed(e)
return (user_obj, None)
def get_user(self, userid):
try:
return CustomUser.objects.get(pk=userid)
except Exception as e:
return None
and add the following settings in your app
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'path_to_custom_authentication_backend',
....
)
}
now in each view/viewset you can access the user object with request.user
By reading the documentation on DRF Authentication and as #neverwalkaloner mentions in his comment, we see that we can access the logged-in user's django.contrib.auth.User instance in a view, by using the request.user attribute.
Reading the documentations of both the recommended JWT modules for DRF:
https://github.com/GetBlimp/django-rest-framework-jwt
https://github.com/davesque/django-rest-framework-simplejwt
I didn't find any evidence that they change/override the method of accesing the logged in user's instance info.
If you are familiar with django rest jwt, you may see a config like this in your settings.py:
JWT_AUTH = {
.....
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
'JWT_SECRET_KEY': SECRET_KEY,
....
}
You can simply create a method for example my_custom_jwt_response_payload_handler like below and address JWT_RESPONSE_PAYLOAD_HANDLER to new handler:
def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': {
'username': user.username, 'id': user.id,
...
}
}
You can add any data that you want in this response. then patch settings.py with your new handler:
JWT_AUTH = {
.....
'JWT_RESPONSE_PAYLOAD_HANDLER':
'localtion-to-my-own-handler-file.my_custom_jwt_response_payload_handler',
....
}
For better understanding i suggest read original source and comments for jwt_response_payload_handler in here
Once you logged in, means you are authenticated by Django, now you can retrieve current user details anywhere in the code.
request.user
request contains all the details of the User Model once the user gets authenticated. otherwise, it will show the Anonymus user.
I am using django-rest-jwt for authentication in my app.
By default it user username field to autenticate a user but I want let the users login using email or username.
Is there any mean supported by django-rest-jwt to accomplish this.
I know the last option would be write my own login method.
No need to write a custom authentication backend or custom login method.
A Custom Serializer inheriting JSONWebTokenSerializer, renaming the 'username_field' and overriding def validate() method.
This works perfectly for 'username_or_email' and 'password' fields where the user can enter its username or email and get the JSONWebToken for correct credentials.
from rest_framework_jwt.serializers import JSONWebTokenSerializer
from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
class CustomJWTSerializer(JSONWebTokenSerializer):
username_field = 'username_or_email'
def validate(self, attrs):
password = attrs.get("password")
user_obj = User.objects.filter(email=attrs.get("username_or_email")).first() or User.objects.filter(username=attrs.get("username_or_email")).first()
if user_obj is not None:
credentials = {
'username':user_obj.username,
'password': password
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
else:
msg = _('Account with this email/username does not exists')
raise serializers.ValidationError(msg)
In urls.py:
url(r'{Your url name}$', ObtainJSONWebToken.as_view(serializer_class=CustomJWTSerializer)),
Building on top of Shikhar's answer and for anyone coming here looking for a solution for rest_framework_simplejwt (since django-rest-framework-jwt seems to be dead, it's last commit was 2 years ago) like me, here's a general solution that tries to alter as little as possible the original validation from TokenObtainPairSerializer:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomJWTSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
credentials = {
'username': '',
'password': attrs.get("password")
}
# This is answering the original question, but do whatever you need here.
# For example in my case I had to check a different model that stores more user info
# But in the end, you should obtain the username to continue.
user_obj = User.objects.filter(email=attrs.get("username")).first() or User.objects.filter(username=attrs.get("username")).first()
if user_obj:
credentials['username'] = user_obj.username
return super().validate(credentials)
And in urls.py:
url(r'^token/$', TokenObtainPairView.as_view(serializer_class=CustomJWTSerializer)),
Found out a workaround.
#permission_classes((permissions.AllowAny,))
def signin_jwt_wrapped(request, *args, **kwargs):
request_data = request.data
host = request.get_host()
username_or_email = request_data['username']
if isEmail(username_or_email):
# get the username for this email by model lookup
username = Profile.get_username_from_email(username_or_email)
if username is None:
response_text = {"non_field_errors":["Unable to login with provided credentials."]}
return JSONResponse(response_text, status=status.HTTP_400_BAD_REQUEST)
else:
username = username_or_email
data = {'username': username, 'password':request_data['password']}
headers = {'content-type': 'application/json'}
url = 'http://' + host + '/user/signin_jwt/'
response = requests.post(url,data=dumps(data), headers=headers)
return JSONResponse(loads(response.text), status=response.status_code)
I check that whether the text that I received is a username or an email.
If email then I lookup the username for that and then just pass that to /signin_jwt/
authentication.py
from django.contrib.auth.models import User
class CustomAuthBackend(object):
"""
This class does the athentication-
using the user's email address.
"""
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
settings.py
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'app_name.authentication.CustomAuthBackend',
]
How it works:
If user try to authenticate using their username django will look at the ModelBackend class. However, if the user adds its email instead, django will try ModelBackend but will not find the logic needed, then will try the CustomAuthBackend class making it work the authentication.
Alternatively, this new DRF Auth project dj-rest-auth seems to provide support for log in by username or email through djangorestframework-simplejwt.
dj-rest-auth works better for authentication and authorization. By default dj-rest-auth provides - username, email and password fields for login. User can provide email and password or username and password. Token will be generated, if the provided values are valid.
If you need to edit these login form, extend LoginSerializer and modify fields. Later make sure to add new custom serializer to settings.py.
REST_AUTH_SERIALIZERS = {
'LOGIN_SERIALIZER': 'yourapp.customlogin_serializers.CustomLoginSerializer'
}
Configuring dj-rest-auth is bit tricky, since it has an open issue related to the refresh token pending. There is workaround suggested for that issue, so you can follow below links and have it configured.
https://medium.com/geekculture/jwt-authentication-in-django-part-1-implementing-the-backend-b7c58ab9431b
https://github.com/iMerica/dj-rest-auth/issues/97
If you use the rest_framework_simplejwt this is a simple mode. views.py
from rest_framework_simplejwt.tokens import RefreshToken
from django.http import JsonResponse
from rest_framework import generics
class EmailAuthToken(generics.GenericAPIView):
def post(self, request):
user_data = request.data
try:
user = authenticate(request, username=user_data['username_or_email'], password=user_data['password'])
if user is not None:
login(request, user)
refresh = RefreshToken.for_user(user)
return JsonResponse({
'refresh': str(refresh),
'access': str(refresh.access_token),
}, safe=False, status=status.HTTP_200_OK)
else:
return JsonResponse({
"detail": "No active account found with the given credentials"
}, safe=False, status=status.HTTP_200_OK)
except:
return Response({'error': 'The format of the information is not valid'}, status=status.HTTP_401_UNAUTHORIZED)
I want to use Tastypie's ApiKeyAuthentication to authenticate a request and then establish a session for the user within a Django view. I have username and api_key for the user. I do not have the user's password. This is the code I currently have:
class ApiKeyPlusWebAuthentication(ApiKeyAuthentication):
def is_authenticated(self, request, **kwargs):
isAuthenticated = super(ApiKeyPlusWebAuthentication, self).is_authenticated(request, **kwargs)
if isAuthenticated:
print request.user.email
return isAuthenticated
#login for access from UIWebView
def login_usingApiKeyAuthentication(request):
auth = ApiKeyPlusWebAuthentication(request)
if auth.is_authenticated(request):
print 'authenticated'
login(request, request.user)
return redirect(reverse(view_name))
else:
print 'NOT authenticated'
messages.error(request, MESSAGE_INVALID_LOGIN)
fail_redirect = redirect(reverse('login'))
return fail_redirect
I am getting an error 'User' object has no attribute 'backend'. This is because I haven't called authenticate(user, password). I am using the Django default authentication backend.
In this scenario, I only have APIKey associated with the user and don't have the raw password for authentication.
One way to handle this may be to create custom authentication backend that bypasses password requirement. However, registering a "password-less" authentication backend in settings.py seems like a hack prone to security breakdown.
So, how can I use ApiKeyAuthentication and then authenticate & login the user in Django establishing a session?
I found a solution to set the backend in another post. You can set the custom backend directly on the user object.
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class PasswordlessAuthBackend(ModelBackend):
"""Log in to Django without providing a password.
"""
def authenticate(self, username=None):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
#csrf_exempt
def login_uiwebview(request):
auth = ApiKeyPlusWebAuthentication(request)
if auth.is_authenticated(request):
view_name = request.POST.get('view_name')
request.user.backend = 'app.views.PasswordlessAuthBackend'
login(request, request.user)
return redirect(view_name)
else:
print 'NOT authenticated'
messages.error(request, MESSAGE_INVALID_LOGIN)
fail_redirect = redirect(reverse('login'))
return fail_redirect
I'm using the default authentication system with django, but I've added on an OpenID library, where I can authenticate users via OpenID. What I'd like to do is log them in, but it seems using the default django auth system, I need their password to authenticate the user. Is there a way to get around this without actually using their password?
I'd like to do something like this...
user = ... # queried the user based on the OpenID response
user = authenticate(user) # function actually requires a username and password
login(user)
I sooner just leave off the authenticate function, but it attaches a backend field, which is required by login.
It's straightforward to write a custom authentication backend for this. If you create yourapp/auth_backend.py with the following contents:
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class PasswordlessAuthBackend(ModelBackend):
"""Log in to Django without providing a password.
"""
def authenticate(self, username=None):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Then add to your settings.py:
AUTHENTICATION_BACKENDS = (
# ... your other backends
'yourapp.auth_backend.PasswordlessAuthBackend',
)
In your view, you can now call authenticate without a password:
user = authenticate(username=user.username)
login(request, user)
This is a bit of a hack but if you don't want to rewrite a bunch of stuff remove the authenticate
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
user would be your User object
In order to do authenticate without password, in your settings.py:
AUTHENTICATION_BACKENDS = [
# auth_backend.py implementing Class YourAuth inside yourapp folder
'yourapp.auth_backend.YourAuth',
# Default authentication of Django
'django.contrib.auth.backends.ModelBackend',
]
In your auth_backend.py:
NOTE: If you have custom model for your app then import from .models CustomUser
from .models import User
from django.conf import settings
# requires to define two functions authenticate and get_user
class YourAuth:
def authenticate(self, request, username=None):
try:
user = User.objects.get(username=username)
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
In your Views for custom login request:
# Your Logic to login user
userName = authenticate(request, username=uid)
login(request, userName)
For further reference, use the django documentation here.
You can easily fix this by creating your own authentication backend and adding it to the AUTHENTICATION_BACKENDS setting.
There are some OpenID backends available already, so with a bit of searching you could save yourself the trouble of writing one.