Auth System
Status: Implemented Depends On: Supabase project (cloud infrastructure)
Overview
Section titled “Overview”The Auth System manages user authentication, session persistence, and device registration for Inklings. It is built on Supabase Auth and follows a strict separation between three concerns:
- Identity (
IdentityStore) — Offline-permanent local user profile. Persisted in the OS keychain. Available without network access. - Auth (
AuthRepository) — Remote service tokens and session management. Requires network access for most operations. - Analytics — Pseudonymous offline-first event queue (separate system, not covered here).
This separation ensures that the app knows “who the user is” (identity) even when it cannot reach the auth service (no network), and that authentication tokens are never conflated with the user’s permanent local identity.
Auth Flow
Section titled “Auth Flow”Architecture
Section titled “Architecture”Framework (Tauri) └── Auth Commands apps/desktop/src-tauri/src/commands/ Tauri command handlers wrapping use cases
Application ├── GetCurrentUserUseCase crates/application/src/auth/get_current_user.rs ├── SignOutUseCase crates/application/src/auth/sign_out.rs ├── RegisterDeviceUseCase crates/application/src/auth/register_device.rs ├── DeauthorizeDeviceUseCase crates/application/src/auth/deauthorize_device.rs ├── ListDevicesUseCase crates/application/src/auth/list_devices.rs ├── IdentityStore (trait) crates/application/src/auth/services.rs └── AuthRepository (trait) crates/application/src/auth/services.rs
Domain ├── AuthUser crates/domain/src/auth.rs └── Device crates/domain/src/auth.rs
Infrastructure └── SupabaseAuthRepository crates/infrastructure/supabase/src/auth/supabase_auth.rs Implements both AuthRepository and IdentityStoreDependencies flow inward: Framework -> Application -> Domain. The SupabaseAuthRepository in infrastructure implements
the application-layer traits.
Domain Entities
Section titled “Domain Entities”AuthUser
Section titled “AuthUser”pub struct AuthUser { pub id: Uuid, pub email: String, pub display_name: Option<String>,}Represents an authenticated user account. Serializable for both Tauri IPC (Specta) and keychain persistence (serde JSON).
Device
Section titled “Device”pub struct Device { pub id: Uuid, pub name: String, pub platform: String, pub last_seen_at: Option<String>,}Represents a registered device linked to a user account. Stored in the Supabase devices table.
Service Traits
Section titled “Service Traits”IdentityStore
Section titled “IdentityStore”Offline-permanent local user identity, persisted in OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service).
pub trait IdentityStore: Send + Sync { fn get_identity(&self) -> AuthResult<Option<AuthUser>>; fn store_identity(&self, user: &AuthUser) -> AuthResult<()>; fn clear_identity(&self) -> AuthResult<()>;}Operations are synchronous and never require network access. The identity survives across sessions — it represents “who is this user” independently of whether they have a valid auth token.
AuthRepository
Section titled “AuthRepository”Remote authentication service for token management, session lifecycle, and device registration.
pub trait AuthRepository: Send + Sync { async fn get_current_user(&self) -> AuthResult<Option<AuthUser>>; async fn register_device(&self, device_name: &str, platform: &str) -> AuthResult<Device>; async fn sign_out(&self) -> AuthResult<()>; async fn store_session(&self, access_token: &str, refresh_token: &str) -> AuthResult<()>; async fn get_session(&self) -> AuthResult<Option<(String, String)>>; async fn clear_session(&self) -> AuthResult<()>; async fn list_devices(&self) -> AuthResult<Vec<Device>>; async fn deauthorize_device(&self, device_id: Uuid) -> AuthResult<()>; async fn refresh_session(&self) -> AuthResult<String>; async fn ensure_valid_token(&self) -> AuthResult<String>;}Uses native RPITIT async fns (Rust 1.75+). Because RPITIT traits are not object-safe, callers use the concrete
Arc<SupabaseAuthRepository> type rather than Arc<dyn AuthRepository>.
Use Cases
Section titled “Use Cases”| Use Case | Description |
|---|---|
GetCurrentUserUseCase | Retrieves the currently authenticated user from the remote service. Returns None if not authenticated. |
SignOutUseCase | Signs out and clears stored session tokens. Best-effort HTTP logout (server-side), always clears local tokens. |
RegisterDeviceUseCase | Registers a device with the user’s account (name + platform). Requires authentication. |
ListDevicesUseCase | Lists all devices registered to the current user. |
DeauthorizeDeviceUseCase | Removes a device by ID from the user’s account. |
All use cases follow the standard pattern: take a repository, expose an execute() method, return AuthResult<T>.
Supabase Implementation
Section titled “Supabase Implementation”SupabaseAuthRepository (crates/infrastructure/supabase/src/auth/supabase_auth.rs) implements both AuthRepository
and IdentityStore.
Session Storage (Dual-Layer)
Section titled “Session Storage (Dual-Layer)”Tokens are stored in two layers:
- In-memory cache (
RwLock<SessionStore>) — fast access for the current process. - OS keychain (via
keyringcrate) — persists across app restarts.
On construction, tokens are restored from the keychain into the in-memory cache. Keychain operations are best-effort — failures are logged but do not block authentication.
Keychain entries (service: "com.inklings.desktop"): - access_token (JWT from Supabase Auth) - refresh_token (Supabase refresh token) - user_identity (JSON-serialized AuthUser)Token Refresh
Section titled “Token Refresh”ensure_valid_token() is the primary entry point for obtaining a valid access token:
- Reads the stored access token.
- Decodes the JWT payload (without signature verification) to check the
expclaim. - If expired (or within 30-second skew buffer), calls
refresh_session(). refresh_session()acquires a dedicatedtokio::sync::Mutex(refresh_lock) to serialize concurrent refresh attempts (prevents TOCTOU race).- Posts to Supabase
/auth/v1/token?grant_type=refresh_token. - On success, stores the new token pair in both memory and keychain.
Exponential Backoff (Token Refresh)
Section titled “Exponential Backoff (Token Refresh)”Failed refresh attempts trigger exponential backoff to prevent hammering the auth service:
- Backoff window:
2^min(failure_count, 5)seconds (caps at 32s). - Tracked via
AtomicU32(failure count) +Mutex<Option<Instant>>(last failure time). check_refresh_backoff()rejects attempts within the window.reset_refresh_backoff()clears state on successful refresh.
Security Measures
Section titled “Security Measures”- SecretString newtype: Wraps token strings to redact them in
DebugandDisplayoutput.expose()provides access when constructing HTTP headers. - Response truncation: Error responses are truncated to 200 bytes to prevent log bloat from large error payloads.
- Keychain-free test mode:
new_in_memory()constructor disables keychain access for tests, avoiding blocking on OS authorization prompts.
Error Types
Section titled “Error Types”pub enum AuthError { NotAuthenticated, // No valid session AuthFailed(String), // Bad credentials, expired token, refresh failure Network { message, details }, // Communication failure (preserves error chain) Storage(String), // Keychain or lock poisoning}Network errors preserve both a human-readable message (from Display) and full debug details (from Debug),
providing both user-facing and diagnostic information.
Key Design Decisions
Section titled “Key Design Decisions”1. Identity vs Auth Separation
Section titled “1. Identity vs Auth Separation”Identity (IdentityStore) and authentication (AuthRepository) are deliberately separate traits. Identity is
offline-permanent — it persists even when the network is unavailable or tokens have expired. This enables:
- Displaying the user’s name/email in the UI without network access.
- Generating analytics events with a stable user identifier offline.
- Operating the app in degraded mode when auth services are unreachable.
2. Serialized Token Refresh
Section titled “2. Serialized Token Refresh”A dedicated tokio::sync::Mutex (refresh_lock) serializes concurrent token refresh attempts. This prevents the TOCTOU
race where two threads both see an expired token and attempt to refresh simultaneously, potentially invalidating each
other’s refresh tokens. Only the first caller refreshes; subsequent callers wait and use the updated token.
3. Best-Effort Keychain Persistence
Section titled “3. Best-Effort Keychain Persistence”Keychain operations (read, write, delete) never fail the overall operation. If the keychain is unavailable (e.g.,
locked, permissions denied), the system continues with in-memory-only tokens. Failures are logged at error! level so
operators can diagnose persistence issues.
4. Best-Effort Server-Side Logout
Section titled “4. Best-Effort Server-Side Logout”sign_out() posts to Supabase’s /auth/v1/logout endpoint but ignores HTTP failures. Local session clearing always
succeeds. This ensures the user can always sign out locally even when offline.
5. No Signature Verification on JWT Decode
Section titled “5. No Signature Verification on JWT Decode”is_jwt_expired() only decodes the JWT payload to read the exp claim — it does NOT verify the signature. Signature
verification is the server’s responsibility. The client only needs to know whether to proactively refresh before the
next API call.
Tauri Integration
Section titled “Tauri Integration”SupabaseAuthRepository is constructed at app startup, wrapped in Arc<Mutex<>>, and stored in AppState. It serves
as the shared instance for:
- Tauri auth commands (sign in, sign out, device management).
CapabilityResolveridentity resolution (for permission guards).- MCP server state (via
McpState::from_app_state()). - Sync engine token acquisition (via
ensure_valid_token()).
Related
Section titled “Related”- Embedding System — Requires authenticated user for cloud model access.
- MCP System — Uses
IdentityStorefor owner permission resolution. - ADR-007: Agent Integration — Unified participant model for humans and agents.
- ADR-008: Capability-Based Permissions — Permission resolution depends on identity.
Was this page helpful?
Thanks for your feedback!