diff --git a/Cargo.lock b/Cargo.lock index f8e4e55..2583c5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,16 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auth0" +version = "0.2.0" +dependencies = [ + "base64", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -139,6 +149,7 @@ dependencies = [ name = "cbtc" version = "0.3.1" dependencies = [ + "auth0", "base64", "canton-api-client", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 99279a8..39f2190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/examples/accept_transfers.rs b/examples/accept_transfers.rs index 7a6794f..d7a9dea 100644 --- a/examples/accept_transfers.rs +++ b/examples/accept_transfers.rs @@ -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. @@ -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?; diff --git a/examples/batch_distribute.rs b/examples/batch_distribute.rs index a823dc0..4c6b5a8 100644 --- a/examples/batch_distribute.rs +++ b/examples/batch_distribute.rs @@ -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, }; diff --git a/examples/batch_with_callback.rs b/examples/batch_with_callback.rs index 01f1470..40fcaaa 100644 --- a/examples/batch_with_callback.rs +++ b/examples/batch_with_callback.rs @@ -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 = @@ -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), }) diff --git a/examples/cancel_offers.rs b/examples/cancel_offers.rs index ca19020..a3b238d 100644 --- a/examples/cancel_offers.rs +++ b/examples/cancel_offers.rs @@ -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!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); @@ -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?; diff --git a/examples/integration_test.rs b/examples/integration_test.rs index f1b3449..78cd66a 100644 --- a/examples/integration_test.rs +++ b/examples/integration_test.rs @@ -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 { @@ -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; @@ -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; diff --git a/examples/stream.rs b/examples/stream.rs index f831a77..49ad9b4 100644 --- a/examples/stream.rs +++ b/examples/stream.rs @@ -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!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); @@ -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), }) diff --git a/src/accept.rs b/src/accept.rs index c9a5bf1..051ce4e 100644 --- a/src/accept.rs +++ b/src/accept.rs @@ -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 @@ -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 { - 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"); diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..3659948 --- /dev/null +++ b/src/auth.rs @@ -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, +} + +impl AuthConfig { + /// Authenticate and return a token. + pub async fn authenticate(&self) -> Result { + 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 { + 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 { + 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")?, + }) + } + } +} diff --git a/src/batch.rs b/src/batch.rs index 66d0c58..2e4f292 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -14,11 +14,8 @@ pub struct Params { pub ledger_host: String, pub registry_url: String, 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, // Optional reference base for unique transfer IDs pub reference_base: Option, } @@ -77,10 +74,7 @@ pub async fn submit_from_csv(params: Params) -> Result<(), String> { ledger_host: params.ledger_host, registry_url: params.registry_url, decentralized_party_id: params.decentralized_party_id, - keycloak_client_id: params.keycloak_client_id, - keycloak_username: params.keycloak_username, - keycloak_password: params.keycloak_password, - keycloak_url: params.keycloak_url, + auth: params.auth, reference_base: params.reference_base, on_transfer_complete: None, }) @@ -110,7 +104,6 @@ pub async fn submit_from_csv(params: Params) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; - use keycloak::login::password_url; use std::env; use std::io::Write; @@ -147,16 +140,7 @@ mod tests { 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: password_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), - &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), - ), + auth: crate::auth::AuthConfig::from_env().expect("Auth config must be available"), reference_base: Some(format!("batch-test-{}", chrono::Utc::now().timestamp())), }; diff --git a/src/cancel_offers.rs b/src/cancel_offers.rs index df0af86..61002e5 100644 --- a/src/cancel_offers.rs +++ b/src/cancel_offers.rs @@ -25,11 +25,8 @@ pub struct WithdrawAllParams { 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 withdrawing a single transfer @@ -140,15 +137,8 @@ pub async fn submit(params: Params) -> Result<(), String> { /// /// Returns a summary of successful and failed withdrawals. pub async fn withdraw_all(params: WithdrawAllParams) -> Result { - 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"); diff --git a/src/distribute.rs b/src/distribute.rs index b26e14c..9e29633 100644 --- a/src/distribute.rs +++ b/src/distribute.rs @@ -12,11 +12,8 @@ pub struct Params { pub ledger_host: String, pub registry_url: String, 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, // Optional reference base for unique transfer IDs (run ID) pub reference_base: Option, // Optional callback for handling each transfer result @@ -39,15 +36,10 @@ pub struct Params { pub async fn submit(params: Params) -> Result { log::debug!("Distributing to {} recipients", params.recipients.len()); - // Authenticate with Keycloak - let mut token_state = transfer::TokenState::new( - params.keycloak_username, - params.keycloak_password, - params.keycloak_client_id.clone(), - params.keycloak_url.clone(), - ) - .await - .map_err(|e| format!("Failed to initialize token state: {}", e))?; + // Authenticate + let mut token_state = transfer::TokenState::new(params.auth) + .await + .map_err(|e| format!("Failed to initialize token state: {}", e))?; let access_token = token_state.get_fresh_token().await?; @@ -109,7 +101,6 @@ pub async fn submit(params: Params) -> Result, + auth_config: crate::auth::AuthConfig, expires_at: std::time::SystemTime, } impl TokenState { - pub async fn new( - username: String, - password: String, - client_id: String, - url: String, - ) -> Result { - let token = keycloak::login::password(keycloak::login::PasswordParams { - client_id: client_id.clone(), - username: username.clone(), - password: password.clone(), - url: url.clone(), - }) - .await?; + pub async fn new(auth_config: crate::auth::AuthConfig) -> Result { + let response = auth_config.authenticate().await?; + + let expires_at = std::time::SystemTime::now() + + std::time::Duration::from_secs( + response.expires_in.saturating_sub(60) as u64, + ); Ok(TokenState { - access_token: token.access_token, - refresh_token: token.refresh_token, - - username, - password, - - client_id, - url, - expires_at: std::time::SystemTime::now() - .checked_sub(std::time::Duration::from_secs( - (token.expires_in - 20) as u64, - )) - .unwrap_or(std::time::SystemTime::now()), + access_token: response.access_token, + refresh_token: response.refresh_token, + auth_config, + expires_at, }) } - /// Get a fresh token, refreshing if needed (within 1 minute of expiry) + /// Get a fresh token, refreshing if needed pub async fn get_fresh_token(&mut self) -> Result { let now = std::time::SystemTime::now(); - let needs_refresh = now >= self.expires_at; - - if needs_refresh { - let auth = match keycloak::login::refresh(keycloak::login::RefreshParams { - client_id: self.client_id.clone(), - refresh_token: self.refresh_token.clone(), - url: self.url.clone(), - }) - .await - { - Ok(auth_response) => auth_response, - Err(e) => { - if e.contains("Token is not active") { - // Try full password login as fallback - let auth_response = - keycloak::login::password(keycloak::login::PasswordParams { - client_id: self.client_id.clone(), - username: self.username.clone(), - password: self.password.clone(), - url: self.url.clone(), - }) - .await?; - - auth_response - } else { - return Err(format!("Failed to refresh JWT: {}", e)); - } - } - }; + if now >= self.expires_at { + let response = self + .auth_config + .refresh(self.refresh_token.as_deref()) + .await?; - self.access_token = auth.access_token.clone(); - self.refresh_token = auth.refresh_token; - // Set expiry to 1 minute before actual expiry + self.access_token = response.access_token; + self.refresh_token = response.refresh_token; self.expires_at = std::time::SystemTime::now() - + std::time::Duration::from_secs(auth.expires_in as u64 - 60); + + std::time::Duration::from_secs( + response.expires_in.saturating_sub(60) as u64, + ); } Ok(self.access_token.clone())