Skip to content

Commit 8e1e912

Browse files
committed
add end_session_endpoint config
1 parent 7258e95 commit 8e1e912

24 files changed

+192
-87
lines changed

crates/cli/src/sync.rs

+1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ pub async fn config_sync(
293293
userinfo_signed_response_alg: provider.userinfo_signed_response_alg,
294294
response_mode,
295295
allow_rp_initiated_logout: provider.allow_rp_initiated_logout,
296+
end_session_endpoint_override: provider.end_session_endpoint,
296297
additional_authorization_parameters: provider
297298
.additional_authorization_parameters
298299
.into_iter()

crates/config/src/sections/upstream_oauth2.rs

+6
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,12 @@ pub struct Provider {
542542
#[serde(default)]
543543
pub allow_rp_initiated_logout: bool,
544544

545+
/// The URL to use when ending a session onto the upstream provider
546+
///
547+
/// Defaults to the `end_session_endpoint` provided through discovery
548+
#[serde(skip_serializing_if = "Option::is_none")]
549+
pub end_session_endpoint: Option<Url>,
550+
545551
/// Additional parameters to include in the authorization request
546552
///
547553
/// Orders of the keys are not preserved.

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

+1
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ pub struct UpstreamOAuthProvider {
241241
pub disabled_at: Option<DateTime<Utc>>,
242242
pub claims_imports: ClaimsImports,
243243
pub allow_rp_initiated_logout: bool,
244+
pub end_session_endpoint_override: Option<Url>,
244245
pub additional_authorization_parameters: Vec<(String, String)>,
245246
}
246247

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

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ mod test_utils {
4747
userinfo_endpoint_override: None,
4848
jwks_uri_override: None,
4949
allow_rp_initiated_logout: false,
50+
end_session_endpoint_override: None,
5051
additional_authorization_parameters: Vec::new(),
5152
ui_order: 0,
5253
}

crates/handlers/src/upstream_oauth2/cache.rs

+13
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ impl<'a> LazyProviderInfos<'a> {
121121
Ok(self.load().await?.userinfo_endpoint())
122122
}
123123

124+
/// Get the end session endpoint for the provider.
125+
///
126+
/// Uses [`UpstreamOAuthProvider.end_session_endpoint_override`] if set,
127+
/// otherwise uses the one from discovery.
128+
pub async fn end_session_endpoint(&mut self) -> Result<&Url, DiscoveryError> {
129+
if let Some(end_session_endpoint) = &self.provider.end_session_endpoint_override {
130+
return Ok(end_session_endpoint);
131+
}
132+
133+
Ok(self.load().await?.end_session_endpoint())
134+
}
135+
124136
/// Get the PKCE methods supported by the provider.
125137
///
126138
/// If the mode is set to auto, it will use the ones from discovery,
@@ -423,6 +435,7 @@ mod tests {
423435
disabled_at: None,
424436
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
425437
allow_rp_initiated_logout: false,
438+
end_session_endpoint_override: None,
426439
additional_authorization_parameters: Vec::new(),
427440
};
428441

crates/handlers/src/upstream_oauth2/link.rs

+1
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ mod tests {
976976
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
977977
response_mode: None,
978978
allow_rp_initiated_logout: false,
979+
end_session_endpoint_override: None,
979980
additional_authorization_parameters: Vec::new(),
980981
ui_order: 0,
981982
},

crates/handlers/src/upstream_oauth2/logout.rs

+25-26
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use mas_storage::{RepositoryAccess, upstream_oauth2::UpstreamOAuthProviderReposi
99
use serde::{Deserialize, Serialize};
1010
use thiserror::Error;
1111
use tracing::error;
12-
use url::Url;
1312

14-
use crate::impl_from_error_for_route;
13+
use super::cache::LazyProviderInfos;
14+
use crate::{MetadataCache, impl_from_error_for_route};
1515

1616
#[derive(Serialize, Deserialize)]
1717
struct LogoutToken {
@@ -40,6 +40,7 @@ pub enum RouteError {
4040
}
4141

4242
impl_from_error_for_route!(mas_storage::RepositoryError);
43+
impl_from_error_for_route!(mas_oidc_client::error::DiscoveryError);
4344

4445
impl From<reqwest::Error> for RouteError {
4546
fn from(err: reqwest::Error) -> Self {
@@ -67,14 +68,15 @@ impl From<reqwest::Error> for RouteError {
6768
/// Returns a `RouteError` if there's an issue accessing the repository
6869
pub async fn get_rp_initiated_logout_endpoints<E>(
6970
url_builder: &UrlBuilder,
71+
metadata_cache: &MetadataCache,
72+
client: &reqwest::Client,
7073
repo: &mut impl RepositoryAccess<Error = E>,
7174
browser_session: &BrowserSession,
7275
) -> Result<UpstreamLogoutInfo, RouteError>
7376
where
7477
RouteError: std::convert::From<E>,
7578
{
7679
let mut result: UpstreamLogoutInfo = UpstreamLogoutInfo::default();
77-
// Set the post-logout redirect URI to our app's logout completion page
7880
let post_logout_redirect_uri = url_builder
7981
.absolute_url_for(&mas_router::Login::default())
8082
.to_string();
@@ -93,42 +95,39 @@ where
9395
})?
9496
.ok_or(RouteError::SessionNotFound)?;
9597

96-
// Get the session and assign its value, wrapped in Some
9798
let upstream_session = repo
9899
.upstream_oauth_session()
99100
.lookup(upstream_oauth2_session_id)
100101
.await?
101102
.ok_or(RouteError::SessionNotFound)?;
102-
// Get the provider and assign its value, wrapped in Some
103+
103104
let provider = repo
104105
.upstream_oauth_provider()
105106
.lookup(upstream_session.provider_id)
106107
.await?
107108
.filter(|provider| provider.allow_rp_initiated_logout)
108109
.ok_or(RouteError::ProviderNotFound)?;
109110

110-
// Look for end session endpoint
111-
// In a real implementation, we'd have end_session_endpoint fields in the
112-
// provider For now, we'll try to construct one from the issuer if
113-
// available
114-
if let Some(issuer) = &provider.issuer {
115-
let end_session_endpoint = format!("{issuer}/protocol/openid-connect/logout");
116-
let mut logout_url = end_session_endpoint;
117-
// Add post_logout_redirect_uri
118-
if let Some(post_uri) = &result.post_logout_redirect_uri {
119-
if let Ok(mut url) = Url::parse(&logout_url) {
120-
url.query_pairs_mut()
121-
.append_pair("post_logout_redirect_uri", post_uri);
122-
url.query_pairs_mut()
123-
.append_pair("client_id", &provider.client_id);
124-
// Add id_token_hint if available
125-
if let Some(id_token) = upstream_session.id_token() {
126-
url.query_pairs_mut().append_pair("id_token_hint", id_token);
127-
}
128-
logout_url = url.to_string();
129-
}
111+
// Add post_logout_redirect_uri
112+
if let Some(post_uri) = &result.post_logout_redirect_uri {
113+
let mut lazy_metadata = LazyProviderInfos::new(metadata_cache, &provider, client);
114+
let mut end_session_url = lazy_metadata.end_session_endpoint().await?.clone();
115+
end_session_url
116+
.query_pairs_mut()
117+
.append_pair("post_logout_redirect_uri", post_uri);
118+
end_session_url
119+
.query_pairs_mut()
120+
.append_pair("client_id", &provider.client_id);
121+
// Add id_token_hint if available
122+
if let Some(id_token) = upstream_session.id_token() {
123+
end_session_url
124+
.query_pairs_mut()
125+
.append_pair("id_token_hint", id_token);
130126
}
131-
result.logout_endpoints.clone_from(&logout_url);
127+
result
128+
.logout_endpoints
129+
.clone_from(&end_session_url.to_string());
132130
}
131+
133132
Ok(result)
134133
}

crates/handlers/src/views/login.rs

+2
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ mod test {
495495
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
496496
response_mode: None,
497497
allow_rp_initiated_logout: false,
498+
end_session_endpoint_override: None,
498499
additional_authorization_parameters: Vec::new(),
499500
ui_order: 0,
500501
},
@@ -537,6 +538,7 @@ mod test {
537538
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,
538539
response_mode: None,
539540
allow_rp_initiated_logout: false,
541+
end_session_endpoint_override: None,
540542
additional_authorization_parameters: Vec::new(),
541543
ui_order: 1,
542544
},

crates/handlers/src/views/logout.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ use mas_router::{PostAuthAction, UrlBuilder};
1717
use mas_storage::{BoxClock, BoxRepository, user::BrowserSessionRepository};
1818
use tracing::warn;
1919

20-
use crate::{BoundActivityTracker, upstream_oauth2::logout::get_rp_initiated_logout_endpoints};
20+
use crate::{
21+
BoundActivityTracker, MetadataCache, upstream_oauth2::logout::get_rp_initiated_logout_endpoints,
22+
};
2123

2224
#[tracing::instrument(name = "handlers.views.logout.post", skip_all, err)]
2325
pub(crate) async fn post(
2426
clock: BoxClock,
2527
mut repo: BoxRepository,
2628
cookie_jar: CookieJar,
2729
State(url_builder): State<UrlBuilder>,
30+
State(metadata_cache): State<MetadataCache>,
31+
State(client): State<reqwest::Client>,
2832
activity_tracker: BoundActivityTracker,
2933
Form(form): Form<ProtectedForm<Option<PostAuthAction>>>,
3034
) -> Result<impl IntoResponse, FancyError> {
@@ -43,9 +47,15 @@ pub(crate) async fn post(
4347

4448
// First, get RP-initiated logout endpoints before actually finishing the
4549
// session
46-
// match get_rp_initiated_logout_endpoints(&url_builder, &mut repo,
47-
// &cookie_jar).await
48-
match get_rp_initiated_logout_endpoints(&url_builder, &mut repo, &session).await {
50+
match get_rp_initiated_logout_endpoints(
51+
&url_builder,
52+
&metadata_cache,
53+
&client,
54+
&mut repo,
55+
&session,
56+
)
57+
.await
58+
{
4959
Ok(logout_info) => {
5060
// If we have any RP-initiated logout endpoints, use the first one
5161
if !logout_info.logout_endpoints.is_empty() {

crates/oauth2-types/src/oidc.rs

+9
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,15 @@ impl VerifiedProviderMetadata {
968968
}
969969
}
970970

971+
/// URL of the authorization server's end session endpoint.
972+
#[must_use]
973+
pub fn end_session_endpoint(&self) -> &Url {
974+
match &self.end_session_endpoint {
975+
Some(u) => u,
976+
None => unreachable!(),
977+
}
978+
}
979+
971980
/// URL of the authorization server's JWK Set document.
972981
#[must_use]
973982
pub fn jwks_uri(&self) -> &Url {

crates/storage-pg/.sqlx/query-7c8fb255bd0d4f29bfdfc17f382ad1b4f2782498ed8f9582c8f28d94c42b3a95.json renamed to crates/storage-pg/.sqlx/query-0a5edf3c6ef2c493b605f537a7b5f8afd0e2a75125197f157aab6cb10a8b3faa.json

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

crates/storage-pg/.sqlx/query-f616051f5e7bdfb5342c18fffe2919706b00ac4bdfd7a41a08b578b50af3adc8.json renamed to crates/storage-pg/.sqlx/query-0dc81401c212d7cf99b50a7de2e276cc3308847141a37d1856d7b007c03863e5.json

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

crates/storage-pg/.sqlx/query-28207621a6b974e8694e687cc7bc642ae6683956b373bc7de3ac5b67e158b623.json

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

0 commit comments

Comments
 (0)