Login previously left the app entirely for a separate browser task. Now uses androidx.browser Custom Tabs, which overlays the app's own activity instead of switching away, and returns automatically on redirect. Deliberately not a WebView, per RFC 8252: an in-app WebView could read the Gitea login form's credentials directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
92 lines
5.3 KiB
Markdown
92 lines
5.3 KiB
Markdown
# TASK-026: Run the OAuth login/consent flow in a Custom Tab instead of the external browser
|
|
|
|
Status: implemented; all required checks pass; device verification pending
|
|
Owner: Claude
|
|
Reviewer: Codex
|
|
|
|
## Outcome
|
|
|
|
Live request from the same testing session: tapping "로그인" previously left the app entirely — a
|
|
separate browser app opened, the user logged into Gitea and clicked "Authorize" there, then was
|
|
handed back to UBGitea via the registered redirect URI. That hand-off felt like leaving the app.
|
|
Login and the OAuth consent screen ("애플리케이션 승인") now run inside an Android Custom Tab, which
|
|
overlays the app's own activity instead of switching to a separate browser task — visually, it
|
|
stays "in the app" the whole way through, and returns to it automatically on redirect. This does
|
|
not and cannot remove the "Authorize" click itself; that consent step is enforced by Gitea's OAuth
|
|
server, not the Android client, and is inherent to OAuth (the whole point of the screen is that the
|
|
user, not the app, has to grant access) — see Security and privacy below for why it also should
|
|
not be worked around by moving login into an in-app WebView.
|
|
|
|
## Scope
|
|
|
|
- Included: `MainActivity.kt` (`openSystemBrowser` renamed to `openAuthorizationTab`, now builds
|
|
and launches a `CustomTabsIntent` instead of a plain `Intent.ACTION_VIEW`), `app/build.gradle.kts`
|
|
and `gradle/libs.versions.toml` (new `androidx.browser:browser` dependency).
|
|
- Excluded: No change to the OAuth protocol itself — `OAuthAuthorizationRequest`,
|
|
`OAuthCallbackValidator`, `Pkce`, `OAuthClientConfig`, the redirect-URI intent-filter in
|
|
`AndroidManifest.xml`, and `RepositoryListViewModel`'s `browserRequests`/`handleCallback` flow are
|
|
all unchanged. The only thing that changed is which UI surface renders the authorization URL.
|
|
|
|
## Design notes
|
|
|
|
- Chrome Custom Tabs (`androidx.browser.customtabs.CustomTabsIntent`) is the standard Android
|
|
middle ground for this: under the hood it's still the real system browser (so it shares session
|
|
cookies, saved passwords, and any existing Gitea login with whatever browser the device already
|
|
uses), but it renders as an overlay on top of the calling activity's task instead of switching to
|
|
a separate browser app — closing it (via redirect or the back button) returns straight to
|
|
UBGitea. No `setToolbarColor`/theming was added, to avoid hardcoding a color inconsistent with
|
|
the app's actual (dynamic/Material You) color scheme; a follow-up can add that later if wanted.
|
|
- The existing redirect-URI `intent-filter` (`https://gitea.ubisampaju.synology.me/ubisampaju/oauth/callback`,
|
|
`autoVerify="true"`) needed no change — Custom Tabs resolve `ACTION_VIEW` redirects exactly the
|
|
way a full external browser does, so `onNewIntent`/`consumeCallback` still fires the same way.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Tapping login opens the Gitea authorization URL in a Custom Tab, not a separate browser app.
|
|
- [x] The OAuth redirect still returns control to `MainActivity` and completes login.
|
|
- [x] `viewModel.browserLaunchFailed()` still fires if no browser capable of handling the URL exists
|
|
(the `ActivityNotFoundException` catch is preserved, unchanged).
|
|
- [x] Build, unit tests, and lint pass.
|
|
- [ ] Full device confirmation: login flow feels "in-app" and completes successfully end to end.
|
|
|
|
## Security and privacy
|
|
|
|
- Deliberately did **not** implement this with an in-app `WebView` loading the Gitea login form
|
|
directly, even though it would look more seamless. RFC 8252 ("OAuth 2.0 for Native Apps")
|
|
specifically prohibits embedded user-agents for this reason: a `WebView` the host app controls can
|
|
read the login form's DOM/network traffic, meaning the app itself could capture the Gitea
|
|
username and password as they're typed — exactly what OAuth's browser-based redirect flow exists
|
|
to prevent. Custom Tabs give the "stays in the app" UX the human product owner asked for without
|
|
that risk, since the credential entry still happens in an isolated browser process the host app
|
|
cannot inspect.
|
|
- No new data is read, stored, or transmitted by this change; it only changes which component
|
|
renders an already-existing authorization URL.
|
|
|
|
## Required checks
|
|
|
|
```text
|
|
./gradlew assembleDebug
|
|
./gradlew testDebugUnitTest
|
|
./gradlew lintDebug
|
|
```
|
|
|
|
## Evidence
|
|
|
|
- `MainActivity.kt`: `openSystemBrowser` → `openAuthorizationTab`, body replaced with
|
|
`CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(uri))`; call site
|
|
(`viewModel.browserRequests.collect(...)`) updated to match.
|
|
- `gradle/libs.versions.toml`: added `browser = "1.8.0"` and
|
|
`androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }`.
|
|
- `app/build.gradle.kts`: added `implementation(libs.androidx.browser)`.
|
|
- `./gradlew compileDebugKotlin`: BUILD SUCCESSFUL.
|
|
- `./gradlew assembleDebug testDebugUnitTest lintDebug`: BUILD SUCCESSFUL in 3m 28s, 53 actionable
|
|
tasks, all unit tests passing, 0 lint errors.
|
|
- Live device: build installed over wireless `adb` (`adb-RFCYA0RD1CZ-ztOzAA._adb-tls-connect._tcp`,
|
|
Galaxy Z Fold, re-paired this session under a fresh pairing code/port after the previous session's
|
|
pairing expired); human product owner asked to confirm the login flow now feels in-app —
|
|
result pending as of this commit.
|
|
|
|
## Review findings
|
|
|
|
None yet — pending Codex/human review.
|