Tastypie, non-orm endpoints - django

I'm trying to figure out if it makes sense to create non-orm endpoints for my api. I've seen the "Using Tastypie With Non-ORM Data Sources" section of the Docs, but I'm thinking more about processing forms and such. For example: Passing data to an end point that would process and send email. Im not gonna save anything to the db, I just want to validate the form and send it. Am I missing something in the docs or am I just barking up the wrong tree?

I think you can just define a function in your views.py and handle the request without it really using tastypie:
Here's an example for registering a new user to to a Django webapp; you can probably repurpose it to send an e-mail instead by processing the form data from the request.
# In views.py:
...
from django.http import HttpResponse
from django.contrib.auth.models import User, Group
from django.contrib.auth import authenticate
from django.http import Http404
from django.utils import timezone
from models import *
from api import *
from django.utils import simplejson
...
# REST endpoint for user registration
# Enforces server-side mediation (input validation)
# On failure, raises Http404
# On success, redirects to registration success page
def register_user(request):
if request.method != 'POST':
raise Http404('Only POSTs are allowed')
# acquire params
username = request.POST['username']
password = request.POST['password']
repeatpw = request.POST['repeatpw']
first_name = request.POST['first_name']
last_name = request.POST['last_name']
# Server-side mediation to check for invalid input
if username == '' or username is None:
raise Http404('Server-side mediation: Invalid Username')
if len(username) > 30:
raise Http404('Server-side mediation: username must be 30 characters or fewer')
if len(first_name) > 30:
raise Http404('Server-side mediation: first name must be 30 characters or fewer')
if len(last_name) > 30:
raise Http404('Server-side mediation: last name msut be 30 characters or fewer')
if len(password) < 4:
raise Http404('Server-side mediation: Password too short')
if password != repeatpw:
raise Http404('Server-side mediation: Password Mismatch')
# This try-except block checks existence of username conflict
try:
test_user_exists = User.objects.get(username__exact=username)
if test_user_exists != None:
raise Http404('Server-side mediation: Username exists')
except User.DoesNotExist:
pass
# Input passes all tests, proceed with user creation
user = User.objects.create_user(username, 'default#nomail.com', password)
group = Group.objects.get(name='Standard')
user.first_name = first_name
user.last_name = last_name
user.groups.add(group)
user.is_staff = False
user.save()
# Build confirmation JSON
confirmation = {
'action': 'register_user',
'username': username,
'success': 'yes',
}
json_return = simplejson.dumps(confirmation)
# return JSON of the success confirmation
return HttpResponse(json_return, mimetype='application/json')

Related

Customizing authentication backend for multi users in Django

So my question is that you have a custom user 'Account' and I'm trying to use Loginview but with only one specific backend but the class does not care about the value passed.
from django.contrib.auth import views as auth_views
from .lists import ProjectCreateView , ProjectUpdateView , ProjectDeleteView
class ClientLogin(auth_views.Loginview):
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.get_user(),'CREA.models.ClientBackend')
return HttpResponseRedirect(self.get_success_url())
urlpatterns = [
path('', ClientLogin.as_view(template_name='authentification/client_login.html',
redirect_authenticated_user=True,next_page='client-home'), name='client-log-in'),
NB: note that the backend works fine if it's the only one specified in the settings but other than that the loginview use all backends one by one, and yes I know that this is the default behaviour but then whats the point of this variable in auth_login
here is the buld in function in django :
def login(request, user, backend=None):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ""
if user is None:
user = request.user
if hasattr(user, "get_session_auth_hash"):
session_auth_hash = user.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash
and not constant_time_compare(
request.session.get(HASH_SESSION_KEY, ""), session_auth_hash
)
):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
try:
backend = backend or user.backend
except AttributeError:
backends = _get_backends(return_tuples=True)
if len(backends) == 1:
_, backend = backends[0]
else:
raise ValueError(
"You have multiple authentication backends configured and "
"therefore must provide the `backend` argument or set the "
"`backend` attribute on the user."
)
else:
if not isinstance(backend, str):
raise TypeError(
"backend must be a dotted import path string (got %r)." % backend
)
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
request.session[BACKEND_SESSION_KEY] = backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, "user"):
request.user = user
rotate_token(request)
user_logged_in.send(sender=user.__class__, request=request, user=user)

