Skip to content

Commit a53c9b4

Browse files
committed
add allow_rp_initiated_logout config
1 parent 12d0a85 commit a53c9b4

27 files changed

+188
-120
lines changed

crates/cli/src/sync.rs

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ pub async fn config_sync(
292292
fetch_userinfo: provider.fetch_userinfo,
293293
userinfo_signed_response_alg: provider.userinfo_signed_response_alg,
294294
response_mode,
295+
allow_rp_initiated_logout: provider.allow_rp_initiated_logout,
295296
additional_authorization_parameters: provider
296297
.additional_authorization_parameters
297298
.into_iter()

crates/config/src/sections/upstream_oauth2.rs

+6
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,12 @@ pub struct Provider {
535535
#[serde(default, skip_serializing_if = "ClaimsImports::is_default")]
536536
pub claims_imports: ClaimsImports,
537537

538+
/// Whether to allow RP-initiated logout
539+
///
540+
/// Defaults to `false`.
541+
#[serde(default)]
542+
pub allow_rp_initiated_logout: bool,
543+
538544
/// Additional parameters to include in the authorization request
539545
///
540546
/// Orders of the keys are not preserved.

crates/data-model/src/upstream_oauth2/provider.rs

+1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ pub struct UpstreamOAuthProvider {
240240
pub created_at: DateTime<Utc>,
241241
pub disabled_at: Option<DateTime<Utc>>,
242242
pub claims_imports: ClaimsImports,
243+
pub allow_rp_initiated_logout: bool,
243244
pub additional_authorization_parameters: Vec<(String, String)>,
244245
}
245246

crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod test_utils {
4242
token_endpoint_override: None,
4343
userinfo_endpoint_override: None,
4444
jwks_uri_override: None,
45+
allow_rp_initiated_logout: false,
4546
additional_authorization_parameters: Vec::new(),
4647
ui_order: 0,
4748
}

crates/handlers/src/upstream_oauth2/cache.rs

+1
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ mod tests {
422422
created_at: clock.now(),
423423
disabled_at: None,
424424
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
425+
allow_rp_initiated_logout: false,
425426
additional_authorization_parameters: Vec::new(),
426427
};
427428

crates/handlers/src/upstream_oauth2/cookie.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ impl UpstreamSessions {
6767
}
6868
/// Returns the session IDs in the cookie
6969
pub fn session_ids(&self) -> Vec<Ulid> {
70-
self.0.iter()
71-
.map(|p| p.session)
72-
.collect()
70+
self.0.iter().map(|p| p.session).collect()
7371
}
7472

7573
/// Save the upstreams sessions to the cookie jar

crates/handlers/src/upstream_oauth2/link.rs

+1
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,7 @@ mod tests {
948948
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
949949
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
950950
response_mode: None,
951+
allow_rp_initiated_logout: false,
951952
additional_authorization_parameters: Vec::new(),
952953
ui_order: 0,
953954
},

crates/handlers/src/upstream_oauth2/logout.rs

+58-56
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
// SPDX-License-Identifier: AGPL-3.0-only
44
// Please see LICENSE in the repository root for full details.
55

6+
use crate::impl_from_error_for_route;
67
use mas_axum_utils::cookies::CookieJar;
78
use mas_router::UrlBuilder;
8-
use mas_storage::{
9-
upstream_oauth2::UpstreamOAuthProviderRepository, RepositoryAccess
10-
};
9+
use mas_storage::{RepositoryAccess, upstream_oauth2::UpstreamOAuthProviderRepository};
1110
use serde::{Deserialize, Serialize};
12-
use tracing::{info, error};
13-
use url::Url;
14-
use crate::impl_from_error_for_route;
1511
use thiserror::Error;
12+
use tracing::{error, warn};
13+
use url::Url;
1614

1715
use super::UpstreamSessionsCookie;
1816

@@ -26,7 +24,6 @@ struct LogoutToken {
2624
pub struct UpstreamLogoutInfo {
2725
/// Collection of logout endpoints that the user needs to be redirected to
2826
pub logout_endpoints: String,
29-
3027
/// Optional post-logout redirect URI to come back to our app
3128
pub post_logout_redirect_uri: Option<String>,
3229
}
@@ -62,7 +59,7 @@ impl From<reqwest::Error> for RouteError {
6259
/// * `url_builder`: URL builder for constructing redirect URIs
6360
/// * `session`: The browser session to log out
6461
/// * `grant_id`: Optional grant ID to use for generating id_token_hint
65-
///
62+
///
6663
/// # Returns
6764
///
6865
/// Information about upstream logout endpoints the user should be redirected to
@@ -74,73 +71,78 @@ pub async fn get_rp_initiated_logout_endpoints<E>(
7471
url_builder: &UrlBuilder,
7572
repo: &mut impl RepositoryAccess<Error = E>,
7673
cookie_jar: &CookieJar,
77-
) -> Result<UpstreamLogoutInfo, RouteError> where RouteError: std::convert::From<E>
74+
) -> Result<UpstreamLogoutInfo, RouteError>
75+
where
76+
RouteError: std::convert::From<E>,
7877
{
7978
let mut result: UpstreamLogoutInfo = UpstreamLogoutInfo::default();
80-
8179
// Set the post-logout redirect URI to our app's logout completion page
8280
let post_logout_redirect_uri = url_builder
8381
.absolute_url_for(&mas_router::Login::default())
8482
.to_string();
8583
result.post_logout_redirect_uri = Some(post_logout_redirect_uri.clone());
8684

8785
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
88-
8986
// Standard location for OIDC end session endpoint
9087
let session_ids = sessions_cookie.session_ids();
9188
if session_ids.is_empty() {
9289
return Ok(result);
93-
}
94-
// We only support the first upstrea session at a time for now
95-
let upstream_session_id = session_ids[0];
96-
let upstream_session = repo
97-
.upstream_oauth_session()
98-
.lookup(upstream_session_id)
99-
.await?
100-
.ok_or(RouteError::SessionNotFound)?;
90+
}
91+
// We only support the first upstream session
92+
let mut provider = None;
93+
let mut upstream_session = None;
94+
for session_id in session_ids {
95+
// Get the session and assign its value, wrapped in Some
96+
let session = repo
97+
.upstream_oauth_session()
98+
.lookup(session_id)
99+
.await?
100+
.ok_or(RouteError::SessionNotFound)?;
101+
// Get the provider and assign its value, wrapped in Some
102+
let prov = repo
103+
.upstream_oauth_provider()
104+
.lookup(session.provider_id)
105+
.await?
106+
.ok_or(RouteError::ProviderNotFound)?;
101107

102-
let provider = repo.upstream_oauth_provider()
103-
.lookup(upstream_session.provider_id)
104-
.await?
105-
.ok_or(RouteError::ProviderNotFound)?;
108+
if prov.allow_rp_initiated_logout {
109+
upstream_session = Some(session);
110+
provider = Some(prov);
111+
break;
112+
}
113+
}
106114

107-
// Look for end session endpoint
108-
// In a real implementation, we'd have end_session_endpoint fields in the provider
109-
// For now, we'll try to construct one from the issuer if available
110-
if let Some(issuer) = &provider.issuer {
111-
let end_session_endpoint = format!("{}/protocol/openid-connect/logout", issuer);
112-
let mut logout_url = end_session_endpoint;
113-
114-
// Add post_logout_redirect_uri
115-
if let Some(post_uri) = &result.post_logout_redirect_uri {
116-
if let Ok(mut url) = Url::parse(&logout_url) {
117-
url.query_pairs_mut()
118-
.append_pair("post_logout_redirect_uri", post_uri);
119-
url.query_pairs_mut()
120-
.append_pair("client_id", &provider.client_id);
121-
122-
// Add id_token_hint if available
123-
if upstream_session.id_token().is_some(){
115+
// Check if we found a provider with allow_rp_initiated_logout
116+
if let Some(provider) = provider {
117+
// Look for end session endpoint
118+
// In a real implementation, we'd have end_session_endpoint fields in the provider
119+
// For now, we'll try to construct one from the issuer if available
120+
if let Some(issuer) = &provider.issuer {
121+
let end_session_endpoint = format!("{}/protocol/openid-connect/logout", issuer);
122+
let mut logout_url = end_session_endpoint;
123+
// Add post_logout_redirect_uri
124+
if let Some(post_uri) = &result.post_logout_redirect_uri {
125+
if let Ok(mut url) = Url::parse(&logout_url) {
126+
url.query_pairs_mut()
127+
.append_pair("post_logout_redirect_uri", post_uri);
124128
url.query_pairs_mut()
125-
.append_pair("id_token_hint", upstream_session.id_token().unwrap());
129+
.append_pair("client_id", &provider.client_id);
130+
// Add id_token_hint if available
131+
if let Some(session) = &upstream_session {
132+
if let Some(id_token) = session.id_token() {
133+
url.query_pairs_mut().append_pair("id_token_hint", id_token);
134+
}
135+
}
136+
logout_url = url.to_string();
126137
}
127-
logout_url = url.to_string();
128138
}
139+
result.logout_endpoints = logout_url.clone();
140+
} else {
141+
warn!(
142+
upstream_oauth_provider.id = %provider.id,
143+
"Provider has no issuer defined, cannot construct RP-initiated logout URL"
144+
);
129145
}
130-
131-
info!(
132-
upstream_oauth_provider.id = %provider.id,
133-
logout_url = %logout_url,
134-
"Adding RP-initiated logout URL based on issuer"
135-
);
136-
137-
result.logout_endpoints = logout_url.clone();
138-
} else {
139-
info!(
140-
upstream_oauth_provider.id = %provider.id,
141-
"Provider has no issuer defined, cannot construct RP-initiated logout URL"
142-
);
143146
}
144-
145147
Ok(result)
146148
}

crates/handlers/src/upstream_oauth2/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ use url::Url;
1818
pub(crate) mod authorize;
1919
pub(crate) mod cache;
2020
pub(crate) mod callback;
21-
pub(crate) mod logout;
2221
mod cookie;
2322
pub(crate) mod link;
23+
pub(crate) mod logout;
2424
mod template;
2525

2626
use self::cookie::UpstreamSessions as UpstreamSessionsCookie;

crates/handlers/src/views/login.rs

+2
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ mod test {
452452
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
453453
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
454454
response_mode: None,
455+
allow_rp_initiated_logout: false,
455456
additional_authorization_parameters: Vec::new(),
456457
ui_order: 0,
457458
},
@@ -493,6 +494,7 @@ mod test {
493494
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
494495
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
495496
response_mode: None,
497+
allow_rp_initiated_logout: false,
496498
additional_authorization_parameters: Vec::new(),
497499
ui_order: 1,
498500
},

crates/handlers/src/views/logout.rs

+3-7
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,19 @@ pub(crate) async fn post(
4343
.await;
4444

4545
// First, get RP-initiated logout endpoints before actually finishing the session
46-
match get_rp_initiated_logout_endpoints(
47-
&url_builder,
48-
&mut repo,
49-
&cookie_jar,
50-
).await {
46+
match get_rp_initiated_logout_endpoints(&url_builder, &mut repo, &cookie_jar).await
47+
{
5148
Ok(logout_info) => {
5249
// If we have any RP-initiated logout endpoints, use the first one
5350
if !logout_info.logout_endpoints.is_empty() {
5451
upstream_logout_url = Some(logout_info.logout_endpoints.clone());
5552
}
56-
},
53+
}
5754
Err(e) => {
5855
warn!("Failed to get RP-initiated logout endpoints: {}", e);
5956
// Continue with logout even if endpoint retrieval fails
6057
}
6158
}
62-
6359
// Now finish the session
6460
repo.browser_session().finish(&clock, session).await?;
6561
}

crates/storage-pg/.sqlx/query-72de26d5e3c56f4b0658685a95b45b647bb6637e55b662a5a548aa3308c62a8a.json

-44
This file was deleted.

crates/storage-pg/.sqlx/query-e25af41189846e26da99e5d8a1462eab5efe330f60ef8c6c813c747424ba7ec9.json crates/storage-pg/.sqlx/query-7c8fb255bd0d4f29bfdfc17f382ad1b4f2782498ed8f9582c8f28d94c42b3a95.json

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)