Skip to content

Fix OAuth redirect loops caused by stale sessions - #13641

Open
dawoodkhan82 wants to merge 3 commits into
mainfrom
codex/fix-issue-13634-oauth-redirect-loop
Open

Fix OAuth redirect loops caused by stale sessions#13641
dawoodkhan82 wants to merge 3 commits into
mainfrom
codex/fix-issue-13634-oauth-redirect-loop

Conversation

@dawoodkhan82

@dawoodkhan82 dawoodkhan82 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Description

OAuth state mismatches could keep reusing a stale session cookie. Starlette also cannot expire a session cookie when its signature is invalid, and Gradio dropped _nb_redirects while generating the next callback URL.

This PR resets the failed OAuth session, explicitly expires the cookie with matching security attributes, and carries the retry count through the callback URL. It adds focused FastAPI TestClient regressions for both behaviors.

Before / after Spaces

Closes: #13634

AI Disclosure

  • I used AI to reproduce the issue, draft the fix and tests, and prepare this PR description. I self-reviewed every changed line and ran the checks below.
  • I did not use AI

Testing and Formatting Your Code

  • python -m pytest test/test_routes.py::TestOAuthSecurity -q (5 passed)
  • ruff format --check gradio/oauth.py test/test_routes.py
  • ruff check gradio/oauth.py
  • ty check gradio/oauth.py test/test_routes.py (no errors; existing unused-ignore warnings)
  • python demo_issue_13634.py

The full test/test_routes.py suite was attempted locally, but tests that bind local ports are blocked in the sandbox. No Playwright tests were added.

Minimal reproduction

from unittest.mock import patch
from urllib.parse import parse_qs, urlparse

from authlib.integrations.base_client.errors import MismatchingStateError
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware

from gradio import oauth


class FakeProvider:
    async def authorize_redirect(self, request, redirect_uri):
        request.session["_state_huggingface_repro"] = {"state": "fresh"}
        return RedirectResponse(redirect_uri)

    async def authorize_access_token(self, request):
        raise MismatchingStateError()


class FakeOAuth:
    def __init__(self):
        self.huggingface = FakeProvider()

    def register(self, **kwargs):
        pass


app = FastAPI()
with (
    patch("authlib.integrations.starlette_client.OAuth", FakeOAuth),
    patch.multiple(
        oauth,
        OAUTH_CLIENT_ID="client",
        OAUTH_CLIENT_SECRET="secret",
        OAUTH_SCOPES="openid",
        OPENID_PROVIDER_URL="https://huggingface.co",
    ),
):
    oauth._add_oauth_routes(app)
app.add_middleware(SessionMiddleware, secret_key="secret", https_only=True)

with TestClient(app, base_url="https://space.hf.space") as client:
    login = client.get(
        "/login/huggingface?_nb_redirects=2&_target_url=/app",
        follow_redirects=False,
    )
    print(parse_qs(urlparse(login.headers["location"]).query))

    client.cookies.set("session", "stale-invalid-cookie", domain="space.hf.space")
    callback = client.get("/login/callback?_target_url=/app", follow_redirects=False)
    print(callback.headers.get("set-cookie"))

Before this fix, the callback query has no _nb_redirects and the stale callback has no Set-Cookie header. After this fix, the callback preserves _nb_redirects=2 and the response explicitly expires session.

The reproduction file is temporary and was not committed.

@gradio-pr-bot

gradio-pr-bot commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/761af727a89879356971c76ca6a7cadbb8b8e13d/gradio-6.20.0-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@761af727a89879356971c76ca6a7cadbb8b8e13d#subdirectory=client/python"

Import Gradio JS Client from this PR via CDN

import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/761af727a89879356971c76ca6a7cadbb8b8e13d/browser.js";

@gradio-pr-bot

Copy link
Copy Markdown
Collaborator

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
gradio patch

  • Reset stale OAuth sessions and preserve the retry count across callbacks

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes OAuth login redirect loops in Hugging Face Spaces by ensuring stale/invalid OAuth session cookies can’t be reused after a state mismatch, and by preserving the _nb_redirects retry counter through the /login/callback redirect URI so the “too many redirects” escape hatch can work reliably.

Changes:

  • Clear/reset OAuth session state on MismatchingStateError and explicitly expire the session cookie in the callback response.
  • Preserve _nb_redirects when generating the callback redirect URI.
  • Add FastAPI TestClient regressions covering retry count preservation and explicit cookie expiration.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
gradio/oauth.py Clears session on OAuth state mismatch, explicitly deletes the session cookie, and propagates _nb_redirects into the callback URL generation.
test/test_routes.py Adds regression tests for _nb_redirects preservation and for expiring stale session cookies on state mismatch.
.changeset/clean-oauth-retries.md Adds a patch-level changeset entry describing the OAuth retry/session reset fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gradio/oauth.py Outdated
Comment on lines +113 to +114
# Reset the session so the next attempt cannot reuse stale OAuth state.
request.session.clear()
Comment thread gradio/oauth.py
Comment on lines 131 to 135
host = os.environ.get("SPACE_HOST")
if host is None: # cannot happen in a Space
raise RuntimeError(
"Gradio is not running in a Space (SPACE_HOST environment variable is not set)."
" Cannot redirect to non-iframe view."
@abidlabs

Copy link
Copy Markdown
Member

I don't think the repro spaces are any good here, for example, they don't even set oauth: true in the space readme or have a gr.LoginButton?

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.

OAuth login gets stuck in infinite redirect loop outside the iframe — callback doesn't clear stale session cookie

4 participants