Skip to content

fix: auto-refresh token, persist to session, fix Object.keys length b… - #101

Open
tibrown wants to merge 1 commit into
tonesto7:masterfrom
tibrown:echo-speaks-server-auth-fix
Open

fix: auto-refresh token, persist to session, fix Object.keys length b…#101
tibrown wants to merge 1 commit into
tonesto7:masterfrom
tibrown:echo-speaks-server-auth-fix

Conversation

@tibrown

@tibrown tibrown commented Jun 29, 2026

Copy link
Copy Markdown

Title: fix: Amazon auth token expiry — scheduled refresh, session persistence, and Object.keys bug

Summary

This PR fixes a longstanding issue where Amazon login sessions expire every 3-4 days despite Amazon's refresh token being valid for 90 days. Three separate bugs were found working together to cause this. All fixes are in index.js.



Background

When a user logs in via the proxy, Amazon issues two things:
- Short-lived session cookies (localCookie, csrf) — valid for approximately 3-4 days
- A long-lived refreshToken — valid for ~90 days

The correct behaviour is to use the refreshToken to silently obtain fresh session cookies well before the short-lived ones expire. The code had scaffolding for this (scheduledUpdatesActive, a commented-out stopScheduledDataUpdates() in the exit handler) but it was never wired up. As a result the server relied entirely on the Hubitat app calling /refreshCookie on its own schedule — and if that schedule was longer than 3-4 days (matching the user's expectation of a 90-day token), auth failed and required a full re-login.



Bug 1 — Object.keys() missing .length (stored session fallback is dead code)

In the alexaLogin flow, after failing to retrieve cookies from the hub, there is a fallback to a locally stored session:

js
// BEFORE (broken)
} else if (sessionData && sessionData.cookieData && Object.keys(sessionData.cookieData) >= 2) {


Object.keys() returns an array. Comparing an array to the number 2 with >= coerces it to NaN, so NaN >= 2 is always false. This branch could never fire. On every server restart the code fell straight through to a full generateAlexaCookie() re-login attempt, discarding any stored session entirely.

js
// AFTER (fixed)
} else if (sessionData && sessionData.cookieData && Object.keys(sessionData.cookieData).length >= 2) {




Bug 2 — Refreshed cookie data not persisted to session.json

In the /refreshCookie endpoint, on a successful refresh the new tokens were stored in memory (runTimeData.savedConfig.cookieData = result) but updSessionItem("cookieData", result) was never called. The session.json file retained the old pre-refresh data. Combined with Bug 1 being fixed, this meant a server restart after a successful refresh would still load stale tokens from disk.

Added updSessionItem("cookieData", result) immediately after the in-memory assignment.



Bug 3 — No scheduled auto-refresh (root cause of the 3-4 day expiry)

The intended design was clearly a background timer that proactively refreshes the token — runTimeData.scheduledUpdatesActive exists, is checked in the exit handler, and the exit handler had a commented-out stopScheduledDataUpdates() stub. But the interval itself was never created.

This PR implements it: after every successful login, a setInterval is started that calls alexaCookie.refreshAlexaCookie(), validates the result, writes it to both memory and session.json, and pushes the fresh tokens to the Hubitat hub via sendCookiesToEndpoint. The interval is guarded by scheduledUpdatesActive so server restarts cannot stack duplicate timers. The exit handler's stub is now wired to clearInterval.



Heroku vs Local interval

The refresh interval is set differently depending on deployment mode:

- Local (useHeroku !== true): every 24 hours. session.json survives restarts so the stored session fallback (Bug 1 fix) handles any gap.
- Heroku (useHeroku === true): every 6 hours. Heroku dynos cycle approximately every 24 hours and wipe the local filesystem, destroying session.json. The hub is the only persistent store in that architecture. Refreshing every 6 hours ensures the hub always holds tokens that are at most 6 hours old when the dyno restarts and re-fetches from the hub — well within Amazon's session TTL.

The startup log clearly states which mode is active:

Starting scheduled cookie refresh (every 6 hours (Heroku))...
// or
Starting scheduled cookie refresh (every 24 hours (local))...




Testing

Verified on a local Linux deployment running under systemd. The session.json restore path (Bug 1) was confirmed working after service restart. No changes to proxy logic, cookie capture, or any other auth flow — only the post-login refresh lifecycle is affected.



Files changed

- index.js — 3 bug fixes + scheduled refresh implementation (~42 lines added, 2 changed)

@x86cpu

x86cpu commented Jul 24, 2026

Copy link
Copy Markdown

Thanks for putting this together. The scheduling and fail-safe handling are a clear improvement — on my setup the 24h timer fires on time, and unlike the previous behavior a failed refresh no longer clobbers the working session (I stayed logged in). So the lifecycle wiring works as intended.

That said, the scheduled refresh itself isn't obtaining new tokens for me. At the 24h mark:

7-24-2026 - 12:33:20pm info:  Scheduled cookie refresh triggered...
7-24-2026 - 12:33:21pm error: Scheduled refresh: failed to obtain new tokens. Error: No tokens in Register response

The No tokens in Register response originates from the /auth/register step in the bundled alexa-cookie lib — Amazon returns a response with no response.success.tokens.bearer, so there's no refresh_token to exchange. Worth noting the register payload still identifies as app_version 2.2.223830.0 / os_version 11.4.1 / device_model iPhone (an iOS 11.4.1 fingerprint from ~2018), which may be why Amazon's register endpoint stopped returning tokens after their mid-2026 changes.

For contrast, a manual GET /refreshCookie succeeds — it re-serializes the existing valid cookies without hitting the register/token path:

Successfully Refreshed Alexa Cookie...
** Alexa Cookie Data sent to Hubitat Cloud Endpoint Successfully! **

So the failure is specifically in the register / token-exchange path, not cookie serialization. It looks like the scheduled refresh depends on a working device-registration / refresh_token flow that isn't succeeding against Amazon's current backend — possibly tied to that stale device fingerprint. The scheduling/persistence changes here are solid, but at least in my environment they don't restore true token refresh, since the underlying register flow appears broken upstream.

Happy to test patches or pull more logs. Environment: local (non-Heroku) mode, built from echo-speaks-server-auth-fix, node:16 container under Podman, Hubitat ES app [Echo Speaks app/devices v4.3.0.1, Server v2.8.0].

@x86cpu

x86cpu commented Jul 24, 2026

Copy link
Copy Markdown

One more finding that's directly actionable for this PR. The scheduled refresh you added fails safe — on a failed register it logs the error and leaves the existing session untouched. But the legacy GET /refreshCookie route still fails destructive: on the same register failure it clears Hubitat's auth and restarts the container. Same underlying error, opposite handling.

Here's the manual endpoint hitting the failure path:

refreshCookie request received
** ERROR: Unsuccessfully refreshed Alexa Cookie it was found to be invalid/expired... **
RESULT: Error: No tokens in Register response / null
** WARNING: We are clearing the Cookie from Hubitat to prevent further requests and server load... **
** Sent Request to Hubitat Cloud Endpoint to Remove All Auth Data Successfully! **
Restarting after cookie refresh attempt

In my case it happened to self-heal — on restart it re-pulled the cookie from Hubitat, that copy was still valid, and it logged back in — but that's a race, not a guarantee. If the "remove all auth data" had taken before the re-fetch, it'd have logged me out and forced a manual re-login.

The concern: now that the register/token flow is unreliable, this makes a routine refresh attempt capable of wiping auth. And the Hubitat app calls this same route on its own refreshCookieDays schedule, so it fires unattended. Since your scheduled path already demonstrates the correct fail-safe behavior (validate, and on failure keep the existing cookie rather than clearing it), applying that same handling to the /refreshCookie route would close the gap — a failed register would then be a no-op instead of an auth-wipe + restart.

Happy to test if you push a change to that route.

Environment: local (non-Heroku) mode, built from echo-speaks-server-auth-fix, node:16 container under Podman. Hubitat: Echo Speaks app/devices v4.3.0.1, Server v2.8.0.

@x86cpu

x86cpu commented Jul 27, 2026

Copy link
Copy Markdown

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.

2 participants