Django Rest JWT login using username or email?

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)

Allauth - How to disable reset password for social accounts?

I want social users are only able to login without password. Therefore, I want to disable password reset at /accounts/password/reset/ for social users.
I know that I need to add a conditional in this code at allauth.accounts.forms.py
class ResetPasswordForm(forms.Form):
email = forms.EmailField(
label=_("E-mail"),
required=True,
widget=forms.TextInput(attrs={"type": "email", "size": "30"}))
def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email)
if not self.users.exists():
raise forms.ValidationError(_("The e-mail address is not assigned"
" to any user account"))
return self.cleaned_data["email"]
I'm thinking about this solution, but I don't know how to do it properly:
elif SocialAccount.objects.filter(provider='facebook'):
raise forms.ValidationError(_("You are using a social account.")
Allauth extendability is not a problem. self.users contain all users with provided email but if you have setting ACCOUNT_UNIQUE_EMAIL=True or just didn't change it (so it's True by default) then you can take just first user. Every user contain socialaccount_set related manager. So we just filter that set by provider name and if there is any item then this user has related socialaccount. So you can inherit allauth form and put additional check there:
# forms.py
class MyResetPasswordForm(ResetPasswordForm):
def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email)
if not self.users.exists():
raise forms.ValidationError(_("The e-mail address is not assigned"
" to any user account"))
# custom code start
elif self.users.first().socialaccount_set.filter(provider='facebook').exists():
raise forms.ValidationError(_("You are using a social account.")
# custom code end
return self.cleaned_data["email"]
And then override allauth settings to use your form in their reset password view:
# your settings
ACCOUNT_FORMS = {
'reset_password': 'your_app.forms.MyResetPasswordForm'
}
I have not tested this code so it can contain typos so feel free to post any further questions.
#bellum answer is kind off right but now Allauth return a list on filter_users_by_email(email)
So this worked for me
from allauth.account.forms import SignupForm, LoginForm, ResetPasswordForm
from allauth.account.adapter import get_adapter
from django.utils.translation import ugettext_lazy as _
from allauth.account.utils import filter_users_by_email
class MyResetPasswordForm(ResetPasswordForm):
def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email)
if not self.users:
raise forms.ValidationError(_("Unfortunately no account with that"
" email was found"))
# custom code start
elif self.users[0].socialaccount_set.filter(provider='google').exists():
raise forms.ValidationError(_("Looks like the email/password combination was not used. Maybe try Social?"))
# custom code end
return self.cleaned_data["email"]
Note: I am using google social authentication if you are using Facebook then change it to facebook.

How to use django.contrib.auth with django-rest-framework?

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.

How to use django inbuilt forms?

I am trying to use the django inbuilt AuthenticationForm to allow users to login using their email address and password. I have changed the authenticate function to accept both username and email to authenticate users.
This is my code so far:
def loginuser(request):
if request.POST:
"""trying to use AuthenticationForm to login and add validations"""
form = AuthenticationForm(request.POST.get('email'),request.POST.get('password'))
user = form.get_user()
if user.is_active:
login(request,user)
render_to_response('main.html',{'user':user})
else:
HttpResponse('user not active')
render_to_response('login.html')
But this is not how the authentication form is used, at least not the correct way.
An example. You can see django.contrib.auth.forms for derails (search AuthenticationForm in the file forms.py).
f = AuthenticationForm( { 'username': request.POST.get( 'email' ), 'password': request.POST.get( 'password' ) } )
try:
if f.is_valid():
login( f.get_user() )
else:
# authentication failed
except ValidationError:
# authentication failed - wrong password/login or user is not active or can't set cookies.
So, modify your code to:
def loginuser(request):
if request.POST:
"""trying to use AuthenticationForm to login and add validations"""
form = AuthenticationForm(request.POST.get('email'),request.POST.get('password'))
try:
if form.is_valid():
# authentication passed successfully, so, we could login a user
login(request,form.get_user())
render_to_response('main.html',{'user':user})
else:
HttpResponse('authentication failed')
except ValidationError:
HttpResponse('Authentication failed - wrong password/login or user is not active or can't set cookies')
render_to_response('login.html')