How OpenHuman Manages OAuth Credentials: Secure Storage and Automatic Token Refresh
OpenHuman stores OAuth credentials in a Rust-based core credential service that persists access tokens, refresh tokens, and expiration metadata in per-provider profiles, automatically refreshing expired tokens internally without exposing secrets to the UI layer.
OpenHuman implements a secure, desktop-native OAuth flow that separates credential management from the frontend layer. As implemented in tinyhumansai/openhuman, the architecture ensures that sensitive tokens never reach the React UI, instead residing in a Rust core that handles storage, expiration checks, and automated refresh cycles via provider-specific endpoints.
OAuth Architecture Overview
The OpenHuman OAuth flow spans three distinct layers: a React frontend for initiation, a desktop deep-link listener for callback interception, and a Rust core for secure credential persistence. When a user clicks "Connect" for a provider like Google or GitHub, the frontend triggers a core RPC call that launches the external browser with a deeplink return URL (openhuman://oauth/...). Upon authorization, the deep-link listener captures the callback, validates the application version, and forwards the token payload to the Rust core. The core then stores the OAuthTokenSet—containing access_token, refresh_token, and expires_at—within an AuthProfile structure that remains isolated from the UI layer.
Initial Credential Capture Flow
Type Definitions and UI Integration
The frontend declares supported providers in app/src/types/oauth.ts, restricting interactions to a defined set of OAuth providers.
// app/src/types/oauth.ts
export type OAuthProvider = 'google' | 'twitter' | 'github' | 'discord';
export interface OAuthProviderConfig {
id: OAuthProvider;
name: string;
icon: React.ComponentType<{ className?: string }>;
color: string;
showOnWelcome?: boolean;
}
UI components initiate the flow by calling oauth.start via the core RPC client, which opens the provider's authorization page in the system browser.
import { coreRpcClient } from '@/utils/tauriCommands/core';
async function startOAuth(provider: OAuthProvider) {
await coreRpcClient.call('oauth.start', { provider });
}
Deep-Link Callback Processing
When the provider redirects to openhuman://oauth/success, the desktopDeepLinkListener.ts captures the URL and extracts the credential payload.
// app/src/utils/desktopDeepLinkListener.ts
async function handleOAuthDeepLink(url: URL) {
const provider = url.searchParams.get('provider')!;
const access = url.searchParams.get('access_token')!;
const refresh = url.searchParams.get('refresh_token');
const expiresIn = Number(url.searchParams.get('expires_in'));
await coreRpcClient.call('oauth.complete', {
provider,
access_token: access,
refresh_token: refresh,
expires_in: expiresIn,
});
}
Version Gate Validation
Before accepting credentials, the listener enforces a minimum version requirement to prevent credential leakage on outdated binaries. The evaluateOAuthAppVersionGate() function in app/src/utils/oauthAppVersionGate.ts validates the application version; if the binary is too old, the flow aborts with a user-visible error before any tokens reach the core.
Core Token Persistence
The Rust core receives the payload via oauth_complete in src/openhuman/credentials/ops.rs and persists the data through AuthService.store_provider_token() in src/openhuman/credentials/core.rs.
// src/openhuman/credentials/core.rs
pub fn store_provider_token(
&self,
provider: &str,
profile_name: &str,
token: &str,
metadata: HashMap<String, String>,
set_active: bool,
) -> Result<AuthProfile> {
let mut profile = AuthProfile::new_token(provider, profile_name, token.to_string());
profile.metadata.extend(metadata);
self.store.upsert_profile(profile.clone(), set_active)?;
Ok(profile)
}
The AuthProfile struct, defined in src/openhuman/credentials/profiles.rs, encapsulates the OAuthTokenSet containing the access token, refresh token, and computed expiration timestamp.
Secure Token Storage in the Rust Core
Credentials reside in the AuthProfilesStore, a Rust-managed key-value structure keyed by provider and profile name. The core maintains the active profile state and ensures that raw refresh_token values never traverse the UI boundary. When downstream components require authentication, they request a bearer token rather than accessing the credential store directly.
The storage mechanism distinguishes between AuthProfileKind::Token for simple bearer tokens and AuthProfileKind::OAuth for full OAuth token sets requiring refresh capabilities.
Automatic Token Refresh Mechanism
When the core detects an expired token during a bearer token request, it triggers an internal refresh cycle without UI involvement. The refresh_token_if_needed function in src/openhuman/credentials/ops.rs checks the OAuthTokenSet.is_expired() method and initiates a provider-specific HTTP call to exchange the stored refresh_token for a new access_token.
// src/openhuman/credentials/ops.rs
pub async fn refresh_token_if_needed(
auth: &AuthService,
provider: &str,
profile: &AuthProfile,
) -> Result<String> {
if let Some(set) = &profile.token_set {
if set.is_expired() && set.refresh_token.is_some() {
let new_set = provider_refresh_call(set.refresh_token.clone().unwrap()).await?;
auth.store_provider_token(
provider,
&profile.name,
&new_set.access_token,
HashMap::new(),
false,
)?;
return Ok(new_set.access_token);
}
}
Ok(profile.token.clone())
}
The refreshed token set overwrites the previous entry in the AuthProfilesStore, updating both the access_token and expires_at fields atomically.
Retrieving Bearer Tokens for API Calls
Authenticated requests obtain tokens through get_provider_bearer_token() in src/openhuman/credentials/core.rs. This method returns only the sanitized bearer string, concealing the refresh token and expiration metadata from the caller.
// src/openhuman/credentials/core.rs
pub fn get_provider_bearer_token(
&self,
provider: &str,
profile_override: Option<&str>,
) -> Result<Option<String>> {
let profile = self.get_profile(provider, profile_override)?;
let Some(profile) = profile else { return Ok(None) };
let credential = match profile.kind {
AuthProfileKind::Token => profile.token,
AuthProfileKind::OAuth => profile.token_set.map(|t| t.access_token),
};
Ok(credential.filter(|t| !t.trim().is_empty()))
}
If the token expired since the last access, the internal refresh logic executes before returning the value, ensuring callers always receive valid credentials.
Summary
- Credential Isolation: Raw OAuth tokens reside exclusively in the Rust core (
src/openhuman/credentials/core.rs), encrypted at rest and isolated from the React frontend. - Automatic Refresh: The core detects expiration via
OAuthTokenSet.is_expired()and transparently refreshes tokens using storedrefresh_tokenvalues insrc/openhuman/credentials/ops.rs. - Security Gatekeeping: The
desktopDeepLinkListener.tsenforces version requirements viaevaluateOAuthAppVersionGate()before accepting callback credentials. - Sanitized Access: Callers receive only bearer tokens through
get_provider_bearer_token(), preventing secret leakage to UI components or skill runtimes.
Frequently Asked Questions
How does OpenHuman store OAuth refresh tokens securely?
OpenHuman stores refresh tokens within the OAuthTokenSet struct in the Rust core's AuthProfilesStore, defined in src/openhuman/credentials/profiles.rs. The storage occurs through AuthService.store_provider_token() in src/openhuman/credentials/core.rs, which persists tokens to disk using the application's secure credential storage instead of browser localStorage or React state. The UI layer only interacts with the core via JSON-RPC calls and never accesses the raw refresh token values directly.
What triggers an OAuth token refresh in OpenHuman?
A token refresh triggers automatically when get_provider_bearer_token() detects that the stored token's expires_at timestamp has passed. The core's refresh_token_if_needed() function in src/openhuman/credentials/ops.rs then executes a provider-specific HTTP request to exchange the stored refresh token for a new access token. This process occurs entirely within the Rust runtime without requiring user interaction or UI notification.
Why does OpenHuman use a version gate for OAuth callbacks?
The evaluateOAuthAppVersionGate() function in app/src/utils/oauthAppVersionGate.ts ensures that outdated application binaries cannot complete OAuth flows, preventing potential security vulnerabilities or credential leakage on legacy builds. If the desktop binary version falls below the minimum requirement, the deep-link listener blocks the credential storage operation and displays an error to the user, forcing an update before authentication can proceed.
Which OAuth providers does OpenHuman support?
According to the type definitions in app/src/types/oauth.ts, OpenHuman supports Google, Twitter (X), GitHub, and Discord OAuth providers. The OAuthProvider type union restricts the system to these four providers, with each configuration specifying provider-specific metadata such as icon components and color schemes for the UI while the core handles the actual token storage generically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →