matrix_sdk/authentication/oauth/
auth_code_builder.rs

1// Copyright 2022 Kévin Commaille
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::borrow::Cow;
16
17use oauth2::{
18    basic::BasicClient as OAuthClient, AuthUrl, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope,
19};
20use ruma::{
21    api::client::discovery::get_authorization_server_metadata::msc2965::Prompt, OwnedDeviceId,
22    UserId,
23};
24use tracing::{info, instrument};
25use url::Url;
26
27use super::{ClientRegistrationMethod, OAuth, OAuthError};
28use crate::{authentication::oauth::AuthorizationValidationData, Result};
29
30/// Builder type used to configure optional settings for authorization with an
31/// OAuth 2.0 authorization server via the Authorization Code flow.
32///
33/// Created with [`OAuth::login()`]. Finalized with [`Self::build()`].
34#[allow(missing_debug_implementations)]
35pub struct OAuthAuthCodeUrlBuilder {
36    oauth: OAuth,
37    registration_method: ClientRegistrationMethod,
38    scopes: Vec<Scope>,
39    device_id: OwnedDeviceId,
40    redirect_uri: Url,
41    prompt: Option<Vec<Prompt>>,
42    login_hint: Option<String>,
43}
44
45impl OAuthAuthCodeUrlBuilder {
46    pub(super) fn new(
47        oauth: OAuth,
48        registration_method: ClientRegistrationMethod,
49        scopes: Vec<Scope>,
50        device_id: OwnedDeviceId,
51        redirect_uri: Url,
52    ) -> Self {
53        Self {
54            oauth,
55            registration_method,
56            scopes,
57            device_id,
58            redirect_uri,
59            prompt: None,
60            login_hint: None,
61        }
62    }
63
64    /// Set the [`Prompt`] of the authorization URL.
65    ///
66    /// If this is not set, it is assumed that the user wants to log into an
67    /// existing account.
68    ///
69    /// [`Prompt::Create`] can be used to signify that the user wants to
70    /// register a new account.
71    pub fn prompt(mut self, prompt: Vec<Prompt>) -> Self {
72        self.prompt = Some(prompt);
73        self
74    }
75
76    /// Set the hint to the Authorization Server about the Matrix user ID the
77    /// End-User might use to log in, as defined in [MSC4198].
78    ///
79    /// [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
80    pub fn user_id_hint(mut self, user_id: &UserId) -> Self {
81        self.login_hint = Some(format!("mxid:{user_id}"));
82        self
83    }
84
85    /// Get the URL that should be presented to login via the Authorization Code
86    /// flow.
87    ///
88    /// This URL should be presented to the user and once they are redirected to
89    /// the `redirect_uri`, the login can be completed by calling
90    /// [`OAuth::finish_login()`].
91    ///
92    /// Returns an error if the client registration was not restored, or if a
93    /// request fails.
94    #[instrument(target = "matrix_sdk::client", skip_all)]
95    pub async fn build(self) -> Result<OAuthAuthorizationData, OAuthError> {
96        let Self {
97            oauth,
98            registration_method,
99            scopes,
100            device_id,
101            redirect_uri,
102            prompt,
103            login_hint,
104        } = self;
105
106        let server_metadata = oauth.server_metadata().await?;
107
108        oauth.use_registration_method(&server_metadata, &registration_method).await?;
109
110        let data = oauth.data().expect("OAuth 2.0 data should be set after registration");
111        info!(
112            issuer = data.issuer.as_str(),
113            ?scopes,
114            "Authorizing scope via the OAuth 2.0 Authorization Code flow"
115        );
116
117        let auth_url = AuthUrl::from_url(server_metadata.authorization_endpoint.clone());
118
119        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
120        let redirect_uri = RedirectUrl::from_url(redirect_uri);
121
122        let client = OAuthClient::new(data.client_id.clone()).set_auth_uri(auth_url);
123        let mut request = client
124            .authorize_url(CsrfToken::new_random)
125            .add_scopes(scopes)
126            .set_pkce_challenge(pkce_challenge)
127            .set_redirect_uri(Cow::Borrowed(&redirect_uri));
128
129        if let Some(prompt) = prompt {
130            // This should be a list of space separated values.
131            let prompt_str = prompt.iter().map(Prompt::as_str).collect::<Vec<_>>().join(" ");
132            request = request.add_extra_param("prompt", prompt_str);
133        }
134
135        if let Some(login_hint) = login_hint {
136            request = request.add_extra_param("login_hint", login_hint);
137        }
138
139        let (url, state) = request.url();
140
141        data.authorization_data.lock().await.insert(
142            state.clone(),
143            AuthorizationValidationData { server_metadata, device_id, redirect_uri, pkce_verifier },
144        );
145
146        Ok(OAuthAuthorizationData { url, state })
147    }
148}
149
150/// The data needed to perform authorization using OAuth 2.0.
151#[derive(Debug, Clone)]
152#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
153pub struct OAuthAuthorizationData {
154    /// The URL that should be presented.
155    pub url: Url,
156    /// A unique identifier for the request, used to ensure the response
157    /// originated from the authentication issuer.
158    pub state: CsrfToken,
159}
160
161#[cfg(feature = "uniffi")]
162#[matrix_sdk_ffi_macros::export]
163impl OAuthAuthorizationData {
164    /// The login URL to use for authorization.
165    pub fn login_url(&self) -> String {
166        self.url.to_string()
167    }
168}