diff --git a/CHANGELOG.md b/CHANGELOG.md index d79b264..f228efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `keycloak::login::token_url(host, realm)` and + `keycloak::login::master_token_url(host)` — build OIDC token endpoint URLs + using the Keycloak 17+ (Quarkus distribution) path layout, which omits the + `/auth` context root. Use these for default Keycloak 17+ deployments where + realm endpoints are served at `/realms/{realm}/...`. The existing + `client_credentials_url`, `password_url`, and `password_master_url` + helpers are unchanged and continue to emit the legacy `/auth/realms/...` + paths for backwards compatibility. + ## [0.5.0] - 2026-05-11 ### Added diff --git a/README.md b/README.md index 88b719e..285be12 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ let auth = login::password(login::PasswordParams { client_id: "your-client-id".to_string(), username: "your-username".to_string(), password: "your-password".to_string(), - url: login::password_url("https://your-keycloak-host", "your-realm"), + url: login::token_url("https://your-keycloak-host", "your-realm"), }).await?; // Use auth.access_token for subsequent API calls @@ -158,7 +158,8 @@ cargo run -p examples --bin delete_executed_transfers - `password(PasswordParams)` - Authenticate with username/password - `client_credentials(ClientCredentialsParams)` - Service account authentication -- `password_url(host, realm)` - Build password grant URL +- `token_url(host, realm)` - Build the realm token endpoint URL (Keycloak 17+ Quarkus layout; append `/auth` to `host` for legacy deployments) +- `master_token_url(host)` - Build the `master` realm token endpoint URL ### `ledger` diff --git a/crates/examples/src/delete_executed_transfers.rs b/crates/examples/src/delete_executed_transfers.rs index 4baebbc..3526a6f 100644 --- a/crates/examples/src/delete_executed_transfers.rs +++ b/crates/examples/src/delete_executed_transfers.rs @@ -85,8 +85,11 @@ async fn main() -> Result<(), String> { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("KEYCLOAK_CLIENT_SECRET") .expect("KEYCLOAK_CLIENT_SECRET must be set"), - url: keycloak::login::client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: keycloak::login::token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/examples/src/list_contracts.rs b/crates/examples/src/list_contracts.rs index 49a6299..c85579d 100644 --- a/crates/examples/src/list_contracts.rs +++ b/crates/examples/src/list_contracts.rs @@ -34,8 +34,11 @@ async fn main() -> Result<(), String> { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), username: env::var("KEYCLOAK_USERNAME").expect("KEYCLOAK_USERNAME must be set"), password: env::var("KEYCLOAK_PASSWORD").expect("KEYCLOAK_PASSWORD must be set"), - url: keycloak::login::password_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: keycloak::login::token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/keycloak/src/login.rs b/crates/keycloak/src/login.rs index f9df241..232cbb8 100644 --- a/crates/keycloak/src/login.rs +++ b/crates/keycloak/src/login.rs @@ -145,6 +145,18 @@ pub async fn password_with_client( Ok(response) } +/// Build the OIDC token endpoint URL for a realm on a Keycloak server +/// that exposes the legacy `/auth` context root (Keycloak ≤ 16, or 17+ +/// started with `--http-relative-path=/auth`). +/// +/// Keycloak serves a single token endpoint per realm that handles every +/// grant type (`client_credentials`, `password`, `refresh_token`, …); this +/// helper is named for its caller in this module ([`client_credentials`]) +/// but the URL itself is not grant-specific. +#[deprecated( + since = "0.5.1", + note = "use `token_url(host, realm)` instead; for legacy `/auth` deployments pass `{host}/auth` as the host" +)] pub fn client_credentials_url(host: &str, realm: &str) -> String { format!( "{}/auth/realms/{}/protocol/openid-connect/token", @@ -152,6 +164,14 @@ pub fn client_credentials_url(host: &str, realm: &str) -> String { ) } +/// Build the OIDC token endpoint URL for a realm on a Keycloak server +/// that exposes the legacy `/auth` context root. Alias of +/// [`client_credentials_url`] kept for call-site readability — the +/// underlying token endpoint is shared across all grant types. +#[deprecated( + since = "0.5.1", + note = "use `token_url(host, realm)` instead; for legacy `/auth` deployments pass `{host}/auth` as the host" +)] pub fn password_url(host: &str, realm: &str) -> String { format!( "{}/auth/realms/{}/protocol/openid-connect/token", @@ -159,10 +179,42 @@ pub fn password_url(host: &str, realm: &str) -> String { ) } +/// Build the OIDC token endpoint URL for the `master` realm on a Keycloak +/// server that exposes the legacy `/auth` context root. The endpoint is +/// shared across grant types. +#[deprecated( + since = "0.5.1", + note = "use `master_token_url(host)` instead; for legacy `/auth` deployments pass `{host}/auth` as the host" +)] pub fn password_master_url(host: &str) -> String { format!("{}/auth/realms/master/protocol/openid-connect/token", host) } +/// Build the OIDC token endpoint URL for a Keycloak realm using the +/// Keycloak 17+ (Quarkus) path layout — no `/auth` context root. +/// +/// Produces `{host}/realms/{realm}/protocol/openid-connect/token`, which is +/// the default for Keycloak 17 and later. A trailing `/` on `host` is +/// trimmed so callers can pass either `https://kc.example.com` or +/// `https://kc.example.com/`. If a deployment still serves the legacy +/// `/auth/realms/...` paths (Keycloak ≤ 16, or 17+ started with +/// `--http-relative-path=/auth`), include `/auth` in the `host` argument, +/// e.g. `https://kc.example.com/auth`. +pub fn token_url(host: &str, realm: &str) -> String { + let host = host.trim_end_matches('/'); + format!("{host}/realms/{realm}/protocol/openid-connect/token") +} + +/// Build the OIDC token endpoint URL for the `master` realm using the +/// Keycloak 17+ (Quarkus) path layout — no `/auth` context root. +/// +/// A trailing `/` on `host` is trimmed. See [`token_url`] for notes on +/// legacy `/auth` deployments. +pub fn master_token_url(host: &str) -> String { + let host = host.trim_end_matches('/'); + format!("{host}/realms/master/protocol/openid-connect/token") +} + pub async fn refresh(params: RefreshParams) -> Result { let client = reqwest::Client::new(); let form = [ diff --git a/crates/ledger/src/active_contracts.rs b/crates/ledger/src/active_contracts.rs index b35a7db..f12ea85 100644 --- a/crates/ledger/src/active_contracts.rs +++ b/crates/ledger/src/active_contracts.rs @@ -108,7 +108,7 @@ fn filter_active_contracts_by_create_argument( mod tests { use super::*; use crate::ledger_end; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; #[tokio::test] @@ -122,8 +122,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_LEDGER_END_CLIENT_SECRET") .expect("LIB_TEST_LEDGER_END_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/ledger/src/ledger_end.rs b/crates/ledger/src/ledger_end.rs index c78ee1a..541335a 100644 --- a/crates/ledger/src/ledger_end.rs +++ b/crates/ledger/src/ledger_end.rs @@ -38,7 +38,7 @@ pub async fn get(params: Params) -> Result { #[cfg(test)] mod tests { use super::*; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; #[tokio::test] @@ -49,8 +49,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_LEDGER_END_CLIENT_SECRET") .expect("LIB_TEST_LEDGER_END_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/ledger/src/websocket/active_contracts.rs b/crates/ledger/src/websocket/active_contracts.rs index 5b7664d..91ed98b 100644 --- a/crates/ledger/src/websocket/active_contracts.rs +++ b/crates/ledger/src/websocket/active_contracts.rs @@ -279,7 +279,7 @@ pub async fn get(params: Params) -> Result, String mod tests { use super::*; use crate::ledger_end; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; use tokio::time::Duration; @@ -294,8 +294,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_LEDGER_END_CLIENT_SECRET") .expect("LIB_TEST_LEDGER_END_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/ledger/src/websocket/update.rs b/crates/ledger/src/websocket/update.rs index c2a4a0b..0c19c05 100644 --- a/crates/ledger/src/websocket/update.rs +++ b/crates/ledger/src/websocket/update.rs @@ -126,7 +126,7 @@ pub async fn subscribe( mod tests { use super::*; use crate::ledger_end; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; use tokio::time::Duration; @@ -142,8 +142,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_LEDGER_END_CLIENT_SECRET") .expect("LIB_TEST_LEDGER_END_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/registry/src/transfer_factory.rs b/crates/registry/src/transfer_factory.rs index f431a3e..74a54c2 100644 --- a/crates/registry/src/transfer_factory.rs +++ b/crates/registry/src/transfer_factory.rs @@ -50,7 +50,7 @@ pub async fn get(params: Params) -> Result Result { #[cfg(test)] mod tests { use super::*; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; use tokio; @@ -68,8 +68,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_AMULET_CLIENT_SECRET") .expect("LIB_TEST_AMULET_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), }; diff --git a/crates/wallet/src/mining_rounds.rs b/crates/wallet/src/mining_rounds.rs index aced281..2aa314c 100644 --- a/crates/wallet/src/mining_rounds.rs +++ b/crates/wallet/src/mining_rounds.rs @@ -261,7 +261,7 @@ pub async fn get_open_mining_rounds( #[cfg(test)] mod tests { use super::*; - use keycloak::login::{ClientCredentialsParams, client_credentials, client_credentials_url}; + use keycloak::login::{ClientCredentialsParams, client_credentials, token_url}; use std::env; #[tokio::test] @@ -270,8 +270,11 @@ mod tests { client_id: env::var("KEYCLOAK_CLIENT_ID").expect("KEYCLOAK_CLIENT_ID must be set"), client_secret: env::var("LIB_TEST_AMULET_CLIENT_SECRET") .expect("LIB_TEST_AMULET_CLIENT_SECRET must be set"), - url: client_credentials_url( - &env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set"), + url: token_url( + &format!( + "{}/auth", + env::var("KEYCLOAK_HOST").expect("KEYCLOAK_HOST must be set") + ), &env::var("KEYCLOAK_REALM").expect("KEYCLOAK_REALM must be set"), ), };