Port the Authress login SDK; rework the theme picker and onboarding - #4
Merged
Conversation
The picker was five FilterChips that all drew from the active theme, so every option looked identical and none of them showed what selecting it would do — the one thing a theme picker exists to communicate. Each option is now a tile painted from its own palette: its base as the background, mantle as the footer, the text and subtext ramp, the seven accent colours, and a pair of miniature mail rows standing in for an inbox. That covers what actually changes when a flavour is applied, so the choice can be made by looking rather than by trying each one. System has no palette of its own, so its tile is split between the two palettes it resolves to, Latte and Mocha, labelled light and dark. Selecting now sets a flavour outright instead of toggling back to system on a second tap. System is an explicit tile, which makes the old toggle both redundant and surprising. Tiles are laid out two per row by chunking rather than with a lazy grid, so the picker can sit inside the scrolling settings column without a nested-scroll conflict, and a trailing odd tile keeps its width instead of stretching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
The /authorize and /oauth/token paths came from b3f6eff, the same commit that invented the Numaeel product and its domains, and neither exists. Authress publishes discovery at the issuer: authorization_endpoint https://login.rhosys.cloud (the issuer root) token_endpoint https://login.rhosys.cloud/api/authentication/oauth/tokens AuthressAuthManager now resolves both through AuthorizationServiceConfiguration.fetchFromIssuer rather than hardcoding paths, so a change on Authress's side cannot silently break sign-in the way a wrong guess already had. The config is fetched once per process behind a mutex. Discovery is a network call, so launchSignIn suspends and returns a Result; LoginScreen clears its loading state if discovery fails instead of hanging on a spinner. The requested scope drops `email` and `offline_access`. Neither appears in scopes_supported, which advertises only openid and profile; refresh tokens come from the refresh_token grant, which is advertised. The application id defaults to app_2EAWGEdtzaeCj7b45DsDtt, matching the web app's VITE_AUTHRESS_APPLICATION_ID, rather than the invented numaeel_android. Separately, onboarding no longer asks about biometric lock. It is a security preference someone sets when they want it, not a decision worth interrupting a first launch for, and Settings already has the toggle. Onboarding drops to four steps, and its "Pick a look" step now uses the same previewing ThemePicker as Settings — it had the same all-identical-chips problem. ThemePicker moves to presentation/components now that two screens share it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
The previous implementation treated Authress as a plain OAuth provider and drove
it through AppAuth. That was wrong in shape, not just in URLs, which is why
guessing endpoints and then discovering them both produced something that could
not work.
@authress/login-react-native does this instead:
1. POST /api/authentication with applicationId, redirectUrl and a PKCE S256
challenge; the response carries an authenticationUrl and an
authenticationRequestId.
2. Open that URL in the device browser.
3. Authress redirects to the app's deep link with code and
authenticationRequestId.
4. POST /api/authentication/{authenticationRequestId}/tokens with the code, the
stored code verifier and the redirect URI.
There is no authorize endpoint and no token endpoint in the OAuth sense, and the
session is not an access/refresh pair — it lives in the `authorization` cookie,
with identity claims in `user`. That is why the discovered endpoints would not
have helped either.
Ported file by file from the SDK: JwtManager for PKCE and payload decoding
including its ten-second exp buffer, AuthStorageManager for the pending PKCE
state, AuthressCookieJar for the session cookies, and AuthressLoginClient for
the flow. The 4xx-on-token-exchange case is treated as success and cleanup, as
the SDK does, because it usually means the code was already redeemed.
AppAuth is dropped for androidx.browser, since the SDK opens the hosted page in
the browser rather than running an OAuth library. TokenStore no longer holds
tokens — only the selected account — and AuthInterceptor takes the bearer from
the session cookie.
This also resolves the duplicate redirect claim: AppAuth's
RedirectUriReceiverActivity is gone, so MainActivity is the only component
claiming the scheme, and it now overrides onNewIntent to forward the redirect
exactly as the SDK's own Android setup instructions describe. The redirect URI
moves to the documented scheme://host/path form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
The port was missing the two methods that make sessions self-maintaining, so a token would go stale and every API call would keep sending it. waitForToken is what the SDK documents for an Authorization header. It returns immediately when a valid session exists and otherwise waits for one being established, so a request issued mid-refresh waits instead of being rejected. AuthInterceptor now goes through it rather than reading the cookie directly. userIsLoggedIn is what actually refreshes: it calls PATCH /session when the current token is gone or expired. Its own documentation recommends calling it on every route change, so AppNavHost now does exactly that. Neither an OkHttp Authenticator reacting to 401 nor a pre-emptive exp check was needed — both would have been a parallel mechanism competing with the one the SDK already defines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
The jar had been merged into a single always-persisted store on the reasoning that the SDK's two-layer arrangement was redundant on Android. That was a judgement call substituted for the reference implementation, which is exactly the kind of deviation worth not making. authStorageManager.ts keeps the live cookie store separate from its encrypted mirror and moves between them at defined points. That structure is restored here, with the call sites matching the SDK one for one: restoreCookies in the constructor, before anything reads a token backupCookies after a successful token exchange backupCookies after a successful session check clear on logout, clearing both layers together restoreCookies keeps the SDK's early return when the live jar already holds cookies, so a stale mirror can never overwrite an active session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
The OAuth endpoint entry described discovering authorize/token endpoints, which the port made irrelevant — Authress has no such exchange. Replaced with the actual flow, and the duplicate-redirect entry is folded in as resolved, since dropping AppAuth left MainActivity as the only claimant. Also corrects the MFA line. Device management is absent from the email API, but the login service does expose GET and DELETE /api/session/devices, which the SDK wraps as getDevices and deleteDevice — so the Settings tab could be rebuilt against those rather than needing a backend change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjmqdZdfZaN7gary9eEBsL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Authentication
The app treated Authress as a plain OAuth provider and drove it through AppAuth against
/authorizeand/oauth/token. Neither path exists — both came fromb3f6eff, the commit that invented the Numaeel product and its domains.Authress is not an OAuth provider in that shape, so correcting the URLs would not have helped either.
@authress/login-react-nativeis now ported directly:The session is held in the
authorizationandusercookies, not an access/refresh pair.Ported file by file:
JwtManagerjwtManager.ts— PKCE S256, payload decode, the 10sexpskew bufferAuthStorageManagerauthStorageManager.ts— pending PKCE state in encrypted storageAuthressCookieJarauthStorageManager.ts— live jar plus encrypted mirror, withbackupCookies/restoreCookiesat the SDK's own call sitesAuthressLoginClientloginClient.ts— the flow,getToken,waitForToken,userIsLoggedIn,logoutSession upkeep uses the SDK's own methods rather than an invented mechanism:
userIsLoggedIn()refreshes viaPATCH /sessionand is called on every route change, as its documentation recommends, andwaitForToken()supplies the Authorization header so a request issued mid-refresh waits rather than being rejected.AppAuth is replaced by
androidx.browser. The hosted page opens in a Custom Tab — the real browser, so passkeys and the password manager work. The browser does not share cookies with the app and does not need to: the token exchange is made by the app's own HTTP client, so the session cookie lands there.This also resolves the duplicate redirect claim. AppAuth's
RedirectUriReceiverActivityno longer competes withMainActivityfor the scheme, andMainActivityforwards the redirect throughonNewIntentas the SDK's Android setup describes.The application id now defaults to
app_2EAWGEdtzaeCj7b45DsDtt, matching the web app'sVITE_AUTHRESS_APPLICATION_ID.Theme picker
Five
FilterChips all drew from the active theme, so every option rendered identically and none previewed what selecting it would do.Each option is now a 168dp tile painted from its own palette —
baseas the ground,mantlefor the footer, thetext/subtextramp, the seven accents, and two miniature mail rows. The two rows are what distinguishes the dark flavours from each other, which a flat swatch would not show. System splits between the two palettes it resolves to.Selecting a flavour now sets it outright; previously tapping the selected chip reverted to system, which was undiscoverable and is redundant now that System is a tile.
Used by both Settings and onboarding's "Pick a look" step, which had the same problem.
Onboarding
Drops the biometric-lock step — a security preference belongs in Settings, not in the way of a first launch. Four steps instead of five.
Verification
compileDebugKotlin,lintDebugandtestDebugUnitTestpass locally and in CI. No warnings.Not verified: nothing has run against a live backend, and no test covers the new auth code. The parts to watch on first real login are the PKCE encoding, the
isscomparison againsthttps://login.rhosys.cloud, and the deep-link parsing.