use crate::app_config::AppConfig; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; use matrix_sdk::authentication::oauth::error::{ BasicErrorResponseType, OAuthDiscoveryError, RequestTokenError, }; use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError, RefreshTokenError}; use ractor::ActorRef; use serde::{Deserialize, Serialize}; use url::Url; /// The full session to persist. #[derive(Debug, Serialize, Deserialize, Clone)] struct StoredSession { /// The OAuth 2.0 user session. user_session: UserSession, /// The OAuth 2.0 client ID. client_id: ClientId, } /// Matrix Gateway session errors #[derive(thiserror::Error, Debug)] enum MatrixClientError { #[error("Failed to destroy previous client data! {0}")] DestroyPreviousData(Box), #[error("Failed to create Matrix database storage directory! {0}")] CreateMatrixDbDir(std::io::Error), #[error("Failed to create database passphrase! {0}")] CreateDbPassphrase(std::io::Error), #[error("Failed to read database passphrase! {0}")] ReadDbPassphrase(std::io::Error), #[error("Failed to build Matrix client! {0}")] BuildMatrixClient(ClientBuildError), #[error("Failed to clear Matrix session file! {0}")] ClearMatrixSessionFile(std::io::Error), #[error("Failed to clear Matrix database storage directory! {0}")] ClearMatrixDbDir(std::io::Error), #[error("Failed to remove database passphrase! {0}")] ClearDbPassphrase(std::io::Error), #[error("Failed to fetch server metadata! {0}")] FetchServerMetadata(OAuthDiscoveryError), #[error("Failed to load stored session! {0}")] LoadStoredSession(std::io::Error), #[error("Failed to decode stored session! {0}")] DecodeStoredSession(serde_json::Error), #[error("Failed to restore stored session! {0}")] RestoreSession(matrix_sdk::Error), #[error("Failed to disconnect user! {0}")] DisconnectUser(anyhow::Error), #[error("Failed to refresh access token! {0}")] InitialRefreshToken(RefreshTokenError), #[error("Failed to parse auth redirect URL! {0}")] ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] BuildAuthRequest(OAuthError), #[error("Failed to finalize authentication! {0}")] FinishLogin(matrix_sdk::Error), #[error("Failed to write session file! {0}")] WriteSessionFile(std::io::Error), } #[derive(serde::Deserialize)] pub struct FinishMatrixAuth { code: String, state: String, } #[derive(Clone)] pub struct MatrixClient { manager: ActorRef, pub email: UserEmail, pub client: Client, } impl MatrixClient { /// Start to build Matrix client to initiate user authentication pub async fn build_client( manager: ActorRef, email: &UserEmail, ) -> anyhow::Result { // Check if we are restoring a previous state let session_file_path = AppConfig::get().user_matrix_session_file_path(email); let is_restoring = session_file_path.is_file(); if !is_restoring { Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?; } // Determine Matrix database path let db_path = AppConfig::get().user_matrix_db_path(email); std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?; // Generate or load passphrase let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); if !passphrase_path.exists() { std::fs::write(&passphrase_path, rand_string(32)) .map_err(MatrixClientError::CreateDbPassphrase)?; } let passphrase = std::fs::read_to_string(passphrase_path) .map_err(MatrixClientError::ReadDbPassphrase)?; let client = Client::builder() .server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver) // Automatically refresh tokens if needed .handle_refresh_tokens() .sqlite_store(&db_path, Some(&passphrase)) .build() .await .map_err(MatrixClientError::BuildMatrixClient)?; let client = Self { manager, email: email.clone(), client, }; // Check metadata let oauth = client.client.oauth(); let server_metadata = oauth .server_metadata() .await .map_err(MatrixClientError::FetchServerMetadata)?; log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); if is_restoring { let session: StoredSession = serde_json::from_str( std::fs::read_to_string(session_file_path) .map_err(MatrixClientError::LoadStoredSession)? .as_str(), ) .map_err(MatrixClientError::DecodeStoredSession)?; // Restore session client .client .restore_session(OAuthSession { client_id: session.client_id, user: session.user_session, }) .await .map_err(MatrixClientError::RestoreSession)?; // Force token refresh to make sure session is still alive, otherwise disconnect user if let Err(refresh_error) = client.client.oauth().refresh_access_token().await { if let RefreshTokenError::OAuth(e) = &refresh_error && let OAuthError::RefreshToken(RequestTokenError::ServerResponse(e)) = &**e && e.error() == &BasicErrorResponseType::InvalidGrant { log::warn!( "Refresh token rejected by server, token must have been invalidated! {refresh_error}" ); client .disconnect() .await .map_err(MatrixClientError::DisconnectUser)?; } return Err(MatrixClientError::InitialRefreshToken(refresh_error).into()); } } // Automatically save session when token gets refreshed client.setup_background_session_save().await; Ok(client) } /// Destroy Matrix client related data fn destroy_data(email: &UserEmail) -> anyhow::Result<(), Box> { let session_path = AppConfig::get().user_matrix_session_file_path(email); if session_path.is_file() { std::fs::remove_file(&session_path) .map_err(MatrixClientError::ClearMatrixSessionFile)?; } let db_path = AppConfig::get().user_matrix_db_path(email); if db_path.is_dir() { std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?; } let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); if passphrase_path.is_file() { std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?; } Ok(()) } /// Initiate OAuth authentication pub async fn initiate_login(&self) -> anyhow::Result { let oauth = self.client.oauth(); let metadata = AppConfig::get().matrix_client_metadata(); let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata"); let auth = oauth .login( Url::parse(&AppConfig::get().matrix_oauth_redirect_url()) .map_err(MatrixClientError::ParseAuthRedirectURL)?, None, Some(client_metadata.into()), None, ) .build() .await .map_err(MatrixClientError::BuildAuthRequest)?; Ok(auth.url) } /// Finish OAuth authentication pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> { let oauth = self.client.oauth(); oauth .finish_login(UrlOrQuery::Query(format!( "state={}&code={}", info.state, info.code ))) .await .map_err(MatrixClientError::FinishLogin)?; log::info!( "User successfully authenticated as {}!", self.client.user_id().unwrap() ); // Persist session tokens self.save_stored_session().await?; Ok(()) } /// Automatically persist session onto disk pub async fn setup_background_session_save(&self) { let this = self.clone(); tokio::spawn(async move { while let Ok(update) = this.client.subscribe_to_session_changes().recv().await { match update { matrix_sdk::SessionChange::UnknownToken { soft_logout } => { log::warn!("Received an unknown token error; soft logout? {soft_logout:?}"); if let Err(e) = this .manager .cast(MatrixManagerMsg::DisconnectClient(this.email)) { log::warn!("Failed to propagate invalid token error: {e}"); } break; } matrix_sdk::SessionChange::TokensRefreshed => { // The tokens have been refreshed, persist them to disk. if let Err(err) = this.save_stored_session().await { log::error!("Unable to store a session in the background: {err}"); } } } } }); } /// Update the session stored on the filesystem. async fn save_stored_session(&self) -> anyhow::Result<()> { log::debug!("Save the stored session for {:?}...", self.email); let user_session: UserSession = self .client .oauth() .user_session() .context("A logged in client must have a session")?; let stored_session = StoredSession { user_session, client_id: self .client .oauth() .client_id() .context("Client ID should be set at this point!")? .clone(), }; let serialized_session = serde_json::to_string(&stored_session)?; std::fs::write( AppConfig::get().user_matrix_session_file_path(&self.email), serialized_session, ) .map_err(MatrixClientError::WriteSessionFile)?; log::debug!("Updating the stored session: done!"); Ok(()) } /// Disconnect user from client pub async fn disconnect(self) -> anyhow::Result<()> { if let Err(e) = self.client.logout().await { log::warn!("Failed to send logout request: {e}"); } // Destroy user associated data Self::destroy_data(&self.email)?; Ok(()) } }