|  | 
|  | 1 | +// Copyright 2025 New Vector Ltd. | 
|  | 2 | +// | 
|  | 3 | +// SPDX-License-Identifier: AGPL-3.0-only | 
|  | 4 | +// Please see LICENSE in the repository root for full details. | 
|  | 5 | + | 
|  | 6 | +use std::collections::HashMap; | 
|  | 7 | + | 
|  | 8 | + | 
|  | 9 | +use mas_axum_utils::cookies::CookieJar; | 
|  | 10 | +use mas_router::UrlBuilder; | 
|  | 11 | +use mas_storage::{ | 
|  | 12 | +    upstream_oauth2::UpstreamOAuthProviderRepository, RepositoryAccess | 
|  | 13 | +}; | 
|  | 14 | +use serde::{Deserialize, Serialize}; | 
|  | 15 | +use tracing::{info, warn, error}; | 
|  | 16 | +use url::Url; | 
|  | 17 | +use crate::{impl_from_error_for_route}; | 
|  | 18 | +use thiserror::Error; | 
|  | 19 | + | 
|  | 20 | +use super::UpstreamSessionsCookie; | 
|  | 21 | + | 
|  | 22 | +/// Back-channel logout token | 
|  | 23 | +///  | 
|  | 24 | +/// See https://openid.net/specs/openid-connect-backchannel-1_0.html#BCTokens | 
|  | 25 | +#[derive(Debug, Serialize, Deserialize)] | 
|  | 26 | +pub struct BackChannelLogoutToken { | 
|  | 27 | +    /// Issuer Identifier | 
|  | 28 | +    pub iss: String, | 
|  | 29 | +     | 
|  | 30 | +    /// Subject Identifier | 
|  | 31 | +    pub sub: String, | 
|  | 32 | +     | 
|  | 33 | +    /// Session ID (if available) | 
|  | 34 | +    #[serde(skip_serializing_if = "Option::is_none")] | 
|  | 35 | +    pub sid: Option<String>, | 
|  | 36 | +     | 
|  | 37 | +    /// Audience | 
|  | 38 | +    pub aud: String, | 
|  | 39 | +     | 
|  | 40 | +    /// Issued At | 
|  | 41 | +    pub iat: i64, | 
|  | 42 | +     | 
|  | 43 | +    /// JWT ID | 
|  | 44 | +    pub jti: String, | 
|  | 45 | +     | 
|  | 46 | +    /// Expiration Time | 
|  | 47 | +    pub exp: i64, | 
|  | 48 | +     | 
|  | 49 | +    /// Events claim - according to the spec, it must contain the "http://schemas.openid.net/event/backchannel-logout" key | 
|  | 50 | +    pub events: HashMap<String, serde_json::Value>, | 
|  | 51 | +} | 
|  | 52 | + | 
|  | 53 | +#[derive(Serialize, Deserialize)] | 
|  | 54 | +struct LogoutToken { | 
|  | 55 | +    logout_token: String, | 
|  | 56 | +} | 
|  | 57 | + | 
|  | 58 | +/// Structure to collect upstream RP-initiated logout endpoints for a user | 
|  | 59 | +#[derive(Debug, Default)] | 
|  | 60 | +pub struct UpstreamLogoutInfo { | 
|  | 61 | +    /// Collection of logout endpoints that the user needs to be redirected to | 
|  | 62 | +    pub logout_endpoints: Vec<String>, | 
|  | 63 | +     | 
|  | 64 | +    /// Optional post-logout redirect URI to come back to our app | 
|  | 65 | +    pub post_logout_redirect_uri: Option<String>, | 
|  | 66 | +} | 
|  | 67 | + | 
|  | 68 | +#[derive(Debug, Error)] | 
|  | 69 | +pub enum RouteError { | 
|  | 70 | +    #[error(transparent)] | 
|  | 71 | +    Internal(Box<dyn std::error::Error + Send + Sync + 'static>), | 
|  | 72 | + | 
|  | 73 | +    #[error("provider was not found")] | 
|  | 74 | +    ProviderNotFound, | 
|  | 75 | + | 
|  | 76 | +    #[error("session was not found")] | 
|  | 77 | +    SessionNotFound, | 
|  | 78 | +} | 
|  | 79 | + | 
|  | 80 | +impl_from_error_for_route!(mas_storage::RepositoryError); | 
|  | 81 | + | 
|  | 82 | +impl From<reqwest::Error> for RouteError { | 
|  | 83 | +    fn from(err: reqwest::Error) -> Self { | 
|  | 84 | +        Self::Internal(Box::new(err)) | 
|  | 85 | +    } | 
|  | 86 | +} | 
|  | 87 | + | 
|  | 88 | +/// Get RP-initiated logout URLs for a user's upstream providers | 
|  | 89 | +/// | 
|  | 90 | +/// This retrieves logout endpoints from all connected upstream providers that | 
|  | 91 | +/// support RP-initiated logout. | 
|  | 92 | +/// | 
|  | 93 | +/// # Parameters | 
|  | 94 | +/// | 
|  | 95 | +/// * `repo`: The repository to use | 
|  | 96 | +/// * `url_builder`: URL builder for constructing redirect URIs | 
|  | 97 | +/// * `session`: The browser session to log out | 
|  | 98 | +/// * `grant_id`: Optional grant ID to use for generating id_token_hint | 
|  | 99 | +///  | 
|  | 100 | +/// # Returns | 
|  | 101 | +/// | 
|  | 102 | +/// Information about upstream logout endpoints the user should be redirected to | 
|  | 103 | +/// | 
|  | 104 | +/// # Errors | 
|  | 105 | +/// | 
|  | 106 | +/// Returns a RouteError if there's an issue accessing the repository | 
|  | 107 | +pub async fn get_rp_initiated_logout_endpoints<E>( | 
|  | 108 | +    url_builder: &UrlBuilder, | 
|  | 109 | +    repo: &mut impl RepositoryAccess<Error = E>, | 
|  | 110 | +    cookie_jar: &CookieJar, | 
|  | 111 | +) -> Result<UpstreamLogoutInfo, RouteError> where RouteError: std::convert::From<E> | 
|  | 112 | +{ | 
|  | 113 | +    let mut result = UpstreamLogoutInfo::default(); | 
|  | 114 | +     | 
|  | 115 | +    // Set the post-logout redirect URI to our app's logout completion page | 
|  | 116 | +    let post_logout_redirect_uri = url_builder | 
|  | 117 | +        .absolute_url_for(&mas_router::Login::default()) | 
|  | 118 | +        .to_string(); | 
|  | 119 | +    result.post_logout_redirect_uri = Some(post_logout_redirect_uri.clone()); | 
|  | 120 | + | 
|  | 121 | +    let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); | 
|  | 122 | +    warn!("sessions_cookie: {:?}", sessions_cookie); | 
|  | 123 | +     | 
|  | 124 | +    // Standard location for OIDC end session endpoint | 
|  | 125 | +    for upstream_session_id in sessions_cookie.session_ids() { | 
|  | 126 | +        let upstream_session = repo | 
|  | 127 | +            .upstream_oauth_session() | 
|  | 128 | +            .lookup(upstream_session_id) | 
|  | 129 | +            .await? | 
|  | 130 | +            .ok_or(RouteError::SessionNotFound)?; | 
|  | 131 | + | 
|  | 132 | +        let provider = repo.upstream_oauth_provider() | 
|  | 133 | +            .lookup(upstream_session.provider_id) | 
|  | 134 | +            .await? | 
|  | 135 | +            .ok_or(RouteError::ProviderNotFound)?; | 
|  | 136 | + | 
|  | 137 | +        // Look for end session endpoint | 
|  | 138 | +        // In a real implementation, we'd have end_session_endpoint fields in the provider | 
|  | 139 | +        // For now, we'll try to construct one from the issuer if available | 
|  | 140 | +        if let Some(issuer) = &provider.issuer { | 
|  | 141 | +            let end_session_endpoint = format!("{}/protocol/openid-connect/logout", issuer); | 
|  | 142 | +            let mut logout_url = end_session_endpoint; | 
|  | 143 | +             | 
|  | 144 | +            // Add post_logout_redirect_uri | 
|  | 145 | +            if let Some(post_uri) = &result.post_logout_redirect_uri { | 
|  | 146 | +                if let Ok(mut url) = Url::parse(&logout_url) { | 
|  | 147 | +                    url.query_pairs_mut() | 
|  | 148 | +                        .append_pair("post_logout_redirect_uri", post_uri); | 
|  | 149 | +                    url.query_pairs_mut() | 
|  | 150 | +                        .append_pair("client_id", &provider.client_id); | 
|  | 151 | +                 | 
|  | 152 | +                    // Add id_token_hint if available | 
|  | 153 | +                    if upstream_session.id_token().is_some(){ | 
|  | 154 | +                        url.query_pairs_mut() | 
|  | 155 | +                            .append_pair("id_token_hint", upstream_session.id_token().unwrap()); | 
|  | 156 | +                    } | 
|  | 157 | +                    logout_url = url.to_string(); | 
|  | 158 | +                } | 
|  | 159 | +            } | 
|  | 160 | +             | 
|  | 161 | +            info!( | 
|  | 162 | +                upstream_oauth_provider.id = %provider.id, | 
|  | 163 | +                logout_url = %logout_url, | 
|  | 164 | +                "Adding RP-initiated logout URL based on issuer" | 
|  | 165 | +            ); | 
|  | 166 | +             | 
|  | 167 | +            result.logout_endpoints.push(logout_url); | 
|  | 168 | +        } else { | 
|  | 169 | +            info!( | 
|  | 170 | +                upstream_oauth_provider.id = %provider.id, | 
|  | 171 | +                "Provider has no issuer defined, cannot construct RP-initiated logout URL" | 
|  | 172 | +            ); | 
|  | 173 | +        }     | 
|  | 174 | +         | 
|  | 175 | +    } | 
|  | 176 | +     | 
|  | 177 | +    Ok(result) | 
|  | 178 | +} | 
0 commit comments