Skip to content

Commit 0daba56

Browse files
committed
fix: Calculation of OIDC endpoint
1 parent 4a8d377 commit 0daba56

File tree

2 files changed

+120
-8
lines changed

2 files changed

+120
-8
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- Fixed URL handling related to OIDC and `rootPath` with and without trailing slashes. Also added a bunch of tests ([#XXX]).
10+
11+
### Changed
12+
13+
- BREAKING: Made `DEFAULT_OIDC_WELLKNOWN_PATH` private. Use `AuthenticationProvider::well_known_url` instead ([#XXX]).
14+
715
## [0.81.0] - 2024-11-05
816

917
### Added
@@ -12,7 +20,7 @@ All notable changes to this project will be documented in this file.
1220

1321
### Changed
1422

15-
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before. ([#903])
23+
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before ([#903]).
1624

1725
[#903]: https://github.com/stackabletech/operator-rs/pull/903
1826

crates/stackable-operator/src/commons/authentication/oidc.rs

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,27 @@ use crate::commons::{
1717

1818
pub type Result<T, E = Error> = std::result::Result<T, E>;
1919

20-
pub const DEFAULT_OIDC_WELLKNOWN_PATH: &str = ".well-known/openid-configuration";
2120
pub const CLIENT_ID_SECRET_KEY: &str = "clientId";
2221
pub const CLIENT_SECRET_SECRET_KEY: &str = "clientSecret";
2322

23+
const DEFAULT_OIDC_WELLKNOWN_PATH: &str = "./.well-known/openid-configuration";
24+
2425
#[derive(Debug, PartialEq, Snafu)]
2526
pub enum Error {
2627
#[snafu(display("failed to parse OIDC endpoint url"))]
2728
ParseOidcEndpointUrl { source: ParseError },
2829

2930
#[snafu(display(
30-
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url '{endpoint}'"
31+
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url \"{endpoint}\""
3132
))]
3233
SetOidcEndpointScheme { endpoint: Url, scheme: String },
34+
35+
#[snafu(display("failed to join the path {path:?} to URL \"{url}\""))]
36+
JoinPath {
37+
source: ParseError,
38+
url: Url,
39+
path: String,
40+
},
3341
}
3442

3543
/// This struct contains configuration values to configure an OpenID Connect
@@ -111,10 +119,8 @@ impl AuthenticationProvider {
111119
}
112120
}
113121

114-
/// Returns the OIDC endpoint [`Url`]. To append the default OIDC well-known
115-
/// configuration path, use `url.join()`. This module provides the default
116-
/// path at [`DEFAULT_OIDC_WELLKNOWN_PATH`].
117-
pub fn endpoint_url(&self) -> Result<Url> {
122+
/// Base [`Url`] without any path set (so only protocol, host, port and such).
123+
fn base_url(&self) -> Result<Url> {
118124
let mut url = Url::parse(&format!(
119125
"http://{host}:{port}",
120126
host = self.hostname.as_url_host(),
@@ -132,10 +138,38 @@ impl AuthenticationProvider {
132138
})?;
133139
}
134140

135-
url.set_path(&self.root_path);
136141
Ok(url)
137142
}
138143

144+
/// Returns the OIDC endpoint [`Url`] without a trailing slash.
145+
///
146+
/// To get the well-known URL, please use [`Self::well_known_url`].
147+
pub fn endpoint_url(&self) -> Result<Url> {
148+
let mut url = self.base_url()?;
149+
// Some tools can not cope with a trailing slash, so let's remove that
150+
url.set_path(&self.root_path.trim_end_matches('/'));
151+
Ok(url)
152+
}
153+
154+
/// Returns the well-known [`Url`] without a trailing slash.
155+
///
156+
/// It is basically the [`Self::endpoint_url`] joined with
157+
/// "./.well-known/openid-configuration", while watching out for URL joining madness.
158+
pub fn well_known_url(&self) -> Result<Url> {
159+
let mut url = self.base_url()?;
160+
161+
// Url::join cuts of the part after the last slash :/
162+
// So we need to make sure we have a trailing slash, so that nothing get's cut of.
163+
let mut root_path_with_trailing_slash = self.root_path.trim_end_matches('/').to_string();
164+
root_path_with_trailing_slash.push('/');
165+
url.set_path(&root_path_with_trailing_slash);
166+
url.join(DEFAULT_OIDC_WELLKNOWN_PATH)
167+
.with_context(|_| JoinPathSnafu {
168+
url: url.clone(),
169+
path: DEFAULT_OIDC_WELLKNOWN_PATH.to_owned(),
170+
})
171+
}
172+
139173
/// Returns the port to be used, which is either user configured or defaulted based upon TLS usage
140174
pub fn port(&self) -> u16 {
141175
self.port
@@ -246,6 +280,8 @@ pub struct ClientAuthenticationOptions<T = ()> {
246280

247281
#[cfg(test)]
248282
mod test {
283+
use rstest::rstest;
284+
249285
use super::*;
250286

251287
#[test]
@@ -338,6 +374,74 @@ mod test {
338374
);
339375
}
340376

377+
#[rstest]
378+
#[case("/", "https://my.keycloak.server/")]
379+
#[case("/realms/sdp", "https://my.keycloak.server/realms/sdp")]
380+
#[case("/realms/sdp/", "https://my.keycloak.server/realms/sdp")]
381+
#[case("/realms/sdp//////", "https://my.keycloak.server/realms/sdp")]
382+
#[case(
383+
"/realms/my/realm/with/slashes//////",
384+
"https://my.keycloak.server/realms/my/realm/with/slashes"
385+
)]
386+
fn root_path_endpoint_url(#[case] root_path: String, #[case] expected_endpoint_url: &str) {
387+
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
388+
"
389+
hostname: my.keycloak.server
390+
rootPath: {root_path}
391+
scopes: [openid]
392+
principalClaim: preferred_username
393+
tls:
394+
verification:
395+
server:
396+
caCert:
397+
webPki: {{}}
398+
"
399+
))
400+
.unwrap();
401+
402+
assert_eq!(oidc.endpoint_url().unwrap().as_str(), expected_endpoint_url);
403+
}
404+
405+
#[rstest]
406+
#[case("/", "https://my.keycloak.server/.well-known/openid-configuration")]
407+
#[case(
408+
"/realms/sdp",
409+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
410+
)]
411+
#[case(
412+
"/realms/sdp/",
413+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
414+
)]
415+
#[case(
416+
"/realms/sdp//////",
417+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
418+
)]
419+
#[case(
420+
"/realms/my/realm/with/slashes//////",
421+
"https://my.keycloak.server/realms/my/realm/with/slashes/.well-known/openid-configuration"
422+
)]
423+
fn root_path_well_known_url(#[case] root_path: String, #[case] expected_well_known_url: &str) {
424+
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
425+
"
426+
hostname: my.keycloak.server
427+
rootPath: {root_path}
428+
scopes: [openid]
429+
principalClaim: preferred_username
430+
tls:
431+
verification:
432+
server:
433+
caCert:
434+
webPki: {{}}
435+
"
436+
))
437+
.unwrap();
438+
439+
assert_eq!(
440+
oidc.well_known_url().unwrap().as_str(),
441+
expected_well_known_url
442+
);
443+
}
444+
341445
#[test]
342446
fn client_env_vars() {
343447
let secret_name = "my-keycloak-client";

0 commit comments

Comments
 (0)