feat(auth): M9 Part 2 — native in-app SSO via the mobile bridge
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m34s
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m34s
Add the app client for the Mobile SSO Authorization Bridge (PLAN.md §4.2): native "Sign in with <provider>" without shipping any OAuth secret. - Pkce: pure-JVM RFC 7636 S256 verifier/challenge + CSRF state, encoded to match the backend's base64url(SHA-256) exactly. - SsoAuthManager (Singleton): mints PKCE+state, builds the /auth/mobile/sso/start URL for a Custom Tab, verifies the returned state, exchanges the one-time code with the stashed verifier, and drives the existing SessionManager.onSignedIn — no new token-storage or refresh code. Pending flow is in-memory (fails closed on process death). Exposes an outcome StateFlow the login screen consumes. - SsoApi + DTOs: GET /auth/providers discovery and POST /auth/mobile/sso/exchange (tagged NO_SESSION so a credential 401 isn't read as an expired session). - MainActivity: runicgateway://auth/callback intent-filter + singleTop; parses the callback Uri (the Android edge) and hands raw params to SsoAuthManager. - LoginScreen/ViewModel: render a button per discovered provider, opening the bridge in a Custom Tab; fall back to the website login hand-off when none. Additive — no other screen's data flow changes; no backend work. Custom scheme only for now (App Links deferred, APP_LINKS.md). Tests (JVM, +14): Pkce vector/charset, start-URL building, and the full complete() flow over a fake SsoApi + real SessionManager (success signs in; state mismatch / missing pending fail without exchanging; error callback → declined; 401 → expired-code; replay finds no pending). Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
@@ -24,14 +24,32 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RunicGateway">
|
||||
|
||||
<!-- singleTop so the SSO Custom Tab returning via the deep link reuses the
|
||||
running task (onNewIntent) instead of stacking a second activity. -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.RunicGateway">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Native SSO callback (M9, PLAN.md §4.2). The bridge deep-links the
|
||||
one-time authorization code back to this fixed, app-owned custom
|
||||
scheme; it must match SsoAuthManager.REDIRECT_URI and the backend's
|
||||
MOBILE_AUTH_REDIRECT_URIS allowlist exactly. Custom scheme only for
|
||||
now — HTTPS App Links are deferred (docs/android/APP_LINKS.md). -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="runicgateway"
|
||||
android:host="auth"
|
||||
android:path="/callback" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- The embedded distributor's persistent ntfy connection (M7, PLAN.md §11).
|
||||
|
||||
Reference in New Issue
Block a user