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 AuthUrl, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, basic::BasicClient as OAuthClient,
19};
20use ruma::{
21 OwnedDeviceId, UserId, api::client::discovery::get_authorization_server_metadata::v1::Prompt,
22};
23use tracing::{info, instrument};
24use url::Url;
25
26use super::{ClientRegistrationData, OAuth, OAuthError};
27use crate::{Result, authentication::oauth::AuthorizationValidationData};
28
29/// Builder type used to configure optional settings for authorization with an
30/// OAuth 2.0 authorization server via the Authorization Code flow.
31///
32/// Created with [`OAuth::login()`]. Finalized with [`Self::build()`].
33#[allow(missing_debug_implementations)]
34pub struct OAuthAuthCodeUrlBuilder {
35 oauth: OAuth,
36 registration_data: Option<ClientRegistrationData>,
37 scopes: Vec<Scope>,
38 device_id: OwnedDeviceId,
39 redirect_uri: Url,
40 prompt: Option<Vec<Prompt>>,
41 login_hint: Option<String>,
42}
43
44impl OAuthAuthCodeUrlBuilder {
45 pub(super) fn new(
46 oauth: OAuth,
47 scopes: Vec<Scope>,
48 device_id: OwnedDeviceId,
49 redirect_uri: Url,
50 registration_data: Option<ClientRegistrationData>,
51 ) -> Self {
52 Self {
53 oauth,
54 registration_data,
55 scopes,
56 device_id,
57 redirect_uri,
58 prompt: None,
59 login_hint: None,
60 }
61 }
62
63 /// Set the [`Prompt`] of the authorization URL.
64 ///
65 /// If this is not set, it is assumed that the user wants to log into an
66 /// existing account.
67 ///
68 /// [`Prompt::Create`] can be used to signify that the user wants to
69 /// register a new account.
70 pub fn prompt(mut self, prompt: Vec<Prompt>) -> Self {
71 self.prompt = Some(prompt);
72 self
73 }
74
75 /// Set a generic login hint to help an identity provider pre-fill the login
76 /// form.
77 ///
78 /// Note: This is not the same as the [`Self::user_id_hint()`] method, which
79 /// is specifically designed to a) take a `UserId` and no other type of hint
80 /// and b) be used directly by MAS and not the identity provider.
81 ///
82 /// The most likely use case for this method is to pre-fill the login page
83 /// using a provisioning link provided by an external party such as
84 /// `https://app.example.com/?server_name=example.org&login_hint=alice` In
85 /// this instance it is up to the external party to make ensure that the
86 /// hint is known to work with their identity provider. For more information
87 /// see `login_hint` in [the specification]
88 ///
89 /// The following methods are mutually exclusive: [`Self::login_hint()`] and
90 /// [`Self::user_id_hint()`].
91 ///
92 /// [the specification]: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
93 pub fn login_hint(mut self, login_hint: String) -> Self {
94 self.login_hint = Some(login_hint);
95 self
96 }
97
98 /// Set the hint to the Authorization Server about the Matrix user ID the
99 /// End-User might use to log in, as defined in [MSC4198].
100 ///
101 /// [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
102 ///
103 /// The following methods are mutually exclusive: [`Self::login_hint()`] and
104 /// [`Self::user_id_hint()`].
105 pub fn user_id_hint(mut self, user_id: &UserId) -> Self {
106 self.login_hint = Some(format!("mxid:{user_id}"));
107 self
108 }
109
110 /// Get the URL that should be presented to login via the Authorization Code
111 /// flow.
112 ///
113 /// This URL should be presented to the user and once they are redirected to
114 /// the `redirect_uri`, the login can be completed by calling
115 /// [`OAuth::finish_login()`].
116 ///
117 /// Returns an error if the client registration was not restored, or if a
118 /// request fails.
119 #[instrument(target = "matrix_sdk::client", skip_all)]
120 pub async fn build(self) -> Result<OAuthAuthorizationData, OAuthError> {
121 let Self { oauth, registration_data, scopes, device_id, redirect_uri, prompt, login_hint } =
122 self;
123
124 let server_metadata = oauth.server_metadata().await?;
125
126 oauth.use_registration_data(&server_metadata, registration_data.as_ref()).await?;
127
128 let data = oauth.data().expect("OAuth 2.0 data should be set after registration");
129 info!(
130 issuer = server_metadata.issuer.as_str(),
131 ?scopes,
132 "Authorizing scope via the OAuth 2.0 Authorization Code flow"
133 );
134
135 let auth_url = AuthUrl::from_url(server_metadata.authorization_endpoint.clone());
136
137 let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
138 let redirect_uri = RedirectUrl::from_url(redirect_uri);
139
140 let client = OAuthClient::new(data.client_id.clone()).set_auth_uri(auth_url);
141 let mut request = client
142 .authorize_url(CsrfToken::new_random)
143 .add_scopes(scopes)
144 .set_pkce_challenge(pkce_challenge)
145 .set_redirect_uri(Cow::Borrowed(&redirect_uri));
146
147 if let Some(prompt) = prompt {
148 // This should be a list of space separated values.
149 let prompt_str = prompt.iter().map(Prompt::as_str).collect::<Vec<_>>().join(" ");
150 request = request.add_extra_param("prompt", prompt_str);
151 }
152
153 if let Some(login_hint) = login_hint {
154 request = request.add_extra_param("login_hint", login_hint);
155 }
156
157 let (url, state) = request.url();
158
159 data.authorization_data.lock().await.insert(
160 state.clone(),
161 AuthorizationValidationData { server_metadata, device_id, redirect_uri, pkce_verifier },
162 );
163
164 Ok(OAuthAuthorizationData { url, state })
165 }
166}
167
168/// The data needed to perform authorization using OAuth 2.0.
169#[derive(Debug, Clone)]
170#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
171pub struct OAuthAuthorizationData {
172 /// The URL that should be presented.
173 pub url: Url,
174 /// A unique identifier for the request, used to ensure the response
175 /// originated from the authentication issuer.
176 pub state: CsrfToken,
177}
178
179#[cfg(feature = "uniffi")]
180#[matrix_sdk_ffi_macros::export]
181impl OAuthAuthorizationData {
182 /// The login URL to use for authorization.
183 pub fn login_url(&self) -> String {
184 self.url.to_string()
185 }
186}