Skip to content

Repository files navigation

Schwab Rust SDK

A Rust SDK for Charles Schwab's Trading and Market Data APIs, providing OAuth2 authentication, REST API access, and WebSocket streaming capabilities.

Warning

RISK WARNING: This SDK is for advanced users and is provided "as is" without warranty of any kind. Automated trading involves significant risk of financial loss. Always test your logic with a test account or "Paper Money" environment before using real capital. Use of this SDK is entirely at your own risk.

Documentation

Features

  • OAuth2 Authentication

    • Authorization code flow with built-in callback server
    • Automatic token refresh with configurable buffer
    • 7-day refresh token expiration handling with notifications
    • HTTPS validation and security enforcement
    • HTTP client session management
    • Token file corruption recovery
  • Security & Token Storage

    • Memory-safe token handling with automatic zeroing (SecretString)
    • Three storage backends: File (basic), EncryptedFile (ChaCha20Poly1305), Keychain (OS-native)
    • Secure by default: macOS uses Keychain, other platforms use EncryptedFile
    • File permissions: Unix 0600 enforcement on token files
    • OWASP compliant: Best-in-class encryption and key derivation (PBKDF2 600k iterations)
  • Market Data APIs

    • Real-time quotes and price history
    • Advanced quote options: Filter by fields and enable indicative quotes
    • Option chains with Greeks
    • Market movers and instruments
    • WebSocket streaming for live data
  • Trading APIs

    • Account management
    • Order placement and management
    • Order Preview: Validate orders without execution
    • Position tracking
    • Transaction history
  • Runtime Resilience

    • Rate limiting (120 req/s with burst of 20)
    • Exponential backoff retry logic
    • 90-second crash detection for streaming
    • WebSocket Ping/Pong heartbeat monitoring (v0.2.0)
    • Timeout detection with automatic reconnection (v0.2.0)
    • Field-based subscription batching (scoped)
    • Token expiry notifications
    • Comprehensive error handling
    • Type-safe bindings for commonly used endpoints (OpenAPI coverage in progress)
  • Streaming Advanced Features (v0.2.0)

    • Bounded/Unbounded channels for backpressure control
    • Automatic Ping/Pong heartbeat (20s interval, 30s timeout)
    • Subscription persistence across reconnections
    • Custom field selection per service
    • All 13 streaming services fully implemented

Security

This SDK implements world-class security with multiple layers of protection, significantly exceeding typical OAuth 2.0 implementations.

Security Features Comparison

Feature rustyschwab Python Reference Advantage
Encryption at Rest ✅ ChaCha20Poly1305 AEAD ❌ Plaintext JSON 100% improvement
Tamper Detection ✅ AEAD authentication tags ❌ None Data integrity
Memory Protection ✅ SecretString + Zeroize ❌ Plain strings Memory safety
Keychain Storage ✅ macOS/Windows/Linux ❌ Not implemented Best-in-class
File Permissions ✅ Enforced 0600 (Unix) ❌ Default perms Access control
PKCE (RFC 7636) ✅ S256 method ❌ Not implemented OAuth 2.1 ready
Thread Safety ✅ Compile-time safe ⚠️ GIL-dependent Concurrency
Secret Logging ✅ Redacted in logs ⚠️ Potential exposure Audit safety

Defense-in-Depth Architecture

Layer 1: Memory Safety

// Tokens wrapped in SecretString with automatic zeroization
pub struct TokenSet {
    access_token: Option<SecretString>,   // Auto-cleared on drop
    refresh_token: Option<SecretString>,  // Never logged
}
  • Prevents memory dumps from exposing tokens
  • Not visible in core dumps or swap files
  • Explicit expose_secret() required for access
  • Compiler prevents accidental logging

Layer 2: Encryption at Rest

// ChaCha20Poly1305 AEAD (authenticated encryption)
- Algorithm: ChaCha20Poly1305 (NCC Group audited, 2020)
- Key: 256-bit random (OS CSPRNG)
- Nonce: 96-bit random per encryption
- Authentication: 128-bit tag prevents tampering

Layer 3: OS-Native Credential Storage

// Cross-platform secure storage
- macOS: Keychain (secure enclave integration)
- Windows: Credential Manager (DPAPI encryption)
- Linux: Secret Service API (GNOME Keyring/KWallet)

Layer 4: File Permissions

// Unix: Enforced 0600 (owner read/write only)
- Verified before every read
- Applied after every write
- Prevents unauthorized access on shared systems

Layer 5: Transport Security

// TLS/HTTPS enforcement
- rustls: Memory-safe TLS (no OpenSSL vulnerabilities)
- HTTPS-only callback URLs (validated at config time)
- WSS for streaming (TLS over WebSocket)

PKCE Support (RFC 7636)

Protect against authorization code interception attacks:

use schwab_rs::auth::OAuthConfig;

let oauth = OAuthConfig {
    app_key: "YOUR_APP_KEY".into(),
    app_secret: "YOUR_SECRET".into(),
    callback_url: "https://127.0.0.1:8080".into(),

    pkce_enabled: true,  // Enabled by default (OAuth 2.1 compliant)
    ..Default::default()
};

PKCE Security Benefits:

  • Code verifier: 256-bit random value (OS CSPRNG)
  • Code challenge: SHA-256 hash with base64url encoding
  • Method: S256 (cryptographically secure)
  • Protection: Prevents authorization code interception/replay attacks

Security Best Practices

1. Choose the Right Token Storage Backend

use schwab_rs::auth::TokenStoreKind;

// Development: Quick start with basic security
token_store_kind: TokenStoreKind::File,  // 0600 permissions only

// Production (Recommended): Encrypted file storage
token_store_kind: TokenStoreKind::EncryptedFile,  // ChaCha20Poly1305 AEAD

// Production (Best): OS-native credential storage
token_store_kind: TokenStoreKind::Keychain,  // Default on macOS

Security Levels:

  • File: Basic (0600 permissions) - Development/testing only
  • EncryptedFile: High (AES-equivalent encryption + tamper detection) - Production recommended
  • Keychain: Best (OS-level encryption + user authentication) - Production default on macOS

2. Enable Token Notifications

use schwab_rs::auth::{OAuthConfig, TokenNotification};
use std::sync::Arc;

let oauth = OAuthConfig {
    on_token_notification: Some(Arc::new(|notification| {
        match notification {
            TokenNotification::RefreshTokenExpiring { hours_remaining } => {
                eprintln!("⚠️  Refresh token expires in {} hours!", hours_remaining);
                // Send alert, log to monitoring, etc.
            }
            TokenNotification::TokenFileCorrupted => {
                eprintln!("🔒 Token file was corrupted and recreated");
            }
            _ => {}
        }
    })),
    ..Default::default()
};

3. Secure Your Credentials

# NEVER commit credentials to version control
echo "schwab_tokens.json" >> .gitignore
echo "schwab_tokens.json.key" >> .gitignore
echo ".env" >> .gitignore

# Use environment variables
export SCHWAB_APP_KEY="your_32_char_key"
export SCHWAB_APP_SECRET="your_16_char_secret"

# Or use a secret management service
# - AWS KMS Secrets Manager
# - HashiCorp Vault
# - Azure Key Vault

4. Production Deployment Checklist

  • Use EncryptedFile or Keychain storage backend
  • Ensure PKCE is enabled (pkce_enabled: true, default)
  • Set up token expiry notifications
  • Configure proper file permissions (0600 on Unix)
  • Use HTTPS-only callback URLs
  • Never log credentials or tokens
  • Rotate credentials regularly
  • Monitor for token expiry events
  • Use rate limiting in production
  • Enable retry logic with exponential backoff

Cryptographic Implementation Details

ChaCha20Poly1305 AEAD (EncryptedFile Backend)

// Encryption process
1. Generate random 256-bit key (once, stored separately)
2. For each encryption:
   - Generate random 96-bit nonce (OS CSPRNG)
   - Encrypt plaintext with ChaCha20
   - Compute Poly1305 authentication tag
   - Output: [nonce(12) || ciphertext || tag(16)]

// Security properties
- Confidentiality: ChaCha20 stream cipher (256-bit key)
- Authenticity: Poly1305 MAC (128-bit tag)
- Tamper detection: Any modification causes decryption failure

PKCE Code Verifier/Challenge (RFC 7636)

// Code verifier generation
1. Generate 32 bytes random (256 bits) from OS CSPRNG
2. Base64URL encode without padding
3. Result: 43-character verifier string

// Code challenge generation
1. SHA-256 hash of verifier
2. Base64URL encode without padding
3. Method: S256 (required by OAuth 2.1)

Vulnerability Reporting

If you discover a security vulnerability, please email security@epistates.com or open a confidential GitHub Security Advisory. Do not open public issues for security vulnerabilities.

Security Dependencies

All security-critical dependencies are industry-standard and regularly audited:

Crate Purpose Security Track Record
ring Cryptography (CSPRNG, PKCE) Used by Google, Chromium, Firefox
rustls TLS implementation Memory-safe, no OpenSSL CVEs
chacha20poly1305 Encryption NCC Group audit (2020)
keyring OS credential storage 1M+ downloads, cross-platform
secrecy Secret handling De facto Rust standard
zeroize Memory clearing Prevents secret leakage

No known vulnerabilities in any security-critical dependency.

Installation

Add to your Cargo.toml:

[dependencies]
schwab-rs = { version = "0.1", features = ["callback-server"] }
schwab-types = "0.1"

Quick Start

1. Set up OAuth Authentication

use schwab_rs::{SchwabClient, SchwabConfig, auth::{OAuthConfig, TokenNotification}};
use std::sync::Arc;

// Configure OAuth with notifications
let oauth_config = OAuthConfig {
    app_key: "YOUR_32_CHAR_APP_KEY".to_string(),
    app_secret: "YOUR_16_CHAR_SECRET".to_string(),
    callback_url: "https://127.0.0.1:8080".to_string(),
    capture_callback: true,  // Auto-capture auth code
    auto_refresh: true,      // Auto-refresh tokens
    refresh_buffer_seconds: 61, // Refresh before expiry
    on_token_notification: Some(Arc::new(|notification| {
        if let TokenNotification::RefreshTokenExpiring { hours_remaining } = notification {
            println!("Warning: Refresh token expires in {} hours", hours_remaining);
        }
    })),
    ..Default::default()
};

// Create client
let config = SchwabConfig { oauth: oauth_config, ..Default::default() };
let client = SchwabClient::new(config)?;
// Start background token management
client.init().await?;

2. Authorize and Get Tokens

use schwab_rs::auth::AuthManager;

// For initial OAuth, create an AuthManager and run the flow once
let auth = AuthManager::new(client_config.oauth.clone())?;
let (auth_url, _code) = auth.authorize().await?;
println!("Open this URL in a browser: {}", auth_url);
// After authorization, tokens are saved to the configured file and auto-refreshed by client.init()

3. Use the API

// Get quotes
let quotes = client.get_quotes(&["AAPL", "MSFT"]).await?;

// Advanced quotes: only request specific fields and enable indicative pricing
let adv_quotes = client.get_quotes_with_options(
    &["AAPL"], 
    Some("quote,reference"), 
    Some(true)
).await?;

// Get option chain
let chain = client.get_option_chain("SPY").await?;

// Preview an order (validate without executing)
let preview = client.preview_order("account_hash", &order).await?;

// Place an order
let response = client.place_order("account_hash", &order).await?;

4. Stream Market Data

use schwab_rs::streaming::{StreamClient, StreamMessage};
use schwab_rs::types::streaming::StreamService;

// Build streaming client
let stream_client = StreamClient::builder()
    .config(stream_config)
    .auth_manager(auth_manager.clone())
    .customer_id(customer_id)
    .build()?;

// Connect and (attempt to) subscribe
stream_client.connect().await?;
stream_client.subscribe(StreamService::LeveloneEquities, vec!["AAPL".into(), "MSFT".into()]).await?;

// Handle messages
if let Some(mut receiver) = stream_client.get_receiver() {
    while let Some(msg) = receiver.recv().await {
        match msg {
            StreamMessage::Data(data) => println!("Market data: {:?}", data),
            StreamMessage::Response(resp) => println!("Response: {:?}", resp),
            StreamMessage::Notify(hb) => println!("Heartbeat: {}", hb.heartbeat),
        }
    }
}

Note: Streaming authentication, reconnection, and subscription handling are fully implemented.

OAuth Callback URL Options

For development, you need an HTTPS callback URL. Options:

1. Cloudflared Tunnel (Recommended)

