Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions examples/JWT_AUTH_EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# JWT Authentication with Electrum Client

This guide demonstrates how to use dynamic JWT authentication with the electrum-client library.

## Overview

The electrum-client now supports embedding authorization tokens (such as JWT Bearer tokens) directly in JSON-RPC requests. This is achieved through an `AuthProvider` callback that is invoked before each request.

## Basic Usage

```rust
use electrum_client::{Client, ConfigBuilder};
use std::sync::{Arc, RwLock};

// Simple example: Static token
fn main() -> Result<(), Box<dyn std::error::Error>> {
let token = "your-jwt-token-here".to_string();

let config = ConfigBuilder::new()
.authorization_provider(Some(Arc::new(move || {
Some(format!("Bearer {}", token))
})))
.build();

let client = Client::from_config("tcp://your-server:50001", config)?;

// All RPC calls will now include: "authorization": "Bearer your-jwt-token-here"
let features = client.server_features()?;
println!("{:?}", features);

Ok(())
}
```

## Advanced: Token Refresh with Keycloak

This example demonstrates automatic token refresh every 4 minutes (before the 5-minute expiration).

```rust
use electrum_client::{Client, ConfigBuilder, ElectrumApi};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::time::sleep;

/// Manages JWT tokens from Keycloak with automatic refresh
struct KeycloakTokenManager {
token: Arc<RwLock<Option<String>>>,
keycloak_url: String,
client_id: String,
client_secret: String,
}

impl KeycloakTokenManager {
fn new(keycloak_url: String, client_id: String, client_secret: String) -> Self {
Self {
token: Arc::new(RwLock::new(None)),
keycloak_url,
client_id,
client_secret,
}
}

/// Get the current token (for the auth provider)
fn get_token(&self) -> Option<String> {
self.token.read().unwrap().clone()
}

/// Fetch a fresh token from Keycloak
async fn fetch_token(&self) -> Result<String, Box<dyn std::error::Error>> {
// Example using reqwest to get JWT from Keycloak
let client = reqwest::Client::new();
let response = client
.post(&format!("{}/protocol/openid-connect/token", self.keycloak_url))
.form(&[
("grant_type", "client_credentials"),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
])
.send()
.await?;

let json: serde_json::Value = response.json().await?;
let access_token = json["access_token"]
.as_str()
.ok_or("Missing access_token")?
.to_string();

Ok(format!("Bearer {}", access_token))
}

/// Background task that refreshes the token every 4 minutes
async fn refresh_loop(self: Arc<Self>) {
loop {
// Refresh every 4 minutes (tokens expire at 5 minutes)
sleep(Duration::from_secs(240)).await;

match self.fetch_token().await {
Ok(new_token) => {
println!("Token refreshed successfully");
*self.token.write().unwrap() = Some(new_token);
}
Err(e) => {
eprintln!("Failed to refresh token: {}", e);
// Keep using old token until we can refresh
}
}
}
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup token manager
let token_manager = Arc::new(KeycloakTokenManager::new(
"https://your-keycloak-server/auth/realms/your-realm".to_string(),
"your-client-id".to_string(),
"your-client-secret".to_string(),
));

// Fetch initial token
let initial_token = token_manager.fetch_token().await?;
*token_manager.token.write().unwrap() = Some(initial_token);

// Start background refresh task
let tm_clone = token_manager.clone();
tokio::spawn(async move {
tm_clone.refresh_loop().await;
});

// Create Electrum client with dynamic auth provider
let tm_for_provider = token_manager.clone();
let config = ConfigBuilder::new()
.authorization_provider(Some(Arc::new(move || {
tm_for_provider.get_token()
})))
.build();

let client = Client::from_config("tcp://your-api-gateway:50001", config)?;

// All RPC calls will automatically include fresh JWT tokens
loop {
match client.server_features() {
Ok(features) => println!("Connected: {:?}", features),
Err(e) => eprintln!("Error: {}", e),
}

tokio::time::sleep(Duration::from_secs(10)).await;
}
}
```

## Integration with BDK

### Using BDK's Convenience Constructor (Recommended)

```rust
use bdk_electrum::{BdkElectrumClient, ConfigBuilder};
use std::sync::Arc;

// Assuming you have a TokenManager as shown above
fn create_bdk_client(
token_manager: Arc<KeycloakTokenManager>
) -> Result<BdkElectrumClient<electrum_client::Client>, Box<dyn std::error::Error>> {

let config = ConfigBuilder::new()
.authorization_provider(Some(Arc::new(move || {
token_manager.get_token()
})))
.build();

// BDK provides a from_config() convenience method
let bdk_client = BdkElectrumClient::from_config(
"tcp://your-api-gateway:50001",
config
)?;

Ok(bdk_client)
}
```

### Alternative: Manual Construction

If you need more control over the underlying client:

```rust
use bdk_electrum::BdkElectrumClient;
use electrum_client::{Client, ConfigBuilder};
use std::sync::Arc;

let config = ConfigBuilder::new()
.authorization_provider(Some(Arc::new(move || {
token_manager.get_token()
})))
.timeout(Some(30))
.build();

let electrum_client = Client::from_config("tcp://your-api-gateway:50001", config)?;
let bdk_client = BdkElectrumClient::new(electrum_client);
```

## JSON-RPC Request Format

With the auth provider configured, each JSON-RPC request will include the authorization field:

```json
{
"jsonrpc": "2.0",
"method": "blockchain.headers.subscribe",
"params": [],
"id": 1,
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

If the provider returns `None`, the authorization field is omitted from the request.

## Thread Safety

The `AuthProvider` type is defined as:
```rust
pub type AuthProvider = Arc<dyn Fn() -> Option<String> + Send + Sync>;
```

This ensures thread-safe access to tokens across all RPC calls.
34 changes: 34 additions & 0 deletions examples/jwt_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
extern crate electrum_client;

use electrum_client::{Client, ConfigBuilder, ElectrumApi};
use std::sync::Arc;

fn main() {
// Example 1: Static JWT token
println!("Example 1: Static JWT token");

let config = ConfigBuilder::new()
.authorization_provider(Some(Arc::new(|| {
// In production, fetch this from your token manager
Some("Bearer example-jwt-token-12345".to_string())
})))
.build();

match Client::from_config("tcp://localhost:50001", config) {
Ok(client) => {
println!("Connected to server with JWT auth");
match client.server_features() {
Ok(features) => println!("Server features: {:#?}", features),
Err(e) => eprintln!("Error fetching features: {}", e),
}
}
Err(e) => {
eprintln!("Connection error: {}", e);
eprintln!("\nNote: This example requires an Electrum server that accepts JWT auth.");
eprintln!("Update the URL and token to match your setup.");
}
}

// Example 2: Dynamic token with refresh
// See JWT_AUTH_EXAMPLE.md for complete implementation with automatic token refresh
}
17 changes: 10 additions & 7 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,22 @@ impl ClientType {
None => {
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
}
};
}
.with_auth_provider(config.authorization_provider().clone());

Ok(ClientType::SSL(client))
} else {
let url = url.replacen("tcp://", "", 1);

Ok(match config.socks5().as_ref() {
None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?),
Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
url.as_str(),
socks5,
config.timeout(),
)?),
None => ClientType::TCP(
RawClient::new(url.as_str(), config.timeout())?
.with_auth_provider(config.authorization_provider().clone()),
),
Some(socks5) => ClientType::Socks5(
RawClient::new_proxy(url.as_str(), socks5, config.timeout())?
.with_auth_provider(config.authorization_provider().clone()),
),
})
}
}
Expand Down
Loading