Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an authentication service to the FFI #820

Merged
merged 7 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ emsdk-*
## User settings
xcuserdata/
.vscode/

## OS garbage
.DS_Store
27 changes: 27 additions & 0 deletions bindings/matrix-sdk-ffi/src/api.udl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ interface Client {
[Throws=ClientError]
void restore_login(string restore_token);

string homeserver();
pixlwave marked this conversation as resolved.
Show resolved Hide resolved

void start_sync();

[Throws=ClientError]
Expand Down Expand Up @@ -152,6 +154,31 @@ interface MediaSource {
string url();
};

[Error]
enum AuthenticationError {
"ClientMissing",
"Generic",
};

interface AuthenticationService {
constructor(string base_path);

[Throws=AuthenticationError]
string homeserver();

[Throws=AuthenticationError]
string? authentication_issuer();

[Throws=AuthenticationError]
boolean supports_password_login();

[Throws=AuthenticationError]
void use_server(string server_name);

[Throws=AuthenticationError]
Client login(string username, string password);
};

interface SessionVerificationEmoji {
string symbol();
string description();
Expand Down
100 changes: 100 additions & 0 deletions bindings/matrix-sdk-ffi/src/authentication_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::sync::Arc;

use parking_lot::RwLock;
pixlwave marked this conversation as resolved.
Show resolved Hide resolved

use super::{client::Client, client_builder::ClientBuilder};

pub struct AuthenticationService {
base_path: String,
client: RwLock<Option<Arc<Client>>>,
}

#[derive(Debug, thiserror::Error)]
pub enum AuthenticationError {
#[error("A successful call to use_server must be made first.")]
ClientMissing,
#[error("An error occurred: {message}")]
Generic { message: String },
}

impl From<anyhow::Error> for AuthenticationError {
fn from(e: anyhow::Error) -> AuthenticationError {
AuthenticationError::Generic { message: e.to_string() }
}
}

impl AuthenticationService {
/// Creates a new service to authenticate a user with.
pub fn new(base_path: String) -> Self {
AuthenticationService { base_path, client: RwLock::new(None) }
}

/// The currently configured homeserver.
pub fn homeserver(&self) -> Result<String, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.map(|client| client.homeserver())
}

/// The OIDC Provider that is trusted by the homeserver. `None` when
/// not configured.
pub fn authentication_issuer(&self) -> Result<Option<String>, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.map(|client| client.authentication_issuer())
}

/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> Result<bool, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.and_then(|client| client.supports_password_login().map_err(AuthenticationError::from))
}

/// Updates the server to authenticate with the specified homeserver.
pub fn use_server(&self, server_name: String) -> Result<(), AuthenticationError> {
// Construct a username as the builder currently requires one.
let username = format!("@auth:{}", server_name);
let client = Arc::new(ClientBuilder::new())
.base_path(self.base_path.clone())
.username(username)
.build()
.map_err(AuthenticationError::from)?;

*self.client.write() = Some(client);
Ok(())
}

/// Performs a password login using the current homeserver.
pub fn login(
&self,
username: String,
password: String,
) -> Result<Arc<Client>, AuthenticationError> {
match self.client.read().as_ref() {
Some(client) => {
let homeserver_url = client.homeserver();

// Create a new client to setup the store path for the username
let client = Arc::new(ClientBuilder::new())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so you create a Client with a fake user to get some metadata about the server. Then you set the correct user here. Am I getting this right?

It might make sense to modify the Client in the bindings to support memory only operation. I guess it isn't that important since the store will only be used once you log in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's correct, it's totally a workaround 🙈. Jonas and Ben had a conversation around whether the client store should be set at all until after login has succeeded. Amongst other things this would help if the user ID returned by the homeserver differed to what was used to log in.

This all seemed like it was a larger thought than just the bindings to me, so I opted for this for now. However if its something I could address in this PR, would be happy to be given some direction and have a go 🙂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we used to have an API where you just pass an optional store path to the Client, similarly to what the FFI Client does. Maybe we should just bring that back, of course only if the sled feature is enabled.

In any case, I'm not sure what we should do, so let's leave bigger changes around this for another time.

.base_path(self.base_path.clone())
.homeserver_url(homeserver_url)
.username(username.clone())
.build()
.map_err(AuthenticationError::from)?;

client
.login(username, password)
.map(|_| client.clone())
.map_err(AuthenticationError::from)
}
None => Err(AuthenticationError::ClientMissing),
}
}
}
25 changes: 25 additions & 0 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use matrix_sdk::{
ruma::{
api::client::{
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
session::get_login_types,
sync::sync_events::v3::Filter,
},
events::room::MediaSource,
Expand Down Expand Up @@ -71,6 +72,30 @@ impl Client {
*self.delegate.write() = delegate;
}

/// The homeserver this client is configured to use.
pub fn homeserver(&self) -> String {
RUNTIME.block_on(async move { self.client.homeserver().await.to_string() })
}

/// The OIDC Provider that is trusted by the homeserver. `None` when
/// not configured.
pub fn authentication_issuer(&self) -> Option<String> {
RUNTIME.block_on(async move {
self.client.authentication_issuer().await.map(|server| server.to_string())
})
}

/// Whether or not the client's homeserver supports the password login flow.
pub fn supports_password_login(&self) -> anyhow::Result<bool> {
RUNTIME.block_on(async move {
let login_types = self.client.get_login_types().await?;
let supports_password = login_types.flows.iter().any(|login_type| {
matches!(login_type, get_login_types::v3::LoginType::Password(_))
});
Ok(supports_password)
})
}

pub fn start_sync(&self) {
let client = self.client.clone();
let state = self.state.clone();
Expand Down
6 changes: 5 additions & 1 deletion bindings/matrix-sdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#![allow(unused_qualifications)]

pub mod authentication_service;
pub mod backward_stream;
pub mod client;
pub mod client_builder;
Expand All @@ -23,7 +24,10 @@ pub static RUNTIME: Lazy<Runtime> =

pub use matrix_sdk::ruma::{api::client::account::register, UserId};

pub use self::{backward_stream::*, client::*, messages::*, room::*, session_verification::*};
pub use self::{
authentication_service::*, backward_stream::*, client::*, messages::*, room::*,
session_verification::*,
};

#[derive(Default, Debug)]
pub struct ClientState {
Expand Down
5 changes: 5 additions & 0 deletions crates/matrix-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ git = "https://github.com/ruma/ruma"
rev = "96155915f"
features = ["client-api-c", "compat", "rand", "unstable-msc2448"]

[dependencies.ruma-client-api]
git = "https://github.com/ruma/ruma"
rev = "96155915f"
features = ["unstable-msc2965"]

[dependencies.tokio-stream]
version = "0.1.8"
features = ["net"]
Expand Down
7 changes: 7 additions & 0 deletions crates/matrix-sdk/src/client/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ impl ClientBuilder {
let base_client = BaseClient::with_store_config(self.store_config);
let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);

let mut authentication_issuer: Option<Url> = None;
let homeserver = match homeserver_cfg {
HomeserverConfig::Url(url) => url,
HomeserverConfig::ServerName(server_name) => {
Expand All @@ -313,14 +314,20 @@ impl ClientBuilder {
err => ClientBuildError::Http(err),
})?;

if let Some(issuer) = well_known.authentication.map(|auth| auth.issuer) {
authentication_issuer = Url::parse(&issuer).ok();
};

well_known.homeserver.base_url
}
};

let homeserver = RwLock::new(Url::parse(&homeserver)?);
let authentication_issuer = authentication_issuer.map(RwLock::new);

let inner = Arc::new(ClientInner {
homeserver,
authentication_issuer,
http_client,
base_client,
server_versions: OnceCell::new_with(self.server_versions),
Expand Down
11 changes: 11 additions & 0 deletions crates/matrix-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ pub struct Client {
pub(crate) struct ClientInner {
/// The URL of the homeserver to connect to.
homeserver: RwLock<Url>,
/// The OIDC Provider that is trusted by the homeserver.
authentication_issuer: Option<RwLock<Url>>,
/// The underlying HTTP client.
http_client: HttpClient,
/// User session data.
Expand Down Expand Up @@ -292,6 +294,15 @@ impl Client {
self.inner.homeserver.read().await.clone()
}

/// The OIDC Provider that is trusted by the homeserver.
pub async fn authentication_issuer(&self) -> Option<Url> {
if let Some(server) = &self.inner.authentication_issuer {
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
Some(server.read().await.clone())
} else {
None
}
}

/// Get the user id of the current owner of the client.
pub fn user_id(&self) -> Option<&UserId> {
self.session().map(|s| s.user_id.as_ref())
Expand Down