Skip to content

Commit de15662

Browse files
sbernauerTechassi
andauthored
fix!: Correctly construct OIDC endpoints (#910)
* fix: Calculation of OIDC endpoint * changelog * udpate tests * clippy * Update crates/stackable-operator/src/commons/authentication/oidc.rs Co-authored-by: Techassi <git@techassi.dev> * fix tests again * Apply suggestions from code review Co-authored-by: Techassi <git@techassi.dev> * Rename well_known_url -> well_known_config_url * Review feedback * Apply suggestions from code review Co-authored-by: Techassi <git@techassi.dev> --------- Co-authored-by: Techassi <git@techassi.dev>
1 parent 4a8d377 commit de15662

File tree

2 files changed

+118
-14
lines changed

2 files changed

+118
-14
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ 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 ([#910]).
10+
11+
### Changed
12+
13+
- BREAKING: Made `DEFAULT_OIDC_WELLKNOWN_PATH` private. Use `AuthenticationProvider::well_known_config_url` instead ([#910]).
14+
15+
[#910]: https://github.com/stackabletech/operator-rs/pull/910
16+
717
## [0.81.0] - 2024-11-05
818

919
### Added
@@ -12,7 +22,7 @@ All notable changes to this project will be documented in this file.
1222

1323
### Changed
1424

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])
25+
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before ([#903]).
1626

1727
[#903]: https://github.com/stackabletech/operator-rs/pull/903
1828

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

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ 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+
/// Do *not* use this for [`Url::join`], as the leading slash will erase the existing path!
24+
const DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH: &str = "/.well-known/openid-configuration";
25+
2426
#[derive(Debug, PartialEq, Snafu)]
2527
pub enum Error {
2628
#[snafu(display("failed to parse OIDC endpoint url"))]
2729
ParseOidcEndpointUrl { source: ParseError },
2830

2931
#[snafu(display(
30-
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url '{endpoint}'"
32+
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url \"{endpoint}\""
3133
))]
3234
SetOidcEndpointScheme { endpoint: Url, scheme: String },
3335
}
@@ -111,10 +113,10 @@ impl AuthenticationProvider {
111113
}
112114
}
113115

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> {
116+
/// Returns the OIDC base [`Url`] without any path segments.
117+
///
118+
/// The base url only contains the scheme, the host, and an optional port.
119+
fn base_url(&self) -> Result<Url> {
118120
let mut url = Url::parse(&format!(
119121
"http://{host}:{port}",
120122
host = self.hostname.as_url_host(),
@@ -132,7 +134,37 @@ impl AuthenticationProvider {
132134
})?;
133135
}
134136

135-
url.set_path(&self.root_path);
137+
Ok(url)
138+
}
139+
140+
/// Returns the OIDC endpoint [`Url`] without a trailing slash.
141+
///
142+
/// To retrieve the well-known OIDC configuration url, please use [`Self::well_known_config_url`].
143+
pub fn endpoint_url(&self) -> Result<Url> {
144+
let mut url = self.base_url()?;
145+
// Some tools can not cope with a trailing slash, so let's remove that
146+
url.set_path(self.root_path.trim_end_matches('/'));
147+
Ok(url)
148+
}
149+
150+
/// Returns the well-known OIDC configuration [`Url`] without a trailing slash.
151+
///
152+
/// The returned url is a combination of [`Self::endpoint_url`] joined with
153+
/// the well-known OIDC configuration path `DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH`.
154+
pub fn well_known_config_url(&self) -> Result<Url> {
155+
let mut url = self.base_url()?;
156+
157+
// Taken from https://docs.rs/url/latest/url/struct.Url.html#method.join:
158+
// A trailing slash is significant. Without it, the last path component is considered to be
159+
// a “file” name to be removed to get at the “directory” that is used as the base.
160+
//
161+
// Because of that behavior, we first need to make sure that the root path doesn't contain
162+
// any trailing slashes to finally append the well-known config path to the url. The path
163+
// already contains a prefixed slash.
164+
let mut root_path_with_trailing_slash = self.root_path.trim_end_matches('/').to_string();
165+
root_path_with_trailing_slash.push_str(DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH);
166+
url.set_path(&root_path_with_trailing_slash);
167+
136168
Ok(url)
137169
}
138170

@@ -246,6 +278,8 @@ pub struct ClientAuthenticationOptions<T = ()> {
246278

247279
#[cfg(test)]
248280
mod test {
281+
use rstest::rstest;
282+
249283
use super::*;
250284

251285
#[test]
@@ -310,12 +344,8 @@ mod test {
310344
.unwrap();
311345

312346
assert_eq!(
313-
oidc.endpoint_url()
314-
.unwrap()
315-
.join(DEFAULT_OIDC_WELLKNOWN_PATH)
316-
.unwrap()
317-
.as_str(),
318-
"https://my.keycloak.server/.well-known/openid-configuration"
347+
oidc.endpoint_url().unwrap().as_str(),
348+
"https://my.keycloak.server/"
319349
);
320350
}
321351

@@ -338,6 +368,70 @@ mod test {
338368
);
339369
}
340370

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

0 commit comments

Comments
 (0)