Skip to content

Commit

Permalink
Merge pull request #3587 from matrix-org/doug/client-oidc-helpers
Browse files Browse the repository at this point in the history
sdk: Move the OIDC helper methods from the FFI's `AuthenticationService` into `Client`
  • Loading branch information
andybalaam authored Jun 21, 2024
2 parents 748f40c + 837cfae commit 2b1bddb
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 223 deletions.
220 changes: 7 additions & 213 deletions bindings/matrix-sdk-ffi/src/authentication_service.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
use std::{
collections::HashMap,
path::Path,
sync::{Arc, RwLock as StdRwLock},
};

use matrix_sdk::{
encryption::BackupDownloadStrategy,
oidc::{
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
registrations::OidcRegistrationsError,
types::{
client_credentials::ClientCredentials,
errors::ClientErrorCode::AccessDenied,
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::{GrantType, Prompt},
requests::GrantType,
},
AuthorizationResponse, Oidc, OidcError,
OidcAuthorizationData, OidcError,
},
reqwest::StatusCode,
ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError,
};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
Expand Down Expand Up @@ -175,24 +171,6 @@ pub struct OidcConfiguration {
pub dynamic_registrations_file: String,
}

/// The data required to authenticate against an OIDC server.
#[derive(uniffi::Object)]
pub struct OidcAuthenticationData {
/// The underlying URL for authentication.
url: Url,
/// A unique identifier for the request, used to ensure the response
/// originated from the authentication issuer.
state: String,
}

#[uniffi::export]
impl OidcAuthenticationData {
/// The login URL to use for authentication.
pub fn login_url(&self) -> String {
self.url.to_string()
}
}

#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
url: String,
Expand Down Expand Up @@ -361,86 +339,31 @@ impl AuthenticationService {
/// returns.
pub async fn url_for_oidc_login(
&self,
) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
) -> Result<Arc<OidcAuthorizationData>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};

let oidc = client.inner.oidc();

let issuer = match oidc.fetch_authentication_issuer().await {
Ok(issuer) => issuer,
Err(error) => {
if error
.as_client_api_error()
.is_some_and(|err| err.status_code == StatusCode::NOT_FOUND)
{
return Err(AuthenticationError::OidcNotSupported);
} else {
return Err(AuthenticationError::ServerUnreachable(error));
}
}
};

let Some(oidc_configuration) = &self.oidc_configuration else {
return Err(AuthenticationError::OidcMetadataMissing);
};

let redirect_url = Url::parse(&oidc_configuration.redirect_uri)
.map_err(|_e| AuthenticationError::OidcMetadataInvalid)?;

self.configure_oidc(&oidc, issuer, oidc_configuration).await?;

let mut data_builder = oidc.login(redirect_url, None)?;
// TODO: Add a check for the Consent prompt when MAS is updated.
data_builder = data_builder.prompt(vec![Prompt::Consent]);
let data = data_builder.build().await?;

Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
client.url_for_oidc_login(oidc_configuration).await
}

/// Completes the OIDC login process.
pub async fn login_with_oidc_callback(
&self,
authentication_data: Arc<OidcAuthenticationData>,
authentication_data: Arc<OidcAuthorizationData>,
callback_url: String,
) -> Result<Arc<Client>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};

let oidc = client.inner.oidc();

let url =
Url::parse(&callback_url).map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;

let response = AuthorizationResponse::parse_uri(&url)
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;

let code = match response {
AuthorizationResponse::Success(code) => code,
AuthorizationResponse::Error(err) => {
if err.error.error == AccessDenied {
// The user cancelled the login in the web view.
return Err(AuthenticationError::OidcCancelled);
}
return Err(AuthenticationError::OidcError {
message: err.error.error.to_string(),
});
}
};

if code.state != authentication_data.state {
return Err(AuthenticationError::OidcCallbackUrlInvalid);
};

oidc.finish_authorization(code).await?;

oidc.finish_login()
.await
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?;
client.login_with_oidc_callback(authentication_data, callback_url).await?;

drop(client_guard);

Expand Down Expand Up @@ -491,135 +414,6 @@ impl AuthenticationService {

Ok(builder)
}

/// Handle any necessary configuration in order for login via OIDC to
/// succeed. This includes performing dynamic client registration against
/// the homeserver's issuer or restoring a previous registration if one has
/// been stored.
async fn configure_oidc(
&self,
oidc: &Oidc,
issuer: String,
configuration: &OidcConfiguration,
) -> Result<(), AuthenticationError> {
if oidc.client_credentials().is_some() {
tracing::info!("OIDC is already configured.");
return Ok(());
};

let oidc_metadata: VerifiedClientMetadata = configuration.try_into()?;
let registrations_file = Path::new(&configuration.dynamic_registrations_file);

if self.load_client_registration(
oidc,
issuer.clone(),
oidc_metadata.clone(),
registrations_file,
) {
tracing::info!("OIDC configuration loaded from disk.");
return Ok(());
}

tracing::info!("Registering this client for OIDC.");
let registration_response =
oidc.register_client(&issuer, oidc_metadata.clone(), None).await?;

// The format of the credentials changes according to the client metadata that
// was sent. Public clients only get a client ID.
let credentials =
ClientCredentials::None { client_id: registration_response.client_id.clone() };
oidc.restore_registered_client(issuer, oidc_metadata, credentials);

tracing::info!("Persisting OIDC registration data.");
self.store_client_registration(oidc, registrations_file)?;

Ok(())
}

/// Stores the current OIDC dynamic client registration so it can be re-used
/// if we ever log in via the same issuer again.
fn store_client_registration(
&self,
oidc: &Oidc,
registrations_file: &Path,
) -> Result<(), AuthenticationError> {
let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?)
.map_err(|_| AuthenticationError::OidcError {
message: String::from("Failed to parse issuer URL."),
})?;
let client_id = oidc
.client_credentials()
.ok_or(AuthenticationError::OidcError {
message: String::from("Missing client registration."),
})?
.client_id()
.to_owned();

let metadata = oidc.client_metadata().ok_or(AuthenticationError::OidcError {
message: String::from("Missing client metadata."),
})?;

let registrations = OidcRegistrations::new(
registrations_file,
metadata.clone(),
self.oidc_static_registrations(),
)?;
registrations.set_and_write_client_id(ClientId(client_id), issuer)?;

Ok(())
}

/// Attempts to load an existing OIDC dynamic client registration for the
/// currently configured issuer.
fn load_client_registration(
&self,
oidc: &Oidc,
issuer: String,
oidc_metadata: VerifiedClientMetadata,
registrations_file: &Path,
) -> bool {
let Ok(issuer_url) = Url::parse(&issuer) else {
tracing::error!("Failed to parse {issuer:?}");
return false;
};
let Some(registrations) = OidcRegistrations::new(
registrations_file,
oidc_metadata.clone(),
self.oidc_static_registrations(),
)
.ok() else {
return false;
};
let Some(client_id) = registrations.client_id(&issuer_url) else {
return false;
};

oidc.restore_registered_client(
issuer,
oidc_metadata,
ClientCredentials::None { client_id: client_id.0 },
);

true
}

fn oidc_static_registrations(&self) -> HashMap<Url, ClientId> {
let registrations = self
.oidc_configuration
.as_ref()
.map(|c| c.static_registrations.clone())
.unwrap_or_default();
registrations
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
return None;
};
Some((issuer, ClientId(client_id.clone())))
})
.collect()
}
}

impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
Expand Down
76 changes: 74 additions & 2 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
use std::{
collections::HashMap,
mem::ManuallyDrop,
path::Path,
sync::{Arc, RwLock},
};

use anyhow::{anyhow, Context as _};
use matrix_sdk::{
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
oidc::{
registrations::{ClientId, OidcRegistrations},
requests::account_management::AccountManagementActionFull,
types::{
client_credentials::ClientCredentials,
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
},
OidcSession,
OidcAuthorizationData, OidcError, OidcSession,
},
ruma::{
api::client::{
Expand All @@ -34,7 +36,7 @@ use matrix_sdk::{
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
Expand All @@ -58,6 +60,7 @@ use url::Url;

use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
use crate::{
authentication_service::{AuthenticationError, OidcConfiguration},
client,
encryption::Encryption,
notification::NotificationClient,
Expand Down Expand Up @@ -350,6 +353,75 @@ impl Client {
}

impl Client {
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns.
pub(crate) async fn url_for_oidc_login(
&self,
oidc_configuration: &OidcConfiguration,
) -> Result<Arc<OidcAuthorizationData>, AuthenticationError> {
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
let static_registrations = oidc_configuration
.static_registrations
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
return None;
};
Some((issuer, ClientId(client_id.clone())))
})
.collect::<HashMap<_, _>>();
let registrations = OidcRegistrations::new(
registrations_file,
oidc_metadata.clone(),
static_registrations,
)?;

let data =
self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await.map_err(
// TODO: Introduce an OidcError in the FFI with a From implementation.
|e| match e {
OidcError::MissingAuthenticationIssuer => AuthenticationError::OidcNotSupported,
OidcError::MissingRedirectUri => AuthenticationError::OidcMetadataInvalid,
_ => AuthenticationError::OidcError { message: e.to_string() },
},
)?;

Ok(Arc::new(data))
}

#[allow(dead_code)] // Will be exposed when AuthenticationService is removed.
pub(crate) async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
self.inner.oidc().abort_authorization(&authorization_data.state).await;
}

/// Completes the OIDC login process.
pub(crate) async fn login_with_oidc_callback(
&self,
authorization_data: Arc<OidcAuthorizationData>,
callback_url: String,
) -> Result<(), AuthenticationError> {
let url = Url::parse(&callback_url).or(Err(AuthenticationError::OidcCallbackUrlInvalid))?;

self.inner.oidc().login_with_oidc_callback(&authorization_data, url).await.map_err(
// TODO: Introduce an OidcError in the FFI with a From implementation.
|e| match e {
Error::Oidc(OidcError::InvalidCallbackUrl) => {
AuthenticationError::OidcCallbackUrlInvalid
}
Error::Oidc(OidcError::InvalidState) => AuthenticationError::OidcCallbackUrlInvalid,
Error::Oidc(OidcError::CancelledAuthorization) => {
AuthenticationError::OidcCancelled
}
_ => AuthenticationError::OidcError { message: e.to_string() },
},
)?;

Ok(())
}

/// Restores the client from an `AuthSession`.
pub(crate) async fn restore_session_inner(
&self,
Expand Down
Loading

0 comments on commit 2b1bddb

Please sign in to comment.