Skip to content

Feature/auth issues 208 211 - #337

Open
Silasdotmd wants to merge 4 commits into
ethos-protocol:mainfrom
Silasdotmd:feature/auth-issues-208-211
Open

Feature/auth issues 208 211#337
Silasdotmd wants to merge 4 commits into
ethos-protocol:mainfrom
Silasdotmd:feature/auth-issues-208-211

Conversation

@Silasdotmd

Copy link
Copy Markdown

Summary
Addresses four Stellar Wave auth-hardening issues. Each is its own commit.

#208 — Add Session/Device List with Remote Sign-Out
Adds GET /auth/sessions, DELETE /auth/sessions/{id}, and DELETE /auth/sessions to the API contract. Both apps now have an "Active Sessions" screen (iOS: SettingsView → SessionsView; Android: new sessions nav route from the vault list top bar) showing device/platform/last-active time, with biometric-gated "Sign out this device" and "Sign out all other devices" actions.

#209 — Implement Silent Token Refresh Before Expiry Using /auth/refresh Proactively
Audited both clients: iOS's AuthStore already scheduled a refresh timer ahead of expiry; Android's ApiClient only refreshed lazily right before the next outgoing request, so a foregrounded-but-idle app could sit on a near-expired token indefinitely. AuthViewModel now polls every 30s while signed in and refreshes within the existing 60s isNearExpiry margin. The shared 60s refresh margin is now documented in shared/api-contract.md.

#210 — Add Passkey Registration Fallback Copy for Devices Without Biometric Hardware
Registration on both platforms only handled the happy path. Android's register() now runs through the same CreateCredentialException mapping the recovery path already had, with copy naming both the biometric-enrollment and PIN/passcode-backed-passkey options. iOS detects the wrapped LAError (biometryNotEnrolled / biometryNotAvailable / passcodeNotSet) and returns a new PasskeyError.biometricUnavailable case with the same guidance.

#211 — Add Automated Tests for /auth/recover/link Token Expiry Handling
Neither platform had a test for a recovery token/proof expiring between initiate and complete, and a 401 here fell back to a generic "Authentication required" with no path forward. Both clients now surface the server's human-readable {"error": "..."} body on a 401 instead of the generic message (ordinary rejected-session 401s are unaffected — they carry no body). Android's recovery flow (models, ApiClient, PasskeyService already existed but were never wired to the UI) is connected end-to-end through AuthViewModel and the existing RecoverySheet, resetting to the "send a new code" step on a rejected token.

Test plan
iOS: SessionsStoreTests, APIClientTests (sessions + recovery-expiry cases), AuthStoreTests (refresh + recovery-expiry cases), PasskeyServiceTests (biometric-unavailable mapping)
Android: SessionsViewModelTest, ApiClientTest (401 body-message cases), AuthViewModelTest (scheduled refresh + recovery cases), PasskeyServiceTest (recovery + no-biometric cases)
Manual: exercise the new Active Sessions screen and "Lost your device?" recovery flow on a device/simulator
Closes #208
Closes #209
Closes #210
Closes #211

…col#208)

Adds GET /auth/sessions plus DELETE /auth/sessions/{id} and DELETE
/auth/sessions to the API contract, and surfaces them as an "Active
Sessions" screen on both platforms showing device/platform/last-active
time with biometric-gated "Sign out this device" / "Sign out all other
devices" actions.
ethos-protocol#209)

iOS's AuthStore already scheduled a refresh timer ahead of token expiry;
Android's ApiClient only refreshed lazily right before the next request,
so a foregrounded-but-idle app could sit on a near-expired token
indefinitely. AuthViewModel now polls every 30s while signed in and
refreshes once within the existing 60s isNearExpiry margin, matching
iOS's lead time. Documents the shared 60s refresh margin in the API
contract.
…ics (ethos-protocol#210)

Registration on both platforms only handled the happy path where the
device has a biometric enrolled. A device with none enrolled (or an
MDM policy disabling it, or no passcode set) surfaced a generic
CredentialManager/ASAuthorization failure instead of guidance. Android's
register() now runs through the same CreateCredentialException mapping
already used by the recovery path; iOS detects the wrapped LAError
(biometryNotEnrolled/biometryNotAvailable/passcodeNotSet) and returns a
dedicated PasskeyError.biometricUnavailable case. Both point the user
at enrolling a biometric or falling back to a device PIN/passcode.
…ors (ethos-protocol#211)

Neither platform had a test for a recovery token/proof expiring between
initiate and complete, and a 401 on that call fell back to the generic
"Authentication required" message with no path forward. Both clients'
401 handling now surfaces the server's human-readable error body when
present (falling back to the existing generic message when it's empty,
as on an ordinary rejected session token). Android's recovery flow
(models, ApiClient, PasskeyService already existed but were never wired
up) is connected end-to-end through AuthViewModel and the existing
RecoverySheet, resetting to the "send a new code" step on a rejected
token instead of leaving the user on a dead end.
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@Silasdotmd Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant