@@ -17,17 +17,19 @@ use crate::commons::{
1717
1818pub type Result < T , E = Error > = std:: result:: Result < T , E > ;
1919
20- pub const DEFAULT_OIDC_WELLKNOWN_PATH : & str = ".well-known/openid-configuration" ;
2120pub const CLIENT_ID_SECRET_KEY : & str = "clientId" ;
2221pub 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 ) ]
2527pub 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) ]
248280mod 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