SchwabApi.Authentication
1.0.0-beta.1
dotnet add package SchwabApi.Authentication --version 1.0.0-beta.1
NuGet\Install-Package SchwabApi.Authentication -Version 1.0.0-beta.1
<PackageReference Include="SchwabApi.Authentication" Version="1.0.0-beta.1" />
<PackageVersion Include="SchwabApi.Authentication" Version="1.0.0-beta.1" />
<PackageReference Include="SchwabApi.Authentication" />
paket add SchwabApi.Authentication --version 1.0.0-beta.1
#r "nuget: SchwabApi.Authentication, 1.0.0-beta.1"
#:package SchwabApi.Authentication@1.0.0-beta.1
#addin nuget:?package=SchwabApi.Authentication&version=1.0.0-beta.1&prerelease
#tool nuget:?package=SchwabApi.Authentication&version=1.0.0-beta.1&prerelease
SchwabApi.Authentication
Cross-platform OAuth 2.0 authentication library for Charles Schwab API.
Installation
dotnet add package SchwabApi.Authentication
Or via NuGet Package Manager:
Install-Package SchwabApi.Authentication
Features
- ✅ Cross-platform: Works on Windows, Linux, and macOS
- ✅ HTTPS localhost: Uses Kestrel + .NET dev certificates for secure OAuth callbacks
- ✅ Headless OAuth flow: Uses system browser + local Kestrel server
- ✅ Automatic token refresh: Background service refreshes tokens before expiration
- ✅ Secure storage: AES-256 encryption with machine-specific keys
- ✅ CSRF protection: State parameter validation
- ✅ Resilience: Retry logic with exponential backoff and circuit breaker
- ✅ Dependency injection: Full Microsoft.Extensions.DependencyInjection support
- ✅ Multi-client support: Optional SQLite storage for multiple accounts
Prerequisites
One-time setup per machine:
dotnet dev-certs https --trust
This generates and trusts a development HTTPS certificate for localhost. Required because Schwab enforces HTTPS on all callback URLs.
- Windows: Certificate added to Windows Certificate Store (automatic trust)
- Linux: Certificate added to ~/.dotnet/corefx/cryptography (may need manual trust depending on distro)
- macOS: Certificate added to macOS Keychain (requires password to trust)
Browser Certificate Warning
Affects all operating systems - This is browser behavior, not OS-specific.
When the OAuth callback redirects to https://127.0.0.1:5001/callback, your browser will show a certificate warning:
- Why? The .NET dev certificate is issued for "localhost", not the IP "127.0.0.1"
- Schwab's portal requires
127.0.0.1in callback URLs (rejects "localhost") - Browser sees hostname mismatch: cert says "localhost", URL says "127.0.0.1"
- Schwab's portal requires
- Is it safe? Yes - this is your own local development server, not a remote site
- What to do? Click "Advanced" → "Proceed to 127.0.0.1 (unsafe)" or equivalent
- Chrome/Edge: Click "Advanced" → "Proceed to 127.0.0.1 (unsafe)"
- Firefox: Click "Advanced" → "Accept the Risk and Continue"
- Safari: Click "Show Details" → "visit this website"
The OAuth flow will complete successfully after accepting the warning. The success page shows briefly before auto-closing.
Quick Start
1. Installation
dotnet add package SchwabApi.Authentication
2. Configure Schwab Developer Portal
- Go to https://developer.schwab.com
- Navigate to your app settings
- Set callback URL to:
https://127.0.0.1:5001/callback- Note: Schwab's portal does not accept
localhost, use127.0.0.1instead
- Note: Schwab's portal does not accept
- Save changes
3. Configure Services
using SchwabApi.Authentication.Extensions;
var builder = Host.CreateApplicationBuilder(args);
// Add Schwab OAuth services
builder.Services.AddSchwabOAuth(options =>
{
options.CallbackPort = 5001; // Kestrel HTTPS port (default)
options.RefreshBuffer = TimeSpan.FromMinutes(5); // Refresh 5 min before expiration
});
var host = builder.Build();
await host.RunAsync();
4. Manage Credentials Securely
IMPORTANT: Never hardcode credentials! Follow the standard pattern:
Recommended Pattern
For public configuration (callback URLs, base URLs):
Create appsettings.json:
{
"Schwab": {
"CallbackUrl": "https://127.0.0.1:5001/callback"
}
}
For confidential credentials (App Key, App Secret): Use environment variables:
# Windows PowerShell
$env:SCHWAB_APP_KEY = "your-app-key"
$env:SCHWAB_APP_SECRET = "your-app-secret"
# Linux/macOS
export SCHWAB_APP_KEY="your-app-key"
export SCHWAB_APP_SECRET="your-app-secret"
Load credentials in code:
using Microsoft.Extensions.Configuration;
// Load configuration from appsettings.json + environment variables
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddEnvironmentVariables()
.Build();
var credentials = new OAuthCredentials
{
ClientId = Environment.GetEnvironmentVariable("SCHWAB_APP_KEY")
?? throw new InvalidOperationException("SCHWAB_APP_KEY environment variable required"),
ClientSecret = Environment.GetEnvironmentVariable("SCHWAB_APP_SECRET")
?? throw new InvalidOperationException("SCHWAB_APP_SECRET environment variable required"),
CallbackUrl = configuration["Schwab:CallbackUrl"]
?? throw new InvalidOperationException("Schwab:CallbackUrl configuration required")
};
Why this pattern?
- ✅ Schwab treats both App Key and App Secret as confidential
- ✅ Credentials stay out of source control
- ✅ Works in CI/CD environments
- ✅ Respects security best practices
See the GettingStarted example for a complete implementation.
5. Perform OAuth Authorization
using SchwabApi.Authentication;
using SchwabApi.Authentication.Models;
// Get OAuth service from DI
var oauthService = host.Services.GetRequiredService<IOAuthService>();
// Authorize (opens browser, starts Kestrel server, waits for callback, exchanges code for tokens)
var tokens = await oauthService.AuthorizeAsync(credentials);
Console.WriteLine($"Access token expires: {tokens.ExpiresAt}");
Console.WriteLine($"Refresh token expires: {tokens.RefreshTokenExpiresAt}");
Note: The credentials object should be loaded from environment variables and configuration files (see step 4), not hardcoded.
6. Use Access Token
// Get current access token (auto-refreshes if needed)
var accessToken = await oauthService.GetAccessTokenAsync(credentials.ClientId);
// Use token in API calls
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await httpClient.GetAsync("https://api.schwabapi.com/v1/accounts");
7. Force Re-authorization
// Delete tokens and perform fresh authorization
var tokens = await oauthService.ForceReauthorizationAsync(credentials);
Architecture
OAuth Flow
- Authorization URL Generation: Creates Schwab OAuth URL with state parameter
- Browser Launch: Opens system default browser (cross-platform)
- Kestrel Server: Starts temporary Kestrel web server on
https://localhost:5001/callback - OAuth Callback: Kestrel captures authorization code from Schwab redirect
- Token Exchange: Exchanges authorization code for access/refresh tokens via Schwab API
- Secure Storage: Encrypts tokens with AES-256 using machine-specific key
- Background Refresh: Automatically refreshes tokens 5 minutes before expiration
Why Kestrel Instead of HttpListener?
Problem: Schwab requires HTTPS for all callback URLs, including localhost. HttpListener requires platform-specific SSL certificate configuration (netsh on Windows, complex setup on Linux/macOS).
Solution: Kestrel (ASP.NET Core's web server) supports HTTPS on localhost using .NET dev certificates, which work cross-platform with a single dotnet dev-certs https --trust command.
Token Lifetimes
- Access Token: 30 minutes
- Refresh Token: 7 days
- Re-authorization Required: Every 7 days
Security
- Encryption: AES-256 with PBKDF2 key derivation from machine ID
- CSRF Protection: State parameter validation (32+ bytes, cryptographically random)
- File Permissions: Tokens file restricted to current user only (chmod 600 on Unix)
- HTTPS Enforcement: All Schwab API calls use TLS
Resilience
- Retry Policy: 3 attempts with exponential backoff [2s, 4s, 8s]
- Circuit Breaker: Opens after 3 failures, 60s cooldown
- Thread Safety: Semaphore prevents concurrent token refresh
Configuration Options
builder.Services.AddSchwabOAuth(options =>
{
// OAuth endpoints
options.AuthorizationEndpoint = "https://api.schwabapi.com/v1/oauth/authorize";
options.TokenEndpoint = "https://api.schwabapi.com/v1/oauth/token";
// Callback server
options.CallbackPort = 8080; // Will try 8080-8089
options.CallbackPath = "/callback";
// Token refresh
options.RefreshBuffer = TimeSpan.FromMinutes(5);
options.RefreshCheckInterval = TimeSpan.FromMinutes(1);
// Resilience
options.RetryAttempts = 3;
options.CircuitBreakerFailureThreshold = 3;
options.CircuitBreakerCooldown = TimeSpan.FromSeconds(60);
// Timeouts
options.RequestTimeout = TimeSpan.FromSeconds(30);
options.StateExpiration = TimeSpan.FromMinutes(10);
});
Multi-Client Support
For managing multiple Schwab accounts or environments:
// Use SQLite storage instead of JSON
builder.Services.AddSchwabOAuthWithSqlite(
databasePath: "schwab_tokens.db",
options =>
{
// Same options as AddSchwabOAuth
});
Events
Subscribe to token refresh events:
oauthService.TokenRefreshed += (sender, e) =>
{
Console.WriteLine($"Token refreshed for {e.ClientId}. New expiration: {e.NewExpiresAt}");
};
oauthService.TokenRefreshFailed += (sender, e) =>
{
Console.WriteLine($"Token refresh failed for {e.ClientId}: {e.Exception.Message}");
if (e.RequiresReauthorization)
{
Console.WriteLine("Full re-authorization required!");
}
};
Cross-Platform Notes
Linux
- Requires
xdg-openfor browser launch (pre-installed on most distros) - Token file permissions set to 600 automatically
macOS
- First run may show firewall prompt for callback server (click "Allow")
- Uses
opencommand for browser launch - Token file permissions set to 600 automatically
Windows
- Uses
UseShellExecutefor browser launch - Token file ACLs set to current user only
License
MIT License - see LICENSE file for details
Contributing
See CONTRIBUTING.md for guidelines
Support
For issues and questions, please visit: https://github.com/veenroid/SchwabSharp/issues
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Polly (>= 8.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|