Implement client-side ATProto OAuth login flow #74
Labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
puregarlic/microclimate!74
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue/9"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the Tauri client-side login flow, completes the authentication loop started in #68, and closes #9 and #70.
What's included
Auth gRPC integration (
src-tauri/src/auth/)AuthManager— new manager responsible for the full session lifecycle:start_login(server_url, identifier)— persists the server URL to the store, connects the auth gRPC client, callsInitiateLogin, and returns{ oauth_url, session_id }await_login(session_id)— opens theAwaitLoginserver-streaming call and blocks until the JWT arrives, then stores it intauri-plugin-storeget_session()— single authoritative check for "is the user authenticated?"; returnsSome(Session)only when bothserver-urlandjwtare present in the storeclear_session()— atomically removes both store keys on logoutSessionstruct — pairsserver_urlandjwtas a first-class value. The JWT and server URL are two fields of one fact ("I am logged into this server"), so they are managed together and checked together.start_logincommand — opens the OAuth URL in the system browser viatauri-plugin-openerafterInitiateLoginsucceeds.await_logincommand — coordination point between managers: once the JWT lands, triggerschannels.connect(session.server_url)so the home screen is fully ready without a separate step.get_session/logoutcommands — replaceget_server_url/set_server_urlfor session management.Session ownership refactor
ChannelsManagerpreviously owned the server URL (stored it intauri-plugin-store, exposedget_server_url/set_server_url). This was incorrect: the server URL is only meaningful in the context of an authenticated session, and treating it as independent mutable state allowed the app to be in inconsistent positions (URL set, no JWT; JWT set, wrong URL).Changes:
ChannelsManagerno longer holds a store reference or knows about the server URL.new()takes no arguments. It is now pure infrastructure: gRPC connectivity, LiveKit rooms, audio.AuthManagertakes overCONFIG_KEY_SERVER_URLfrom the store and persists it instart_login(before the browser opens, so it survives a crash mid-flow).set_server_url/get_server_urlTauri commands removed.lib.rsstartup now callsauth_manager.get_session()to check for a persisted session and connect channels if one is found.build.rsInitiateLoginResponsegains#[serde(rename_all = "camelCase")]so it serialises as{ oauthUrl, sessionId }, matching what the XState actor expects.Frontend
connect.tsx/loginMachine— drives the full login flow:start_logininvoke →{ oauthUrl, sessionId }returned, browser opens to OAuth URLawait_login(sessionId)invoke blocks in the background while the user completes OAuth/serverauthenticated.tsx—clientLoadernow callsget_sessioninstead ofget_server_url; redirects to/connectif no session.settings.tsx— "Change Server" callslogoutand redirects to/connectdirectly.Closes
Notes for reviewer
ChannelsManagerwill gain anArc<AuthManager>reference when the JWT interceptor is implemented (#69). The dependency direction isChannelsManager → AuthManager(channels needs the JWT, not the other way around). The current refactor intentionally prepares for this without introducing the circular reference that would result from the opposite arrangement.Sessionis not yet exported as a TypeScript binding — theauthenticated.tsxloader uses an inline type. This can be formalised withts-rswhen the session shape stabilises.@ -0,0 +63,4 @@}#[instrument(skip(self))]pub async fn start_login(is it worth guarding against calling this more than once at a time? if you have an in-progress login and this gets called again, would it overwrite the current client. does that have any implications?
@ -0,0 +89,4 @@#[instrument(skip(self))]pub async fn await_login(&self, session_id: String) -> Result<(), Box<dyn Error>> {let mut guard = self.client.lock().await;since this locks client, if there's a reason the user would want to call start_login again (retrying? maybe logging into another server?) this await_login call would block the subsequent start_login call until the RPC call returns (eventually, most likely because of timeout)
maybe you could
if AuthServiceClient implements Clone
@ -0,0 +57,4 @@/// Clears the stored session (server URL + JWT). Called on logout.#[instrument(skip(self))]pub fn clear_session(&self) {should we do something with the client here? as it is, you could call await_login again without first calling start_login, since you still have a valid client, right?
Yeah, I caught this too, I've got a local fix for this.
GetChannelTokenprocedure 1fd50b0822I've added code addressing #69--so not only is the client performing the auth process, the server now requires a Bearer token for the
channelsservice, and uses the user's resolved DID in place of a randomly-generated name.@ -0,0 +108,4 @@) -> Result<InitiateLoginResponse, AuthError> {if self.client.try_lock()this is a race condition, I think. if you call start_login twice and they both pass this first if statement, the two invocations race to the *self.client.lock().await call at the bottom. maybe this function should hold the lock continuously through to the end?