Skip to content

Commit 3df3814

Browse files
committed
docs: add references on how to use authentication
- add two new examples: `jwt_auth.rs` and `jwt_dynamic_auth.rs` as references on how to build a client with authentication.
1 parent 49eee0a commit 3df3814

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

examples/jwt_auth.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! # JWT Static Authentication with Electrum Client
2+
//!
3+
//! This example demonstrates how to use an static JWT_TOKEN authentication with the
4+
//! electrum-client library.
5+
6+
use bitcoin::Txid;
7+
use electrum_client::{Client, ConfigBuilder, ElectrumApi};
8+
use std::{str::FromStr, sync::Arc};
9+
10+
const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:50002";
11+
12+
const GENESIS_HEIGHT: usize = 0;
13+
const GENESIS_TXID: &str = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
14+
15+
fn main() {
16+
// A static JWT_TOKEN (i.e JWT_TOKEN="Bearer jwt_token...")
17+
let auth_provider = Arc::new(move || {
18+
let jwt_token = std::env::var("JWT_TOKEN").expect("JWT_TOKEN env variable not set");
19+
Some(jwt_token)
20+
});
21+
22+
// The Electrum Server URL (i.e `ELECTRUM_URL` environment variable, or defaults to `ELECTRUM_URL` const above)
23+
let electrum_url = std::env::var("ELECTRUM_URL").unwrap_or(ELECTRUM_URL.to_owned());
24+
25+
// Builds the electrum-client `Config`.
26+
let config = ConfigBuilder::new()
27+
.validate_domain(false)
28+
.authorization_provider(Some(auth_provider))
29+
.build();
30+
31+
// Builds & Connect electrum-client `Client`.
32+
match Client::from_config(&electrum_url, config) {
33+
Ok(client) => {
34+
println!(
35+
"Successfully connected to Electrum Server: {:#?}; with JWT authentication!",
36+
electrum_url
37+
);
38+
39+
// try to call the `server.features` method, it can fail on some servers.
40+
match client.server_features() {
41+
Ok(features) => println!(
42+
"Successfully fetched the `server.features`!\n{:#?}",
43+
features
44+
),
45+
Err(e) => eprintln!("Failed to fetch the `server.features`!\nError: {:#?}", e),
46+
}
47+
48+
// try to call the `blockchain.block.header` method, it should NOT fail.
49+
let genesis_height = GENESIS_HEIGHT;
50+
match client.block_header(genesis_height) {
51+
Ok(header) => {
52+
println!(
53+
"Successfully fetched the `Header` for given `height`={}!\n{:#?}",
54+
genesis_height, header
55+
);
56+
}
57+
Err(err) => eprintln!(
58+
"Failed to fetch the `Header` for given `height`!\nError: {:#?}",
59+
err
60+
),
61+
}
62+
63+
// try to call the `blockchain.transaction.get` method, it should NOT fail.
64+
let genesis_txid =
65+
Txid::from_str(GENESIS_TXID).expect("SHOULD have a valid genesis `txid`");
66+
match client.transaction_get(&genesis_txid) {
67+
Ok(tx) => {
68+
println!(
69+
"Successfully fetched the `Transaction` for given `txid`={}!\n{:#?}",
70+
genesis_txid, tx
71+
);
72+
}
73+
Err(err) => eprintln!(
74+
"Failed to fetch the `Transaction` for given `txid`!\nError: {:#?}",
75+
err
76+
),
77+
}
78+
}
79+
Err(err) => {
80+
eprintln!(
81+
"Failed to build and connect `Client` to {:#?}!\nError: {:#?}\n",
82+
electrum_url, err
83+
);
84+
eprintln!("NOTE: This example requires an Electrum Server that handle/accept JWT authentication!");
85+
eprintln!("Try to update the `ELECTRUM_URL` and `JWT_TOKEN to match your setup.");
86+
}
87+
}
88+
}

