Closed jmichalicek closed 1 month ago
Thanks for raising another bug report!
My feeling is that if the user already authenticated via username/password or some other method and this is just being used as 2fa then the original authentication backend should remain set on the user.
This sounds sensible to me, we shouldn't override the backend if the user is already authenticated.
Regarding the model backend, I'm not 100% sure what the best course of action would be.
We could create a custom backend that performs essentially no logic and only functions to indicate a user was logged in through a Passkey.
Cons:
Alternatively, there could be some way to indicate which backend should be used in the call to login
. Perhaps a setting or something? Looking at the documentation for selecting the authentication backend, perhaps we could set user.backend
before calling login?
More thoughts and approaches welcome, I don't usually venture into this area of Django so my experiences are fairly limited.
I've started playing and have what appears to be a functional back end. It works through django's built in django.contrib.auth.authenticate()
. That just loops through the configured authentication backends in order and passes in whatever kwargs were passed to authenticate()
. So there we use a kwarg which is specific to this backend (seems to be the norm. If there is some sort of collision for a user, they could always subclass this backend and change the kwarg it uses). Once a backend returns a user the authenticate()
function sets user.backend
, which is then used by django.contrib.auth.login()
to know which backend to continue with. That is also stored in the session and is used by AuthenticationMiddleware
to know which backend to call get_user()
on after authentication and login have been completed successfully.
I want to sleep on it overnight and take another look tomorrow with fresh eyes (sort of fresh, it'll be after work).
Alright, I think this does the trick. I've left a pretty lengthy explanation on the PR.
Thanks for all of your work on this. Adding passkeys support as a standard thing in our projects at work is one of our near future goals. I evaluated other options out there and was unhappy with them for several reasons. I started planning my own and then found this project, which is very similar to what I was planning, so I'm happy to help out with this rather than have to create and maintain my own from the ground up.
With that said, I've got a other PRs I'd like to make based on things I've seen (some definitely needing discussion first, this is your project, not mine and I just want to be helpful). The first one, off the top of my head, is just around some of the type annotations. I can make an issue or just a straight PR with the details. I'd really like to help get this off the ground and as useful as possible, so I'm happy to help in a number of places, depending on free time and/or my employer giving me time.
Thank you for the kind words, much appreciated 🙏
To add my own motivation for building this: I created this package because I wanted away with TOTP codes and wasn't happy with the Django packages out there that already implement Passkey support. If you care to watch: I gave a talk about wagtail-mfa; a package that uses django-otp-webauthn and integrates it with Wagtail. https://www.youtube.com/watch?v=qJwg2kFtFW4
I'd be open to work on the type annotations. I don't usually add them to my projects so not sure what the best practices are, feel free to educate me.
Right now, I think this package mostly needs:
I'll shoot you an email with my contact details, that would probably be a quicker and easier way to discuss your ideas than by creating issues I feel.
I'll definitely check out your talk. I'm also a big wagtail user. I've been fortunate enough to be able to discover a bug and contribute back to the project, use it one one of my personal websites, and have used it (and very much abused and did terrible things with it) at work.
I'm more than happy to help with all of those things. I've been a big proponent of pushing testing and type hints (and various other code quality stuff, I'm happy to help with linters, formatters, making them simple to use, etc.) at work. And documentation is of course always good.
We've got a cookeicutter project template we use as a base for all of our projects (with everything from terraform stuff to configure standard services, github, etc. to standard django tooling and layout we use) which I am building this into so that it's always easily available to us. As we get some new projects going (agency life, it's always feast or famine) and I start needing to override and customize stuff, I'm sure I'll have feedback and suggestions there as well.
I just now got your email, so I'll definitely follow up this evening or tomorrow.
Tested with django-opt-webauthn 0.13, django 5.0.6, python 3.11.7
When django has multiple authentication backends configured and the user is not already logged in, such as using this for passwordless authentication, an exception is raised when calling django.contrib.auth's
login()
function. A full stack trace is at the bottom.For example in django settings:
I'm happy to play with this and make a PR, probably some time this coming week.
Based on work with other auth backend related code, I suspect the most django-ish approach to resolving this is to provide a very simple custom back end which can then be added to django's
AUTHENTICATION_BACKENDS
setting and thendjango.contrib.auth.authenticate()
could be called either inBeginCredentialAuthenticationView
if the user is not already authenticated or just beforelogin()
(asauth_login()
) is called inCompleteCredentialAuthenticationView
with the authenticating device passed into it . My feeling is that if the user already authenticated via username/password or some other method and this is just being used as 2fa then the original authentication backend should remain set on the user.Alternately, that back end could just be set directly when calling
login()
, but that feels off to me to specify a backend which the user may not have set in their authentication backends, so I think the use of the built inauthenticate()
method is probably the way to go.The custom backend feels a bit weird because I think there would be almost no logic since most of the logic needs to happen in the views whether this is being used for passwordless auth or not, and if some other auth happened first and the logic was in this new auth backend then logic which needs to be called anyway would end up skipped.