Skip to content

Commit ca0f266

Browse files
committed
feat: add dynamic JWT authorization support via callback provider
1 parent 5d7be37 commit ca0f266

File tree

6 files changed

+601
-11
lines changed

6 files changed

+601
-11
lines changed

examples/JWT_AUTH_EXAMPLE.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# JWT Authentication with Electrum Client
2+
3+
This guide demonstrates how to use dynamic JWT authentication with the electrum-client library.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Basic Usage
10+
11+
```rust
12+
use electrum_client::{Client, ConfigBuilder};
13+
use std::sync::{Arc, RwLock};
14+
15+
// Simple example: Static token
16+
fn main() -> Result<(), Box<dyn std::error::Error>> {
17+
let token = "your-jwt-token-here".to_string();
18+
19+
let config = ConfigBuilder::new()
20+
.authorization_provider(Some(Arc::new(move || {
21+
Some(format!("Bearer {}", token))
22+
})))
23+
.build();
24+
25+
let client = Client::from_config("tcp://your-server:50001", config)?;
26+
27+
// All RPC calls will now include: "authorization": "Bearer your-jwt-token-here"
28+
let features = client.server_features()?;
29+
println!("{:?}", features);
30+
31+
Ok(())
32+
}
33+
```
34+
35+
## Advanced: Token Refresh with Keycloak
36+
37+
This example demonstrates automatic token refresh every 4 minutes (before the 5-minute expiration).
38+
39+
```rust
40+
use electrum_client::{Client, ConfigBuilder, ElectrumApi};
41+
use std::sync::{Arc, RwLock};
42+
use std::time::Duration;
43+
use tokio::time::sleep;
44+
45+
/// Manages JWT tokens from Keycloak with automatic refresh
46+
struct KeycloakTokenManager {
47+
token: Arc<RwLock<Option<String>>>,
48+
keycloak_url: String,
49+
client_id: String,
50+
client_secret: String,
51+
}
52+
53+
impl KeycloakTokenManager {
54+
fn new(keycloak_url: String, client_id: String, client_secret: String) -> Self {
55+
Self {
56+
token: Arc::new(RwLock::new(None)),
57+
keycloak_url,
58+
client_id,
59+
client_secret,
60+
}
61+
}
62+
63+
/// Get the current token (for the auth provider)
64+
fn get_token(&self) -> Option<String> {
65+
self.token.read().unwrap().clone()
66+
}
67+
68+
/// Fetch a fresh token from Keycloak
69+
async fn fetch_token(&self) -> Result<String, Box<dyn std::error::Error>> {
70+
// Example using reqwest to get JWT from Keycloak
71+
let client = reqwest::Client::new();
72+
let response = client
73+
.post(&format!("{}/protocol/openid-connect/token", self.keycloak_url))
74+
.form(&[
75+
("grant_type", "client_credentials"),
76+
("client_id", &self.client_id),
77+
("client_secret", &self.client_secret),
78+
])
79+
.send()
80+
.await?;
81+
82+
let json: serde_json::Value = response.json().await?;
83+
let access_token = json["access_token"]
84+
.as_str()
85+
.ok_or("Missing access_token")?
86+
.to_string();
87+
88+
Ok(format!("Bearer {}", access_token))
89+
}
90+
91+
/// Background task that refreshes the token every 4 minutes
92+
async fn refresh_loop(self: Arc<Self>) {
93+
loop {
94+
// Refresh every 4 minutes (tokens expire at 5 minutes)
95+
sleep(Duration::from_secs(240)).await;
96+
97+
match self.fetch_token().await {
98+
Ok(new_token) => {
99+
println!("Token refreshed successfully");
100+
*self.token.write().unwrap() = Some(new_token);
101+
}
102+
Err(e) => {
103+
eprintln!("Failed to refresh token: {}", e);
104+
// Keep using old token until we can refresh
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
#[tokio::main]
112+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
113+
// Setup token manager
114+
let token_manager = Arc::new(KeycloakTokenManager::new(
115+
"https://your-keycloak-server/auth/realms/your-realm".to_string(),
116+
"your-client-id".to_string(),
117+
"your-client-secret".to_string(),
118+
));
119+
120+
// Fetch initial token
121+
let initial_token = token_manager.fetch_token().await?;
122+
*token_manager.token.write().unwrap() = Some(initial_token);
123+
124+
// Start background refresh task
125+
let tm_clone = token_manager.clone();
126+
tokio::spawn(async move {
127+
tm_clone.refresh_loop().await;
128+
});
129+
130+
// Create Electrum client with dynamic auth provider
131+
let tm_for_provider = token_manager.clone();
132+
let config = ConfigBuilder::new()
133+
.authorization_provider(Some(Arc::new(move || {
134+
tm_for_provider.get_token()
135+
})))
136+
.build();
137+
138+
let client = Client::from_config("tcp://your-api-gateway:50001", config)?;
139+
140+
// All RPC calls will automatically include fresh JWT tokens
141+
loop {
142+
match client.server_features() {
143+
Ok(features) => println!("Connected: {:?}", features),
144+
Err(e) => eprintln!("Error: {}", e),
145+
}
146+
147+
tokio::time::sleep(Duration::from_secs(10)).await;
148+
}
149+
}
150+
```
151+
152+
## Integration with BDK
153+
154+
### Using BDK's Convenience Constructor (Recommended)
155+
156+
```rust
157+
use bdk_electrum::{BdkElectrumClient, ConfigBuilder};
158+
use std::sync::Arc;
159+
160+
// Assuming you have a TokenManager as shown above
161+
fn create_bdk_client(
162+
token_manager: Arc<KeycloakTokenManager>
163+
) -> Result<BdkElectrumClient<electrum_client::Client>, Box<dyn std::error::Error>> {
164+
165+
let config = ConfigBuilder::new()
166+
.authorization_provider(Some(Arc::new(move || {
167+
token_manager.get_token()
168+
})))
169+
.build();
170+
171+
// BDK provides a from_config() convenience method
172+
let bdk_client = BdkElectrumClient::from_config(
173+
"tcp://your-api-gateway:50001",
174+
config
175+
)?;
176+
177+
Ok(bdk_client)
178+
}
179+
```
180+
181+
### Alternative: Manual Construction
182+
183+
If you need more control over the underlying client:
184+
185+
```rust
186+
use bdk_electrum::BdkElectrumClient;
187+
use electrum_client::{Client, ConfigBuilder};
188+
use std::sync::Arc;
189+
190+
let config = ConfigBuilder::new()
191+
.authorization_provider(Some(Arc::new(move || {
192+
token_manager.get_token()
193+
})))
194+
.timeout(Some(30))
195+
.build();
196+
197+
let electrum_client = Client::from_config("tcp://your-api-gateway:50001", config)?;
198+
let bdk_client = BdkElectrumClient::new(electrum_client);
199+
```
200+
201+
## JSON-RPC Request Format
202+
203+
With the auth provider configured, each JSON-RPC request will include the authorization field:
204+
205+
```json
206+
{
207+
"jsonrpc": "2.0",
208+
"method": "blockchain.headers.subscribe",
209+
"params": [],
210+
"id": 1,
211+
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
212+
}
213+
```
214+
215+
If the provider returns `None`, the authorization field is omitted from the request.
216+
217+
## Thread Safety
218+
219+
The `AuthProvider` type is defined as:
220+
```rust
221+
pub type AuthProvider = Arc<dyn Fn() -> Option<String> + Send + Sync>;
222+
```
223+
224+
This ensures thread-safe access to tokens across all RPC calls.