examples/jwt_dynamic_auth.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! # JWT Dynamic Authentication
2+
//!
3+
//! ## Advanced: Token Refresh with Keycloak
4+
//!
5+
//! This example demonstrates how to use dynamic JWT authentication with the
6+
//! electrum-client library.
7+
//!
8+
//! ## Overview
9+
//!
10+
//! The electrum-client supports embedding authorization tokens (such as JWT
11+
//! Bearer tokens) directly in JSON-RPC requests. This is achieved through an
12+
//! [`AuthProvider`](electrum_client::config::AuthProvider) callback that is
13+
//! invoked before each request.
14+
//!
15+
//! In order to have an automatic token refresh (e.g it expires every 5 minutes),
16+
//! you should use a shared token holder (e.g KeycloakTokenManager)
17+
//! behind an `Arc<RwLock<...>>` and spawn a background task to refresh it.
18+
//!
19+
//! ## JSON-RPC Request Format
20+
//!
21+
//! With the auth provider configured, each JSON-RPC request will include the
22+
//! authorization field:
23+
//!
24+
//! ```json
25+
//! {
26+
//! "jsonrpc": "2.0",
27+
//! "method": "blockchain.headers.subscribe",
28+
//! "params": [],
29+
//! "id": 1,
30+
//! "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
31+
//! }
32+
//! ```
33+
//!
34+
//! If the provider returns `None`, the authorization field is omitted from the
35+
//! request.
36+
//!
37+
//! ## Thread Safety
38+
//!
39+
//! The `AuthProvider` type is defined as:
40+
//!
41+
//! ```rust,ignore
42+
//! pub type AuthProvider = Arc<dyn Fn() -> Option<String> + Send + Sync>;
43+
//! ```
44+
//!
45+
//! This ensures thread-safe access to tokens across all RPC calls.
46+
47+
use electrum_client::{Client, ConfigBuilder, ElectrumApi};
48+
use std::sync::{Arc, RwLock};
49+
use std::time::Duration;
50+
use tokio::time::sleep;
51+
52+
/// Manages JWT tokens from Keycloak with automatic refresh
53+
struct KeycloakTokenManager {
54+
token: Arc<RwLock<Option<String>>>,
55+
keycloak_url: String,
56+
grant_type: String,
57+
client_id: String,
58+
client_secret: String,
59+
}
60+
61+
impl KeycloakTokenManager {
62+
fn new(
63+
keycloak_url: String,
64+
grant_type: String,
65+
client_id: String,
66+
client_secret: String,
67+
) -> Self {
68+
Self {
69+
token: Arc::new(RwLock::new(None)),
70+
keycloak_url,
71+
client_id,
72+
client_secret,
73+
grant_type,
74+
}
75+
}
76+
77+
/// Get the current token (for the auth provider)
78+
fn get_token(&self) -> Option<String> {
79+
self.token.read().unwrap().clone()
80+
}
81+
82+
/// Fetch a fresh token from Keycloak
83+
async fn fetch_token(&self) -> Result<String, Box<dyn std::error::Error>> {
84+
let url = format!("{}/protocol/openid-connect/token", self.keycloak_url);
85+
86+
// if you're using other HTTP client (i.e `reqwest`), you can probably use `.form` methods.
87+
// it's currently not implemented in `bitreq`, needs to be built manually.
88+
let body = format!(
89+
"grant_type={}&client_id={}&client_secret={}",
90+
self.grant_type, self.client_id, self.client_secret
91+
);
92+
93+
let response = bitreq::post(url)
94+
.with_header("Content-Type", "application/x-www-form-urlencoded")
95+
.with_body(body)
96+
.send_async()
97+
.await?;
98+
99+
let json: serde_json::Value = response.json()?;
100+
let access_token = json["access_token"]
101+
.as_str()
102+
.ok_or("Missing access_token")?
103+
.to_string();
104+
105+
Ok(format!("Bearer {}", access_token))
106+
}
107+
108+
/// Background task that refreshes the token every 4 minutes
109+
async fn refresh_loop(self: Arc<Self>) {
110+
loop {
111+
// Refresh every 4 minutes (tokens expire at 5 minutes)
112+
sleep(Duration::from_secs(240)).await;
113+
114+
match self.fetch_token().await {
115+
Ok(new_token) => {
116+
println!("Token refreshed successfully");
117+
// In a background thread/task, periodically update the token
118+
*self.token.write().unwrap() = Some(new_token);
119+
}
120+
Err(e) => {
121+
eprintln!("Failed to refresh token: {}", e);
122+
// Keep using old token until we can refresh
123+
}
124+
}
125+
}
126+
}
127+
}
128+
129+
#[tokio::main]
130+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
131+
// The Electrum Server URL (i.e `ELECTRUM_URL` environment variable)
132+
let electrum_url = std::env::var("ELECTRUM_URL")
133+
.expect("SHOULD have the `ELECTRUM_URL` environment variable!");
134+
135+
// The JWT_TOKEN manager setup (i.e Keycloak server URL, client ID and secret)
136+
let keycloak_url = std::env::var("KEYCLOAK_URL")
137+
.expect("SHOULD have the `KEYCLOAK_URL` environment variable!");
138+
139+
let grant_type = std::env::var("GRANT_TYPE").unwrap_or("client_credentials".to_string());
140+
let client_id =
141+
std::env::var("CLIENT_ID").expect("SHOULD have the `CLIENT_ID` environment variable!");
142+
let client_secret = std::env::var("CLIENT_SECRET")
143+
.expect("SHOULD have the `CLIENT_SECRET` environment variable!");
144+
145+
// Setup `KeycloakTokenManager`
146+
let token_manager = Arc::new(KeycloakTokenManager::new(
147+
keycloak_url,
148+
grant_type,
149+
client_id,
150+
client_secret,
151+
));
152+
153+
// Fetch initial token
154+
let jwt_token = token_manager.fetch_token().await?;
155+
156+
println!("JWT_TOKEN='{}'", &jwt_token);
157+
158+
*token_manager.token.write().unwrap() = Some(jwt_token);
159+
160+
// Start background refresh task
161+
let tm_clone = token_manager.clone();
162+
tokio::spawn(async move {
163+
tm_clone.refresh_loop().await;
164+
});
165+
166+
// Create Electrum client with dynamic auth provider
167+
let tm_for_provider = token_manager.clone();
168+
let config = ConfigBuilder::new()
169+
.authorization_provider(Some(Arc::new(move || tm_for_provider.get_token())))
170+
.build();
171+
172+
let client = Client::from_config(&electrum_url, config)?;
173+
174+
// All RPC calls will automatically include fresh JWT tokens
175+
loop {
176+
match client.server_features() {
177+
Ok(features) => println!("Connected: {:?}", features),
178+
Err(e) => eprintln!("Error: {}", e),
179+
}
180+
181+
tokio::time::sleep(Duration::from_secs(10)).await;
182+
}
183+
}

0 commit comments

Comments
 (0)