Android App Google SSO invalid_grant — exchange_access_token fails after user details are resolved

Environment

Detail Value
Open edX Release Ulmo
Tutor Version v21.0.5
Deployment AWS EC2, native Open edX (no custom plugins)

Summary

Google SSO works correctly on the web, but fails on the Android mobile app at the exchange_access_token endpoint with {"error":"invalid_grant","error_description":"access_token is not valid"}. Interestingly, the server logs confirm that the Google access token is successfully validated upstream and user details are resolved — yet the endpoint returns HTTP 400.


Setup

  • One Google Cloud project with two OAuth 2.0 credentials: one for the Web platform, one for the Android platform.
  • Email/password login on Android works without issues.

Request Being Made (from Android app)

bash

curl 'https://ourlms.com/oauth2/exchange_access_token/google-oauth2/' \
  -X POST \
  -H 'accept: application/json' \
  -H 'content-type: application/x-www-form-urlencoded' \
  -H 'user-agent: Dalvik/2.1.0 (Linux; U; Android 12; ...)' \
  --data-raw 'access_token=<GOOGLE_ACCESS_TOKEN>&client_id=<DJANGO_OAUTH_CLIENT_ID>&token_type=JWT&asymmetric_jwt=true'

Response

json

{"error": "invalid_grant", "error_description": "access_token is not valid"}

HTTP Status: 400 Bad Request


Server Logs

lms-1  | 2026-06-24 22:40:26,804 INFO 13 [tracking] [user None] [ip 43.241.146.120] logger.py:41 - {"name": "/oauth2/exchange_access_token/google-oauth2/", "context": {"user_id": null, "path": "/oauth2/exchange_access_token/google-oauth2/", "course_id": "", "org_id": "", "enterprise_uuid": ""}, "username": "", "session": "", "ip": "43.241.146.120", "agent": "Dalvik/2.1.0 (Linux; U; Android 12; Redmi Note 9 Pro Max Build/SKQ1.211019.001) 1.0.0", "host": "ourlms.com", "referer": "", "accept_language": "", "event": "{\"GET\": {}, \"POST\": {\"access_token\": [\"<ACCESS TOKEN GENERATED BY GOOGLE>\"], \"client_id\": [\"<CLIENT id>\"], \"token_type\": [\"JWT\"], \"asymmetric_jwt\": [\"true\"]}}", "time": "2026-06-24T17:10:26.803941+00:00", "event_type": "/oauth2/exchange_access_token/google-oauth2/", "event_source": "server", "page": null}


lms-1  | 2026-06-24 22:40:27,020 INFO 13 [common.djangoapps.third_party_auth.pipeline] [user None] [ip 43.241.146.120] pipeline.py:1048 - [THIRD_PARTY_AUTH] get_username complete: details={'username': 'mahendra', 'email': 'mahendra@gmail.com', 'fullname': 'Mahendra', 'first_name': 'Mahendra', 'last_name': 'Chaudhari'}, final_username=mahendra


lms-1  | [pid: 13|app: 0|req: 102/318] 172.18.0.9 () {44 vars in 758 bytes} [Wed Jun 24 22:40:26 2026] POST /oauth2/exchange_access_token/google-oauth2/ => generated 73 bytes in 246 msecs (HTTP/1.1 400) 8 headers in 536 bytes (1 switches on core 0)

The pipeline successfully reaches get_username and resolves user details from the Google token. The failure happens after this step during the actual token exchange / grant validation.


What I’ve Verified

  • Google access token is fresh and valid (confirmed via Google’s tokeninfo endpoint)
  • Web SSO with the same Google account works correctly
  • Android OAuth credentials in Google Console are correctly configured (package name + SHA-1 fingerprint)
  • The client_id used in the request matches the Django OAuth Toolkit application created for the Android app
  • Email/password login on Android works

Has anyone faced this issue?
Any guidance or pointers to resolve this issue would be greatly appreciated.

Thanks

Hi, Mahendra.
This isn’t a token or credentials problem - the Google access token is valid and the user’s details resolve correctly. The mobile token-exchange endpoint (POST /oauth2/exchange_access_token/google-oauth2/) runs the social-auth pipeline with AUTH_ENTRY_LOGIN_API, which only signs in an account that already exists and is linked to the Google identity (association is done by verified email). For a first-time or not-yet-linked Google user there is no account to return, so the pipeline ends without a user and the endpoint responds with:

{"error": "invalid_grant", "error_description": "access_token is not valid"}

That’s why it fails after get_username resolves the details, and why the same Google account works on the web - the web SSO flow auto-creates the account on first login, while the mobile LOGIN_API path deliberately does not.

Resolution

  • A first-time user must register/link the account first: in the app use Register → Continue with Google. That flow posts the Google token to the registration endpoint, which creates the account and links the Google identity; after that, Sign in with Google returns 200 and works normally. (Once linked, the exact same token exchanges successfully - verified end-to-end.)
  • On the app side, the Sign In screen was showing a generic “Something went wrong” for this case. It now shows a clear message - “This Google account is not linked with any account. Please register.” - matching iOS, plus a fix to request the full openid email profile scope. PR: fix: handle unregistered account on Google/social sign-in by PavloNetrebchuk · Pull Request #492 · openedx/openedx-app-android · GitHub