django-authlib - Authentication utils for Django¶
authlib is a collection of authentication utilities for implementing passwordless authentication. This is achieved by either sending cryptographically signed links by email, or by fetching the email address from third party providers such as Google, Facebook and Twitter. After all, what’s the point in additionally requiring a password for authentication when the password can be easily resetted on most websites when an attacker has access to the email address?
Goals¶
Stay small, simple and extensible.
Offer tools and utilities instead of imposing a framework on you.
Usage¶
Install
django-authlibusing pip into your virtualenv.Add
authlib.backends.EmailBackendtoAUTHENTICATION_BACKENDS.Adding
authlibtoINSTALLED_APPSis optional and only useful if you want to use the bundled translation files. There are no required database tables or anything of the sort.Have a user model which has a email field named
emailas username. For convenience a base user model and manager are available in theauthlib.base_usermodule,BaseUserandBaseUserManager. TheBaseUserManageris automatically available asobjectswhen you extend theBaseUser.Use the bundled views or write your own. The bundled views give feedback using
django.contrib.messages, so you may want to check that those messages are visible to the user.
The Google, Facebook and Twitter OAuth clients require the following settings:
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETFACEBOOK_CLIENT_IDFACEBOOK_CLIENT_SECRETTWITTER_CLIENT_IDTWITTER_CLIENT_SECRET
Note that you have to configure the Twitter app to allow email access, this is not enabled by default.
Note
If you want to use OAuth2 providers in development mode (without HTTPS) you
could add the following lines to your settings.py:
if DEBUG:
# NEVER set this variable in production environments!
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
This is required because of the strictness of oauthlib which only wants HTTPS URLs (and rightly so).
Use of bundled views¶
The following URL patterns are an example for using the bundled views.
For now you’ll have to dig into the code (it’s not much, at the time of
writing django-authlib’s Python code is less than 500 lines):
from django.conf.urls import url
from authlib import views
from authlib.facebook import FacebookOAuth2Client
from authlib.google import GoogleOAuth2Client
from authlib.twitter import TwitterOAuthClient
urlpatterns = [
url(
r"^login/$",
views.login,
name="login",
),
url(
r"^oauth/facebook/$",
views.oauth2,
{
"client_class": FacebookOAuth2Client,
},
name="accounts_oauth_facebook",
),
url(
r"^oauth/google/$",
views.oauth2,
{
"client_class": GoogleOAuth2Client,
},
name="accounts_oauth_google",
),
url(
r"^oauth/twitter/$",
views.oauth2,
{
"client_class": TwitterOAuthClient,
},
name="accounts_oauth_twitter",
),
url(
r"^email/$",
views.email_registration,
name="email_registration",
),
url(
r"^email/(?P<code>[^/]+)/$",
views.email_registration,
name="email_registration_confirm",
),
url(
r"^logout/$",
views.logout,
name="logout",
),
]
Admin OAuth2¶
The authlib.admin_oauth app allows using Google OAuth2 to allow all
users with the same email domain to authenticate for Django’s
administration interface. You have to use authlib’s authentication
backend (EmailBackend) for this.
Installation is as follows:
Follow the steps in the “Usage” section above.
Add
authlib.admin_oauthto yourINSTALLED_APPSbeforedjango.contrib.admin, so that our login template is picked up.Add
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETto your settings as described above.Add a
ADMIN_OAUTH_PATTERNSsetting. The first item is the domain, the second the email address of a staff account. If no matching staff account exists, authentication fails:
ADMIN_OAUTH_PATTERNS = [
(r"@example\.com$", "admin@example.com"),
]
Add an entry to your URLconf:
urlpatterns = [
url(r"", include("authlib.admin_oauth.urls")),
# ...
]
Add
https://yourdomain.com/admin/__oauth__/as a valid redirect URI in your Google developers console.
Please note that the authlib.admin_oauth.urls module assumes that the admin
site is registered at /admin/. If this is not the case you can integrate
the view yourself under a different URL.
It is also allowed to use a callable instead of the email address in the
ADMIN_OAUTH_PATTERNS setting; the callable is passed the result of matching
the regex. If a resulting email address does not exist, authentication (of
course) fails:
ADMIN_OAUTH_PATTERNS = [
(r"^.*@example\.org$", lambda match: match[0]),
]
If a pattern succeeds but no matching user with staff access is found processing continues with the next pattern. This means that you can authenticate users with their individual accounts (if they have one) and fall back to an account for everyone having a Google email address on your domain:
ADMIN_OAUTH_PATTERNS = [
(r"^.*@example\.org$", lambda match: match[0]),
(r"@example\.com$", "admin@example.com"),
]
You could also remove the fallback line; in this case users can only authenticate if they have a personal staff account.
Little Auth¶
The authlib.little_auth app contains a basic user model with email
as username that can be used if you do not want to write your own user
model but still profit from authlib’s authentication support.
Usage is as follows:
Add
authlib.little_authto yourINSTALLED_APPSSet
AUTH_USER_MODEL = "little_auth.User"Optionally also follow any of the steps above.
Email Registration¶
For email registration to work, two templates are needed:
registration/email_registration_email.txtregistration/email_registration.html
A starting point would be:
email_registration_email.txt:
Subject (1st line)
Body (3rd line onwards)
{{ url }}
...
email_registration.html:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important: {% endif %}
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}
{% translate "Please correct the error below." %}
{% else %}
{% translate "Please correct the errors below." %}
{% endif %}
</p>
{% endif %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="errornote">
{{ error }}
</p>
{% endfor %}
{% endif %}
<form action='{% url "email_registration" %}' method="post" >
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit" value="login">
</form>
The above template is inspired from:
More details are documented in the relevant module.
Roles¶
authlib.roles provides a lightweight role-based permission system for
staff users. Instead of assigning individual Django permissions to each
staff member, you define named roles in AUTHLIB_ROLES and attach a
permission-checking callback to each role.
RoleField is a CharField that stores the role on the user model and
hooks into Django’s permission system. authlib.little_auth.User already
includes a role = RoleField() field, so if you use Little Auth you only
need to configure the setting.
Setup¶
Add authlib.backends.PermissionsBackend to AUTHENTICATION_BACKENDS,
before any backend that checks database-level permissions (such as
ModelBackend or EmailBackend):
AUTHENTICATION_BACKENDS = [
"authlib.backends.PermissionsBackend",
"authlib.backends.EmailBackend", # or any other auth backend
]
PermissionsBackend routes has_perm() calls to the role callback
injected by RoleField. It also implements get_all_permissions() by
iterating every permission in the database through the callback, which is
what drives the Django admin’s per-app sidebar visibility.
PermissionsBackend must come first for two reasons: it avoids an
unnecessary database query when the role callback already has an answer, and
it ensures that deny patterns cannot be bypassed by a database-level
permission grant that would otherwise short-circuit the check.
Configuration¶
Add AUTHLIB_ROLES to your settings. Each key is a role identifier; each
value is a dict with at minimum a "title" (used as the human-readable
choice label) and optionally a "callback" function:
from functools import partial
from django.utils.translation import gettext_lazy as _
from authlib.roles import allow_deny_globs
AUTHLIB_ROLES = {
"default": {
"title": _("Default"),
# No callback → no extra permissions beyond Django's own checks
},
"readonly": {
"title": _("Read-only"),
# Grant all view permissions, nothing else
"callback": partial(allow_deny_globs, allow={"*.view_*"}),
},
"editor": {
"title": _("Editor"),
# Grant everything except user/auth management
"callback": partial(
allow_deny_globs,
allow={"*"},
deny={"auth.*", "little_auth.*", "admin_sso.*"},
),
},
"support": {
"title": _("Support"),
# Grant all permissions
"callback": partial(allow_deny_globs, allow={"*"}),
},
}
The callback receives three keyword arguments: user, perm, and
obj. It should return True to grant the permission, raise
django.core.exceptions.PermissionDenied to explicitly deny it, or return
a falsy value to let Django’s normal permission checks continue.
When only one role is defined the RoleField renders as a hidden input in
forms, so you can add the field to existing models without cluttering the UI.
allow_deny_globs¶
authlib.roles.allow_deny_globs is a ready-made callback that matches the
permission string ("app_label.codename") against two lists of
fnmatch-style glob patterns:
deny– patterns checked first; a match raisesPermissionDenied.allow– patterns checked second; a match grants the permission.
Use functools.partial to bind the pattern lists, as shown above.
Because permissions are matched by glob rather than enumerated explicitly,
roles automatically cover new models as they are added. For example, an
"editor" role with allow={"cms.*"} will grant access to every new
CMS plugin model without any manual permission assignment.
Adding RoleField to a custom user model¶
If you are not using Little Auth, add the field to your own user model:
from authlib.roles import RoleField
class MyUser(AbstractBaseUser, ...):
role = RoleField()
Then run makemigrations. The field’s deconstruct method omits the
choices from the migration so that adding or renaming roles does not require
a new migration.
Change log¶
Next version¶
Added two missing methods to the
PermissionsBackendso that the admin app list works correctly.Added verification of the
nextcookie value also when setting the cookie, not just when reading it.Added Python 3.13, Django 5.2a1.
0.17 (2024-08-19)¶
Changed the roles implementation to allow using arbitrary names for the role field.
Stopped crashing when encountering an unknown role – doing nothing in
has_permis an acceptable fallback.Force account selection when failing to authenticate once in the Django admin using a Google account.
Added support for Django 5.1.
Exempted our login views from the
LoginRequiredMiddleware.Dropped Django 4.1 from the CI. 3.2 is still there.
Changed the default
authlib.little_authadmin to hide the user permissions field; permissions should preferrably be added via authlib roles, or less preferrably via group permissions.
0.16 (2023-09-17)¶
Fixed
pyproject.tomlso that data files are actually included.Dropped compatibility with Python 3.8.
Added utilities for role-based permissions. The idea is to allow a less manual way to specify permissions for groups of users, e.g. content managers which should automatically have access to all models in a list of apps without having to manually update the list of permissions in the Django administration interface.
0.15 (2023-07-07)¶
Added Python 3.11.
Switched to hatchling and ruff.
Added the option to create admin users during admin OAuth if one doesn’t exist already. The
ADMIN_OAUTH_CREATE_USER_CALLBACKsetting should be set to the Python path of a callable receiving the request and the email address; this callable can (but doesn’t have to) create a new user for the email address if one doesn’t exist already. The default is to not create any users. AddingADMIN_OAUTH_CREATE_USER_CALLBACK = "authlib.admin_oauth.views.create_superuser"makes creation of new superuser accounts automatic.
0.14 (2023-03-21)¶
Added Django 4.1 and 4.2 to the CI matrix.
Made the bundled OAuth2 views pass the exception message to
messages.errorto ease debugging a bit.Changed the confirmation code used by
authlib.emailto be base64 encoded. This avoids problems where some email clients would mangle the link because of the included email address. Older codes are still accepted for the moment.Added a note regarding
OAUTHLIB_INSECURE_TRANSPORTto the README.
0.13 (2022-02-28)¶
Added a
default_auto_fieldto thelittle_authappconfig.
0.12 (2022-01-04)¶
Added pre-commit.
Dropped Python < 3.8, Django < 3.2.
Added docs for how to integrate the email registration functionality.
0.11 (2021-11-22)¶
Switched to a declarative setup.
Switched from Travis CI to GitHub actions.
Added Python 3.10, Django 4.0 to the CI.
Avoided the additional request to Google endpoints since the access token already contains identity information in the
id_tokenfield.
0.10 (2020-10-04)¶
Modified
authlib.admin_oauthto persist the users’ email address and pass it to Google as alogin_hintso that website managers do not have to repeatedly select the account over and over.Allowed specifying arbitrary query parameters for Google’s authorization URL.
Fixed an
authlib.admin_oauthcrash when fetching user data fails.Replaced
ugettext*withgettext*.Replaced
url()withre_path().Fixed a crash when creating
little_authusers with invalid email addresses.Stopped carrying over login hints from one user to the other in the Google OAuth client…
BACKWARDS INCOMPATIBLE Dropped the request argument from
authlib.email.get_confirmation_code, it wasn’t used, ever.
0.9 (2019-02-09)¶
Dropped support for Python 2.
Fixed a few problems around inactive users where authlib would either handle them incorrectly or reveal that inactive users exist.
Added many unittests, raised the code coverage to 100% (except for the uncovered Facebook and Twitter OAuth clients). Switched to mocking requests and responses instead of simply replacing the
GoogleOAuth2Clientfor testing.Moved the
BaseUserandBaseUserManagertoauthlib.base_userfor consistency withdjango.contrib.auth.base_user.Dropped the useless
OAuthClientbase class.Removed compatibility code for Django<1.11 when verifying whether a redirection URL is safe.
Changed the
retrieve_nextimplementations to only consider HTTPS URLs as safe when processing HTTPS requests.Changed the admin OAuth functionality to also use the cookies code from
authlib.viewsfor redirecting users after authentication.Fixed a possible crash in the Twitter OAuth flow when the token from the authentication redirect cannot be determined anymore.
Fixed a crash in the OAuth2 view if fetching user data fails.
0.8 (2018-11-17)¶
BACKWARDS INCOMPATIBLE Replaced the email registration functionality of referencing users with arbitrary payloads. This allows not only verifying the email address but also additional data which may or may not be related to the user in question. On the other hand the comparison of
last_logintimestamps is gone, which means that links may be reused as long as less thanmax_ageseconds have passed. This makes it even more important to keepmax_agesmall. The change mostly affects the functions inauthlib.email.
0.7 (2018-11-04)¶
Fixed a race condition when creating new users by using
get_or_createinstead of some homegrownexistsandcreatetrickery.Changed all locations to pass
new_useras keyword argument topost_login_response.Changed the
admin/login.htmltemplate inauthlib.admin_oauthto make the SSO button a bit more prominent. Also, replaced “SSO” with “Google” because that is all that is supported right now.Added the possibility to use callables in
ADMIN_OAUTH_PATTERNSinstead of hard-coded staff email addresses.Extracted the confirmation code generation from
get_confirmation_urlasget_confirmation_code.Fixed usage of deprecated Google OAuth2 scopes.
Added compatibility with Python 2.
Extracted the post login redirect cookie setting into a new
set_next_cookiedecorator.Dropped compatibility shims for Django<1.11.
Changed the
EmailBackendto use_default_managerinstead of assuming that the default manager is calledobjects.Fixed an edge case bug where
render_to_mailwould crash when encountering an empty text for the subject and body.Enforced keyword-only usage of the views and functions in
authlib.viewswhere it is appropriate.Removed the default messages emitted when creating a new user and when logging out.
Added a
post_logout_responsecallable and argument toauthlib.views.logoutto customize messages and redirects after logging an user out.Added a
email_logincallable and argument to theoauth2andemail_registrationview to customize the creation, authentication and login of users.Changed the
EmailRegistrationFormto save the request asself.request, notself._request. Made use of this for moving the email sending to the form class as well, further shortening the view.
0.6 (2017-12-04)¶
Fixed usage of a few deprecated APIs.
Modified
little_auth.Userto fall back to an obfuscated email address if the full name is empty.Made it possible to override the default max age of three hours for magic links sent by email.
Fixed a problem where the
little_authmigrations were depending on the latestdjango.contrib.authmigration instead of the first migration without good reason.
0.5 (2017-05-17)¶
Moved from
ADMIN_OAUTH_DOMAINStoADMIN_OAUTH_PATTERNSto allow regular expression searching.Finally started adding tests.
Added django-authlib documentation to Read the Docs.
0.4 (2017-05-11)¶
Added some documentation to the README.
Google client: Removed the deprecated profile scope, and switched to online access only (we do not need offline access).
Added the
authlib.admin_oauthapp for a minimal Google OAuth2 authentication solution for Django’s administration interface.Added the
authlib.little_authapp containing a minimal user model with email as username for a quick and dirtyauth.Userreplacement.Allow overriding the view name used in
authlib.email.get_confirmation_url.
0.3 (2016-12-08)¶
Fixed the redirect URL generation of the Facebook and Google client.
Changed the name of the post login redirect cookie from
nexttoauthlib-nextto hopefully prevent clashes.Authentication providers may also return
Noneas email address; handle this case gracefully by showing an error message instead of crashing.Pass full URLs, not only paths to the OAuth2 libraries because otherwise, secure redirect URLs aren’t recognized as such.
0.2 (2016-11-22)¶
Added views for registration and logging in and out.
Added a base user model and an authentication backend for authenticating using email addresses only.
0.1 (2016-11-21)¶
Initial release containing helpers for authentication using an email address, either verified by sending a magic link or retrieved from Facebook, Google or Twitter.