password reset implementation with djoser - django

I wanted to use djoser for the reset password functionality and as per the documentation:
PASSWORD_RESET_CONFIRM_URL
URL to your frontend password reset page. It should contain {uid} and
{token} placeholders, e.g. #/password-reset/{uid}/{token}. You should
pass uid and token to reset password confirmation endpoint.
I have done the following:
PASSWORD_RESET_CONFIRM_URL': 'reset/password/reset/confirm/{uid}/{token}',
url
url(r'^reset/password/reset/confirm/(?P<uid>[\w-]+)/(?P<token>[\w-]+)/$', PasswordResetView.as_view(),),
View :
class PasswordResetView(APIView):
def get (self, request, uid, token):
post_data = {'uid': uid, 'token': token}
return Response(post_data)
In my mail I get this link : http://127.0.0.1:8000/reset/password/reset/confirm/Mjk/538-954dccbc1b06171eff4d
This is obvious that I will get :
{
"uid": "Mjk",
"token": "538-954dccbc1b06171eff4d"
}
as my output but I wanted to go to auth/password/reset/confirm when the user clicks the link in the mail.

Lets describe the actions first:
The user clicks on the link to reset the password. reset password
(Here you need a form to obtain a username or email address, depending on your settings) The user enters the username and clicks Submit.
The user receives an email with a link to reset the password.
The link opens the browser, which contains the form "Create a new password".
The user enters a new password and sends a form
The browser redirects the page to the home page and gives feedback that the password has been reset.
You can then use following method to reset the password.
#template
<p>Use the form below to change your password. Your password cannot be the same as your username.</p>
<form role="form" method="post">
{% csrf_token %}
<input type="password" name="password1" placeholder="New Password">
<input type="submit">
</form>
#view
from django.shortcuts import redirect, render
from djoser.conf import django_settings
def reset_user_password(request, uid, token):
if request.POST:
password = request.POST.get('password1')
payload = {'uid': uid, 'token': token, 'new_password': password}
url = '{0}://{1}{2}'.format(
django_settings.PROTOCOL, django_settings.DOMAIN, reverse('password_reset_confirm'))
response = requests.post(url, data=payload)
if response.status_code == 204:
# Give some feedback to the user. For instance:
# https://docs.djangoproject.com/en/2.2/ref/contrib/messages/
messages.success(request, 'Your password has been reset successfully!')
return redirect('home')
else:
return Response(response.json())
else:
return render(request, 'templates/reset_password.html')

It's always important to catch errors, handle redirects and add the default renderer classes. In my case, I ended up using the following.
#view
import requests
from django.contrib import messages
from django.contrib.sites.models import Site
from django.http import HttpResponseRedirect
from django.shortcuts import render
from rest_framework.decorators import (api_view, permission_classes,
renderer_classes)
from rest_framework.permissions import AllowAny
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
#api_view(('GET', 'POST'))
#renderer_classes((TemplateHTMLRenderer, JSONRenderer))
#permission_classes([AllowAny])
def reset_user_password(request, **kwargs):
# uses djoser to reset password
if request.POST:
current_site = Site.objects.get_current()
#names of the inputs in the password reset form
password = request.POST.get('new_password')
password_confirmation = request.POST.get('password_confirm')
#data to accept. the uid and token is obtained as keyword arguments in the url
payload = {
'uid': kwargs.get('uid'),
'token': kwargs.get('token'),
'new_password': password,
're_new_password': password_confirmation
}
djoser_password_reset_url = 'api/v1/auth/users/reset_password_confirm/'
protocol = 'https'
headers = {'content-Type': 'application/json'}
if bool(request) and not request.is_secure():
protocol = 'http'
url = '{0}://{1}/{2}'.format(protocol, current_site,
djoser_password_reset_url)
response = requests.post(url,
data=json.dumps(payload),
headers=headers)
if response.status_code == 204:
# Give some feedback to the user.
messages.success(request,
'Your password has been reset successfully!')
return HttpResponseRedirect('/')
else:
response_object = response.json()
response_object_keys = response_object.keys()
#catch any errors
for key in response_object_keys:
decoded_string = response_object.get(key)[0].replace("'", "\'")
messages.error(request, f'{decoded_string}')
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
# if the request method is a GET request, provide the template to show. in most cases, the password reset form.
else:
return render(request, 'account/password_reset_from_key.html')
I added the allowany permission as the API endpoint does not need any authentication.
In URLs;
path('password/reset/<str:uid>/<str:token>/',
reset_user_password,
name='reset_user_password'),
Finally, on the main settings file, I updated the Djoser reset password URL to match the URL I'm building above so that I ensure that the user is redirected to the page I'm intending.
DJOSER = {
"PASSWORD_RESET_CONFIRM_URL":
"dashboard/auth/password/reset/{uid}/{token}/",
}

Related

Confirm Form Resubmission with Context Required Django

I have an account page that lets users edit their accounts. When the save button is pressed, I run the following code:
request.user.first_name = request.POST['first_name']
request.user.last_name = request.POST['last_name']
request.user.save()
context['alert'] = {
'title': "Your information has been updated",
'color': 'primary',
}
As you can see, I pass in context that, in my template, renders as an alert. However, when I refresh the page, it says: Confirm Form Resubmission
How can I get rid of this error? Thanks!
A successful POST request normally results in a redirect, this is the so-called Post/Redirect/Get pattern [wiki].
In order to send messages to the user, you can make use of the messaging framework [Django-doc]. So you can add a message for a user, and then render it in the other view(s):
from django.shortcuts import redirect
from django.contrib import messages
def my_view(request):
request.user.first_name = request.POST['first_name']
request.user.last_name = request.POST['last_name']
request.user.save()
messages.success(request, 'Your information has been updated')
return redirect('name-of-some-view')

Django Cookie with Login function

I'm trying to set my first cookie with Django when users are logged on my application.
When user is logged, the template is well-displayed but none cookie in my application which is named : Cookie
My function looks like :
def Login(request):
error = False
if request.method == "POST":
form = ConnexionForm(request.POST)
if form.is_valid():
username = form.cleaned_data["username"]
password = form.cleaned_data["password"]
user = authenticate(username=username, password=password)
if user:
login(request, user)
toto = GEDCookie(request)
return render(request, 'Home_Homepage.html', {'toto':toto})
else:
error = True
else:
form = ConnexionForm()
return render(request, 'Authentication_Homepage.html', locals())
#csrf_exempt
def GEDCookie(request):
SID = Logger.login("test", "10test")
response = HttpResponse("Cookie")
response.set_cookie('Cookie', SID, max_age=None)
return response
I missed something in my script ?
This isn't how you use cookies at all.
Inside your Login view, you're calling a separate view - GEDCookie that returns an HTTP response. But instead of returning that response directly to the user, which would set the cookie, you're for some reason trying to insert it in a template. That doesn't make sense.
If you want to set a cookie in your login view, you need to do so on the response that you return to the user.
Note also that after a successful login (or other post), you should always redirect, not display a template directly. So:
if user:
login(request, user)
response = redirect('home')
response.set_cookie('whatever')
return response
Finally, you almost certainly don't need a cookie here in any case. If you want to store data related to the current user, use the session.
As you can clearly see that you are not attaching your cookie to your real response, you are passing it as the context in render function which is an issue.
def Login(request):
error = False
if request.method == "POST":
form = ConnexionForm(request.POST)
if form.is_valid():
username = form.cleaned_data["username"]
password = form.cleaned_data["password"]
user = authenticate(username=username, password=password)
if user:
login(request, user)
SID = Logger.login("test", "10test")
response = render(request, 'Home_Homepage.html', {})
response.set_cookie('Cookie', SID, max_age=None)
return response
else:
error = True
else:
form = ConnexionForm()
return render(request, 'Authentication_Homepage.html', locals())
https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpResponse.set_cookie Please refer this link for individual arguments of inbuilt function.
Create signal.py in app. where your user model is present or add in main project directory and Add below snippet in signal.py
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from django.dispatch import receiver
from django.dispatch import Signal
from allauth.account.signals import user_logged_in # it signal for post login
from django.shortcuts import render
#receiver(user_logged_in) # Decorator of receiving signal while user going to logged in
def post_login(sender, user, request, response, **kwargs):
response.set_cookie('team', 'india') # This will set cookie
return response
In given snippet, default response will come in argument, so direct redirect to that response, if you want to change then render other template using render/redirect django.shortcuts methods like below,
response = render(request, 'base.html', {})

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)

How to get User session in Login handling in Django?

I am learning Django. Well, more or less ;)
I am writing a login form. Because I want to learn how stuff works and because I need to do this.
I am not able to get things right. The user is, after authentication, not known in my "success" view as it seems, because it redirects back to the login.
I did set up my MIDDLEWARE_CLASSES.
"Funny thing" was that I thought it went well, because it seems to work. However, I figured out that was an old session with my user credentials. Now I deleted all session/cookie stuff and I it is not working.
This is what I have so far:
# views.py
from django.shortcuts import render_to_response
from django.contrib.auth import authenticate
from django.template import RequestContext
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
def login(request):
username = password = usertype = login_error_message = ''
if request.POST:
username = request.POST.get('username')
password = request.POST.get('password')
usertype = request.POST.get('usertype')
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
return HttpResponseRedirect(reverse('login:success'))
else:
login_error_message = "Not active."
else:
login_error_message = "Unknown user"
return render_response(request, 'login/login.html', {
'login_error_message': login_error_message,
'usertype': usertype
})
def success(request):
if request.user.is_authenticated():
user = request.user
return render_response(request, 'login/success.html', {
'username': user.username,
})
else:
return HttpResponseRedirect('/login/')
# https://djangosnippets.org/snippets/3/
# Wrapper simplifies using RequestContext in render_to_response.
# overkill at this point, but may come in handy in view files with more views
def render_response(req, *args, **kwargs):
kwargs['context_instance'] = RequestContext(req)
return render_to_response(*args, **kwargs)
And this are my url's:
# urls.py
from django.conf.urls import patterns, url
from diataal.apps.login import views
urlpatterns = patterns('',
url(r'^$', views.login),
url(r'^success/', views.success, name='success'),
)
My url shows the login page. If I do not add the check on the user in the "success" view, the success page is shown (a 302 response from login and then a 200 response from the success page); that seems ok to me.
But with the check, I get a 302 response from the "success" view and I am back at the login page again...
I have no clue how to solve this... What am I doing wrong?
In your login view, you only authenticate the user. You do not actually login the user.
authenticate only takes the username and password and returns a user object if the credentials are valid. This is a pre-cursor to the actual login call

using django-email-as-username how can I return with a message to the user?

I am trying to use django-email-as-username and I can't find a way to return to the login page with a message for the user in case the data provided by the user is wrong.
this my urls.py
url(r'^login$', 'django.contrib.auth.views.login', {'authentication_form': EmailAuthenticationForm}, name='login'),
this is my views.py
def login_user(request):
if request.method == 'POST':
email = request.POST.get('email')
password = request.POST.get('password')
if email and password:
user = authenticate(email=email, password=password)
if user:
return HttpResponseRedirect('/')
else:
message = 'No such user'
else:
message = 'both email and password are required!'
else:
message = 'wrong method!'
I think you've made an error in that you're providing your own login_user view in your views.py module, but linking to Django's django.contrib.auth.views.login in your URL conf.
You should probably just use django.contrib.auth.views.login as the login view.
You'll also need to provide a registration/login.html template. You'll be able to get at the error information, as the form is passed through to the context. You can use the admin's login template as an example.
In case of authentication failure, you have to render/redirect your own login page with {{ message }}