pennersr/django-allauth

Google provider login do not authenticate specific user for production env

Andrioden opened this issue · 6 comments

Edit: Seems like the problem only occur for one specific user, but it would still be nice with better logging.

There is a pretty wierd combination of what works and not

  • OK: Discord provider + all envs + any combination
  • OK: Google provider + local windows + any combination
  • OK: Google provider + prod linux + any implementation + random user (woot???)
  • FAIL: Google provider + prod linux + any implementation + same user as owner of google ouath app

Version

  • pipenv version: django-allauth = {extras = ["socialaccount"], version = "==0.63.1"}

What happens:

  1. User click redirect to provider url
  2. Google login shows, click through it (no consent screen)
  3. User is redirected to callback_url
  4. User Is redirected back to my page, but user is not authenticated in django

Notes:

  • I have tried to reproduce the error by setting wrong client_id and secret locally, but that gives different errors.
  • I have tripple-checked the google oauth configuration, cant find anything wrong. No special users, urls ok. And i know the current config worked on earlier versions.
  • I feel like I am at fault here, but i have no logs that indicate what is wrong. So am stuck.

Do you have any suggestions of what can be wrong? Or could i request you log better if anything is wrong when receiving the callback? django-allauth now silently fail at receiving the callback.


settings.py

INSTALLED_APPS = (
    # Standard
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.humanize",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # Apps
    "compressor",
    "cachalot",
    # allauth
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.discord",
    "allauth.socialaccount.providers.google",
    # Custom
    "game",
    "monitor",
    "social",
)

MIDDLEWARE = (
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "allauth.account.middleware.AccountMiddleware",
    "monitor.middleware.TimeMonitorMiddleware",
)

HEADLESS_ONLY = True
HEADLESS_FRONTEND_URLS = {
    "socialaccount_login_error": "/account/error/",
}

AUTHENTICATION_BACKENDS = [
    # Needed to log in by username in Django admin, regardless of `allauth`
    "django.contrib.auth.backends.ModelBackend",
    # `allauth` specific authentication methods, such as login by e-mail
    "allauth.account.auth_backends.AuthenticationBackend",
]

SOCIALACCOUNT_PROVIDERS = {
    "discord": {
        "APP": config.oauth_discord,
    },
    "google": {
        "APP": config.oauth_google,
        "SCOPE": [
            "profile",
            "email",
        ],
        "AUTH_PARAMS": {
            "access_type": "online",
        },
        "OAUTH_PKCE_ENABLED": True,
    },
}

Logs from production - failing

POST /_allauth/browser/v1/auth/provider/redirect
GET /account/google/login/callback/?state=***&code=***&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=0&prompt=none
GET /account/logged-in/
UserType: SimpleLazyObject
User: AnonymousUser, is_authenticated: False, is_active: False, is_anonymous: True

Logs from local dev - working

POST /_allauth/browser/v1/auth/provider/redirect
GET /account/google/login/callback/?state=***code=***&scope=email%20profile%20openid%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email&authuser=0&prompt=consent
UserType: SimpleLazyObject
User: ***my-user***, is_authenticated: True, is_active: True, is_anonymous: False
GET /account/logged-in/

Here is a comparison of the two callbacks from google, they are different slightly:

    1. scope is encoded different, but this might be because they are logged differently
    1. different prompt value. But dunno...

image

If i revert from headless to the old integrated allauth way, i get this view after logging in
image

Given the screenshot from the non-headless version, it seems that the user you are logging in is in conflict (by email) with an already existing local user, which is why that intermediary sign up form pops up. If you try to complete that form it will likely inform you that the email address is taken. The headless endpoint is likely doing the same, returning a 401 -- and pointing (using flows) to the same flow that the non-headless version is showing.

Thanks for the reply. My god I am stupid, I have a createsuperuser created user with the taken/failing email.

I see that you closed the issue, but i still suggest you either:

  • Add some sort of error logging when this happen, especially considering headless.
  • Break the callback chain by throwing an exception

But this is just a regular flow that should be handled, it is not an error flow at all. The headless response should show a flow with ID provider_signup that is pending.

The headless response should show a flow with ID provider_signup that is pending.

I am not able to figure out where or how i check this. Where in the below flow should i do this? How do I hook in my logic?

The user flow

  1. User start provider login with
        this.#postForm("_allauth/browser/v1/auth/provider/redirect", {
            provider: provider,
            callback_url: "/account/logged-in/",
            process: "login",
            csrfmiddlewaretoken: Cookies.get("csrftoken")
        })
  1. POST /_allauth/browser/v1/auth/provider/redirect

  2. User log in to google account at https://accounts.google.com/signin/oauth/v2

  3. GET /account/google/login/callback/?state=<removed>&code=<removed>&scope=email%20profile%20openid%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email&authuser=0&prompt=consent

  4. User is redirected to the callback_url, which is the following code

@ensure_csrf_cookie
def logged_in_redirector(request: WSGIRequest) -> HttpResponse:
    # request.user = AnonymousUser object
    if PlayerRepo.exists(PlayerQ(user=auth(request.user))):
        return redirect("/game/")
    else:
        return redirect("/account/register/")

Debug object
image

@Andrioden I don't understand your step 5. The user is redirect from the provider to the allauth callback URL. There, the sociallogin as returned from the provider is handled accordingly and a redirect to the frontend should take place. Then, your frontend boots up, should check the current session to see what the actual authentication status is, and handle accordingly. This is all demonstrated in the React example application.

I see, so you intend people to let the frontend handle this, however I wanted to do this check in the backend. I also have a feeling you don't want to give the answer to me straight up how to do that. : P

So i took the challenge and followed your react code -> through the auth/session call, into allauth backend, and i ended up here, which leads me to this code.

email_in_use = not request.user.is_authenticated and request.session.get("socialaccount_sociallogin")`

or full code

@ensure_csrf_cookie
def logged_in_redirector(request: WSGIRequest) -> HttpResponse:
    if not request.user.is_authenticated and request.session.get("socialaccount_sociallogin"):
        return render(request=request, template_name="account/error.html", context={"error": "Email in use"})
    elif PlayerRepo.exists(PlayerQ(user=auth(request.user))):
        return redirect("/game/")
    else:
        return redirect("/account/register/")

I have tested that it works on my login cases, thou its probably not correct to always conclude the email is in use if we reach this state, but ill se how it goes.