# Install cloudflared
brew install cloudflared  # macOS

# Create tunnel to local port 8080
cloudflared tunnel --url http://localhost:8080

# Use the generated HTTPS URL as your callback_url

2. ngrok

ngrok http 8080
# Use the HTTPS URL provided

3. Self-Signed Certificate

# Generate certificate
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

# Configure in your app to use HTTPS

Token Management

Access Token (30 minutes)

  • Automatically refreshed before expiry
  • Background task handles refresh seamlessly

Refresh Token (7 days)

  • Must restart OAuth flow when expired
  • SDK detects expiration and returns appropriate error
  • Optional notifications warn before expiry

Testing Token Scenarios

# Run the auth test example
cd examples/auth_test
cargo run -- --app-key YOUR_KEY --app-secret YOUR_SECRET status

Environment Variables

export SCHWAB_APP_KEY="your_32_character_app_key"
export SCHWAB_APP_SECRET="your_16_char_secret"
export SCHWAB_CALLBACK_URL="https://your-callback-url"
export SCHWAB_TOKENS_FILE="schwab_tokens.json"

Examples

See the examples/ directory for working examples:

  • auth_test/ - OAuth flow testing and token management
  • oauth_flow/ - Minimal end-to-end OAuth
  • oauth_callback_server/ - Local callback capture
  • comprehensive/ - Combined flows and API usage

Feature flags

  • callback-server: runs a local Axum server to capture OAuth callback

Token Storage Backends

The SDK provides three secure token storage options (automatically selected by platform):

  1. File (Basic security)

    • Plain JSON with 0600 permissions (owner read/write only)
    • Useful for quick migration from older versions
  2. EncryptedFile (High security) ⭐ Default on Linux/Windows

    • ChaCha20Poly1305 authenticated encryption (NCC Group audited)
    • Tamper detection via AEAD authentication tag
    • Random key generation with secure storage
    • 0600 permissions on both token and key files
  3. Keychain (Best security) ⭐ Default on macOS

    • OS-native secure credential storage
    • macOS: Keychain, Windows: Credential Manager, Linux: Secret Service
    • Protected by user authentication
    • Cross-platform via keyring crate

Configure Token Storage

use schwab_rs::{SchwabClient, SchwabConfig};
use schwab_rs::auth::{OAuthConfig, TokenStoreKind};

let oauth = OAuthConfig {
    app_key: "YOUR_APP_KEY".into(),
    app_secret: "YOUR_SECRET".into(),
    callback_url: "https://127.0.0.1:8080".into(),

    // Choose storage backend (defaults to Keychain on macOS, EncryptedFile elsewhere)
    token_store_kind: TokenStoreKind::Keychain,        // OS-native (best)
    // token_store_kind: TokenStoreKind::EncryptedFile, // Encrypted file (recommended)
    // token_store_kind: TokenStoreKind::File,          // Plain file (basic)

    ..Default::default()
};

let cfg = SchwabConfig { oauth, ..Default::default() };
let client = SchwabClient::new(cfg)?;
client.init().await?; // Tokens automatically use selected storage

Security Features:

  • Memory safety: Tokens use SecretString with automatic zeroing on drop
  • Encryption: ChaCha20Poly1305 AEAD (EncryptedFile backend)
  • File permissions: 0600 enforcement on Unix systems
  • Keychain support: Cross-platform OS-native storage
  • No logging: Tokens never appear in debug/log output

Configuration

The SDK provides sensible defaults that can be customized:

use schwab_rs::config::{SchwabConfig, ClientConfig, RateLimitConfig, RetryConfig};
use std::time::Duration;

let config = SchwabConfig {
    client: ClientConfig {
        timeout: Duration::from_secs(10),
        rate_limit: RateLimitConfig {
            enabled: true,
            requests_per_second: 120,
            burst_size: 20,
        },
        retry: RetryConfig {
            max_retries: 3,
            initial_backoff: Duration::from_secs(1),
            max_backoff: Duration::from_secs(30),
            backoff_multiplier: 2.0,
            retry_on_status: vec![429, 500, 502, 503, 504],
        },
        ..Default::default()
    },
    ..Default::default()
};

Error Handling

The SDK provides comprehensive error types for robust error handling:

use schwab_rs::error::Error;

match client.get_quotes(&["AAPL"]).await {
    Ok(quotes) => println!("Success: {:?}", quotes),
    Err(Error::Auth(e)) => println!("Authentication error: {}", e),
    Err(Error::RateLimit { retry_after }) => println!("Rate limit exceeded, retry after {}s", retry_after),
    Err(Error::Network(e)) => println!("Network error: {}", e),
    Err(e) => println!("Other error: {}", e),
}

Important Notes

  1. Exact Callback URL Match: Must exactly match registered URL (HTTPS only, no trailing slash)
  2. Key Lengths: App key must be 32 chars, secret must be 16 chars
  3. Rate Limits: SDK defaults to 120 requests/second with burst of 20
  4. Token Expiration: Access tokens expire in ~30 minutes; refresh tokens in ~7 days
  5. Session Management: HTTP client is recreated after token refresh
  6. Token Storage: Defaults to Keychain (macOS) or EncryptedFile (other platforms)
  7. Token Notifications: Optional callbacks for expiry warnings and session events

Known limitations and notes:

  • Endpoints under crates/schwab-rs/src/endpoints/ now delegate to SchwabClient methods (thin wrappers)
  • Optional PKCE can be enabled via OAuthConfig.pkce_enabled
  • SchwabClient uses transport::http::HttpTransport for REST

Streaming: Advanced Features (v0.2.0)

Bounded Channels for Backpressure Control

Control memory usage with bounded channels:

use schwab_rs::{StreamConfig, ChannelKind};

let mut config = StreamConfig::default();

// Unbounded (default - no backpressure)
config.channel_kind = ChannelKind::Unbounded;

// Bounded (recommended for production - prevents memory growth)
config.channel_kind = ChannelKind::Bounded(10000); // 10k message buffer

let stream = StreamClient::builder()
    .config(config)
    .auth_manager(auth_manager)
    .customer_id(customer_id)
    .build()?;

Custom Field Selection

Request only needed fields for bandwidth optimization:

use schwab_rs::types::streaming::StreamService;

// Request lean field set (Symbol, Bid, Ask, Last, Volume)
stream.set_service_fields(
    StreamService::LeveloneEquities,
    "0,1,2,3,8".to_string()
);

stream.subscribe_level_one_equities(&["AAPL", "MSFT"]).await?;

Heartbeat Monitoring

Automatic Ping/Pong heartbeat with timeout detection (v0.2.0):

let mut config = StreamConfig::default();
config.heartbeat_interval = Duration::from_secs(20); // Ping every 20s
config.ping_timeout = Duration::from_secs(30);       // Reconnect if no Pong

// Automatic timeout detection and reconnection!

Complete Examples

See examples/ directory for production-ready patterns:

  • streaming_demo.rs - All 13 services demonstrated
  • streaming_data_processing.rs - Async processing pattern
  • streaming_quotes.rs - Real-time quotes dashboard
cargo run --example streaming_quotes --features callback-server

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

This project is dual-licensed under the MIT License and Apache License 2.0.

Disclaimer

This SDK is not affiliated with, endorsed by, or officially connected with Charles Schwab & Co., Inc. Use at your own risk. Always test thoroughly with sandbox data before using in production.

Support

For issues and questions:

Development

OAuth setup

Set required environment variables (or place in .env):

export SCHWAB_APP_KEY=...
export SCHWAB_APP_SECRET=...
export SCHWAB_CALLBACK_URL=https://127.0.0.1:8080

Optional:

export SCHWAB_PKCE_ENABLED=false
export SCHWAB_TOKEN_STORE=file          # Options: file, encrypted_file, keychain

Examples

# Basic OAuth flow
cargo run -p oauth-flow

# Comprehensive SDK demonstration
cargo run -p comprehensive

# Authenticate and test token management
cargo run -p auth_test -- --app-key KEY --app-secret SECRET status

# Streaming demos
cargo run -p streaming-examples --bin streaming_demo
cargo run -p streaming-examples --bin streaming_quotes
cargo run -p streaming-examples --bin streaming_processing

About

A Rust SDK for Charles Schwab's Trading and Market Data APIs, providing OAuth2 authentication, REST API access, and WebSocket streaming capabilities.

Topics

Resources

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages