A Django-focused Auth0 integration providing automated OIDC flows, account linking, and connected account (My Account API). It's been created to support the Auth0 Token Vault feature, which requires Connected Accounts flow.
It's an opinionated library focused on the needs of our own products. Feel free to fork it and adapt it to your needs.
This library uses
uuid7 for the ID columns when the Python version is 3.14 or higher. If you're using Python 3.12 or 3.13, it will use
uuid4 instead. If your project updates to Python 3.14, it will break. We know this behavior is unacceptable for a library. That's why we are letting you know in advance. Again, fork this library and adapt it to your needs.
The sample_app demonstrates how to use the library. It implements the following rules:
- Only required scopes are requested for social connections.
- Additional scopes are requested during the connected account request flow (progressive consent).
- When the user creates a connected account, that connected account is eligible for automatic account linking.
- Consider the following scenario:
- If a user logs in with
xpto@acme.comand addsqwerty@gmail.comas a connected account, logging in later with the Gmail address will link both, withxpto@acme.comremaining the primary account. No confirmation is required. - The same is true when the connected account matches the primary account.
- If a user logs in with
- Consider the following scenario:
- If a user signs up with an email/password, logs out, and later logs back in using a social connection with that same email, the accounts are automatically linked. The original email/password account is used as the primary account.
- If a user signs up via social, logs out, and later tries to log in with a password using the same email, they'll need to re-authenticate with the original social provider to link the accounts. The primary account is the social one.
Auth0 used to be the 'Stripe of Identity' sort of thing, known for its great developer experience. Lately, Iβm not so sure. I almost gave up on it, but after finding some workarounds, I decided to build this library. Iβm sharing it because seeing these issues go unaddressed hurts my software developer soul. π¬
Read the following Auth0 Community Questions for more details:
- Auth0 Fails to Store Refresh Tokens for Linked Accounts.
- I had built an integration using Token Vault, and it stopped. Understand why.
At the time of writing this README (2026-02-13), My Account API is not GA yet. It means this library might eventually break if Auth0 changes its API, again. π
Add auth0_oauth_client to your project dependencies. The library requires:
- Django 4.2+
requestsPyJWTauth0-python
INSTALLED_APPS = [
# ...
"auth0_oauth_client",
]The library ships with Django models (AccountLinking, ConnectedAccount,
AccountToken), so run migrations after installing:
python manage.py migrateAdd the AUTH0_OAUTH_CLIENT dict to your Django settings:
AUTH0_OAUTH_CLIENT = {
"auth0_domain": "your-tenant.auth0.com",
"auth0_management_api_domain": "your-tenant.auth0.com",
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"connections_for_account_linking": ["google-oauth2", "Username-Password-Authentication"],
# Optional
"authorization_params": {
"scope": "openid profile email offline_access",
"audience": "https://your-api.example.com",
},
"custom_scopes": {
"google-oauth2": [
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/userinfo.email",
],
},
"pushed_authorization_requests": False,
}The library does not ship with views or URL patterns. You implement them in your Django app using the
auth_client singleton. See the API Reference below and the
sample_app for a full working example.
from auth0_oauth_client import auth_client
from django.shortcuts import redirect
from django.urls import reverse
def login_view(request):
callback_url = request.build_absolute_uri(reverse("auth:callback"))
flow_url = auth_client.start_login(request, callback_url)
return redirect(flow_url)
def callback_view(request):
callback_url = request.build_absolute_uri(request.get_full_path())
auth_client.complete_login(request, callback_url)
return redirect("/dashboard/")
def logout_view(request):
return_to = request.build_absolute_uri("/")
logout_url = auth_client.logout(request, return_to=return_to)
return redirect(logout_url)All settings live under the AUTH0_OAUTH_CLIENT dictionary in your Django settings module.
| Key | Type | Description |
|---|---|---|
auth0_domain |
str |
Your Auth0 tenant domain (e.g. "acme.us.auth0.com"). Used for OAuth endpoints and the MyAccount API. |
auth0_management_api_domain |
str |
Domain for the Auth0 Management API. Often the same as auth0_domain, but can differ if you use a custom domain for login vs management. |
client_id |
str |
The Client ID of your Auth0 application. |
client_secret |
str |
The Client Secret of your Auth0 application. Used for token exchange and M2M grants. |
connections_for_account_linking |
list[str] |
List of Auth0 connection names you have in your account (e.g. ["google-oauth2", "Username-Password-Authentication"]). The library uses this to decide when to trigger the account linking flow. |
| Key | Type | Default | Description |
|---|---|---|---|
audience |
str |
None |
Default API audience for access tokens. |
authorization_params |
dict |
{} |
Default parameters sent on every /authorize call. Typically contains scope and audience. The scope value can be a string or a dict mapping audiences to scope strings. |
pushed_authorization_requests |
bool |
False |
Whether to use Pushed Authorization Requests (PAR). |
custom_scopes |
dict[str, list[str]] |
{} |
Connection-specific scopes for the Connected Accounts flow. Keyed by connection name (e.g. "google-oauth2"), with a list of OAuth scopes. These are merged with any scopes passed at call time. |
Derived setting:
my_account_audienceis automatically set tohttps://{auth0_domain}/me/and used for all MyAccount API calls.
Import the singleton client:
from auth0_oauth_client import auth_clientGenerates a PKCE code verifier/challenge and random state, stores the transaction in the Django session, and returns the Auth0
/authorize URL to redirect the user to.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The current Django request. |
redirect_uri |
str |
The absolute callback URL Auth0 will redirect to after authentication. |
authorization_params |
dict | None |
Extra query parameters to include in the /authorize URL (e.g. {"connection": "google-oauth2"}). |
Returns: Authorization URL (str).
callback_url = request.build_absolute_uri(reverse("auth:callback"))
flow_url = auth_client.start_login(request, callback_url)
return redirect(flow_url)Validates the state parameter, exchanges the authorization code for tokens via PKCE, decodes the ID token to extract
userinfo, and stores the session. Also creates/updates an AccountToken record for account linking.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The current Django request. |
callback_url |
str |
The full callback URL including query parameters (use request.build_absolute_uri(request.get_full_path())). |
Returns: Token set dict containing access_token, id_token, refresh_token, userinfo, expires_at, etc.
Raises: MissingRequiredArgumentOauthClientError if code or state is missing.
ApiOauthClientError on state mismatch or token exchange failure.
callback_url = request.build_absolute_uri(request.get_full_path())
auth_client.complete_login(request, callback_url)Clears the library's session data from the Django session and returns the Auth0 /v2/logout URL.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The current Django request. |
return_to |
str | None |
URL the user is redirected to after Auth0 logout. |
Returns: Auth0 logout URL (str).
logout_url = auth_client.logout(request, return_to=request.build_absolute_uri("/"))
django_logout(request) # Also clear Django's own session
return redirect(logout_url)Returns the Auth0 user ID (sub claim) from the current session, or None if there is no session.
Returns the refresh token from the current session, or None if there is no session.
These methods manage connected accounts through the Auth0 MyAccount API, enabling features like Token Vault.
auth_client.start_connect_account(request, connection, redirect_uri, scopes=None, authorization_params=None) -> str
Initiates the connected account flow. Gets an access token for the MyAccount API, calls the connect endpoint, and returns the URL to redirect the user to.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The current Django request. |
connection |
str |
The connection name (e.g. "google-oauth2"). |
redirect_uri |
str |
Callback URL after the connection flow completes. |
scopes |
list[str] | None |
Additional scopes to request. Merged with custom_scopes from settings. |
authorization_params |
dict | None |
Extra parameters for the authorization request. |
Returns: Connect URL (str) to redirect the user to.
Raises: MissingRequiredArgumentOauthClientError if redirect_uri is empty.
callback_url = request.build_absolute_uri(reverse("auth:callback"))
flow_url = auth_client.start_connect_account(
request,
connection="google-oauth2",
redirect_uri=callback_url,
scopes=["https://www.googleapis.com/auth/calendar"],
)
return redirect(flow_url)Completes the connected account flow. Validates the state and
connect_code, calls the MyAccount API completion endpoint, and stores a ConnectedAccount record.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The current Django request. |
callback_url |
str |
The full callback URL including query parameters. |
Returns: Connected account metadata dict (contains id, connection, etc.).
Raises: MissingRequiredArgumentOauthClientError, ApiOauthClientError.
# In your callback view, check for connect_code to distinguish from login
if "connect_code" in request.GET:
auth_client.complete_connect_account(request, callback_url)Lists all connected accounts for the authenticated user via the MyAccount API. Requires the
read:me:connected_accounts scope.
Lists all available connections that can be used for connected accounts. Requires the
read:me:connected_accounts scope.
Deletes a connected account by its ID, both from Auth0 and from the local database. Requires the
delete:me:connected_accounts scope.
Raises: MissingRequiredArgumentOauthClientError if connected_account_id is empty.
These methods handle automatic and manual account linking when users sign in with multiple identity providers that share the same email.
Checks whether the current login requires account linking. Handles two scenarios automatically:
- Social provider login: Automatically merges and links accounts if a matching account exists.
- Password login: Stores a pending linking payload in the session and returns
is_pending_account_linking: True, requiring the user to confirm by re-authenticating with the original provider.
Returns: {"is_pending_account_linking": bool, "is_account_linked": bool}.
Completes a pending account linking flow (the manual confirmation path). Verifies the user re-authenticated with the correct account, merges metadata, links the accounts in Auth0, and updates local records.
Returns: {"success": True} on success,
{"success": False, "used_different_account": True} if the wrong account was used, or
None if there was no pending linking.
Returns the pending account linking payload from the session, or
None if there is no pending flow. Useful for rendering a confirmation page.
Cancels a pending account linking flow by removing the payload from the session.
auth_client.get_access_token_for_connection_using_user_refresh_token(refresh_token, connection: str) -> GoogleTokenResult
Exchanges a user's refresh token for a connection-specific access token using the federated connection access token grant. This is the core mechanism for calling third-party APIs (e.g. Google, Facebook) on behalf of the user.
| Parameter | Type | Description |
|---|---|---|
refresh_token |
str |
The user's Auth0 refresh token. |
connection |
str |
The connection name (e.g. "google-oauth2"). |
Returns: Dict with access_token, expires_in, and scope.
refresh_token = request.user.youruser.idp_refresh_token
result = auth_client.get_access_token_for_connection_using_user_refresh_token(
refresh_token, "google-oauth2"
)
# Use result["access_token"] to call Google APIsFetches the full user profile from the Auth0 Management API using an M2M (client credentials) token. The M2M token is cached automatically. Results are cached for 24 hours and invalidated when _update_user is called for the same user.
| Parameter | Type | Description |
|---|---|---|
user_id |
str |
The Auth0 user ID (e.g. "auth0|abc123"). |
Returns: Full Auth0 user profile dict including identities, email, user_metadata, etc.
Returns the userinfo payload from the current session, or None if no session exists.
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
The Django request. |
Returns: The userinfo dict from the session (decoded from the ID token at login), or None.
Readiness probe for the Auth0 Management API integration. In a single round-trip to /oauth/token, verifies that:
client_id+client_secretcan mint an M2M token for the Management API audience.- The issued token's
issandaudclaims match the configured tenant. - The token's
scopeclaim contains every entry inrequired_scopes.
Always bypasses the M2M token cache and never writes to it, so credential rotation and scope drift are detected on every probe without disturbing production token TTLs.
Auth0 silently drops unauthorized scopes from the token response rather than erroring, so inspecting the issued JWT's scope claim is the only way to verify the full grant in a single call.
| Parameter | Type | Description |
|---|---|---|
timeout |
float |
HTTP timeout in seconds for the token request. Defaults to 5.0. |
required_scopes |
Iterable[str] |
Scopes the caller's project needs granted on its M2M client_grant. Empty (default) skips the scope check. |
Returns: {"ok": True, "expires_in": int, "granted_scopes": list[str], "latency_ms": int}.
Raises: Auth0PingError with one of three stage values:
stage |
Populates | Remediation |
|---|---|---|
m2m_token |
status_code (when HTTP) |
Check client_id/client_secret; confirm M2M app is authorized for the Management API audience. |
token_claims |
β | Check auth0_management_api_domain matches what Auth0 actually issues. |
scope_grant |
missing_scopes (frozenset) |
Grant the listed missing scopes on the M2M client_grant. |
Example: a django-health-check backend that asserts the project's full M2M scope grant on every probe.
import logging
from auth0_oauth_client import auth_client, Auth0PingError
from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import ServiceUnavailable
_logger = logging.getLogger("yourapp")
# Source of truth for these scopes: the project's Terraform `auth0_client_grant`.
_REQUIRED_M2M_SCOPES = frozenset({
"read:users",
"read:users_app_metadata",
"update:users",
})
class Auth0HealthCheck(BaseHealthCheckBackend):
critical_service = True
def check_status(self):
try:
auth_client.ping(timeout=5.0, required_scopes=_REQUIRED_M2M_SCOPES)
except Auth0PingError as exc:
_logger.exception("Auth0 healthcheck failed at stage %s", exc.stage)
detail = f"Auth0 {exc.stage} check failed"
if exc.missing_scopes:
detail += f" (missing: {sorted(exc.missing_scopes)})"
self.add_error(ServiceUnavailable(detail), exc)