mirror of
https://github.com/priyanshujain/margin-calendar.git
synced 2026-10-02 11:07:04 +00:00
Sign in on a phone with no console work, in the browser's own session
Mobile OAuth reused the desktop client all along; what stopped it was the browser. Sending the user out to Safari or Chrome backgrounds the app, iOS suspends it, and the redirect carrying the code arrives at a socket nobody is accepting on. The consent page now opens in front of the app instead, in SFSafariViewController or a Chrome Custom Tab, so the loopback listener stays live and the existing `installed` client is enough. Verified against Google's real consent screen on a simulator and an emulator. A per-platform client is still supported and is now an upgrade rather than a prerequisite. On iOS it buys ASWebAuthenticationSession, which shares Safari's session so nobody is asked to sign in to Google twice. Android needs nothing: Custom Tabs share Chrome's cookies, measured rather than assumed. iOS session sharing could not be confirmed on the simulator and wants a real device. Never an app-owned WebView: Google blocks it, and rightly, since a webview the app controls can read the password typed into it. Cancelling is no longer reported as a failure. AuthEvent carries a `cancelled` flag, set by comparing against the constant every back-out path returns, and Google's `access_denied` on desktop counts too. Five frontend bugs found by driving the real UI, not by reading it: the details card slid under the tab bar leaving its buttons unhittable; the ghost click after a touch pressed a button in the card that tap had just opened, opening the editor by itself; the swipe that pages the day was dead over every read-only block; 84px of macOS traffic-light lane was reserved on platforms with no traffic lights; and the desktop header ignored the top safe area on an iPad. A first launch now says what to do next rather than showing an empty grid, and accounts are named as Google accounts throughout.
This commit is contained in:
1 parent
661100dfdc
commit
d4c3a304b5
31 files changed
+1319
-114
No files matched your search
+25
-18
@@ -11,27 +11,34 @@ locked to `ipc:` exactly as margin has it.
|
||||
|
||||
## Authentication
|
||||
|
||||
Lifted from `margin/src-tauri/src/gdrive.rs`, then split in two when mobile arrived. Both halves
|
||||
build the same consent URL with PKCE S256 and a CSRF state parameter and open it in the system
|
||||
browser through the opener plugin, never an in-app webview: Google blocks the embedded-webview
|
||||
flow, and it deserves to be blocked, because a webview the app controls can read the password
|
||||
typed into it. They differ only in how the answer comes back.
|
||||
Lifted from `margin/src-tauri/src/gdrive.rs`. Every platform builds the same consent URL with PKCE
|
||||
S256 and a CSRF state parameter, and every platform opens it in the system's browser, never in a
|
||||
webview this app owns: Google blocks the embedded-webview flow, and it deserves to be blocked,
|
||||
because a webview the app controls can read the password typed into it.
|
||||
|
||||
Desktop binds a listener on `127.0.0.1:0` and catches the redirect on the loopback socket. The
|
||||
verifier lives on that listener's stack for the two minutes it is alive.
|
||||
The default flow is the same one on all five platforms. Bind a listener on `127.0.0.1:0`, ask
|
||||
Google to redirect there, and catch the code on the loopback socket. Google allows a Desktop client
|
||||
any loopback port without registering it, and the token endpoint checks the client id, the secret
|
||||
and the redirect rather than the operating system, so a phone can use the desktop client too. What
|
||||
makes that safe to rely on is where the browser is: on mobile the consent page opens **in front of**
|
||||
the app, in `SFSafariViewController` or a Chrome Custom Tab, so this process stays foreground and
|
||||
its listener stays live. Sending the user out to Safari would suspend it and the redirect would
|
||||
arrive at a socket nobody is accepting on. `google/browser.rs` is that surface, and dismissing it
|
||||
once the code lands.
|
||||
|
||||
Mobile cannot do that. iOS will not keep a background listener alive dependably, and Google
|
||||
rejects loopback redirects for Android and iOS client types outright, so the redirect is a custom
|
||||
URI scheme the OS routes back to the app through `tauri-plugin-deep-link`. There is no listener to
|
||||
hold the verifier, the browser is a separate app and this process may be backgrounded while the
|
||||
user consents, so the verifier waits in `AuthState.pending` and `handle_redirect` picks it up
|
||||
whenever the link lands. It is taken rather than read, so a replayed link cannot start a second
|
||||
exchange.
|
||||
The second flow runs where a platform has been given its own OAuth client, which is Google's stated
|
||||
guidance and what to fall back on if they ever enforce it. Those clients are public, have no secret,
|
||||
and redirect to a custom URI scheme rather than to loopback. The verifier has no listener stack to
|
||||
live on, so it waits in `AuthState.pending` until the callback lands, and is taken rather than read
|
||||
so a replayed link cannot start a second exchange.
|
||||
|
||||
That split forces one more difference. A desktop client is confidential and has a secret; an
|
||||
Android or iOS client is public and has none, so `google-credentials.json` carries up to three
|
||||
clients and the build embeds the one for the platform it is compiling for. Details and the
|
||||
console steps are in [mobile.md](mobile.md).
|
||||
Where the callback lands differs. Android opens the external browser and the OS routes the scheme
|
||||
back through `tauri-plugin-deep-link`, which is also the arrival route on a cold start. iOS uses
|
||||
`ASWebAuthenticationSession`, which reports the URL straight to a completion handler and is the only
|
||||
iOS surface that shares Safari's cookies, so an account already signed in on the phone is offered by
|
||||
name. That last point is the reason the choice is not merely academic on iOS, and
|
||||
[mobile.md](mobile.md) argues it out. `load_credentials` decides once, by whether the block is in
|
||||
the file, and everything downstream follows from that.
|
||||
|
||||
The scope is `https://www.googleapis.com/auth/calendar` plus `openid email`. Calendar is a
|
||||
sensitive scope, so an unverified client shows the unverified-app interstitial and caps at 100
|
||||
|
||||
Reference in new issue
Block a user