fix(auth): adapt EU login flow to current Smart Gigya/OIDC cloud#10
Open
jusii wants to merge 1 commit into
Open
fix(auth): adapt EU login flow to current Smart Gigya/OIDC cloud#10jusii wants to merge 1 commit into
jusii wants to merge 1 commit into
Conversation
Smart's EU OIDC backend changed in several incompatible ways and broke
the integration's login flow before any password reaches the wire.
This commit ports the EU flow over to the same shape used by the
working pySmartHashtag library (org.more.hashtag Android app).
Step 1 — context redirect chain
* Send a browser-shaped User-Agent (Hello Smart Android webview UA;
constants.py: EU_WEBVIEW_USER_AGENT). Without it Smart's API
Gateway returns 403 ForbiddenException.
* Walk up to 5 redirect hops looking for the first Location whose
query carries ?context=. The context= parameter moved from hop 1
(auth.smart.com) to hop 2 (app.id.smart.com/proxy) and may shift
again. const.py URL_ALLOWLIST gains app.id.smart.com.
Step 1.5 — Gigya bootstrap cookies (new)
* GET socialize.eu1.gigya.com/socialize.getIDs to obtain a fresh
gmid + ucid pair, then build a Cookie header:
gmid=<gmid>; ucid=<ucid>; hasGmid=ver4;
gig_bootstrap_<APIKey>=auth_ver4
This declares to Gigya that "the SDK has been bootstrapped" and is
required for accounts.login to be accepted.
* const.py: EU_GIGYA_SOCIALIZE_BASE.
Step 2 — Gigya accounts.login
* Send the bootstrap cookie, plus Origin: https://app.id.smart.com,
x-requested-with header, and a pageURL body field. Without these
Gigya returns errorCode=400006 "Invalid parameter value / Your
request is blocked because of security issues" with the
misleading "missingKey" flag.
* Coerce sessionInfo.expires_in to int (Gigya returns it as a
string in some responses).
Step 3 — /authorize/continue
* The OIDC server now authenticates this hop via cookies, not query
parameters. Send only context+login_token in the URL, with the
bootstrap cookie plus glt_<APIKey>=<login_token> in the Cookie
header.
* Walk up to 5 redirect hops; extract access_token + refresh_token
+ id_token from the final Location's query string OR fragment.
Smart's current flow lands at:
/login-app/api/v1/FinalPage?code=...
-> /login-app/api/v1/MobileData?access_token=...
&id_token=...
&refresh_token=...
* Surface Smart's /proxy?mode=error redirects as actionable errors
instead of opaque "no access_token in fragment".
Step 4 — POST /auth/account/session/secure (signature)
* Pre-serialize the body once with stdlib json.dumps and POST it
via data=<str> rather than json=<dict>. The HMAC-SHA1 signature
is computed over body bytes; passing json=<dict> let aiohttp
re-encode using HA's orjson serializer (no spaces), which
produced different bytes than what we signed and tripped the
cloud's "Check signature error" code 1445. Same approach the
INTL flow already uses.
Diagnostics + error reporting
* _redact() helper scrubs access_token/refresh_token/code/login_token
/password/authCode values from any URL or JSON body before it
hits the log, so DEBUG output is safe to share.
* Distinguish credential errors (Gigya 4xxxxx codes) from upstream
errors (rate limit, API misconfiguration). Old code raised
"EU Gigya login failed: invalid credentials" for both cases.
* Surface the API gateway's message on Step 4 failures.
End-to-end verified against a real EU account: returns Account with
populated api_access_token, api_refresh_token, api_user_id, and
expires_at. The INTL flow is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Smart's EU OIDC backend (Gigya tenant + AWS API gateway + auth.smart.com OIDC server) changed in several incompatible ways and the integration's EU login flow now fails before any password reaches the wire (visible to users as "Cannot connect" / "EU context request did not redirect: status=403"). Issues #1, #3, #5 all appear to track this same family of failures.
This PR ports the EU flow over to the same shape used by the working
pySmartHashtaglibrary (the API used by theorg.more.hashtagAndroid app, which still logs in successfully). INTL flow is unchanged.What broke
/login-app/api/v1/authorize?context=on hop 1context=moved to hop 2 (new hostapp.id.smart.com)accounts.loginOriginheader +pageURLbody field/authorize/continuegmid,ucid,hasGmid,gig_bootstrap_<APIKey>,glt_<APIKey>); returns auth code on a 2-hop redirect chain ending at/login-app/api/v1/MobileData?access_token=...(query, not fragment)/auth/account/session/securejson.dumps(body)bytes but POSTedjson=<dict>. HA's shared session uses orjson which omits spaces, so the bytes on the wire differed from the bytes signed →code=1445 message=Check signature errorChanges
const.py— addsEU_WEBVIEW_USER_AGENT,EU_GIGYA_SOCIALIZE_BASE; allowlistsapp.id.smart.com.auth.pyStep 1 — sends Android-webview UA; walks up to 5 redirect hops to find the first Location carrying?context=.auth.pyStep 1.5 (new) —GET socialize.eu1.gigya.com/socialize.getIDsto obtain a freshgmid+ucidpair and build the bootstrap cookie.auth.pyStep 2 — sends bootstrap cookie +Origin: https://app.id.smart.com+x-requested-with+pageURLbody field; coercesexpires_into int (Gigya returns string in some responses).auth.pyStep 3 — sends onlycontext+login_tokenin URL with bootstrap cookie +glt_<APIKey>=<login_token>; walks up to 5 hops looking foraccess_tokenin query OR fragment; surfaces/proxy?mode=errorredirects as actionable errors.auth.pyStep 4 — pre-serializes body once with stdlibjson.dumpsand POSTs viadata=<str>so the signed bytes match what's on the wire (same approach the INTL flow already uses).auth.py—_redact()helper scrubs token-like values from URLs and JSON bodies before they hit DEBUG logs; distinguishes credential errors (Gigya 4xxxxx) from upstream errors; surfaces error codes/messages verbatim instead of the misleading "invalid credentials" string.INTL flow is untouched.
manifest.jsonversion is not bumped in this PR — you may want to do that as part of merging.Test plan
Accountreturned with populatedapi_access_token(277 chars),api_refresh_token,api_user_id,expires_at.main): config flow completes, devices appear in Settings → Devices & Services, vehicle data refreshes.Notes
The hardcoded constants in this PR (
EU_WEBVIEW_USER_AGENTvalue, theauth_ver4literal in the bootstrap cookie, etc.) match whatpySmartHashtaguses; if Smart's webview rotates these in future, the diagnostic logging added here (DEBUG-level per-hop status/location/body with token redaction) should make the next break easier to localise.