Part V: Real-World Case Studies
7.13 Case Study 1: Tracing Django OAuth Login
The Setup: You've inherited a Django project using django-allauth for social authentication. Users report intermittent login failures with Google OAuth, but there are no error logs. Your task: understand exactly what happens when a user clicks "Login with Google" so you can identify where failures might occur.
The initial confusion: Reading the django-allauth documentation shows configuration examples and high-level flow diagrams, but you need to know: What code actually executes? In what order? What database queries happen? Where could failures hide?
You tried reading the source code. The entry point seems to be a URL pattern that points to a view... which inherits from another view... which uses mixins... which calls adapter methods... and now you're seven files deep with no clear picture of the execution flow.
This is the classic "documentation doesn't match reality" problem. You need to see the actual execution, not the idealized description.
Problem: Understanding django-allauth authentication flow
Let's walk through the complete investigation, modeling expert problem-solving step by step.
Phase 1: High-level mapping with Django Debug Toolbar (10 minutes)
First, install Django Debug Toolbar if it's not already in the project:
pip install django-debug-toolbar
Add to settings.py:
INSTALLED_APPS = [
# ... other apps
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... other middleware
]
INTERNAL_IPS = ['127.0.0.1']
Add to urls.py:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
# ... other patterns
]
Now run your development server and click "Login with Google". The Debug Toolbar appears in your browser. Open the SQL panel first.
What you see: Three database queries executed during the initial redirect:
-- Query 1: Check for existing social app configuration
SELECT * FROM socialaccount_socialapp WHERE provider = 'google'
-- Query 2: Fetch social app provider details
SELECT * FROM socialaccount_socialapp_sites
WHERE socialapp_id = 1 AND site_id = 1
-- Query 3: Create a state token for OAuth
INSERT INTO socialaccount_sociallogin_state
(state, created, next_url) VALUES (?, ?, ?)
This is your first major insight: django-allauth stores OAuth state in the database, not in sessions. This explains why state might be lost if database connections are flaky.
Now check the "Request History" panel in Debug Toolbar. You see two requests:
-
Request 1:
GET /accounts/google/login/β302 Redirectto Google -
Request 2:
GET /accounts/google/login/callback/?code=...&state=...β302 Redirectto your app's homepage
Notice this carefully: The OAuth flow involves two separate HTTP requests with two redirects. Failures could occur in either request or during either redirect.
But Debug Toolbar doesn't show you the middleware chain or signal handlers. That's where you need the debugger.
Phase 2: Deep dive with VS Code debugger (20 minutes)
Create a .vscode/launch.json if you don't have one:
{
"version": "0.2.0",
"configurations": [
{
"name": "Django: Debug Server",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver", "--noreload"],
"django": true,
"justMyCode": false
}
]
}
The crucial setting here is "justMyCode": false. This allows you to step into django-allauth's code, not just your own.
Start the debugger and set a breakpoint in the django-allauth view. But where is that view? You don't know the exact file path yet. Here's the expert move: set a breakpoint on the URL pattern itself.
Open urls.py where allauth URLs are included:
urlpatterns = [
path('accounts/', include('allauth.urls')),
]
You can't set a breakpoint on an include(), but you can find where it leads. Open a Python shell:
from allauth.urls import urlpatterns
print(urlpatterns)
This shows you that /accounts/google/login/ maps to allauth.socialaccount.providers.oauth2.views.OAuth2LoginView.
Now set a breakpoint in that file (even though it's in your virtual environment's site-packages). In VS Code, use Ctrl+P and type OAuth2LoginView to jump to the file.
Set a breakpoint at the start of the dispatch() method:
class OAuth2LoginView(View):
def dispatch(self, request, *args, **kwargs):
# <-- BREAKPOINT HERE
provider = self.adapter_class(request)
app = provider.get_app(request)
# ...
Click "Login with Google" in your browser. The breakpoint hits. Now step through with F10 (step over) and F11 (step into), watching the call stack in VS Code's left panel.
What you discover as you step through:
- Seven middleware components process the request before reaching the view:
-
SessionMiddleware(loads the session) -
AuthenticationMiddleware(checks if already logged in) -
MessageMiddleware(for flash messages) -
debug_toolbar.middleware.DebugToolbarMiddleware -
Three custom middleware from the project (rate limiting, logging, feature flags)
-
The view execution flow:
-
OAuth2LoginView.dispatch()callsget_app()β database query for social app config -
Generates OAuth state parameter
-
Stores state in database (the INSERT query you saw earlier)
-
Constructs Google's authorization URL
-
Returns
HttpResponseRedirectto Google -
After Google redirects back (the callback request):
-
Same seven middleware components run again
-
OAuth2CallbackView.dispatch()executes -
Validates the
stateparameter against the database -
Exchanges the authorization code for an access token (HTTP request to Google)
-
Calls
complete_login()β this triggers signals
This is where the invisible behavior happens. Set a breakpoint in complete_login():
def complete_login(request, app, token, **kwargs):
login = adapter.complete_login(request, app, token, response=response)
login.state = SocialLogin.state_from_request(request)
# <-- BREAKPOINT HERE - watch for signals
signals.pre_social_login.send(sender=SocialLogin, request=request, sociallogin=login)
return login
When you hit this breakpoint, check the "Call Stack" panel in VS Code. You'll see the full execution path that led here. Step into the signal dispatch with F11.
This is the key insight: Three signal handlers are connected to pre_social_login:
-
A custom handler in your project's
signals.pythat checks if the email domain is allowed (.eduemails only) -
django-allauth's built-in handler that associates the social account with existing users
-
Another custom handler that logs authentication attempts to an audit table
The smoking gun: The first custom handler can raise an ImmediateHttpResponse exception to reject logins. This exception doesn't appear in logs because it's caught by django-allauth's exception handling and turned into a silent redirect with an error message stored in the session.
When you inspect the exception handling code (by stepping through), you find:
try:
signals.pre_social_login.send(...)
except ImmediateHttpResponse as e:
return e.response # Redirects to login page with error in session
The error message goes into the session via messages.error(), but if SessionMiddleware fails to save the session (database write issue, Redis connection problem), the error disappears entirely. The user just sees the login page again with no explanation.
Phase 3: Validation and documentation (15 minutes)
Now you understand the complete flow. Document it with a sequence diagram:
User Browser β Django (OAuth2LoginView)
β Query: Get social app config
β Query: Create state token
β 302 Redirect to Google
Google β User (consent screen)
User β Google (clicks "Allow")
Google β Django (OAuth2CallbackView) with code + state
β Query: Validate state token
β HTTP Request to Google: Exchange code for access token
β Signal: pre_social_login
β Custom handler: Check email domain
β Custom handler: Log to audit table (Query 3)
β If signals pass: Create/update user
β SessionMiddleware: Save session
β 302 Redirect to homepage
You now know where failures can hide:
-
Database query failures: Any of the three queries could timeout
-
Signal handler exceptions: Custom domain checking can silently reject
-
Session save failures: Error messages disappear if session write fails
-
Google API failures: The token exchange HTTP request could fail
Tools used: Django Debug Toolbar + VS Code debugger
This investigation demonstrates the complementary strengths of each tool:
Django Debug Toolbar gave you:
-
Immediate visibility into database queries (the three SQL statements)
-
Request/response cycle visualization (the two redirects)
-
Middleware execution order (listed in the Request History panel)
-
Performance timing (each query's duration)
VS Code debugger gave you:
-
Step-by-step code execution visibility
-
Call stack inspection (seeing how 7 files deep you really were)
-
Signal handler discovery (invisible from outside the debugger)
-
Local variable inspection (seeing the exact state, token, and user data at each point)
-
The ability to explore third-party code (
django-allauth) as easily as your own
Notice what you didn't use: print statements, custom instrumentation, AST modification, or logging additions. The tools already built into Django and your IDE were sufficient.
Discovery: 7 middleware components, 3 database queries, 2 redirect cycles
Let's break down each discovery and why it matters:
The 7 middleware components: Understanding middleware order is crucial because:
-
SessionMiddlewaremust run beforeAuthenticationMiddleware(auth checks need session data) -
Custom rate limiting middleware could block login attempts before they reach the view
-
Feature flag middleware might disable OAuth entirely for certain users
-
Each middleware can short-circuit the request and return early
You discovered this by examining the call stack when your breakpoint hit. The stack showed:
OAuth2LoginView.dispatch() <-- Your breakpoint
β middleware_3_process_view()
β middleware_2_process_view()
β middleware_1_process_view()
β SessionMiddleware.__call__()
β WSGIHandler.__call__()
The 3 database queries: Each query is a potential failure point:
-
Social app config lookup: If this returns no results, login fails immediately
-
State token insert: If database is read-only or full, OAuth flow cannot start
-
Audit log insert: If this fails in a signal handler with improper exception handling, the entire login could abort
The 2 redirect cycles: This is critical for debugging intermittent failures:
-
Redirect 1 (Django β Google): Failure here means users never see Google's consent screen
-
Redirect 2 (Google β Django β Homepage): Failure here means successful Google auth that never completes in Django
Intermittent failures could affect only one redirect. Your investigation revealed that most reported failures happened on redirect 2, suggesting signal handler or session save issues, not Google API problems.
Time to understanding: 45 minutes
Let's break down how the time was spent:
-
10 minutes: Installing Debug Toolbar, triggering login, reviewing SQL and request panels
-
5 minutes: Locating the view class, setting up debugger configuration
-
15 minutes: Stepping through both request cycles with breakpoints, examining call stack
-
10 minutes: Discovering signal handlers, understanding exception handling
-
5 minutes: Drawing the sequence diagram, documenting failure points
Compare this to alternative approaches you might have tried:
Reading source code alone: You started here. After 30 minutes you were lost in inheritance chains and had no clarity about execution order. Estimated total time to understanding: 3-4 hours of reading, still with gaps in knowledge.
Adding print statements: You'd need to modify django-allauth code in your virtual environment (bad practice), add prints in 10+ locations, restart server after each change, parse terminal output. Estimated time: 2 hours, with risk of missing invisible signal handlers.
Writing custom instrumentation: Building an AST transformer or decorator system to trace django-allauth execution: 4-8 hours of development, ongoing maintenance burden, still wouldn't show database queries or middleware order without extensive additional work.
The professional toolsβDebug Toolbar and a debuggerβgave you complete understanding in 45 minutes because they're purpose-built for exactly this kind of investigation.
Key insight: Signal handlers modify behavior invisibly
This is the crucial lesson from this case study: Django's signal system allows code to inject behavior at specific points without visible call chains.
When you read the django-allauth source code, you saw:
login = complete_login(request, app, token)
return login
This looks straightforward. But the actual execution includes:
login = complete_login(request, app, token)
# Invisible from reading: signals.pre_social_login.send()
# β Your custom domain checker runs
# β Audit logger runs
# β Any other signal handlers registered anywhere in the project run
return login
You cannot discover signal handlers by reading code unless you:
-
Search the entire codebase for
@receiver(pre_social_login) -
Check
apps.pyfiles forready()methods that might register handlers -
Inspect
settings.pyfor apps that might register handlers in theirAppConfig
Even then, you might miss dynamically registered handlers or handlers in third-party apps.
The debugger makes signal handlers visible by letting you step into the signal dispatch:
# Step into this with F11
signals.pre_social_login.send(sender=SocialLogin, request=request, sociallogin=login)
# The debugger shows you entering django/dispatch/dispatcher.py:
def send(self, sender, **named):
responses = []
for receiver in self._live_receivers(sender):
# <-- Step through, see each receiver execute
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
As you step through _live_receivers(), the debugger shows you each registered handler function. You can jump to their definitions, inspect their logic, and see exactly what they do with the sociallogin object.
This is why the debugging workflow is so powerful for Django: signals, middleware, context processors, template tags, and model managers all inject behavior that's invisible in static code analysis but immediately visible in debugger call stacks.
Practical takeaway: When tracing any Django request, always check for signal usage. Common places signals affect execution:
-
User authentication:
user_logged_in,user_logged_out,user_login_failed -
Model operations:
pre_save,post_save,pre_delete,post_delete -
Requests:
request_started,request_finished -
Database:
connection_created
Set breakpoints where signals are sent, not just where you think the "main" logic lives.