matrix_sdk/authentication/oidc/
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::{api::client::discovery::get_authorization_server_metadata::msc2965::Prompt, UserId};
21use tracing::{info, instrument};
22use url::Url;
23
24use super::{Oidc, OidcError};
25use crate::{authentication::oidc::AuthorizationValidationData, Result};
26
27/// Builder type used to configure optional settings for authorization with an
28/// OpenID Connect Provider via the Authorization Code flow.
29///
30/// Created with [`Oidc::login()`]. Finalized with [`Self::build()`].
31#[allow(missing_debug_implementations)]
32pub struct OidcAuthCodeUrlBuilder {
33    oidc: Oidc,
34    scopes: Vec<Scope>,
35    redirect_uri: Url,
36    prompt: Option<Vec<Prompt>>,
37    login_hint: Option<String>,
38}
39
40impl OidcAuthCodeUrlBuilder {
41    pub(super) fn new(oidc: Oidc, scopes: Vec<Scope>, redirect_uri: Url) -> Self {
42        Self { oidc, scopes, redirect_uri, prompt: None, login_hint: None }
43    }
44
45    /// Set the [`Prompt`] of the authorization URL.
46    ///
47    /// If this is not set, it is assumed that the user wants to log into an
48    /// existing account.
49    ///
50    /// [`Prompt::Create`] can be used to signify that the user wants to
51    /// register a new account.
52    pub fn prompt(mut self, prompt: Vec<Prompt>) -> Self {
53        self.prompt = Some(prompt);
54        self
55    }
56
57    /// Set the hint to the Authorization Server about the Matrix user ID the
58    /// End-User might use to log in, as defined in [MSC4198].
59    ///
60    /// [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
61    pub fn user_id_hint(mut self, user_id: &UserId) -> Self {
62        self.login_hint = Some(format!("mxid:{user_id}"));
63        self
64    }
65
66    /// Get the URL that should be presented to login via the Authorization Code
67    /// flow.
68    ///
69    /// This URL should be presented to the user and once they are redirected to
70    /// the `redirect_uri`, the authorization can be completed by calling
71    /// [`Oidc::finish_authorization()`].
72    ///
73    /// Returns an error if the client registration was not restored, or if a
74    /// request fails.
75    #[instrument(target = "matrix_sdk::client", skip_all)]
76    pub async fn build(self) -> Result<OidcAuthorizationData, OidcError> {
77        let Self { oidc, scopes, redirect_uri, prompt, login_hint } = self;
78
79        let data = oidc.data().ok_or(OidcError::NotAuthenticated)?;
80        info!(
81            issuer = data.issuer,
82            ?scopes,
83            "Authorizing scope via the OpenID Connect Authorization Code flow"
84        );
85
86        let provider_metadata = oidc.provider_metadata().await?;
87        let auth_url = AuthUrl::from_url(provider_metadata.authorization_endpoint().clone());
88
89        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
90        let redirect_uri = RedirectUrl::from_url(redirect_uri);
91
92        let client = OauthClient::new(data.client_id.clone()).set_auth_uri(auth_url);
93        let mut request = client
94            .authorize_url(CsrfToken::new_random)
95            .add_scopes(scopes)
96            .set_pkce_challenge(pkce_challenge)
97            .set_redirect_uri(Cow::Borrowed(&redirect_uri));
98
99        if let Some(prompt) = prompt {
100            // This should be a list of space separated values.
101            let prompt_str = prompt.iter().map(Prompt::as_str).collect::<Vec<_>>().join(" ");
102            request = request.add_extra_param("prompt", prompt_str);
103        }
104
105        if let Some(login_hint) = login_hint {
106            request = request.add_extra_param("login_hint", login_hint);
107        }
108
109        let (url, state) = request.url();
110
111        data.authorization_data
112            .lock()
113            .await
114            .insert(state.clone(), AuthorizationValidationData { redirect_uri, pkce_verifier });
115
116        Ok(OidcAuthorizationData { url, state })
117    }
118}
119
120/// The data needed to perform authorization using OpenID Connect.
121#[derive(Debug, Clone)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
123pub struct OidcAuthorizationData {
124    /// The URL that should be presented.
125    pub url: Url,
126    /// A unique identifier for the request, used to ensure the response
127    /// originated from the authentication issuer.
128    pub state: CsrfToken,
129}
130
131#[cfg(feature = "uniffi")]
132#[matrix_sdk_ffi_macros::export]
133impl OidcAuthorizationData {
134    /// The login URL to use for authorization.
135    pub fn login_url(&self) -> String {
136        self.url.to_string()
137    }
138}