Authenticate user after password reset in Django - django

I use standard Django view, password_reset_confirm(), to reset user's password. After user follows password reset link in the letter, he enters new password and then view redirects him to the site root:
urls.py
url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'django.contrib.auth.views.password_reset_confirm', {
'template_name': 'website/auth/password_reset_confirm.html',
'post_reset_redirect': '/',
}, name='password_reset_confirm'),
After Django redirects user, he is not authenticated. I don't want him to type password again, instead, I want to authenticate him right after he set new password.
To implement this feature, I created a delegate view. It wraps standard one and handles its output. Because standard view redirects user only if password reset succeeded, I check status code of response it returns, and if it's a redirect, retrieve user from DB again and authenticate him.
urls.py
url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.password_reset_confirm_delegate, {
'template_name': 'website/auth/password_reset_confirm.html',
'post_reset_redirect': '/',
}, name='password_reset_confirm'),
views.py
#sensitive_post_parameters()
#never_cache
def password_reset_confirm_delegate(request, **kwargs):
response = password_reset_confirm(request, **kwargs)
# TODO Other way?
if response.status_code == 302:
try:
uid = urlsafe_base64_decode(kwargs['uidb64'])
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
pass
else:
user = authenticate(username=user.username, passwordless=True)
login(request, user)
return response
backends.py
class PasswordlessAuthBackend(ModelBackend):
"""Log in to Django without providing a password.
"""
def authenticate(self, username, passwordless=False):
if not passwordless:
return 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
settings.py
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'website.backends.PasswordlessAuthBackend'
)
Are there any better ways to do this?

Starting Django 1.11 your can do this by using the class based view. You need to override the password_reset_confirm url to pass post_reset_login=True and success_url to PasswordResetConfirmView:
urlpatterns += [
url(
r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.PasswordResetConfirmView.as_view(
post_reset_login=True,
success_url=reverse_lazy('studygroups_login_redirect')
),
name='password_reset_confirm'
),
]

Vanilla Django
Since password_reset_confirm is not class-based-view, you cant cleanly customize it in any significant way without resorting to middleware-type tricks. Therefore what you are doing seems to be the most efficient way at the moment.
If django would be been passing request to the SetPasswordForm (similar to how DRF passes request to serializers), you could of overwritten the form's save() to login the user there however as now that is also not possible.
3rd party packages
You can also look into other 3rd party libs which implement auth as class based views. From a quick google search, the most promising are:
django-password-reset. Seems to provide complete replacement for auth.views as class based views. You can overwrite form_valid in order to authenticate user.
django-class-based-auth-views (not complete)

Related

Override Django LoginView Error Message With Custom AuthenticationForm

I have a class-based view that subclasses LoginView.
from django.contrib.auth.views import LoginView
class CustomLoginView(LoginView):
def get_success_url(self):
url = self.get_redirect_url()
return url or reverse_lazy('knowledgebase:user_home', kwargs={
'username':self.request.user.username,
})
I want to override the error message if a user's email is not yet active because they have to click a link sent to their email address. The current default message looks like this:
Instead of saying:
Please enter a correct email address and password. Note that both
fields may be case-sensitive.
I want to say something to the effect of:
Please confirm your email so you can log in.
I tried:
accounts/forms.py
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext as _
class PickyAuthenticationForm(AuthenticationForm):
def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(
_("Please confirm your email so you can log in."),
code='inactive',
)
accounts/views.py
class CustomLoginView(LoginView): # 1. <--- note: this is a class-based view
form_class = PickyAuthenticationForm # 2. <--- note: define form here?
def get_success_url(self):
url = self.get_redirect_url()
return url or reverse_lazy('knowledgebase:user_home', kwargs={
'username':self.request.user.username,
})
The result is absolutely no effect when I try to log in with a user that does exist, but hasn't verified their email address yet.
AuthenticationForm docs.
Method - 1
Django uses ModelBackend as default AUTHENTICATION_BACKENDS and which does not authenticate the inactive users.
This is also stated in Authorization for inactive users sections,
An inactive user is one that has its is_active field set to False. The
ModelBackend and RemoteUserBackend authentication backends prohibits
these users from authenticating. If a custom user model doesn’t have
an is_active field, all users will be allowed to authenticate.
So, set AllowAllUsersModelBackend as your AUTHENTICATION_BACKENDS in settings.py
# settings.py
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']
How much does it affect my Django app?
It doesn't affect anything other than the authentication. If we look into the source code of AllowAllUsersModelBackend class we can see it just allowing the inactive users to authenticate.
Method - 2
Personally, I don't recommend this method since method-1 is the Django way of tackling this issue.
Override the clean(...) method of PickyAuthenticationForm class and call the AllowAllUsersModelBackend backend as,
from django.contrib.auth.backends import AllowAllUsersModelBackend
class PickyAuthenticationForm(AuthenticationForm):
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username is not None and password:
backend = AllowAllUsersModelBackend()
self.user_cache = backend.authenticate(self.request, username=username, password=password)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(
"Please confirm your email so you can log in.",
code='inactive',
)
Result Screenshot
You need to use AllowAllUsersModelBackend
https://docs.djangoproject.com/en/3.0/ref/contrib/auth/#django.contrib.auth.backends.AllowAllUsersModelBackend
Here you will get instruction for setting custom backend
https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#specifying-authentication-backends
Hope it helps.
I'm not convinced setting up a custom backend is the solution when I simply want to override a message. I did a temporary fix by defining form_invalid. Yes it's hacky but for now, it'll do the trick. Doubt this will help anyone but it was interesting to discover form.errors. Maybe someone can build off this to solve their specific problem.
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
#TODO: This is EXTREMELY HACKY!
if form.errors:
email = form.cleaned_data.get('username')
if User.objects.filter(email=email, username=None).exists():
if len(form.errors['__all__']) == 1:
form.errors['__all__'][0] = 'Please confirm your email to log in.'
return self.render_to_response(self.get_context_data(form=form))

Django (using TokenAuthentication): "non_field_errors": "Unable to log in with provided credentials?

I'm using httpie to test my custom authentication.
http POST http://127.0.0.1:8000/api-token-auth/ username='username1' password='Password123'
I did create a custom auth object using AbstractUser.
Using TokenAuthentication, I followed the docs and added my custom TokenAuthentication in my REST_FRAMEWORK settings:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'regis.models.CustomAuthentication',
)
}
And added rest_framework.authtoken in my installed apps.
My AUTHENTICATION_BACKEND is as follows:
AUTHENTICATION_BACKENDS = [ 'regis.models.CustomAuthentication' ]
And here is my custom authentication class:
class CustomAuthentication(authentication.TokenAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
print(username)
user_model = get_user_model()
if not username:
return None
try:
user = user_model.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
urls.py:
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token),
]
I'm pretty much following the DRF docs (http://www.django-rest-framework.org/api-guide/authentication/#custom-authentication). If there's any additional info needed to solve this, please let me know and I'll update. Any help on what I'm missing would be great.
To add: Just out of curiosity, do I need to make a custom authentication system if I have a custom user?
UPDATE:
I just deleted the class above, and just added the rest_framework.authentication.TokenAuthentication in my REST_FRAMEWORK settings. I'm still using a custom authentication which fetches my user.
It looks like this (not going to format it. SO sucks at formatting code from VIM):
class CustomAuthentication(object):
def authenticate(self, email=None, password=None):
User = get_user_model()
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return None
if user.check_password(password):
return user
return None
def get_user(self, user_id):
try:
user_model = get_user_model()
user = user_model.objects.get(pk=user_id)
except User.DoesNotExist:
return None
I used this Django docs to create that code: https://docs.djangoproject.com/en/1.10/topics/auth/customizing/
If you search for the error string in the DRF code, you find this (in authtoken/serializers.py:
from django.contrib.auth import authenticate
...
if username and password:
user = authenticate(username=username, password=password)
if user:
# From Django 1.10 onwards the `authenticate` call simply
# returns `None` for is_active=False users.
# (Assuming the default `ModelBackend` authentication backend.)
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg, code='authorization')
...
So it looks like depending on which version of Django you're using, either these credentials are incorrect, or the user is not active (for Django >= 1.10)?
Have you tried logging in manually in the admin with these credentials to verify them?
OK I solved it. Inside my settings, I just had to remove the AUTHENTICATIONS_BACKEND. I thought my custom backend was different for merely logging a user in and the token authentication backend worked to get that token.

