Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`

Expand Down
7 changes: 5 additions & 2 deletions crates/examples/src/delete_executed_transfers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
};
Expand Down
7 changes: 5 additions & 2 deletions crates/examples/src/list_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
};
Expand Down
52 changes: 52 additions & 0 deletions crates/keycloak/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,76 @@ 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",
host, realm
)
}

/// 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",
host, realm
)
}

/// 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")
}
Comment thread
schronck marked this conversation as resolved.

/// 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<Response, String> {
let client = reqwest::Client::new();
let form = [
Expand Down
9 changes: 6 additions & 3 deletions crates/ledger/src/active_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/ledger/src/ledger_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub async fn get(params: Params) -> Result<Response, String> {
#[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]
Expand All @@ -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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/ledger/src/websocket/active_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ pub async fn get(params: Params) -> Result<Vec<models::JsActiveContract>, 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;

Expand All @@ -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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/ledger/src/websocket/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/registry/src/transfer_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub async fn get(params: Params) -> Result<common::transfer_factory::Response, S
mod tests {
use super::*;
use crate::consts;
use keycloak::login::{PasswordParams, password, password_url};
use keycloak::login::{PasswordParams, password, token_url};
use std::collections::HashMap;
use std::env;
use std::ops::Add;
Expand All @@ -65,8 +65,11 @@ mod tests {
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: password_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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/wallet/src/amulet_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub async fn get(params: Params) -> Result<AmuletRules, String> {
#[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;

Expand All @@ -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"),
),
};
Expand Down
9 changes: 6 additions & 3 deletions crates/wallet/src/mining_rounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"),
),
};
Expand Down