Arezgitfield notes / engineering
SecurityUPDATED JUL 12, 2026

Secure Token Storage in React and Tauri Desktop Applications

A practical architecture for short-lived access tokens, rotating refresh tokens, OS credential vaults, IPC validation, logout, and offline entitlements in Tauri.

AREZGIT / FIELD NOTESECURITY
A desktop application combines a web rendering layer with native authority. That does not make browser storage safer. Tokens in localStorage , ordinary settings files, or an application SQLi
READ / VERIFY / APPLYTECHNICALLY REVIEWED

A desktop application combines a web rendering layer with native authority. That does not make browser storage safer. Tokens in localStorage, ordinary settings files, or an application SQLite database are readable by injected frontend code and frequently accessible to other local processes or backups.

Secure desktop authentication needs a small native credential boundary, short-lived access tokens, rotating refresh tokens, and strict control over which webview code can invoke privileged commands.

Separate access and refresh credentials

An access token should be short-lived and held in memory. It authorizes API requests for minutes, which limits the useful lifetime of accidental exposure.

A refresh token lasts longer and creates new sessions, so store it only in the operating system's credential vault. Rotate it after every successful refresh and invalidate the previous value server-side.

The server should store only a cryptographic hash of each refresh token. A database leak should not provide immediately usable sessions.

A desktop login response can return:

{
  "accessToken": "short-lived-token",
  "refreshToken": "single-use-refresh-token",
  "account": {
    "id": "account-id",
    "email": "developer@example.com",
    "role": "user"
  }
}

The React layer passes the refresh token once to a narrow native storage command, then removes it from component state. The access token remains in memory and is never persisted.

Use the operating system credential vault

On Windows, use Windows Credential Manager. On Linux, use a Secret Service implementation or KWallet-compatible path. On macOS, use Keychain.

The native layer should expose purpose-specific commands such as:

store_account_refresh_token
read_account_refresh_token
delete_account_refresh_token

Avoid a generic "store any secret under any key" command available to the webview. Narrow commands reduce the authority exposed through IPC and make allowlisting easier.

Use a stable service name and an account-specific entry identifier that contains no secret. Handle vault-unavailable errors explicitly. Do not fall back to plaintext storage for convenience.

If Linux users do not have a supported credential service running, explain how to enable one and allow local non-account features to continue.

Harden the Tauri command boundary

Every native command is a privileged API. Validate all string lengths, identifier formats, URLs, and paths in Rust even when React already validates them.

Apply least privilege to Tauri capabilities. Only the intended application window should invoke credential commands. Do not enable broad shell, filesystem, opener, or network capabilities when a purpose-specific command can do the job.

For URLs, allowlist schemes and hosts. For filesystem operations, resolve canonical paths and ensure they remain within an explicitly selected repository or output directory. Never build shell commands from untrusted strings.

Return safe error codes to the webview. Native errors can contain filesystem paths, provider messages, or internal state that should not appear in UI or analytics.

Rotate refresh tokens atomically

The refresh flow is:

  1. React has no valid access token.
  2. The native layer reads the refresh token from the vault.
  3. React sends it to the refresh endpoint over TLS.
  4. The server verifies its hash and active session state.
  5. The server marks the old session token rotated and creates a new token.
  6. The client stores the new refresh token in the vault.
  7. The new access token remains in memory.

The server should perform rotation in one database batch or transactionally equivalent operation.

Client storage can still fail after server rotation. Design the response and UI for this case. If the new refresh token cannot be saved, revoke the affected session if possible, clear memory, and ask the user to sign in again. Do not keep using the old token.

Detect refresh-token reuse

If a rotated token appears again, it may indicate that an old copy was stolen or duplicated. Revoke the session family associated with that token and require authentication again on the affected device family.

Record a security event containing bounded metadata such as account ID, session family, platform, timestamp, and a request identifier. Do not record the token.

Reuse handling should be deterministic under concurrent refresh attempts. One request may succeed; the competing old-token request should trigger the defined protection rather than create two valid descendants.

Keep OAuth browser flows bound to the desktop request

Desktop OAuth commonly opens the system browser and returns through a custom URL scheme or loopback listener. Protect the flow with high-entropy state and PKCE.

Allowlist exact desktop redirect URIs. Use a one-time, short-lived authorization code when transferring the result from the backend callback to the desktop. Do not put provider access tokens or product refresh tokens directly in the callback URL.

Validate the custom scheme payload, consume the code once, and close the browser handoff with clear success or failure UI.

Provider tokens for GitHub or another integration are separate credentials. Store them in distinct vault entries and revoke them independently from the Arezgit account session.

Design logout and session management

Local logout must:

  • Call the server to revoke the current session when reachable.
  • Delete the refresh token from the OS vault.
  • Clear the in-memory access token and account state.
  • Clear cached entitlement material as appropriate.
  • Leave non-sensitive local repository settings intact.

Offer a server-backed session list with device name, platform, creation time, and last use. Users should be able to revoke another session and all sessions.

Account deletion should revoke every session and remove or anonymize personal data according to the product's privacy contract.

Support paid features offline with signed entitlements

Do not persist a boolean such as premium=true. It is editable and cannot express expiry, source, or feature scope.

Use a server-signed entitlement containing:

  • Subject account
  • Plan and entitlement source
  • Allowed features
  • Issue time
  • Expiration time
  • Token identifier
  • Audience and issuer

The desktop bundles only the public verification key. A short offline period allows the application to keep working during an outage while periodically revalidating subscription changes. The underlying lifetime entitlement can remain permanent even though its offline proof expires and refreshes.

Keep the entitlement in protected storage when practical and verify its signature, audience, issuer, and dates before every premium decision boundary.

Test the failure paths

Before release, test:

  • Vault available and unavailable
  • First login and application restart
  • Access-token expiry
  • Successful refresh rotation
  • Concurrent refresh
  • Reuse of a rotated token
  • Server unavailable during startup
  • Vault write failure after rotation
  • Logout online and offline
  • Session revocation from another device
  • OAuth state mismatch and expired code
  • Offline entitlement valid, expired, malformed, and incorrectly signed

Inspect logs and crash output to confirm no token appears. Search memory persistence paths, settings, SQLite, URLs, and analytics payloads.

The secure architecture is intentionally simple: access token in memory, refresh token in the OS vault, server-side hashes and rotation, narrow native IPC, and signed offline entitlement evidence. Complexity belongs in the well-tested boundary so the rest of the desktop application can operate without handling long-lived secrets.