examples/jwt_auth.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
extern crate electrum_client;
2+
3+
use electrum_client::{Client, ConfigBuilder, ElectrumApi};
4+
use std::sync::Arc;
5+
6+
fn main() {
7+
// Example 1: Static JWT token
8+
println!("Example 1: Static JWT token");
9+
10+
let config = ConfigBuilder::new()
11+
.authorization_provider(Some(Arc::new(|| {
12+
// In production, fetch this from your token manager
13+
Some("Bearer example-jwt-token-12345".to_string())
14+
})))
15+
.build();
16+
17+
match Client::from_config("tcp://localhost:50001", config) {
18+
Ok(client) => {
19+
println!("Connected to server with JWT auth");
20+
match client.server_features() {
21+
Ok(features) => println!("Server features: {:#?}", features),
22+
Err(e) => eprintln!("Error fetching features: {}", e),
23+
}
24+
}
25+
Err(e) => {
26+
eprintln!("Connection error: {}", e);
27+
eprintln!("\nNote: This example requires an Electrum server that accepts JWT auth.");
28+
eprintln!("Update the URL and token to match your setup.");
29+
}
30+
}
31+
32+
// Example 2: Dynamic token with refresh
33+
// See JWT_AUTH_EXAMPLE.md for complete implementation with automatic token refresh
34+
}

src/client.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,22 @@ impl ClientType {
120120
None => {
121121
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
122122
}
123-
};
123+
}
124+
.with_auth_provider(config.authorization_provider().clone());
124125

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

129130
Ok(match config.socks5().as_ref() {
130-
None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?),
131-
Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
132-
url.as_str(),
133-
socks5,
134-
config.timeout(),
135-
)?),
131+
None => ClientType::TCP(
132+
RawClient::new(url.as_str(), config.timeout())?
133+
.with_auth_provider(config.authorization_provider().clone()),
134+
),
135+
Some(socks5) => ClientType::Socks5(
136+
RawClient::new_proxy(url.as_str(), socks5, config.timeout())?
137+
.with_auth_provider(config.authorization_provider().clone()),
138+
),
136139
})
137140
}
138141
}

0 commit comments

Comments
 (0)