Feature request: delegate more password related operations to auth backends (#23896)

226 views
Skip to first unread message

Roman Akopov

unread,
Dec 11, 2014, 4:40:09 AM12/11/14
to django-d...@googlegroups.com

Hello,

This is my fist post, so I'm not fully aware of any posting policies you have, but at least I'll try to present my ideas in clear way.

Very brief description of what I suggest:

Optionally delegate password change and password reset to authentication backend.

Motivation:

Django framework is very popular and widely used not only for public, but for private projects too. To make it clear, by private I mean not only personal, but mostly corporate intranet projects.

One of the important part of any corporate project is some kind of single sing on (SSO) or, at least, integration with external authentication backend. From my experience it may be any service able to validate credentials. LDAP is most often used for authentication purposes, however, it is not the only available choice. I had projects with authentication delegated to custom HTTP web-services, SMTP, POP3 and IMAP servers, and so on. Corporate intranet you have to deal with may be a zoo of unbelievably non-standard software. Also, even if we’ll talk about LDAP, it may be a few, more than one, LDAP servers you have to try authentication against, like Active Directory and OpenLDAP.

Current implementation of User model as well as standard forms, delegates to backend authentication only, but not password reset or change. This forces to reimplementation of user model and/or reimplementation of standard forms for very common tasks, high coupling and bad architecture. Also, if left as is, behavior is very inconsistent, since user is authenticated against one password database (LDAP), but changes or resets password in another (relational model database). And while LDAP may be administered externally by standard web tools, this is not a good option too, since means inconsistent user experience. And custom HTTP services cannot be administered by standard tools at all.

Implementation:

As far as I can see User model is already annotated with backend field
https://github.com/django/django/blob/master/django/contrib/auth/__init__.py#L75
so it is possible, and looks simple, to delegate password change and reset to backend, if backend support these operations, which means AbstractBaseUser.set_password method
https://github.com/django/django/blob/master/django/contrib/auth/models.py#L226

should be refactored to two methods: reset_password and change_password instead of one set_password, and these two method should delegate operation to backend if supported. (set_password may be left as alias of reset_password). Signatures will be

def reset_password(self, new_password)
def change_password(self, old_password, new_password)

Also authentication forms
https://github.com/django/django/blob/master/django/contrib/auth/models.py#L226

should be refactored to use these new reset_password and change_password methods. There must be two methods, as change_password and reset_password may not be both available, also change_password requires old password as backend may require it. Returning to LDAP, there may be various policies which should be respected, like not to change password too often, but reset whenever you want, so we need two separate methods.

Backward compatibility:

As far as I can see change is backwards compatible. Authentication backends not providing extra operations will behave old way without any change.

Patch:

I wanted my design to be reviewed before I’ll try to provide any patch. I'mm pretty sure I've missed something, so discussion is welcome. Also, this will be my first code for Django project, so I'll probably break some rules and will need some help.

With best regards,
Roman

Tim Graham

unread,
Dec 11, 2014, 10:49:05 AM12/11/14
to django-d...@googlegroups.com
User is only annotated with backend when calling authenticate(). On subsequent requests, or in non-request situations like the Python shell, how will you know which backend to delegate to?

How do existing LDAP backends deal with this problem? (or do they just ignore it?)
...

Roman Akopov

unread,
Dec 11, 2014, 1:31:52 PM12/11/14
to django-d...@googlegroups.com
All right, that's what I've missed. Thank you, for this point.

Existing LDAP backends I've reviewed do not support password change/reset at all.

Roman Akopov

unread,
Dec 11, 2014, 2:05:22 PM12/11/14
to django-d...@googlegroups.com
I've researched a little more, and looks like there is BACKEND_SESSION_KEY so it is possible to annotate user with backend on subsequent requests.
https://github.com/django/django/blob/master/django/contrib/auth/__init__.py#L169
Look like it should be a one line fix for this like
user.backend = backend
I think this will not break any existing code.

At non-request situations for non-authenticated User model, i.e. instantiated from database, or just created and saved, we can simply fallback to old behavior. Actually, I think this is correct, because a user can have valid credentials in various backends simultaneously. Imagine we have three backends: Model, LDAP, SMTP. So if you authenticate with credentials valid for SMTP backend, you can't change password because SMTP does not support this at all. It should be something like methods throwing NotImplementedError. If you authenticate with credentials valid for LDAP backend you can change password and that change will be performed against LDAP. If you authenticate with credentials valid for Model backend you can change password in model database. This looks like consistent behavior, since you change password you just used to authenticate, not some other password you probably even do not know about. And if no custom backend is registered or no backend information is available, we fallback to default one. Of course there is a question, how to reset LDAP password from admin interface. The answer will be "you cannot". This looks sane for me, because end-users are happy using just one software for all their tasks and LDAP administrator should use LDAP tools for administrative tasks anyway, and password reset is just a small one, there will be permission management, group membership and all of these tools should not be doubled in Django admin.


On Thursday, December 11, 2014 7:49:05 PM UTC+4, Tim Graham wrote:

Tim Graham

unread,
Dec 12, 2014, 6:19:26 PM12/12/14
to django-d...@googlegroups.com
I haven't used external authentication backends for any projects, but I still think the concept of dynamically changing how the password change form/views work based on which backend you authenticated with is too much complexity. This scheme feels very brittle and I'm not sure that making this change in Django offers much benefit (in reduced code, for example) in the end.

Roman Akopov

unread,
Dec 13, 2014, 4:28:52 AM12/13/14
to django-d...@googlegroups.com
Tim,

It's not about the benefit, it's about the possibility. The one simply cannot use two external backends with support of password change because each backend will have to provide own User model. I understand your point, and I agree that some logic I suggest seems controversial, but current implementation of plugable authentication backends is technically incomplete, because password cannot be changed or reset, and ideologically broken, because full functionality cannot be correctly added even with a lot of code. Actually whole contrib.auth should be replaced to allow pluggable authentication backends to provide password change or password reset independently.

Also, if you think it is good idea to force old behavior even if backend supports extended functionality, what stops us from introducing two boolean settings AUTHENTICATION_BACKEND_DELEGATE_PASSWORD_CHANGE and AUTHENTICATION_BACKEND_DELEGATE_PASSWORD_RESET which will be False by default? This will force old behavior while allowing to use extended backends if required.

I want it to be clear, I'm not asking for specific functionality, I'm asking for extensibility. External authentication is complex subject by itself and fighting with incomplete Django extensibility interface does not make it simpler, but better extensibility interface does.

Maybe, it will be wiser to discuss this feature request with people actually using external authentication, not core developers, to let them share their experience and maybe suggest better ways to implement this from the users/app developers point of view. Maybe this feature is desired one, but I'm just first who asked or maybe nobody actually needs it and I;m just Django-pervert. I can talk about my end users only, and about theirs user experience, you have no externally authenticated users at all. If we've done with technical side, I mean it seems clear what should be changed at source code level, and if only specific behavior logic is to be discussed, maybe it is better to discuss this non-core developer question with wider audience. What do you think?

Tim Graham

unread,
Dec 13, 2014, 7:35:17 AM12/13/14
to django-d...@googlegroups.com
Yes, I am by no means speaking definitively on the issue -- just voicing some concerns that I see. I'd love if people with experience with third-party authentication backends would join the discussion.

Michael Manfre

unread,
Dec 13, 2014, 8:15:30 AM12/13/14
to django-d...@googlegroups.com
I've used third party authentication backends, but your request seems a bit odd to me. I think I'm being caught up on the some of the specifics without enough context. Why can you not use a custom User that stores the backend that created it (or last used for authentication) and overrides set_password to delegate the password saving to the referenced authentication backend? You would need to also create your own subclass of ModelBackend that does the original User.set_password behavior. The only scenario that I can think of where your implementation would get a bit more complicated is if you wanted to support multiple active sessions using different authentication backends for the same user object.

Regards,
Michael Manfre

--
You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-develop...@googlegroups.com.
To post to this group, send email to django-d...@googlegroups.com.
Visit this group at http://groups.google.com/group/django-developers.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/f73abdc9-be42-4c37-a188-171a2ea88525%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Roman Akopov

unread,
Dec 13, 2014, 11:27:22 AM12/13/14
to django-d...@googlegroups.com
First, of course I can use custom User model, but two indendetly developed backends, like backend authenticating against LDAP, and backend authenticating against custom HTTP service will provide two User models, each with set_password implementation supporting only one backend and I'll have to choose which backend I need. And if, because of any reason, I need some other custom User model, I will not be able to user any of the backends. I cannot list exact reasons to use custom User model, I never needed it, but since Django supports them, need is real. Actually I may use some application replacing User model without real need, like adding birthday field. We had such recruiting related app, but dropped it later. It does not matter if need was real, it is about if I have to make hard choice between that app and authentication backend, between one backend and another backend. There can be only one User model, custom or default.

Second, I'll have to rewrite standard forms, because logic valid for database "if check: set" is completely broken for other backends. set_password, i.e. setting new password without knowing old one, generally requires administrator privileges. Can I work around this by executing set_password at administrator level? Yes, technically I can, but that will be incorrect, because the fact that I know old password does not necessarily mean I'm allowed to change it. Here is link to related policy for Active Directory
Also, password reset ignores password history policy, etc. So "if check: set" is not generally correct way to change password and I'll need to rewrite standard forms, standard views using standard forms. At this point I already wrote so much code and know so much about Django internals, that I'm very close to complete replacement of contrib.auth because it is easier to drop it and write replacement, rather than try to extend something not extendable.

Finally, imagine the following use case very close to real one (actually even simplified, because I have to deal with 27 companies, not 3). There are three companies, X, Y and Z belonging to one big holding, so some services and resources are shared, however companies are independent and have very different IT legacies. X has Windows based infrastructure with Active Directory, Y has Linux based infrastructure with OpenLDAP and Z uses Google Apps. Nether will store user credentials in Django database. This is sane security requirement, because when employee is fired, there must be a single point to disable his access to all corporate services, thus everything is bound to some king of directory. So I have Django-based web-site providing holding-wide shared service where users login using AD credentials, OpenLDAP credentials and Google credentials. Users from AD are authenticated against LDAP, users from OpenLDAP are authenticated against LDAP too, but actual backends are different because LDAP implementation differs a little. For instance object attributes are different, AD requires authentication to validate password (seems crazy to have to know one password to check another one, but yes, it's for real, do not know exact details however) while OpenLDAP does not, and Google Apps has no specific authentication interface, so we just try to login using SMTP protocol.

And in real life for one company I have Java HTTP web service published through some Barracuda Networks hardware firewall instead of direct LDAP access, because some InfoSec guy said it's more secure and we should do this to pass some certification; and some other mindblowing authentication schemes.

The problem is that some employees are field ones, use mostly Django web-site and have no desktop or no recurrent access to desktop or terminal to change/reset their password, so I was asked many times to implement this somehow. OK, I'm answering to my boss "WON'T FIX" every month or so, but maybe there is a way to implement pluggable backends with password change/reset support without rewrining/replacing standard auth? It does not seem too hard.

And again, my case may be very specific, I fully understand this, but looking at some other corporate environments, I see similar problems. So feel this issue is common enough, that's why I started this discussion.

Roman

Josh Smeaton

unread,
Dec 14, 2014, 1:10:58 AM12/14/14
to django-d...@googlegroups.com
I've been maintaining a custom django backend for quite awhile now. It delegates authentication and authorization to an internal API. I do not use a custom User model at all.

The backend completely handles the reset/change password logic, and the form uses the methods on the backend directly. If you have multiple backends then you need to work out where to save the used backend (either on the User model or in the session) so you can access it from the form.

If you're using multiple backends and have different form requirements per backend, then there is no way (that I can imagine) to create a one-stop-form to solve all requirements. Build multiple forms, check which backend is in use and display the appropriate form from the view. Call methods on the backend from the form.

Your requirements aren't unusual, but it's something that can be solved quite easily by user code, and extremely difficult to generalise. The backends take the most work, especially if you're implementing your own custom permissions. Forms are the easy part. I can't see why you'd ever need to modify the User just to support backends, but I may be missing something.

Happy to share anonymised backend and forms if you wish.

Josh

Roman Akopov

unread,
Dec 14, 2014, 2:07:32 AM12/14/14
to django-d...@googlegroups.com
Great!

I would like to take a look at any source code you can share.

Josh Smeaton

unread,
Dec 14, 2014, 5:17:16 AM12/14/14
to django-d...@googlegroups.com
https://gist.github.com/jarshwah/c5b9abebb452f2e3286f

I've removed some of the error handling and custom application bits, and I've also renamed the classes. But this is the meat of auth for my project. You'll notice that the backend is hardcoded in the constructor, because we only use this backend. If you had multiple, you'd need to figure out which backend or form to show, but that should be trivial. The built-in auth already caches the backend in use, so you could just check that property I think.

Regards,

Roman Akopov

unread,
Dec 14, 2014, 6:45:50 AM12/14/14
to django-d...@googlegroups.com
Thanks a lot.

To make it clear, do I understand right that you have some custom view to process APIPasswordChangeForm and not using django.contrib.auth.views.password_change?

Josh Smeaton

unread,
Dec 14, 2014, 4:18:07 PM12/14/14
to django-d...@googlegroups.com

Roman Akopov

unread,
Dec 15, 2014, 1:40:02 PM12/15/14
to django-d...@googlegroups.com
All right, that's exactly what I'm talking about.

You wrote more than 50 lines of python code, reimplemented standard inextensible features, just to call one function. And changing password is simple task actually, reseting password needs to deal with tokens, emails, etc. So it will be a few Much more code to write and much more things to understand. I'm not sure how Django tokens work, for instance, or how to use them correctly, so I'll probably copy-paste hoping I broke nothing. Default password_reset view has 12 parameters. Should I match signature of my custom view or not? There are a lot of questions related to reimplementing, which can be answered only by deep learning of Django sources.

And what if you have two backends? As far as I understand you'll have to dispatch this somehow in the view or form, i.e. you'll have to invent extensibility by yourself, or introduce tight coupling by mixing actions related to hardcoded list of backends. That is what I do not like in the first place.

Josh, what do you think? Do you see anything I missed? Or maybe you have some different priorities? I highly interested in you opinion.

Roman

Josh Smeaton

unread,
Dec 15, 2014, 5:18:48 PM12/15/14
to django-d...@googlegroups.com
Ok, I see where your suggestions could make a lot of sense. Thanks for persisting with me.

I've updated my gist again with what a refactor could allow it to look like: https://gist.github.com/jarshwah/c5b9abebb452f2e3286f

The backend remains unchanged. The form removes most custom handling, assuming that it would call methods on the user which delegates to the backend (which then calls methods on the user again if the ModelBackend is in use).

I keep my view as-is, but I could probably just use the contrib.auth.views.password_change view.

So yes, I can see the value in what you're proposing. But I would have a few concerns.

1. You'd need to make sure that you don't break backwards-compatibility
2. You shouldn't introduce new settings unless there's an extremely good reason
3. The refactored methods should be general enough to work with the vast majority of custom backends

I think you should put a patch together. It'll be easier to see the benefits when there is a working design to be reviewed. It's much easier to discuss an implementation rather than the idea for an implementation. Although if the idea is rejected, you should still be able to implement multiple backends in a relatively generic way, by dispatching based on the user.backend property.

Josh
Reply all
Reply to author
Forward
0 new messages