Django- how to use built-in login view with email instead of username?

I am using built-in login in my app. There is some custom backends or packages to handle this. but many of them are not what i am looking.
i made email unique via django-registration while registering. now all i want is to ask email in login page instead of username.
but if i use some custom backends like django email as username it crashes when using with django-registration.
i dont want to change all authentication backend , i just want to change login page.
in the rest of site , i am gonna use username. p.e in my custom admin page when i write:
welcome {{user}}
it must render username. not e-mail.
i need to find the way out from this. i am stuck.
thank you.
By default django.contrib.auth.urls will create a log in page from this pattern
(r'^login/$', 'django.contrib.auth.views.login'),
You need to avoid/override this url then create a new view to handle a new type of login.
e.g.
create a new login url in your urls.py
(r'^emaillogin/$', 'email_login_view'),
create view to support login with email in views.py
# get default authenticate backend
from django.contrib.auth import authenticate, login
from django.contrib.auth.models import User
# create a function to resolve email to username
def get_user(email):
try:
return User.objects.get(email=email.lower())
except User.DoesNotExist:
return None
# create a view that authenticate user with email
def email_login_view(request):
email = request.POST['email']
password = request.POST['password']
username = get_user(email)
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
# Redirect to a success page.
else:
# Return a 'disabled account' error message
else:
# Return an 'invalid login' error message.
Ref : https://docs.djangoproject.com/en/1.4/topics/auth/#django.contrib.auth.login
The above approach does not work anymore on django 1.9. A different approach might be to override the auth form used in the view as:
class EmailLoginForm(AuthenticationForm):
def clean(self):
try:
self.cleaned_data["username"] = get_user_model().objects.get(email=self.data["username"])
except ObjectDoesNotExist:
self.cleaned_data["username"] = "a_username_that_do_not_exists_anywhere_in_the_site"
return super(EmailLoginForm, self).clean()
Then when defining the login url, define as this:
url(r'^login/$', django.contrib.auth.views.login, name="login", kwargs={"authentication_form": EmailLoginForm}),
url(r'^', include('django.contrib.auth.urls')),
The best thing about the above approach you are not really touching anything in the auth process. It's not really a "clean" solution but it's a quick workaround. As you define the login path before including auth.urls, it will be evaluated instead of the base login form

Custom Auth backend with middleware

I tried looking at this answer, as well as using django sessions here.
The login with my custom auth works fine, but I want to validate the token on every request with middleware, and I can't figure out how to store the token so that it may be accessed from both the middleware as well as views.
I tried storing a session variable from my auth backend, but I would always get a key error when trying to access it from my views.
Is there a good way to do this?
Thanks!
class MyAuthBackend(object):
supports_inactive_user = False
supports_object_permissions = False
supports_anonymous_user = False
def authenticate(self, username=None, password=None):
# This makes a call to my API to varify login, then return token if valid. I need to make login_valid accessible to my middleware and views.
login_valid = auth.login(username,password)
if login_valid:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username, password='never_used')
user.is_active = True
user.save()
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
class MyAuthMiddleware(object):
def process_request(self, request):
if not request.user.is_anonymous():
# API call to my backend to check if token is still valid. If not, return to login page.
token_variable = ???????????
if isTokenStillValid(token_variable):
return
else:
return HttpResponseRedirect('/accounts/login/?next=%s' % request.path)
Are you using the default django.contrib.auth login view for logging in? It seems to completely clear the session during the login process (which happens after your authentication backend is called, in contrib.auth.login function, described here).
I think you might either try to write your own login view, with an alternative login function that preserves the auth token, or store the token somewhere else (database table, cache system). The latter might make it difficult to allow multiple simultaneous logins for one user.

django authentication without a password

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.