Django Custom Authentication Not Authenticating Custom User

253 Views Asked by At

So I am trying to build a website using Django where there are two separate user bases, one is user and the other is agent. Now I imported User from from django.contrib.auth.models import User and have a separate model for Agent. What I want is for there to be different login urls for users and agents. An agent can be an agent without having to be registered as a user. Now I have tried this code, and what this does is after trying to log in using agent credentials, it logs in and returns the correct html page but doesn't even authenticate if those credentials are right or wrong. Like the agent_login is still logging me in even after providing it with the wrong credentials. Now I want to know what the problem is and how to fix it so that it works properly.

My models.py:

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    phonenumber = models.IntegerField()

    def __str__(self):
        return f'{self.user.username} Profile'

class Agent(models.Model):
    name = models.CharField(max_length=100)
    password = models.CharField(max_length=200)
    image = models.ImageField(upload_to = 'Images/')
    bio = models.TextField()
    instagram = models.URLField(max_length=100)
    twitter = models.URLField(max_length=100)
    facebook = models.URLField(max_length=100)
    linkedin = models.URLField(max_length=100)
    is_featured = models.BooleanField(default = False)
    slug = models.SlugField(default='')

    def set_password(self, raw_password):
        self.password = make_password(raw_password)
        self.save()  

    def __str__(self):
        return f'{self.name} Agent Profile'

My forms.py:

class AgentSignInForm(AuthenticationForm):
    username = forms.CharField(max_length=100)
    password = forms.PasswordInput()
    def clean(self):
        cleaned_data = super().clean()
        username = cleaned_data.get('username')
        password = cleaned_data.get('password')
        return cleaned_data

My views.py:

def agent_login(request):
    if request.method == 'POST':
        form = AgentSignInForm(request.POST)
        username = request.POST['username']
        password = request.POST['password']
        agent = AgentBackend.authenticate(request)
        return render(request, 'users/agent_dashboard.html', {'agent': agent})
    else:
        form = AgentSignInForm()
    return render(request, 'users/agent_login.html', {'form': form,})

    
class AgentDashboard(DetailView):
    model = Agent
    template_name = 'users/agent_dashboard.html'
    slug_field = 'slug'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        agent = self.get_object()
        properties = Property.objects.filter(agent=agent)
        context['properties'] = properties
        return context

My settings.py:

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "users.backends.AgentBackend",
]

My backends.py:

from .models import Agent
from contextlib import suppress
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password

class AgentBackend(BaseBackend):
    def authenticate(request, username=None, password=None, **kwargs):
        # Skip authentication attempts without credentials
        if username is None or password is None:
            return

        # Get agent by name, check the password hash and return it if everything is ok
        with suppress(Agent.DoesNotExist):  
            agent = Agent.objects.get(name=username)
            if check_password(password, agent.password):
                return agent
        return None

    def get_user(self, slug):
        # Fetch and return agent if it exists
        with suppress(Agent.DoesNotExist):
            return Agent.objects.get(slug=slug)
        return None

My admin.py:

class AgentAdmin(admin.ModelAdmin):

    def save_model(self, request, obj, form, change):
        if 'password' in form.changed_data:
            obj.set_password(form.cleaned_data['password'])
        super().save_model(request, obj, form, change)

admin.site.register(Agent, AgentAdmin)
4

There are 4 best solutions below

10
Pycm On

1)

Frist make sure your normal view functions to look like below. (expect login view and register view).

from django.contrib.auth.decorators import login_required


@login_required
def my_view(request):

2)

Then,

1) You only authenticate(check if pass and u_name is correct) them, You should log them in. Change below code to,

        username = request.POST['username']
        password = request.POST['password']
        agent = AgentBackend.authenticate(request)

. Like below -

username = request.POST['username']
password = request.POST['password']
agent = AgentBackend.authenticate(request=request,username=username,password=password)
if agent is not None:
    login(request,agent,backend='AgentBackend')
    return render(request, 'users/agent_dashboard.html', {'agent': agent})
else:
    form = AgentSignInForm()
    return render(request, 'users/agent_login.html', {'form': form,})

You need to log them in(save them as logged in users in DB).

3)

Update backends.py code as below.

with suppress(Agent.DoesNotExist):
    agent = Agent.objects.get(name=username)
    if (agent):
        if check_password(password, agent.password):
            return agent  # Return agent if credentials are correct
        
        else:
            return None 
    else:
        return None

.

You maybe already logged in current browser, so try using new incognito browser window.

1
Ahtisham On

From docs:

The order of AUTHENTICATION_BACKENDS matters, so if the same username and password is valid in multiple backends, Django will stop processing at the first positive match.

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "users.backends.AgentBackend",
]

In your settings you are giving preference to Django's default authentication where your user gets authenticated and your custom authentication is not touched hence always logging in.

You just need to swap the order and it will give your custom authentication priority and if it fails then it will use default authentication of django.

7
VonC On

Make sure the authenticate method in AgentBackend returns None if the credentials are incorrect. That is important: if it mistakenly returns an Agent object even with wrong credentials, the user will be logged in.

The authenticate function should be called with the correct parameters (username and password), and login should only be called if authenticate returns a valid user object.

When calling login(request, user), you might need to specify the backend explicitly if Django does not automatically detect the correct backend. For example, login(request, agent, backend='users.backends.AgentBackend'), as mentioned in Using the Django authentication system / How to log a user in¶.

As suggested in Pycm's answer, using the @login_required decorator on views that require authentication can help make sure unauthenticated access is properly managed.

Your agent_login would be:

from django.contrib.auth import authenticate, login
from django.shortcuts import render
from .forms import AgentSignInForm

