Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Custom user authentication in Django, with tests
Kimmo Sääskilahti
Kimmo Sääskilahti

Posted on • Originally published atkimmosaaskilahti.fi

     

Custom user authentication in Django, with tests

Cover image byFranck onUnsplash

In theprevious part, we created a custom user model in Django. In this part, I'd like to show how to roll custom authentication. Neither custom user model nor custom authentication are required for the granular role-based access control, but I'd like this series to be a complete tour of authentication and authorization in Django. The code accompanying the series can be found inGitHub. So let's get started!

Django authentication 101

Authentication is the process of figuring out who the user claims to be and verifying the claim. In Django's authentication system, the "low-level" approach to verifying the user identity is to callauthenticate fromdjango.contrib.auth.authenticate. This function checks the user identity against eachauthentication backend configured inAUTHENTICATION_BACKENDS variable ofsettings.py.

By default, Django usesModelBackend as the only authentication backend. It's instructive to look into the implementation ofModelBackend inGitHub:

classModelBackend(BaseBackend):defauthenticate(self,request,username=None,password=None,**kwargs):ifusernameisNone:username=kwargs.get(UserModel.USERNAME_FIELD)ifusernameisNoneorpasswordisNone:returntry:user=UserModel._default_manager.get_by_natural_key(username)exceptUserModel.DoesNotExist:# Run the default password hasher once to reduce the timing# difference between an existing and a nonexistent user (#20760).UserModel().set_password(password)else:ifuser.check_password(password)andself.user_can_authenticate(user):returnuser...
Enter fullscreen modeExit fullscreen mode

TheModelBackend fetches the appropriate user from the backend using either the givenusername or theUSERNAME_FIELD defined in user model. The backend then checks the password and also checks if the user can authenticate (checking if the user hasis_active set toTrue). Quite simple, eh?

As we'll be authenticating our users with their username (e-mail) and password in this series, we could use theModelBackend. However, it's instructive to write our own backend. Also, we'll get rid of all the unnecessary boilerplate inModelBackend coming from Django's default permission system, which we won't need.

Custom authentication backend

Every authentication backend in Django should have methodsauthenticate() andget_user(). Theauthenticate() method should check the credentials it gets and return a user object that matches those credentials if the credentials are valid. If the credentials are not valid, it should returnNone.

Here's a simple implementation ofCheckPasswordBackend:

# rbac/core/auth.pyimporttypingfromrbac.core.modelsimportUserfromrbac.coreimportservicesclassCheckPasswordBackend:defauthenticate(self,request=None,email=None,password=None)->typing.Optional[User]:user=services.find_user_by_email(email=email)ifuserisNone:returnNonereturnuserifuser.check_password(password)elseNonedefget_user(self,user_id)->typing.Optional[User]:try:returnUser.objects.get(id=user_id)exceptUser.DoesNotExist:returnNone
Enter fullscreen modeExit fullscreen mode

We useservices.find_user_by_email method created in the previous post for fetching the user by email. If the password matches, we return the corresponding user. And that's it! Let's set Django to use this backend for authentication:

# rbac/settings.pyAUTHENTICATION_BACKENDS=["rbac.core.auth.CheckPasswordBackend"]
Enter fullscreen modeExit fullscreen mode

Now, whenever we callauthenticate fromdjango.contrib.auth, we're essentially callingauthenticate() fromCheckPasswordBackend.

Why did we also defineget_user above inCheckPasswordBackend? That's a very good question. The answer is that Django documentation says it should be implemented, but I have no idea why. Please drop a comment if you know!

So now we have a great new authentication backend, how do we actually use it? We write a viewauth/login that allows users to login with their email and password. If the user identity is verified, we log them in by callinglogin(). This creates a session for the user and stores thesessionid in a cookie, allowing the user to perform authenticated requests.

Before implementing thelogin view, let's be responsible developers and write tests.

Tests

To test login, we need to create a sample user. We do that in apytest fixture:

# tests/test_views.pyimportpytestfromrbac.core.servicesimportcreate_userTEST_USER_NAME="Jane Doe"TEST_USER_EMAIL="jane@example.org"TEST_USER_PASSWORD="aösdkfjgösdgäs"@pytest.fixturedefsample_user():user=create_user(name=TEST_USER_NAME,email=TEST_USER_EMAIL,password=TEST_USER_PASSWORD)returnuser
Enter fullscreen modeExit fullscreen mode

Now let's use this fixture in two tests. The first test verifies that logging in with invalid password returns 401:

fromdjango.testimportClient@pytest.mark.django_dbdeftest_login_fails_with_invalid_credentials(sample_user):client=Client()response=client.post("/auth/login",dict(email=TEST_USER_EMAIL,password="wrong-password"),content_type="application/json",)assertresponse.status_code==401assert"sessionid"notinclient.cookies
Enter fullscreen modeExit fullscreen mode

We're using theDjango test client for making requests from tests without actually running the server.

The second test verifies that login succeeds with valid credentials:

@pytest.mark.django_dbdeftest_login_succeeds_with_valid_credentials(sample_user):client=Client()assert"sessionid"notinclient.cookiesresponse=client.post("/auth/login",dict(email=TEST_USER_EMAIL,password=TEST_USER_PASSWORD),content_type="application/json",)assertresponse.status_code==200assert"sessionid"inclient.cookies
Enter fullscreen modeExit fullscreen mode

At this point, we can startpytest-watch with the commandptw -- tests/test_views.py and code until the tests pass. If you haven't addedpytest-watch torequirements-dev.txt yet, you should do it now.

Login view

Let's now add the view for logging in a user. We expect users to post their email and password in a JSON request body. Here's how we parse the body, authenticate the user, and log them in:

# rbac/core/auth.pyimportjsonfromdjango.contrib.authimportauthenticate,loginfromdjango.httpimportHttpResponsefromdjango.views.decorators.httpimportrequire_http_methods@require_http_methods(["POST"])deflogin_view(request):body=json.loads(request.body.decode())user=authenticate(request,email=body["email"],password=body["password"])ifuser:login(request,user)returnHttpResponse("OK")else:returnHttpResponse("Unauthorized",status=401)
Enter fullscreen modeExit fullscreen mode

If the call toauthenticate returns a valid user, we login the user, create a session and set the session cookie. Otherwise, we return 401.

Now we need to define the endpoint for our view:

# rbac/core/auth.pyfromdjango.urls.confimportre_pathurlpatterns=[re_path("^login$",login_view),]
Enter fullscreen modeExit fullscreen mode

We also need to define a new route namedauth inrbac/urls.py:

# rbac/core/urls.pyfromdjango.conf.urlsimportinclude,re_pathfromrbac.coreimportviewsurlpatterns=[re_path(r"^$",views.index),re_path(r"^auth/",include("rbac.core.auth")),]
Enter fullscreen modeExit fullscreen mode

With all this done, your tests should pass with flying colors.

Congratulations, you should now have a much deeper understanding of how authentication works in Django! Please leave a comment how you liked the article. In the next parts, we'll work towards role-based access control. See you next time!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

ML Software Developer enthusiastic about Python, TypeScript, Scala, machine learning and functional programming.
  • Location
    Helsinki
  • Education
    D.Sc. (computational science)
  • Work
    Lead Software Developer
  • Joined

More fromKimmo Sääskilahti

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp