From 928f99562216e0a941381d7cb20efc16d92c1861 Mon Sep 17 00:00:00 2001 From: richardanyalai Date: Mon, 18 May 2026 14:26:06 +0200 Subject: [PATCH 1/3] feat(keycloak): added Quarkus-style token URL helpers --- CHANGELOG.md | 11 +++++++++++ crates/keycloak/src/login.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) 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/crates/keycloak/src/login.rs b/crates/keycloak/src/login.rs index f9df241..00a9455 100644 --- a/crates/keycloak/src/login.rs +++ b/crates/keycloak/src/login.rs @@ -145,6 +145,12 @@ pub async fn password_with_client( Ok(response) } +/// Build the OIDC token URL for the `client_credentials` grant on a +/// Keycloak server that exposes the legacy `/auth` context root (Keycloak +/// ≤ 16, or 17+ started with `--http-relative-path=/auth`). +/// +/// For Keycloak 17+ defaults (Quarkus distribution, no `/auth` prefix), +/// use [`token_url`] instead. pub fn client_credentials_url(host: &str, realm: &str) -> String { format!( "{}/auth/realms/{}/protocol/openid-connect/token", @@ -152,6 +158,10 @@ pub fn client_credentials_url(host: &str, realm: &str) -> String { ) } +/// Build the OIDC token URL for the `password` grant on a Keycloak server +/// that exposes the legacy `/auth` context root. +/// +/// For Keycloak 17+ defaults, use [`token_url`] instead. pub fn password_url(host: &str, realm: &str) -> String { format!( "{}/auth/realms/{}/protocol/openid-connect/token", @@ -159,10 +169,34 @@ pub fn password_url(host: &str, realm: &str) -> String { ) } +/// Build the OIDC token URL for the `master` realm on a Keycloak server +/// that exposes the legacy `/auth` context root. +/// +/// For Keycloak 17+ defaults, use [`master_token_url`] instead. pub fn password_master_url(host: &str) -> String { format!("{}/auth/realms/master/protocol/openid-connect/token", host) } +/// Build the OIDC token 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. 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 { + format!("{host}/realms/{realm}/protocol/openid-connect/token") +} + +/// Build the OIDC token URL for the `master` realm using the Keycloak 17+ +/// (Quarkus) path layout — no `/auth` context root. +/// +/// See [`token_url`] for notes on legacy `/auth` deployments. +pub fn master_token_url(host: &str) -> String { + format!("{host}/realms/master/protocol/openid-connect/token") +} + pub async fn refresh(params: RefreshParams) -> Result { let client = reqwest::Client::new(); let form = [ From a6e6f027f2b8fa299ae3977fb5fa7bf53b64c533 Mon Sep 17 00:00:00 2001 From: richardanyalai Date: Tue, 19 May 2026 11:27:53 +0200 Subject: [PATCH 2/3] docs(keycloak): clarify token URL helpers and trim trailing slash --- crates/keycloak/src/login.rs | 41 ++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/keycloak/src/login.rs b/crates/keycloak/src/login.rs index 00a9455..b88a8f1 100644 --- a/crates/keycloak/src/login.rs +++ b/crates/keycloak/src/login.rs @@ -145,9 +145,14 @@ pub async fn password_with_client( Ok(response) } -/// Build the OIDC token URL for the `client_credentials` grant on a -/// Keycloak server that exposes the legacy `/auth` context root (Keycloak -/// ≤ 16, or 17+ started with `--http-relative-path=/auth`). +/// 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. /// /// For Keycloak 17+ defaults (Quarkus distribution, no `/auth` prefix), /// use [`token_url`] instead. @@ -158,8 +163,10 @@ pub fn client_credentials_url(host: &str, realm: &str) -> String { ) } -/// Build the OIDC token URL for the `password` grant on a Keycloak server -/// that exposes the legacy `/auth` context root. +/// 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. /// /// For Keycloak 17+ defaults, use [`token_url`] instead. pub fn password_url(host: &str, realm: &str) -> String { @@ -169,31 +176,37 @@ pub fn password_url(host: &str, realm: &str) -> String { ) } -/// Build the OIDC token URL for the `master` realm on a Keycloak server -/// that exposes the legacy `/auth` context root. +/// 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. /// /// For Keycloak 17+ defaults, use [`master_token_url`] instead. pub fn password_master_url(host: &str) -> String { format!("{}/auth/realms/master/protocol/openid-connect/token", host) } -/// Build the OIDC token URL for a Keycloak realm using the Keycloak 17+ -/// (Quarkus) path layout — no `/auth` context root. +/// 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. If a deployment still serves the -/// legacy `/auth/realms/...` paths (Keycloak ≤ 16, or 17+ started with +/// 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 URL for the `master` realm using the Keycloak 17+ -/// (Quarkus) path layout — no `/auth` context root. +/// Build the OIDC token endpoint URL for the `master` realm using the +/// Keycloak 17+ (Quarkus) path layout — no `/auth` context root. /// -/// See [`token_url`] for notes on legacy `/auth` deployments. +/// 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") } From e7607c8fdf04f949bc5b03769951fbd53c2fefdd Mon Sep 17 00:00:00 2001 From: richardanyalai Date: Tue, 19 May 2026 11:47:20 +0200 Subject: [PATCH 3/3] refactor(keycloak)!: deprecate legacy token URL helpers --- README.md | 5 +++-- .../examples/src/delete_executed_transfers.rs | 7 +++++-- crates/examples/src/list_contracts.rs | 7 +++++-- crates/keycloak/src/login.rs | 19 ++++++++++++------- crates/ledger/src/active_contracts.rs | 9 ++++++--- crates/ledger/src/ledger_end.rs | 9 ++++++--- .../ledger/src/websocket/active_contracts.rs | 9 ++++++--- crates/ledger/src/websocket/update.rs | 9 ++++++--- crates/registry/src/transfer_factory.rs | 9 ++++++--- crates/wallet/src/amulet_rules.rs | 9 ++++++--- crates/wallet/src/mining_rounds.rs | 9 ++++++--- 11 files changed, 67 insertions(+), 34 deletions(-) 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 b88a8f1..232cbb8 100644 --- a/crates/keycloak/src/login.rs +++ b/crates/keycloak/src/login.rs @@ -153,9 +153,10 @@ pub async fn password_with_client( /// 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. -/// -/// For Keycloak 17+ defaults (Quarkus distribution, no `/auth` prefix), -/// use [`token_url`] instead. +#[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", @@ -167,8 +168,10 @@ pub fn client_credentials_url(host: &str, realm: &str) -> String { /// 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. -/// -/// For Keycloak 17+ defaults, use [`token_url`] instead. +#[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", @@ -179,8 +182,10 @@ 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. -/// -/// For Keycloak 17+ defaults, use [`master_token_url`] instead. +#[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) } 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"), ), };