Skip to content

fix(auth): adapt EU login flow to current Smart Gigya/OIDC cloud#10

Open
jusii wants to merge 1 commit into
onpremcloudguy:mainfrom
jusii:fix/eu-auth-current-cloud
Open

fix(auth): adapt EU login flow to current Smart Gigya/OIDC cloud#10
jusii wants to merge 1 commit into
onpremcloudguy:mainfrom
jusii:fix/eu-auth-current-cloud

Conversation

@jusii
Copy link
Copy Markdown

@jusii jusii commented May 1, 2026

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 pySmartHashtag library (the API used by the org.more.hashtag Android app, which still logs in successfully). INTL flow is unchanged.

What broke

Step Old behaviour New behaviour
Step 1 — /login-app/api/v1/authorize Returned 302 with ?context= on hop 1 Returns 403 ForbiddenException unless a browser-shaped UA is sent. Even with UA, context= moved to hop 2 (new host app.id.smart.com)
Step 2 — accounts.login Worked from a fresh client Returns errorCode=400006 "request is blocked because of security issues" without a Gigya bootstrap cookie + Origin header + pageURL body field
Step 3 — /authorize/continue Authenticated via URL params Authenticates via cookies (gmid, 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)
Step 4 — /auth/account/session/secure Worked Existing code computed HMAC-SHA1 over stdlib json.dumps(body) bytes but POSTed json=<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 error

Changes

  • const.py — adds EU_WEBVIEW_USER_AGENT, EU_GIGYA_SOCIALIZE_BASE; allowlists app.id.smart.com.
  • auth.py Step 1 — sends Android-webview UA; walks up to 5 redirect hops to find the first Location carrying ?context=.
  • auth.py Step 1.5 (new)GET socialize.eu1.gigya.com/socialize.getIDs to obtain a fresh gmid+ucid pair and build the bootstrap cookie.
  • auth.py Step 2 — sends bootstrap cookie + Origin: https://app.id.smart.com + x-requested-with + pageURL body field; coerces expires_in to int (Gigya returns string in some responses).
  • auth.py Step 3 — sends only context+login_token in URL with bootstrap cookie + glt_<APIKey>=<login_token>; walks up to 5 hops looking for access_token in query OR fragment; surfaces /proxy?mode=error redirects as actionable errors.
  • auth.py Step 4 — pre-serializes body once with stdlib json.dumps and POSTs via data=<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.json version is not bumped in this PR — you may want to do that as part of merging.

Test plan

  • Verified end-to-end against a real EU account from a standalone async runner: Account returned with populated api_access_token (277 chars), api_refresh_token, api_user_id, expires_at.
  • Verified end-to-end inside Home Assistant (HACS install of this fork's main): config flow completes, devices appear in Settings → Devices & Services, vehicle data refreshes.
  • Each Step 1/2/3 break point and its fix verified independently with curl probes against the live Smart cloud.
  • INTL flow not exercised by the fork owner (no INTL account); no logic changes there but worth a smoke check before merge.

Notes

The hardcoded constants in this PR (EU_WEBVIEW_USER_AGENT value, the auth_ver4 literal in the bootstrap cookie, etc.) match what pySmartHashtag uses; 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.

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.
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