Django User Integration ¶
TL;DR
By default Django Gauth is session-only — it never touches Django's User
model, so there are no models and no migrations. Flip on
GOOGLE_USER_INTEGRATION and a successful Google sign-in also opens a real Django
auth session: request.user becomes the signed-in user, @login_required and
LoginRequiredMixin work, and the admin recognises them — all by mapping onto
your existing AUTH_USER_MODEL. Still zero django-gauth migrations.
Two modes, one flow ¶
Django Gauth deliberately ships off this feature so the lightweight, model-free story stays the default:
| Session-only (default) | User Integration (GOOGLE_USER_INTEGRATION = True) |
|
|---|---|---|
request.user |
AnonymousUser |
the Google-authenticated User |
@login_required / LoginRequiredMixin |
✗ | ✓ |
| Django admin login | ✗ | ✓ |
django-gauth models / migrations |
none | still none |
| Auth check | check_gauth_authentication() |
request.user.is_authenticated |
The magic is that integration reuses your project's own auth_user table via a
get-or-create — Django Gauth adds no tables of its own.
How it works ¶
When integration is on, callback() does one extra
step after verifying the Google ID token: it calls Django's authenticate() with the
verified id_info, and if a user comes back, login().
sequenceDiagram
participant U as User
participant G as Google
participant C as callback()
participant B as GoogleAuthBackend
participant DB as auth_user
U->>G: Sign in
G->>C: redirect + code
C->>C: verify id_token → id_info
C->>B: authenticate(request, id_info=id_info)
B->>DB: lookup by email (iexact)
alt no match & AUTO_CREATE
B->>DB: create user (unusable password)
end
B-->>C: User (or None)
C->>C: login(request, user)
C-->>U: redirect — request.user is set
The identity mapping is done by GoogleAuthBackend, which delegates
to get_or_create_user_from_id_info().
The backend is passive — called without an id_info it returns None, so ordinary
username/password logins fall straight through to ModelBackend.
Setup ¶
Three small edits — a flag, a backend, and (usually already present) the auth app + middleware.
# 1. Turn it on.
GOOGLE_USER_INTEGRATION = True
# 2. Register the backend (keep ModelBackend for password/admin logins).
AUTHENTICATION_BACKENDS = [
"django_gauth.backends.GoogleAuthBackend",
"django.contrib.auth.backends.ModelBackend",
]
# 3. These are Django defaults — make sure they're present.
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
# ...
"django_gauth",
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# ...
]
No migrations for django-gauth
You do not run makemigrations django_gauth — it has no models. Integration maps
onto the auth_user table you already migrate as part of django.contrib.auth.
Controlling the mapping ¶
Five GOOGLE_USER_* settings
tune the behaviour:
GOOGLE_USER_REQUIRE_VERIFIED_EMAIL(defaultTrue) — refuse sign-in unless Google reports the email as verified. This is your primary anti-hijack guard.GOOGLE_USER_LOOKUP_FIELD(default"email") — whichUserfield to match, always case-insensitively.GOOGLE_USER_AUTO_CREATE(defaultTrue) — create aUseron first sign-in, or restrict access to pre-provisioned accounts whenFalse.GOOGLE_USER_MAPPER— a dotted path to(id_info, user, created) -> Nonethat runs on every sign-in, so you can copy claims (roles, staff status, avatar) onto the user.
# myapp/auth.py
def map_google_user(id_info, user, created):
# Grant staff to your Google Workspace domain; sync the display name.
if id_info.get("hd") == "mycompany.com":
user.is_staff = True
user.first_name = id_info.get("given_name", user.first_name)
New users are created with an unusable password (set_unusable_password()), so the
only way in is Google — there's no password to guess.
Protecting views ¶
With integration on, the stock Django tools just work:
from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
return render(request, "dashboard.html")
Django Gauth also ships gauth_required and GauthRequiredMixin,
which are dual-mode: they use request.user when integrated and fall back to the
session check otherwise — so the same guard protects a view whether or not you've enabled
integration, and adds a response="json" option for SPA/XHR callers.
from django_gauth import gauth_required
@gauth_required(response="json") # 401 JSON instead of a redirect
def api_me(request):
return JsonResponse({"email": request.user.email})
Probing state from an SPA ¶
session_status() gains a top-level user_id
when integration is on — always present, and non-null only when a Django user is
authenticated. It lets a frontend correlate the session with a server-side user without
ever exposing the opaque sub claim:
When not to enable it ¶
Keep integration off if you want the lightest possible footprint — a pure
session/JWT-style flow with no User rows, ideal for a stateless SPA backend or a service
that only needs "is this a valid Google session?". You can always turn it on later; the
switch is additive and your existing session-only code keeps working unchanged.