Context
The web console moved to cookie-session auth (SDK 0.47): login/signup/OIDC set the HttpOnly nvisy.session + readable nvisy.csrf cookies and return no token. That works for the browser, but it breaks the Tauri desktop app, and the fix needs a server capability that doesn't exist yet.
Why desktop can't use cookies (verified): the desktop routes all API calls through Tauri's Rust HTTP client (tauri-plugin-http) — it must, because the webview can't reach an arbitrary self-hosted API origin (CORS). That Rust client has its own reqwest cookie jar, invisible to the webview's document.cookie, and a bare HttpOnly session cookie (no Max-Age) isn't even persisted across app restarts. So the desktop can't read nvisy.csrf for the CSRF echo, can't observe the session via document.cookie, and can't keep a session across launches. Cookie auth is structurally the wrong fit for the desktop.
Chosen desktop approach: external-browser auth (the gh / Linear / Slack pattern). The desktop opens the user's real system browser to log in — which is also the only way OIDC (Google/Microsoft) can work on desktop, since Google blocks OAuth inside embedded webviews — and the server hands an API token back to the app via a custom deep-link redirect. The token is then sent as Authorization: Bearer … on the Rust HTTP client (the SDK still supports apiToken; the auth extractor already falls back to Bearer when there's no session cookie — jwt_header.rs).
What the server needs to provide
A desktop authorization flow whose callback, on success, redirects to a registered deep-link URI with an API token in the URL (never a Set-Cookie — the app's webview can't see it).
1. Accept a deep-link redirect target
- Add
nvisy://auth/callback (final scheme TBD) to the allowed redirect targets, alongside the existing OIDC_ALLOWED_REDIRECT_ORIGINS. A custom scheme, not an http origin — the validation in auth_oidc.rs (begin_flow rejects non-allow-listed targets, line ~312) needs to permit it.
- This is a desktop-only target; keep it separate from the web frontend origins.
2. Both password and OIDC in the browser, one token out
The browser-based desktop login should support both:
- Password: a server-hosted login page (or reuse the existing web login) that authenticates and, for a desktop-originated flow, ends by redirecting to the deep-link with a token.
- OIDC (Google/Microsoft): reuse the existing
/auth/{provider}/start + callback. The providers still redirect to the server (unchanged); only the server's final hop changes — for a desktop flow it emits a token to nvisy://auth/callback instead of setting a cookie on a web origin.
3. Return the token in the callback URL
Reuse the existing fragment-token mechanism (redirect_to_frontend already supports RedirectResult::Token { name: "token", value } in auth_oidc.rs) — the desktop callback should redirect to:
nvisy://auth/callback#token=<api_jwt>
(fragment preferred over query, consistent with the current sign-in token return; ?signin=error on failure). The token is a normal API JWT the desktop stores in its OS keychain and sends as Authorization: Bearer.
4. Distinguish desktop vs web at flow start
The flow needs to know it's a desktop flow (→ emit a token) vs a web flow (→ set a cookie). Most natural: infer it from the redirect target — a nvisy:// target means desktop/token, an http origin means web/cookie. (Alternatively an explicit client=desktop param.) Whichever you pick, please note it in the response so the SDK/client can match.
Contract summary (what the client will implement against)
- Start: desktop opens the system browser to a server authorize URL with
redirect_uri=nvisy://auth/callback.
- In-browser: user authenticates (password and/or Google/Microsoft) on the server's own origin.
- Callback: server redirects to
nvisy://auth/callback#token=<jwt> on success (?signin=error on failure). No Set-Cookie on this response.
- After: desktop stores the token in the keychain, sends it as
Authorization: Bearer. The existing Bearer path in the auth extractor already accepts it.
Notes / open questions for the server side
- Final scheme name (
nvisy:// vs something else) — client will match.
- Token lifetime for desktop: should this mint a longer-lived API token (like the api-tokens endpoint) rather than a short session JWT, so desktop doesn't silently expire? Your call — flag what you choose.
- Does the deep-link redirect target belong in a new env var (e.g.
DESKTOP_ALLOWED_REDIRECT_SCHEMES) separate from OIDC_ALLOWED_REDIRECT_ORIGINS?
- Confirm the provider (Google/Microsoft) redirect URIs stay pointed at the server callback (they should — only the server's final hop is the deep link).
The web cookie flow stays exactly as-is; this is purely the additional desktop path. The desktop client work (deep-link plugin, keychain, Bearer client) is scoped and ready to implement once this contract lands.
Context
The web console moved to cookie-session auth (SDK 0.47):
login/signup/OIDC set the HttpOnlynvisy.session+ readablenvisy.csrfcookies and return no token. That works for the browser, but it breaks the Tauri desktop app, and the fix needs a server capability that doesn't exist yet.Why desktop can't use cookies (verified): the desktop routes all API calls through Tauri's Rust HTTP client (
tauri-plugin-http) — it must, because the webview can't reach an arbitrary self-hosted API origin (CORS). That Rust client has its own reqwest cookie jar, invisible to the webview'sdocument.cookie, and a bare HttpOnly session cookie (noMax-Age) isn't even persisted across app restarts. So the desktop can't readnvisy.csrffor the CSRF echo, can't observe the session viadocument.cookie, and can't keep a session across launches. Cookie auth is structurally the wrong fit for the desktop.Chosen desktop approach: external-browser auth (the
gh/ Linear / Slack pattern). The desktop opens the user's real system browser to log in — which is also the only way OIDC (Google/Microsoft) can work on desktop, since Google blocks OAuth inside embedded webviews — and the server hands an API token back to the app via a custom deep-link redirect. The token is then sent asAuthorization: Bearer …on the Rust HTTP client (the SDK still supportsapiToken; the auth extractor already falls back to Bearer when there's no session cookie —jwt_header.rs).What the server needs to provide
A desktop authorization flow whose callback, on success, redirects to a registered deep-link URI with an API token in the URL (never a
Set-Cookie— the app's webview can't see it).1. Accept a deep-link redirect target
nvisy://auth/callback(final scheme TBD) to the allowed redirect targets, alongside the existingOIDC_ALLOWED_REDIRECT_ORIGINS. A custom scheme, not an http origin — the validation inauth_oidc.rs(begin_flowrejects non-allow-listed targets, line ~312) needs to permit it.2. Both password and OIDC in the browser, one token out
The browser-based desktop login should support both:
/auth/{provider}/start+ callback. The providers still redirect to the server (unchanged); only the server's final hop changes — for a desktop flow it emits a token tonvisy://auth/callbackinstead of setting a cookie on a web origin.3. Return the token in the callback URL
Reuse the existing fragment-token mechanism (
redirect_to_frontendalready supportsRedirectResult::Token { name: "token", value }inauth_oidc.rs) — the desktop callback should redirect to:(fragment preferred over query, consistent with the current sign-in token return;
?signin=erroron failure). The token is a normal API JWT the desktop stores in its OS keychain and sends asAuthorization: Bearer.4. Distinguish desktop vs web at flow start
The flow needs to know it's a desktop flow (→ emit a token) vs a web flow (→ set a cookie). Most natural: infer it from the redirect target — a
nvisy://target means desktop/token, an http origin means web/cookie. (Alternatively an explicitclient=desktopparam.) Whichever you pick, please note it in the response so the SDK/client can match.Contract summary (what the client will implement against)
redirect_uri=nvisy://auth/callback.nvisy://auth/callback#token=<jwt>on success (?signin=erroron failure). NoSet-Cookieon this response.Authorization: Bearer. The existing Bearer path in the auth extractor already accepts it.Notes / open questions for the server side
nvisy://vs something else) — client will match.DESKTOP_ALLOWED_REDIRECT_SCHEMES) separate fromOIDC_ALLOWED_REDIRECT_ORIGINS?The web cookie flow stays exactly as-is; this is purely the additional desktop path. The desktop client work (deep-link plugin, keychain, Bearer client) is scoped and ready to implement once this contract lands.