diff --git a/docs/android-client.md b/docs/android-client.md index b5f8d66..d939f29 100644 --- a/docs/android-client.md +++ b/docs/android-client.md @@ -74,6 +74,57 @@ Use this for any future UI differences between the web client and the Android app (hiding elements, adjusting layout, etc.). +## OAuth / login flow + +Android WebView **does not share cookies** with the system browser. When the user clicks **Login**, the app opens gnexus-auth in the user's default browser (Chrome, DuckDuckGo, etc.) so SSO works. After successful authentication, the backend redirects the browser to `/auth/mobile-done?sid=` instead of setting a cookie. + +`/auth/mobile-done` is a bridge page styled with the gnexus UI kit. It: +1. Immediately attempts `window.location.href = "intent://auth/callback?sid=xxx#Intent;scheme=navi;package=com.navi.client;end"` — Chrome understands this and opens the app automatically. +2. If the browser blocks the redirect (DuckDuckGo, Firefox), a styled fallback button **"Open Navi App"** appears after 1.5s. Tapping it triggers the same Intent URL with a user gesture, which the browser cannot block. + +### Deep-link intent filter + +`AndroidManifest.xml` declares an intent filter for `navi://auth/callback`: + +```xml + + + + + + +``` + +The activity uses `launchMode="singleTask"` so `onNewIntent` is called even when the app is already running. + +### Cookie injection + +`MainActivity` handles the incoming intent in both `onCreate` (cold start) and `onNewIntent` (app already running): + +```kotlin +override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + intent.data?.let { uri -> + if (uri.scheme == "navi" && uri.host == "auth" && uri.path == "/callback") { + val sid = uri.getQueryParameter("sid") + val prefs = getSharedPreferences("navi", Context.MODE_PRIVATE) + val serverUrl = prefs.getString("server_url", null) + if (sid != null && serverUrl != null) { + CookieManager.getInstance().setCookie( + serverUrl, + "navi_auth_session=$sid; Path=/" + ) + webView.loadUrl(serverUrl) + } + } + } +} +``` + +After the cookie is set, the WebView reloads the server URL and the user is authenticated. + +--- + ## Back navigation Hardware back button navigates WebView history if available (`webView.canGoBack()`), otherwise falls through to the default Android behaviour (closes the activity). diff --git a/docs/api.md b/docs/api.md index 69e3d72..fb64dc1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -33,19 +33,32 @@ #### `GET /auth/callback` -OAuth callback. Validates state, exchanges code for tokens, creates DB session, sets cookie. +OAuth callback. Validates state, exchanges code for tokens, creates DB session. **Query params** - `code` — authorization code from gnexus-auth - `state` — state parameter -**Response `302`** → Location: `/` +**Response `302`** +- **Browser** → Location: `/` (with `Set-Cookie`) +- **Android** → Location: `/auth/mobile-done?sid=` **Errors** - `400` — invalid state, PKCE failure, or token exchange failed --- +#### `GET /auth/mobile-done` + +Bridge page for Android OAuth. Renders HTML that attempts an automatic deep-link back into the native app via Chrome Intent URL (`intent://...`), and falls back to a manual button for browsers that block automatic navigation to custom schemes (e.g. DuckDuckGo). Styled with the gnexus UI kit design system. + +**Query params** +- `sid` — session id that will become the `navi_auth_session` cookie + +**Response `200`** — HTML page + +--- + #### `POST /auth/logout` Logout current user. Deletes DB session and clears cookie. diff --git a/docs/auth.md b/docs/auth.md index bf5e37a..69d9dfc 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -101,7 +101,7 @@ ## OAuth login flow -### 1. Initiate login +### Browser flow (default) ``` GET /auth/login @@ -112,6 +112,44 @@ 2. Stores PKCE verifier and state in `InMemoryPkceStore` / `InMemoryStateStore` (from `gnexus-auth-client-py`) 3. Returns `302` redirect to gnexus-auth `/oauth/authorize` +After the user logs in at gnexus-auth, they are redirected back to `/auth/callback`, where the backend exchanges the code, creates a DB session, sets an `httpOnly` cookie, and redirects to `/`. + +### Android flow (WebView bridge) + +Android WebView **cannot share cookies** with the system browser (Chrome, DuckDuckGo, etc.). Therefore the Android app uses a **bridge-page flow** that avoids cookies entirely: + +``` +GET /auth/login ← Backend detects Android via User-Agent (NaviAndroid/1.0) + ↓ 302 +gnexus-auth /oauth/authorize ← Opens in external browser (SSO works) + ↓ +GET /auth/callback?code=...&state=... + ↓ 302 +/auth/mobile-done?sid= + ↓ JS redirect +intent://auth/callback?sid=...#Intent;scheme=navi;package=com.navi.client;end + ↓ +Android intercepts intent → CookieManager.setCookie() → reload WebView +``` + +Key points: +- Backend detects Android from the HTTP `User-Agent` header (`NaviAndroid/1.0`) rather than a query parameter. +- For Android, `/auth/callback` **does not set a cookie** — instead it redirects to `/auth/mobile-done`. +- `/auth/mobile-done` renders a bridge page styled with the gnexus UI kit (dark theme, success accent border). It immediately attempts a Chrome Intent URL auto-redirect; if the browser blocks it, a fallback button appears after 1.5s. +- The `intent://` URL is intercepted by Android's `intent-filter` for `navi://auth/callback`, which delivers the `sid` to `MainActivity.onNewIntent`. The app then sets the `navi_auth_session` cookie via `CookieManager` and reloads the WebView. + +### 1. Initiate login + +``` +GET /auth/login +``` + +Navi backend: +1. Calls `client.build_authorization_request(return_to="/", scopes=["openid", "email", "profile", "roles", "permissions"])` +2. Stores PKCE verifier and state in `InMemoryPkceStore` / `InMemoryStateStore` (from `gnexus-auth-client-py`) +3. **For Android**: stores `platform=android` alongside the state metadata so callback knows to skip the cookie +4. Returns `302` redirect to gnexus-auth `/oauth/authorize` + ### 2. User logs in at gnexus-auth User authenticates with gnexus-auth, approves scopes, and is redirected back to: @@ -129,8 +167,8 @@ 4. Determines `role` from `role_ids` and collects `permissions` from `permission_ids` 5. **Upserts** `navi_users` row (id, email, display_name, role, permissions) 6. **Creates** `user_auth_sessions` row with encrypted tokens -7. Sets `navi_auth_session` cookie -8. Redirects to `/` +7. **Browser**: sets `navi_auth_session` cookie and redirects to `/` +8. **Android**: redirects to `/auth/mobile-done?sid=` ### 4. Subsequent requests