🏠

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:

  1. Request 1: GET /accounts/google/login/ β†’ 302 Redirect to Google

  2. Request 2: GET /accounts/google/login/callback/?code=...&state=... β†’ 302 Redirect to 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:

  1. Seven middleware components process the request before reaching the view:
  2. SessionMiddleware (loads the session)

  3. AuthenticationMiddleware (checks if already logged in)

  4. MessageMiddleware (for flash messages)

  5. debug_toolbar.middleware.DebugToolbarMiddleware

  6. Three custom middleware from the project (rate limiting, logging, feature flags)

  7. The view execution flow:

  8. OAuth2LoginView.dispatch() calls get_app() β†’ database query for social app config

  9. Generates OAuth state parameter

  10. Stores state in database (the INSERT query you saw earlier)

  11. Constructs Google's authorization URL

  12. Returns HttpResponseRedirect to Google

  13. After Google redirects back (the callback request):

  14. Same seven middleware components run again

  15. OAuth2CallbackView.dispatch() executes

  16. Validates the state parameter against the database

  17. Exchanges the authorization code for an access token (HTTP request to Google)

  18. 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:

  1. A custom handler in your project's signals.py that checks if the email domain is allowed (.edu emails only)

  2. django-allauth's built-in handler that associates the social account with existing users

  3. 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:

Tools used: Django Debug Toolbar + VS Code debugger

This investigation demonstrates the complementary strengths of each tool:

Django Debug Toolbar gave you:

VS Code debugger gave you:

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:

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:

  1. Social app config lookup: If this returns no results, login fails immediately

  2. State token insert: If database is read-only or full, OAuth flow cannot start

  3. 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:

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:

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:

  1. Search the entire codebase for @receiver(pre_social_login)

  2. Check apps.py files for ready() methods that might register handlers

  3. Inspect settings.py for apps that might register handlers in their AppConfig

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:

Set breakpoints where signals are sent, not just where you think the "main" logic lives.