use std::borrow::Cow;
use oauth2::{
AuthUrl, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, basic::BasicClient as OAuthClient,
};
use ruma::{
OwnedDeviceId, UserId, api::client::discovery::get_authorization_server_metadata::v1::Prompt,
};
use tracing::{info, instrument};
use url::Url;
use super::{ClientRegistrationData, OAuth, OAuthError};
use crate::{Result, authentication::oauth::AuthorizationValidationData};
#[allow(missing_debug_implementations)]
pub struct OAuthAuthCodeUrlBuilder {
oauth: OAuth,
registration_data: Option<ClientRegistrationData>,
scopes: Vec<Scope>,
device_id: OwnedDeviceId,
redirect_uri: Url,
prompt: Option<Vec<Prompt>>,
login_hint: Option<String>,
}
impl OAuthAuthCodeUrlBuilder {
pub(super) fn new(
oauth: OAuth,
scopes: Vec<Scope>,
device_id: OwnedDeviceId,
redirect_uri: Url,
registration_data: Option<ClientRegistrationData>,
) -> Self {
Self {
oauth,
registration_data,
scopes,
device_id,
redirect_uri,
prompt: None,
login_hint: None,
}
}
pub fn prompt(mut self, prompt: Vec<Prompt>) -> Self {
self.prompt = Some(prompt);
self
}
pub fn login_hint(mut self, login_hint: String) -> Self {
self.login_hint = Some(login_hint);
self
}
pub fn user_id_hint(mut self, user_id: &UserId) -> Self {
self.login_hint = Some(format!("mxid:{user_id}"));
self
}
#[instrument(target = "matrix_sdk::client", skip_all)]
pub async fn build(self) -> Result<OAuthAuthorizationData, OAuthError> {
let Self { oauth, registration_data, scopes, device_id, redirect_uri, prompt, login_hint } =
self;
let server_metadata = oauth.server_metadata().await?;
oauth.use_registration_data(&server_metadata, registration_data.as_ref()).await?;
let data = oauth.data().expect("OAuth 2.0 data should be set after registration");
info!(
issuer = server_metadata.issuer.as_str(),
?scopes,
"Authorizing scope via the OAuth 2.0 Authorization Code flow"
);
let auth_url = AuthUrl::from_url(server_metadata.authorization_endpoint.clone());
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let redirect_uri = RedirectUrl::from_url(redirect_uri);
let client = OAuthClient::new(data.client_id.clone()).set_auth_uri(auth_url);
let mut request = client
.authorize_url(CsrfToken::new_random)
.add_scopes(scopes)
.set_pkce_challenge(pkce_challenge)
.set_redirect_uri(Cow::Borrowed(&redirect_uri));
if let Some(prompt) = prompt {
let prompt_str = prompt.iter().map(Prompt::as_str).collect::<Vec<_>>().join(" ");
request = request.add_extra_param("prompt", prompt_str);
}
if let Some(login_hint) = login_hint {
request = request.add_extra_param("login_hint", login_hint);
}
let (url, state) = request.url();
data.authorization_data.lock().await.insert(
state.clone(),
AuthorizationValidationData { server_metadata, device_id, redirect_uri, pkce_verifier },
);
Ok(OAuthAuthorizationData { url, state })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct OAuthAuthorizationData {
pub url: Url,
pub state: CsrfToken,
}
#[cfg(feature = "uniffi")]
#[matrix_sdk_ffi_macros::export]
impl OAuthAuthorizationData {
pub fn login_url(&self) -> String {
self.url.to_string()
}
}