Skip to content

Relax oauthlib scope check for Google group enrollment#712

Merged
payamnj merged 2 commits into
masterfrom
fix/oauth-scope-relax
Jul 10, 2026
Merged

Relax oauthlib scope check for Google group enrollment#712
payamnj merged 2 commits into
masterfrom
fix/oauth-scope-relax

Conversation

@payamnj

@payamnj payamnj commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes an OAuth redirect failure reported from production logs:

```
{"levelname": "ERROR", "name": "django_email_learning.oauth_integrations.views", "message": "Error processing OAuth redirect: Scope has changed from "https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly\" to "https://www.googleapis.com/auth/admin.directory.group.readonly https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/userinfo.email openid"."}
Bad Request: /email_learning/oauth/redirect/


**Root cause**: Google commonly grants additional scopes we didn't request (`openid`, `userinfo.email`, `userinfo.profile`) alongside the two we do request (`admin.directory.user.readonly`, `admin.directory.group.readonly`) in `GoogleGroupEnrollmentHandler._build_flow()`. `oauthlib` (used internally by `google-auth-oauthlib`/`requests-oauthlib`) treats *any* difference between requested and granted scopes as fatal by default — it literally does \`raise Warning(...)\` — unless \`OAUTHLIB_RELAX_TOKEN_SCOPE\` is set. That exception propagates out of \`flow.fetch_token()\` in \`handle_redirect()\`, gets caught by the generic \`except Exception\` in \`oauth_integrations/views.py\`, and turns an otherwise-successful authorization into a 400.

## Fix

Set \`OAUTHLIB_RELAX_TOKEN_SCOPE=1\` immediately before \`flow.fetch_token()\` in \`GoogleGroupEnrollmentHandler.handle_redirect()\` (\`django_email_learning/oauth_integrations/group_enrollment/google_group_enrollment_handler.py\`).

## Verification

Before writing the fix, I reproduced the exact bug locally by mocking the Google token endpoint response with the same superset-of-scopes payload from the log line above and confirming \`oauthlib\` raises the identical \`Warning: Scope has changed from ... to ...\` message. Then confirmed \`OAUTHLIB_RELAX_TOKEN_SCOPE=1\` resolves it. Both are captured as tests:

- \`test_handle_redirect_succeeds_when_google_grants_additional_scopes\` — reproduces the reported scenario (Google granting a superset of scopes); I verified this test actually fails without the fix (temporarily reverted it, confirmed the test catches the regression, restored it) before finalizing.
- \`test_handle_redirect_succeeds_with_exact_scope_match\` — confirms the normal/expected case still works.

## Test plan

- [x] \`pytest -q\` — 774 passed, 81.32% coverage
- [x] \`mypy\` — no issues across 163 source files
- [x] \`ruff check\` / \`ruff format\` — clean
- [x] Manually confirmed the new test fails without the fix and passes with it

payamnj and others added 2 commits July 10, 2026 11:03
Google commonly grants additional scopes we didn't request (openid,
userinfo.email, userinfo.profile) alongside the ones we did. oauthlib
treats any scope mismatch as fatal by default, raising
`Warning: Scope has changed from "..." to "..."` from
flow.fetch_token() — this propagated up through handle_redirect() and
got caught by the generic exception handler in
oauth_integrations/views.py, turning an otherwise-successful
authorization into a 400.

Set OAUTHLIB_RELAX_TOKEN_SCOPE=1 before fetch_token() so oauthlib
doesn't reject a superset of the requested scopes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tests/oauth_integrations/test_google_group_enrollment_handler.py imports
requests and exercises the real google_auth_oauthlib.flow.Flow to verify
the OAUTHLIB_RELAX_TOKEN_SCOPE fix end-to-end, but google-auth-oauthlib
was only declared under the optional "google" extra, not the dev
dependency group CI installs via `make dev-install` (poetry install
--with dev). CI failed to even collect the test module as a result.

Add it to dev, matching how openai is already there for the "ai" extra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@payamnj payamnj merged commit aabe4e0 into master Jul 10, 2026
9 checks passed
@payamnj payamnj deleted the fix/oauth-scope-relax branch July 10, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant