# How OpenHuman Manages OAuth Credentials: Secure Storage and Automatic Token Refresh

> Learn how OpenHuman securely manages OAuth credentials with Rust. Discover automatic token refresh and secure storage, keeping secrets safe from the UI.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: how-to-guide
- Published: 2026-05-12

---

**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`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/types/oauth.ts), restricting interactions to a defined set of OAuth providers.

```typescript
// 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.

```typescript
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`](https://github.com/tinyhumansai/openhuman/blob/main/desktopDeepLinkListener.ts) captures the URL and extracts the credential payload.

```typescript
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/credentials/ops.rs) and persists the data through `AuthService.store_provider_token()` in [`src/openhuman/credentials/core.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/credentials/core.rs).

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`.

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/credentials/core.rs). This method returns only the sanitized bearer string, concealing the refresh token and expiration metadata from the caller.

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/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 stored `refresh_token` values in [`src/openhuman/credentials/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/credentials/ops.rs).
- **Security Gatekeeping**: The [`desktopDeepLinkListener.ts`](https://github.com/tinyhumansai/openhuman/blob/main/desktopDeepLinkListener.ts) enforces version requirements via `evaluateOAuthAppVersionGate()` 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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/credentials/profiles.rs). The storage occurs through `AuthService.store_provider_token()` in [`src/openhuman/credentials/core.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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.