@@ -17,17 +17,19 @@ use crate::commons::{
17
17
18
18
pub type Result < T , E = Error > = std:: result:: Result < T , E > ;
19
19
20
- pub const DEFAULT_OIDC_WELLKNOWN_PATH : & str = ".well-known/openid-configuration" ;
21
20
pub const CLIENT_ID_SECRET_KEY : & str = "clientId" ;
22
21
pub const CLIENT_SECRET_SECRET_KEY : & str = "clientSecret" ;
23
22
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
+
24
26
#[ derive( Debug , PartialEq , Snafu ) ]
25
27
pub enum Error {
26
28
#[ snafu( display( "failed to parse OIDC endpoint url" ) ) ]
27
29
ParseOidcEndpointUrl { source : ParseError } ,
28
30
29
31
#[ 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}\" "
31
33
) ) ]
32
34
SetOidcEndpointScheme { endpoint : Url , scheme : String } ,
33
35
}
@@ -111,10 +113,10 @@ impl AuthenticationProvider {
111
113
}
112
114
}
113
115
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 > {
118
120
let mut url = Url :: parse ( & format ! (
119
121
"http://{host}:{port}" ,
120
122
host = self . hostname. as_url_host( ) ,
@@ -132,7 +134,37 @@ impl AuthenticationProvider {
132
134
} ) ?;
133
135
}
134
136
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
+
136
168
Ok ( url)
137
169
}
138
170
@@ -246,6 +278,8 @@ pub struct ClientAuthenticationOptions<T = ()> {
246
278
247
279
#[ cfg( test) ]
248
280
mod test {
281
+ use rstest:: rstest;
282
+
249
283
use super :: * ;
250
284
251
285
#[ test]
@@ -310,12 +344,8 @@ mod test {
310
344
. unwrap ( ) ;
311
345
312
346
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/"
319
349
) ;
320
350
}
321
351
@@ -338,6 +368,70 @@ mod test {
338
368
) ;
339
369
}
340
370
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
+
341
435
#[ test]
342
436
fn client_env_vars ( ) {
343
437
let secret_name = "my-keycloak-client" ;
0 commit comments