fractal/utils/
oidc.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Helper methods for OIDC-aware compatibility, to avoid pulling in all the
//! dependencies of the `experimental-oidc` SDK feature only to use a few
//! methods.

use std::error::Error;

use matrix_sdk::Client;
use ruma::{api::client::discovery::get_authentication_issuer, OwnedDeviceId};
use serde::Deserialize;
use tracing::{debug, warn};
use url::Url;

/// Get the URL of the OIDC [authentication issuer] for the current homeserver
/// of the given Matrix client.
///
/// [authentication issuer]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
pub(crate) async fn fetch_auth_issuer(client: &Client) -> Option<Url> {
    let res = client
        .send(get_authentication_issuer::msc2965::Request::new())
        .await;

    if let Err(error) = &res {
        debug!("Could not fetch authentication issuer: {error:?}");
    }

    let issuer = res.ok()?.issuer;

    match issuer.parse() {
        Ok(url) => Some(url),
        Err(error) => {
            warn!("Could not parse authentication issuer `{issuer}` as a URL: {error}");
            None
        }
    }
}

/// Part of an OIDC provider metadata.
#[derive(Debug, Clone, Deserialize)]
struct ProviderMetadata {
    account_management_uri: Url,
}

/// Get the [account management URL] of the given authentication issuer with the
/// given Matrix client.
///
/// [account management URL]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
pub(crate) async fn discover_account_management_url(
    client: &Client,
    issuer: Url,
) -> Result<Url, Box<dyn Error + Send + Sync>> {
    let mut config_url = issuer;
    // If the path does not end with a slash, the last segment is removed when
    // using `join`.
    if !config_url.path().ends_with('/') {
        let mut path = config_url.path().to_owned();
        path.push('/');
        config_url.set_path(&path);
    }

    let config_url = config_url.join(".well-known/openid-configuration")?;

    let http_client = client.http_client();
    let body = http_client
        .get(config_url)
        .send()
        .await?
        .error_for_status()?
        .bytes()
        .await?;

    let metadata = serde_json::from_slice::<ProviderMetadata>(&body)?;
    Ok(metadata.account_management_uri)
}

/// The possible [account management] actions.
///
/// [account management]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
#[derive(Debug, Clone)]
pub(crate) enum AccountManagementAction {
    /// View the user profile.
    Profile,
    /// Log out the session with the given device ID.
    SessionEnd { device_id: OwnedDeviceId },
    /// Deactivate the account.
    AccountDeactivate,
}

impl AccountManagementAction {
    /// The serialized action name.
    fn action_name(&self) -> &str {
        match self {
            Self::Profile => "org.matrix.profile",
            Self::SessionEnd { .. } => "org.matrix.session_end",
            Self::AccountDeactivate => "org.matrix.account_deactivate",
        }
    }

    /// Extra query field as a `(name, value)` tuple to add for this action.
    fn extra_data(&self) -> Option<(&str, &str)> {
        match self {
            Self::SessionEnd { device_id } => Some(("device_id", device_id.as_str())),
            _ => None,
        }
    }

    /// Add the given action to the given account management url
    pub(crate) fn add_to_account_management_url(&self, url: &mut Url) {
        let mut query_pairs = url.query_pairs_mut();
        query_pairs.append_pair("action", self.action_name());

        if let Some((name, value)) = self.extra_data() {
            query_pairs.append_pair(name, value);
        }
    }
}