[Bug]: OIDC login fails on iOS Safari — WorkerBridge stays UNASSIGNED on fresh session
#7754 opened on May 8, 2026
Description
What happened?
Describe the bug
On iOS Safari (and as a PWA), OIDC login fails on any fresh browser session. After successfully authenticating with the OpenID provider and being redirected back to /openid-cb?token=…, the app hangs on "Loading global preferences" and displays "No Server Configured" at the bottom of the screen. The user-token key is never written to asyncStorage.
The same flow works correctly on desktop Safari immediately after a password logout (i.e. with an established SharedWorker session). The failure is specific to iOS Safari on a fresh session where no prior SharedWorker state exists.
Expected behaviour
OIDC login completes successfully and the user is authenticated, consistent with the behaviour on desktop browsers.
Actual behaviour
The app loads at the correct /openid-cb?token=… URL (confirmed via window.location.href) but the OIDC callback handler silently fails to write the auth token to asyncStorage. The app shows "No Server Configured" indefinitely.
Root cause (investigated)
Using eruda (mobile JS console) and SharedWorker log inspection, the following was observed on iOS:
[WorkerBridge] Connected to SharedWorker coordinator
[SharedWorker] Tab connected — 2 tab(s), 1 unassigned, groups: ["__lobby": leader + 0 follower(s)]
[WorkerBridge] Role: UNASSIGNED
[SharedWorker] Last tab left budget "__lobby" — removing group
[SharedWorker] Tab resume confirmed while unassigned — 1 tab(s), 1 unassigned, groups: [none]
On a fresh iOS session, the tab connecting at /openid-cb starts as UNASSIGNED and never transitions to a leader role. The OIDC callback handler depends on the WorkerBridge being in an established state to write user-token to asyncStorage. Because the WorkerBridge is UNASSIGNED, this write fails silently and the callback never completes.
On desktop Safari (with an existing SharedWorker session from a prior password login or OIDC session), the tab is already an established lobby leader when the OIDC callback runs, so the write succeeds.
Confirmed via inspection of asyncStorage after failed iOS callback:
Keys: ["did-bootstrap", "server-url"]
// "user-token" is absent — the OIDC callback did not write it
Confirmed via inspection of asyncStorage on working desktop session:
Keys: ["did-bootstrap", "lastBudget", "server-url", "user-token"]
// "user-token" is present — the OIDC callback wrote it successfully
Environment
- Actual Budget version: 26.5.0
- Deployment: Self-hosted Docker (
actualbudget/actual-server) - Affected: iOS Safari (fresh session), iOS PWA (fresh session)
- Not affected: Desktop Safari, desktop Chrome, desktop Safari with iPhone user-agent emulation
- Auth method: OpenID Connect (password auth disabled), Pocket ID as provider
Additional context
- The OpenID provider successfully issues the auth code, and the server correctly processes
/openid/callbackand issues a token (302 to/openid-cb?token=…is observed in server logs) - The token in the URL is valid — manually writing it to asyncStorage under
user-tokenand navigating to/via the eruda console results in successful authentication - A manual page reload of
/openid-cb?token=…after the initial failure can succeed intermittently, once the SharedWorker has had time to settle - Disabling iOS Safari "Prevent Cross-Site Tracking" does not resolve the issue
ACTUAL_OPENID_SERVER_HOSTNAMEis correctly configured
Suggested fix
The OIDC callback handler at /openid-cb should not depend on the WorkerBridge being in an established (leader) role to write the auth token. On a fresh session, the tab will always start as UNASSIGNED at this route. The handler should either:
- Write
user-tokenandserver-urldirectly toasyncStorage(bypassing the WorkerBridge), then redirect to/; or - Wait for the WorkerBridge to reach a ready state before proceeding with the token write
How can we reproduce the issue?
Steps to reproduce
- Deploy Actual Budget with OIDC configured
- On iOS Safari, clear all website data for the Actual Budget domain
- Navigate to the Actual Budget instance — the login screen loads and correctly displays the server URL
- Tap Sign in with OpenID
- Authenticate with the OpenID provider
- Provider redirects back to
/openid-cb?token=<uuid> - App hangs on "Loading global preferences" — "No Server Configured" shown in footer
Where are you hosting Actual?
Docker
What browsers are you seeing the problem on?
Safari
Operating System
Mobile Device