def agent_login(request):
    if request.method == 'POST':
        form = AgentSignInForm(request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            agent = authenticate(request, username=username, password=password)
            if agent is not None:
                login(request, agent, backend='users.backends.AgentBackend')
                return render(request, 'users/agent_dashboard.html', {'agent': agent})
            else:
                # Add a message or logic to handle failed authentication
                pass
        else:
            # Handle invalid form
            pass
    else:
        form = AgentSignInForm()
    return render(request, 'users/agent_login.html', {'form': form})

Make sure the authenticate method correctly checks credentials and only returns an Agent object if the credentials are valid.
And verify that the password stored for Agent is hashed and that you are using Django's password hashing mechanisms, as illustrated in "Web Security in Django – How to Build a Secure Web Application / Password Hashing" by Isah Jacob, using check_password(password, encoded, setter=None, preferred='default').

from .models import Agent
from contextlib import suppress
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password

class AgentBackend(BaseBackend):
    def authenticate(request, username=None, password=None, **kwargs):
        if username is None or password is None:
            return None  # Return None if credentials are missing

        with suppress(Agent.DoesNotExist):
            agent = Agent.objects.get(name=username)
            if check_password(password, agent.password):
                return agent  # Return agent if credentials are correct

        return None  # Return None if credentials are incorrect

As mentioned, try testing in incognito mode to avoid issues with cached sessions. Make sure the server is fully restarted after changing the backend or settings.


It returns an error saying cannot access local variable 'agent' where it is not associated with a value

The variable agent might be used in a part of the code where it has not been defined or initialized yet, probably in the agent_login function in your views.py. You need to make sure agent is always defined before it is used.

from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import AgentSignInForm

def agent_login(request):
    if request.method == 'POST':
        form = AgentSignInForm(request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            agent = authenticate(request, username=username, password=password)

            if agent is not None:
                login(request, agent, backend='users.backends.AgentBackend')
                return redirect('dashboard')
            else:
                messages.error(request, 'Invalid login credentials. Please try again.')
        else:
            messages.error(request, 'Invalid form submission.')
            for field, errors in form.errors.items():
                for error in errors:
                    messages.error(request, f"{field}: {error}")
    else:
        form = AgentSignInForm()

    return render(request, 'users/agent_login.html', {'form': form})

The if statement now checks both the request method and form validation. The authenticate and login logic is inside this if block, ensuring agent is only referenced when it is defined.

The variable agent is now defined inside the if block, where it is guaranteed to have a value if the form data is valid and the authentication is successful.

The final render call is placed outside the if block to handle both initial page load (GET request) and the case where either form validation fails or authentication fails.

In your agent_login.html template, you need to add code to display these messages.

{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

The template code for displaying messages should be placed in agent_login.html to show feedback to the user.


One thing to note here is that, on my website, agents apply to be agents where they submit their CV and details through mail to HR, then if they meet requirements an interview is scheduled and certain agents are hired. Then those agents are created by a superuser in the admin page.

Here what I'm seeing is, that since the password field in the agent model is a CharField, passwords aren't really hashed, and I am not using Django's password hashing mechanisms for agents. Maybe this could be a cause for the code not working.

The fact that the password field in the Agent model is a CharField and that passwords are not hashed is indeed relevant: in Django, passwords should be stored in a hashed format for security reasons, and the authentication system (including check_password) is designed to work with hashed passwords.

The persistent "Invalid form submission" error could be due to the mismatch in password handling. The form expects a hashed password, but it is receiving a plain text password.

When creating an Agent instance (especially through the Django admin or any form), make sure the password is hashed before saving. Modify the Agent model to include a method for setting passwords, similar to Django's User model.

from django.contrib.auth.hashers import make_password

class Agent(models.Model):
     # existing fields 

     def set_password(self, raw_password):
          self.password = make_password(raw_password)
          self.save()

Use agent.set_password(raw_password) when creating or updating an agent's password.
Make sure when an Agent is created or updated through the admin, the set_password method is used.

Since agents are created by the superuser, revise this process to hash passwords. You might need a custom admin form for Agent to handle password hashing: if the form submission error persists even after ensuring password hashing, you would need to review the AgentSignInForm.


So, after updating the code and changing the max_length of the password field to 200 so that it can store hashed values, when I create new agents or update the passwords of existing agents from admin panel, it is still stored as CharField values not hashed values.

You need to make sure the set_password method you have added to your Agent model is being used whenever an agent's password is created or updated. That is especially important in the Django admin interface, where you might need to customize the way the Agent model is handled.

Create a custom ModelAdmin for your Agent model that handles password hashing correctly. Override the save_model method to use the set_password method when saving the Agent.

from django.contrib import admin
from .models import Agent

class AgentAdmin(admin.ModelAdmin):
    # Optionally, specify list_display, fieldsets, etc.

    def save_model(self, request, obj, form, change):
        if 'password' in form.changed_data:
            obj.set_password(form.cleaned_data['password'])
        super().save_model(request, obj, form, change)

admin.site.register(Agent, AgentAdmin)

In the AgentAdmin class, the save_model method is overridden. It checks if the password field has been changed (using form.changed_data). If the password is changed, it uses the set_password method to hash the new password before saving the agent. After hashing the password, it calls the superclass's save_model method to handle the actual saving of the agent.

6
arjun On

You are rendering agent_dashboard.html even if there is no agent. Render or redirect only if authenticate function returns agent instance. Also instead of rendering the template, redirect to the agent dashboard page after successful login


from django.http import HttpResponse
from django.contrib.auth import login

def agent_login(request):
    if request.method == 'POST':
       form = AgentSignInForm(request.POST)
       username = request.POST['username']
       password = request.POST['password']
       agent = AgentBackend.authenticate(request, username, password)
       if not agent:
          return HttpResponse("Invalid user credentials")
       login(request, agent, backend='path.to.your.AgentBackend') 
       return redirect("agent_dashboard_url")
       ...................

EDIT: You can store the hashed password by overriding model save method like this.

class Agent(models.Model):
    name = models.CharField(max_length=100)
    password = models.CharField(max_length=200)
    .......

    def save(self, *args, **kwargs):
        # hash the password before saving
        self.password = make_password(self.password)
        super().save(*args, **kwargs)