@@ -80,6 +80,8 @@ pub struct OidcConfig {
8080 pub scopes : Vec < Scope > ,
8181 pub additional_audience_verifier : AudienceVerifier ,
8282 pub site_prefix : String ,
83+ pub redirect_uri : String ,
84+ pub logout_uri : String ,
8385}
8486
8587impl TryFrom < & AppConfig > for OidcConfig {
@@ -95,6 +97,10 @@ impl TryFrom<&AppConfig> for OidcConfig {
9597
9698 let app_host = get_app_host ( config) ;
9799
100+ let site_prefix_trimmed = config. site_prefix . trim_end_matches ( '/' ) ;
101+ let redirect_uri = format ! ( "{site_prefix_trimmed}{SQLPAGE_REDIRECT_URI}" ) ;
102+ let logout_uri = format ! ( "{site_prefix_trimmed}{SQLPAGE_LOGOUT_URI}" ) ;
103+
98104 Ok ( Self {
99105 issuer_url : issuer_url. clone ( ) ,
100106 client_id : config. oidc_client_id . clone ( ) ,
@@ -111,6 +117,8 @@ impl TryFrom<&AppConfig> for OidcConfig {
111117 config. oidc_additional_trusted_audiences . clone ( ) ,
112118 ) ,
113119 site_prefix : config. site_prefix . clone ( ) ,
120+ redirect_uri,
121+ logout_uri,
114122 } )
115123 }
116124}
@@ -131,6 +139,19 @@ impl OidcConfig {
131139 . id_token_verifier ( )
132140 . set_other_audience_verifier_fn ( self . additional_audience_verifier . as_fn ( ) )
133141 }
142+
143+ /// Creates a logout URL with the given redirect URI
144+ #[ must_use]
145+ pub fn create_logout_url ( & self , redirect_uri : & str ) -> String {
146+ let timestamp = chrono:: Utc :: now ( ) . timestamp ( ) ;
147+ let signature = compute_logout_signature ( redirect_uri, timestamp, & self . client_secret ) ;
148+ let query = form_urlencoded:: Serializer :: new ( String :: new ( ) )
149+ . append_pair ( "redirect_uri" , redirect_uri)
150+ . append_pair ( "timestamp" , & timestamp. to_string ( ) )
151+ . append_pair ( "signature" , & signature)
152+ . finish ( ) ;
153+ format ! ( "{}?{}" , self . logout_uri, query)
154+ }
134155}
135156
136157fn get_app_host ( config : & AppConfig ) -> String {
@@ -157,16 +178,6 @@ fn get_app_host(config: &AppConfig) -> String {
157178 host
158179}
159180
160- fn build_absolute_uri ( app_host : & str , relative_path : & str , scheme : & str ) -> anyhow:: Result < String > {
161- let mut base_url = Url :: parse ( & format ! ( "{scheme}://{app_host}" ) )
162- . with_context ( || format ! ( "Failed to parse app_host: {app_host}" ) ) ?;
163- base_url. set_path ( "" ) ;
164- let absolute_url = base_url
165- . join ( relative_path)
166- . with_context ( || format ! ( "Failed to join path {relative_path}" ) ) ?;
167- Ok ( absolute_url. to_string ( ) )
168- }
169-
170181pub struct ClientWithTime {
171182 client : OidcClient ,
172183 end_session_endpoint : Option < EndSessionUrl > ,
@@ -248,6 +259,29 @@ impl OidcState {
248259 . map_err ( |e| anyhow:: anyhow!( "Could not verify the ID token: {e}" ) ) ?;
249260 Ok ( claims)
250261 }
262+
263+ /// Builds an absolute redirect URI by joining the relative redirect URI with the client's redirect URL
264+ pub async fn build_absolute_redirect_uri (
265+ & self ,
266+ relative_redirect_uri : & str ,
267+ ) -> anyhow:: Result < String > {
268+ let client_guard = self . get_client ( ) . await ;
269+ let client_redirect_url = client_guard
270+ . redirect_uri ( )
271+ . ok_or_else ( || anyhow ! ( "OIDC client has no redirect URL configured" ) ) ?;
272+ let absolute_redirect_uri = client_redirect_url
273+ . url ( )
274+ . join ( relative_redirect_uri)
275+ . with_context ( || {
276+ format ! (
277+ "Failed to join redirect URI {} with client redirect URL {}" ,
278+ relative_redirect_uri,
279+ client_redirect_url. url( )
280+ )
281+ } ) ?
282+ . to_string ( ) ;
283+ Ok ( absolute_redirect_uri)
284+ }
251285}
252286
253287pub async fn initialize_oidc_state (
@@ -364,23 +398,12 @@ async fn handle_request(oidc_state: &OidcState, request: ServiceRequest) -> Midd
364398 log:: trace!( "Started OIDC middleware request handling" ) ;
365399 oidc_state. refresh_if_expired ( & request) . await ;
366400
367- let redirect_uri = format ! (
368- "{}{}" ,
369- oidc_state. config. site_prefix. trim_end_matches( '/' ) ,
370- SQLPAGE_REDIRECT_URI
371- ) ;
372- let logout_uri = format ! (
373- "{}{}" ,
374- oidc_state. config. site_prefix. trim_end_matches( '/' ) ,
375- SQLPAGE_LOGOUT_URI
376- ) ;
377-
378- if request. path ( ) == redirect_uri {
401+ if request. path ( ) == oidc_state. config . redirect_uri {
379402 let response = handle_oidc_callback ( oidc_state, request) . await ;
380403 return MiddlewareResponse :: Respond ( response) ;
381404 }
382405
383- if request. path ( ) == logout_uri {
406+ if request. path ( ) == oidc_state . config . logout_uri {
384407 let response = handle_oidc_logout ( oidc_state, request) . await ;
385408 return MiddlewareResponse :: Respond ( response) ;
386409 }
@@ -507,11 +530,12 @@ async fn process_oidc_logout(
507530 . ok ( )
508531 . flatten ( ) ;
509532
510- let scheme = request. connection_info ( ) . scheme ( ) . to_string ( ) ;
511533 let mut response =
512534 if let Some ( end_session_endpoint) = oidc_state. get_end_session_endpoint ( ) . await {
513- let absolute_redirect_uri =
514- build_absolute_uri ( & oidc_state. config . app_host , & params. redirect_uri , & scheme) ?;
535+ let absolute_redirect_uri = oidc_state
536+ . build_absolute_redirect_uri ( & params. redirect_uri )
537+ . await ?;
538+
515539 let post_logout_redirect_uri =
516540 PostLogoutRedirectUrl :: new ( absolute_redirect_uri. clone ( ) ) . with_context ( || {
517541 format ! ( "Invalid post_logout_redirect_uri: {absolute_redirect_uri}" )
@@ -596,23 +620,6 @@ fn verify_logout_params(params: &LogoutParams, client_secret: &str) -> anyhow::R
596620 Ok ( ( ) )
597621}
598622
599- #[ must_use]
600- pub fn create_logout_url ( redirect_uri : & str , site_prefix : & str , client_secret : & str ) -> String {
601- let timestamp = chrono:: Utc :: now ( ) . timestamp ( ) ;
602- let signature = compute_logout_signature ( redirect_uri, timestamp, client_secret) ;
603- let query = form_urlencoded:: Serializer :: new ( String :: new ( ) )
604- . append_pair ( "redirect_uri" , redirect_uri)
605- . append_pair ( "timestamp" , & timestamp. to_string ( ) )
606- . append_pair ( "signature" , & signature)
607- . finish ( ) ;
608- format ! (
609- "{}{}?{}" ,
610- site_prefix. trim_end_matches( '/' ) ,
611- SQLPAGE_LOGOUT_URI ,
612- query
613- )
614- }
615-
616623impl < S > Service < ServiceRequest > for OidcService < S >
617624where
618625 S : Service < ServiceRequest , Response = ServiceResponse < BoxBody > , Error = Error > + ' static ,
@@ -654,7 +661,7 @@ async fn process_oidc_callback(
654661 redirect_target,
655662 } = parse_login_flow_state ( & tmp_login_flow_state_cookie) ?;
656663 let redirect_target =
657- validate_redirect_url ( redirect_target. to_string ( ) , & oidc_state. config . site_prefix ) ;
664+ validate_redirect_url ( redirect_target. to_string ( ) , & oidc_state. config . redirect_uri ) ;
658665
659666 log:: info!( "Redirecting to {redirect_target} after a successful login" ) ;
660667 let mut response = build_redirect_response ( redirect_target) ;
@@ -898,28 +905,25 @@ fn make_oidc_client(
898905 let client_id = openidconnect:: ClientId :: new ( config. client_id . clone ( ) ) ;
899906 let client_secret = openidconnect:: ClientSecret :: new ( config. client_secret . clone ( ) ) ;
900907
901- let redirect_path = format ! (
902- "{}{}" ,
903- config. site_prefix. trim_end_matches( '/' ) ,
904- SQLPAGE_REDIRECT_URI
905- ) ;
906- let mut redirect_url =
907- RedirectUrl :: new ( format ! ( "https://{}{}" , config. app_host, redirect_path, ) ) . with_context (
908- || {
909- format ! (
910- "Failed to build the redirect URL; invalid app host \" {}\" " ,
911- config. app_host
912- )
913- } ,
914- ) ?;
908+ let mut redirect_url = RedirectUrl :: new ( format ! (
909+ "https://{}{}" ,
910+ config. app_host, config. redirect_uri,
911+ ) )
912+ . with_context ( || {
913+ format ! (
914+ "Failed to build the redirect URL; invalid app host \" {}\" " ,
915+ config. app_host
916+ )
917+ } ) ?;
915918 let needs_http = match redirect_url. url ( ) . host ( ) {
916919 Some ( openidconnect:: url:: Host :: Domain ( domain) ) => domain == "localhost" ,
917920 Some ( openidconnect:: url:: Host :: Ipv4 ( _) | openidconnect:: url:: Host :: Ipv6 ( _) ) => true ,
918921 None => false ,
919922 } ;
920923 if needs_http {
921924 log:: debug!( "App host seems to be local, changing redirect URL to HTTP" ) ;
922- redirect_url = RedirectUrl :: new ( format ! ( "http://{}{}" , config. app_host, redirect_path, ) ) ?;
925+ redirect_url =
926+ RedirectUrl :: new ( format ! ( "http://{}{}" , config. app_host, config. redirect_uri, ) ) ?;
923927 }
924928 log:: info!( "OIDC redirect URL for {}: {redirect_url}" , config. client_id) ;
925929 let client =
@@ -1092,13 +1096,8 @@ impl AudienceVerifier {
10921096}
10931097
10941098/// Validate that a redirect URL is safe to use (prevents open redirect attacks)
1095- fn validate_redirect_url ( url : String , site_prefix : & str ) -> String {
1096- let redirect_uri = format ! (
1097- "{}{}" ,
1098- site_prefix. trim_end_matches( '/' ) ,
1099- SQLPAGE_REDIRECT_URI
1100- ) ;
1101- if url. starts_with ( '/' ) && !url. starts_with ( "//" ) && !url. starts_with ( & redirect_uri) {
1099+ fn validate_redirect_url ( url : String , redirect_uri : & str ) -> String {
1100+ if url. starts_with ( '/' ) && !url. starts_with ( "//" ) && !url. starts_with ( redirect_uri) {
11021101 return url;
11031102 }
11041103 log:: warn!( "Refusing to redirect to {url}" ) ;
@@ -1142,7 +1141,20 @@ mod tests {
11421141 #[ test]
11431142 fn logout_url_generation_and_parsing_are_compatible ( ) {
11441143 let secret = "super_secret_key" ;
1145- let generated = create_logout_url ( "/after" , "https://example.com" , secret) ;
1144+ let config = OidcConfig {
1145+ issuer_url : IssuerUrl :: new ( "https://example.com" . to_string ( ) ) . unwrap ( ) ,
1146+ client_id : "test_client" . to_string ( ) ,
1147+ client_secret : secret. to_string ( ) ,
1148+ protected_paths : vec ! [ ] ,
1149+ public_paths : vec ! [ ] ,
1150+ app_host : "example.com" . to_string ( ) ,
1151+ scopes : vec ! [ ] ,
1152+ additional_audience_verifier : AudienceVerifier :: new ( None ) ,
1153+ site_prefix : "https://example.com" . to_string ( ) ,
1154+ redirect_uri : format ! ( "https://example.com{SQLPAGE_REDIRECT_URI}" ) ,
1155+ logout_uri : format ! ( "https://example.com{SQLPAGE_LOGOUT_URI}" ) ,
1156+ } ;
1157+ let generated = config. create_logout_url ( "/after" ) ;
11461158
11471159 let parsed = Url :: parse ( & generated) . expect ( "generated URL should be valid" ) ;
11481160 assert_eq ! ( parsed. path( ) , SQLPAGE_LOGOUT_URI ) ;
0 commit comments