Skip to content
Draft
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ autoexamples = false
uninlined_format_args = "allow"

[dependencies]
auth0 = { path = "../canton-lib/crates/auth0" }
ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.3.1" }
keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.3.1" }
registry = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.3.1" }
Expand Down
10 changes: 2 additions & 8 deletions examples/accept_transfers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Example: Accept all pending CBTC transfers
///
/// Run with: cargo run -p examples --bin accept_transfers
/// Run with: cargo run --example accept_transfers
///
/// This example uses the `cbtc::accept::accept_all` method to automatically
/// fetch and accept all pending CBTC TransferInstruction contracts for your party.
Expand All @@ -20,13 +20,7 @@ async fn main() -> Result<(), String> {
registry_url: env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"),
decentralized_party_id: env::var("DECENTRALIZED_PARTY_ID")
.expect("DECENTRALIZED_PARTY_ID must be set"),
keycloak_client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"),
keycloak_username: env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set"),
keycloak_password: env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set"),
keycloak_url: keycloak::login::password_url(
&env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"),
&env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"),
),
auth: cbtc::auth::AuthConfig::from_env()?,
};

cbtc::accept::accept_all(params).await?;
Expand Down
8 changes: 1 addition & 7 deletions examples/batch_distribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,7 @@ async fn main() -> Result<(), String> {
ledger_host: env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"),
registry_url: env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"),
decentralized_party_id: decentralized_party,
keycloak_client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"),
keycloak_username: env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set"),
keycloak_password: env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set"),
keycloak_url: keycloak::login::password_url(
&env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"),
&env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"),
),
auth: cbtc::auth::AuthConfig::from_env()?,
reference_base: None,
};

Expand Down
16 changes: 1 addition & 15 deletions examples/batch_with_callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,6 @@ async fn main() -> Result<(), String> {
let decentralized_party_id =
std::env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set");

let keycloak_client_id =
std::env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set");
let keycloak_username =
std::env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set");
let keycloak_password =
std::env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set");
let keycloak_url = keycloak::login::password_url(
&std::env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"),
&std::env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"),
);

// Read CSV file
println!("Reading CSV from: {}", csv_path);
let mut reader =
Expand Down Expand Up @@ -105,10 +94,7 @@ async fn main() -> Result<(), String> {
ledger_host,
registry_url,
decentralized_party_id,
keycloak_client_id,
keycloak_username,
keycloak_password,
keycloak_url,
auth: cbtc::auth::AuthConfig::from_env()?,
reference_base: Some(format!("batch-{}", chrono::Utc::now().timestamp())),
on_transfer_complete: Some(callback),
})
Expand Down
14 changes: 1 addition & 13 deletions examples/cancel_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ async fn main() -> Result<(), String> {
let decentralized_party_id =
env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set");

let keycloak_client_id =
env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set");
let keycloak_username = env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set");
let keycloak_password = env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set");
let keycloak_url = keycloak::login::password_url(
&env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"),
&env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"),
);

println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Withdraw Pending CBTC Transfers");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Expand All @@ -39,10 +30,7 @@ async fn main() -> Result<(), String> {
ledger_host,
registry_url,
decentralized_party_id,
keycloak_client_id,
keycloak_username,
keycloak_password,
keycloak_url,
auth: cbtc::auth::AuthConfig::from_env()?,
})
.await?;

Expand Down
30 changes: 18 additions & 12 deletions examples/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,12 @@ async fn cleanup_sender_offers(sender: &PartyConfig, decentralized_party_id: &st
ledger_host: sender.ledger_host.clone(),
registry_url: registry_url.to_string(),
decentralized_party_id: decentralized_party_id.to_string(),
keycloak_client_id: sender.keycloak_client_id.clone(),
keycloak_username: sender.keycloak_username.clone(),
keycloak_password: sender.keycloak_password.clone(),
keycloak_url: sender.keycloak_url.clone(),
auth: cbtc::auth::AuthConfig::Keycloak {
client_id: sender.keycloak_client_id.clone(),
username: sender.keycloak_username.clone(),
password: sender.keycloak_password.clone(),
url: sender.keycloak_url.clone(),
},
})
.await;
match result {
Expand Down Expand Up @@ -296,10 +298,12 @@ async fn main() -> Result<(), String> {
ledger_host: receiver.ledger_host.clone(),
registry_url: registry_url.clone(),
decentralized_party_id: decentralized_party_id.clone(),
keycloak_client_id: receiver.keycloak_client_id.clone(),
keycloak_username: receiver.keycloak_username.clone(),
keycloak_password: receiver.keycloak_password.clone(),
keycloak_url: receiver.keycloak_url.clone(),
auth: cbtc::auth::AuthConfig::Keycloak {
client_id: receiver.keycloak_client_id.clone(),
username: receiver.keycloak_username.clone(),
password: receiver.keycloak_password.clone(),
url: receiver.keycloak_url.clone(),
},
})
.await?;
sender_has_pending_offer = false;
Expand Down Expand Up @@ -352,10 +356,12 @@ async fn main() -> Result<(), String> {
ledger_host: sender.ledger_host.clone(),
registry_url: registry_url.clone(),
decentralized_party_id: decentralized_party_id.clone(),
keycloak_client_id: sender.keycloak_client_id.clone(),
keycloak_username: sender.keycloak_username.clone(),
keycloak_password: sender.keycloak_password.clone(),
keycloak_url: sender.keycloak_url.clone(),
auth: cbtc::auth::AuthConfig::Keycloak {
client_id: sender.keycloak_client_id.clone(),
username: sender.keycloak_username.clone(),
password: sender.keycloak_password.clone(),
url: sender.keycloak_url.clone(),
},
})
.await?;
receiver_has_pending_offer = false;
Expand Down
14 changes: 1 addition & 13 deletions examples/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ async fn main() -> Result<(), String> {
let decentralized_party_id =
env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set");

let keycloak_client_id =
env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set");
let keycloak_username = env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set");
let keycloak_password = env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set");
let keycloak_url = keycloak::login::password_url(
&env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"),
&env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"),
);

println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Stream CBTC Configuration");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Expand Down Expand Up @@ -117,10 +108,7 @@ async fn main() -> Result<(), String> {
ledger_host: ledger_host.clone(),
registry_url: registry_url.clone(),
decentralized_party_id: decentralized_party_id.clone(),
keycloak_client_id: keycloak_client_id.clone(),
keycloak_username: keycloak_username.clone(),
keycloak_password: keycloak_password.clone(),
keycloak_url: keycloak_url.clone(),
auth: cbtc::auth::AuthConfig::from_env()?,
reference_base: Some(format!("stream-{}", chrono::Utc::now().timestamp())),
on_transfer_complete: Some(callback),
})
Expand Down
18 changes: 4 additions & 14 deletions src/accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ pub struct AcceptAllParams {
pub registry_url: String,
/// Decentralized party ID for CBTC
pub decentralized_party_id: String,
// Keycloak authentication
pub keycloak_client_id: String,
pub keycloak_username: String,
pub keycloak_password: String,
pub keycloak_url: String,
/// Authentication config (Keycloak or Auth0)
pub auth: crate::auth::AuthConfig,
}

/// Result of accepting a single transfer
Expand Down Expand Up @@ -139,15 +136,8 @@ pub async fn submit(params: Params) -> Result<(), String> {
///
/// Returns a summary of successful and failed acceptances.
pub async fn accept_all(params: AcceptAllParams) -> Result<AcceptAllResult, String> {
log::debug!("Authenticating with Keycloak...");
let auth = keycloak::login::password(keycloak::login::PasswordParams {
client_id: params.keycloak_client_id,
username: params.keycloak_username,
password: params.keycloak_password,
url: params.keycloak_url,
})
.await
.map_err(|e| format!("Authentication failed: {}", e))?;
log::debug!("Authenticating...");
let auth = params.auth.authenticate().await?;

log::debug!("✓ Authenticated successfully");

Expand Down
158 changes: 158 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/// Authentication configuration for either Keycloak or Auth0.
///
/// Auto-detects the provider based on which credentials are available.
/// If `AUTH0_DOMAIN` env var is set, uses Auth0. Otherwise uses Keycloak.
pub enum AuthConfig {
Keycloak {
client_id: String,
username: String,
password: String,
url: String,
},
Auth0 {
client_id: String,
client_secret: String,
audience: String,
url: String,
},
}

/// Unified authentication response.
pub struct AuthResponse {
pub access_token: String,
pub expires_in: u32,
/// Only available with Keycloak password flow
pub refresh_token: Option<String>,
}

impl AuthConfig {
/// Authenticate and return a token.
pub async fn authenticate(&self) -> Result<AuthResponse, String> {
match self {
AuthConfig::Keycloak {
client_id,
username,
password,
url,
} => {
let response =
keycloak::login::password(keycloak::login::PasswordParams {
client_id: client_id.clone(),
username: username.clone(),
password: password.clone(),
url: url.clone(),
})
.await
.map_err(|e| format!("Keycloak authentication failed: {e}"))?;

Ok(AuthResponse {
access_token: response.access_token,
expires_in: response.expires_in,
refresh_token: Some(response.refresh_token),
})
}
AuthConfig::Auth0 {
client_id,
client_secret,
audience,
url,
} => {
let response =
auth0::login::client_credentials(auth0::login::ClientCredentialsParams {
client_id: client_id.clone(),
client_secret: client_secret.clone(),
audience: audience.clone(),
url: url.clone(),
})
.await
.map_err(|e| format!("Auth0 authentication failed: {e}"))?;

Ok(AuthResponse {
access_token: response.access_token,
expires_in: response.expires_in,
refresh_token: None,
})
}
}
}

/// Refresh the token. For Keycloak, uses refresh_token flow with password
/// fallback. For Auth0, re-authenticates (no refresh tokens in M2M flow).
pub async fn refresh(
&self,
refresh_token: Option<&str>,
) -> Result<AuthResponse, String> {
match self {
AuthConfig::Keycloak {
client_id, url, ..
} => {
// Try refresh token first
if let Some(rt) = refresh_token {
match keycloak::login::refresh(keycloak::login::RefreshParams {
client_id: client_id.clone(),
refresh_token: rt.to_string(),
url: url.clone(),
})
.await
{
Ok(response) => {
return Ok(AuthResponse {
access_token: response.access_token,
expires_in: response.expires_in,
refresh_token: Some(response.refresh_token),
});
}
Err(e) => {
if !e.contains("Token is not active") {
return Err(format!("Failed to refresh JWT: {e}"));
}
// Fall through to password login
}
}
}
// Fallback to password login
self.authenticate().await
}
AuthConfig::Auth0 { .. } => {
// Auth0 M2M has no refresh tokens — just re-authenticate
self.authenticate().await
}
}
}

/// Build from environment variables. Auto-detects provider.
///
/// If `AUTH0_DOMAIN` is set, uses Auth0 (requires `AUTH0_CLIENT_ID`,
/// `AUTH0_CLIENT_SECRET`, `AUTH0_AUDIENCE`).
///
/// Otherwise uses Keycloak (requires `KEYCLOAK_HOST`, `KEYCLOAK_REALM`,
/// `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_USERNAME`, `KEYCLOAK_PASSWORD`).
pub fn from_env() -> Result<Self, String> {
if let Ok(auth0_domain) = std::env::var("AUTH0_DOMAIN") {
Ok(AuthConfig::Auth0 {
url: auth0::login::auth0_url(&auth0_domain),
client_id: std::env::var("AUTH0_CLIENT_ID")
.map_err(|_| "AUTH0_CLIENT_ID must be set")?,
client_secret: std::env::var("AUTH0_CLIENT_SECRET")
.map_err(|_| "AUTH0_CLIENT_SECRET must be set")?,
audience: std::env::var("AUTH0_AUDIENCE")
.map_err(|_| "AUTH0_AUDIENCE must be set")?,
})
} else {
Ok(AuthConfig::Keycloak {
url: keycloak::login::password_url(
&std::env::var("KEYCLOAK_HOST")
.map_err(|_| "KEYCLOAK_HOST must be set")?,
&std::env::var("KEYCLOAK_REALM")
.map_err(|_| "KEYCLOAK_REALM must be set")?,
),
client_id: std::env::var("KEYCLOAK_CLIENT_ID")
.map_err(|_| "KEYCLOAK_CLIENT_ID must be set")?,
username: std::env::var("KEYCLOAK_USERNAME")
.map_err(|_| "KEYCLOAK_USERNAME must be set")?,
password: std::env::var("KEYCLOAK_PASSWORD")
.map_err(|_| "KEYCLOAK_PASSWORD must be set")?,
})
}
}
}
Loading