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?
- Stay small, simple and extensible.
- Offer tools and utilities instead of imposing a framework on you.
- 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, Microsoft, Facebook and Twitter OAuth clients require the following settings:
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETMICROSOFT_CLIENT_IDMICROSOFT_CLIENT_SECRETFACEBOOK_CLIENT_IDFACEBOOK_CLIENT_SECRETTWITTER_CLIENT_IDTWITTER_CLIENT_SECRET
Microsoft logins accept work, school and personal accounts from any tenant by
default. Set MICROSOFT_TENANT_ID to your directory (tenant) ID, which is
shown on the app registration's overview page in the Azure portal, to only
accept accounts of your organization; the authorization and token endpoints are
derived from it. Single-tenant app registrations require this setting.
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).
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.microsoft import MicrosoftOAuth2Client
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/microsoft/$",
views.oauth2,
{
"client_class": MicrosoftOAuth2Client,
},
name="accounts_oauth_microsoft",
),
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",
),
]The authlib.admin_oauth app allows using Google or Microsoft 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]),
]Note the ^.* in the pattern above: the callable is passed the match, not
the email address, so match[0] is only the part of the address which the
pattern matched. (r"@example\.org$", lambda match: match[0]) returns
"@example.org" and therefore never authenticates anyone. Use
match.string if you'd rather not anchor the pattern. A system check
(authlib.E004) runs the callables in ADMIN_OAUTH_PATTERNS against an
example address generated from their pattern and complains if what comes back
isn't an email address, so this class of mistake fails manage.py check
instead of only failing logins.
Failing logins are logged to the authlib.admin_oauth logger together with
the addresses the patterns produced; the message shown in the browser only
mentions the visitor's own address, since anyone with an account at the OAuth
provider can reach the view.
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.
Single sign-on with an identity provider which enforces MFA is only worth
something if a password can't be used instead: as long as Django still accepts
one, the MFA is optional in practice. disable_passwords closes the admin
site's login form and its password change page:
from django.contrib import admin
from django.urls import include, path
from authlib.admin_oauth.passwords import disable_passwords
disable_passwords(admin.site)
urlpatterns = [
path("", include("authlib.admin_oauth.urls")),
path("admin/", admin.site.urls),
# ...
]The login page then only shows the single sign-on buttons: no username and
password inputs are rendered at all, and a POST with credentials is refused
before authenticate() is called, so no password ever reaches an
authentication backend. The password change page only explains itself.
Call disable_passwords before the admin site's URLs are built -- at the top
of the ROOT_URLCONF module as above, or in an admin.py, which is imported
while the apps are loading. The authlib.E012 system check complains if the
call came too late for the password change page.
Locally you probably have no OAuth credentials, so skip the call in development:
if not settings.DEBUG:
disable_passwords(admin.site)The templates come from authlib.admin_oauth; pass login_template and/or
password_change_template to use your own (authlib.E011 tells you if a
template cannot be loaded).
Only the admin site is affected. Everything else which sets passwords keeps
working: django.contrib.auth's password change and password reset views,
the user admin's "change password" form, manage.py changepassword. The
first two matter more than they look, since any active staff session gets into
the admin whether or not the admin's login form created it -- so manage.py
check warns (authlib.W003) when they appear in the URLconf. If the
non-staff users of your site do need passwords, silence the check:
SILENCED_SYSTEM_CHECKS = ["authlib.W003"]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_APPS - Set
AUTH_USER_MODEL = "little_auth.User" - Optionally also follow any of the steps above.
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.
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.
Add authlib.backends.RolePermissionsBackend to
AUTHENTICATION_BACKENDS, before any backend that checks
database-level permissions (such as ModelBackend or EmailBackend):
AUTHENTICATION_BACKENDS = [
"authlib.backends.RolePermissionsBackend",
"authlib.backends.EmailBackend", # or any other auth backend
]RolePermissionsBackend 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.
It answers permission checks and nothing else -- it authenticates nobody, so
it has to be accompanied by a backend which does. Add
django.contrib.auth.backends.ModelBackend if your project has
username/password logins. (Until it was renamed this class extended
ModelBackend, so password logins used to run through it, which nothing
told you about.)
RolePermissionsBackend 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.
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.
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.
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.