refactor(auth): make auxiliary functions reusable outside LoginWithQrCode
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
committed by
Damir Jelić
parent
d4d40945e8
commit
68075b65fb
@@ -34,11 +34,9 @@ use super::{
|
||||
messages::{LoginFailureReason, QrAuthMessage},
|
||||
secure_channel::EstablishedSecureChannel,
|
||||
};
|
||||
#[cfg(doc)]
|
||||
use crate::authentication::oauth::OAuth;
|
||||
use crate::{
|
||||
Client,
|
||||
authentication::oauth::{ClientRegistrationData, OAuthError},
|
||||
authentication::oauth::{ClientRegistrationData, OAuth, OAuthError},
|
||||
};
|
||||
|
||||
async fn send_unexpected_message_error(
|
||||
@@ -52,6 +50,191 @@ async fn send_unexpected_message_error(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn finish_login(
|
||||
client: &Client,
|
||||
mut channel: EstablishedSecureChannel,
|
||||
registration_data: Option<&ClientRegistrationData>,
|
||||
state: SharedObservable<LoginProgress>,
|
||||
) -> Result<(), QRCodeLoginError> {
|
||||
let oauth = client.oauth();
|
||||
|
||||
// Register the client with the OAuth 2.0 authorization server.
|
||||
trace!("Registering the client with the OAuth 2.0 authorization server.");
|
||||
let server_metadata = register_client(&oauth, registration_data).await?;
|
||||
|
||||
// We want to use the Curve25519 public key for the device ID, so let's generate
|
||||
// a new vodozemac `Account` now.
|
||||
let account = vodozemac::olm::Account::new();
|
||||
let public_key = account.identity_keys().curve25519;
|
||||
let device_id = public_key;
|
||||
|
||||
// Let's tell the OAuth 2.0 authorization server that we want to log in using
|
||||
// the device authorization grant described in [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628).
|
||||
trace!("Requesting device authorization.");
|
||||
let auth_grant_response =
|
||||
request_device_authorization(&oauth, &server_metadata, device_id).await?;
|
||||
|
||||
// Now we need to inform the other device of the login protocols we picked and
|
||||
// the URL they should use to log us in.
|
||||
trace!("Letting the existing device know about the device authorization grant.");
|
||||
let message =
|
||||
QrAuthMessage::authorization_grant_login_protocol((&auth_grant_response).into(), device_id);
|
||||
channel.send_json(&message).await?;
|
||||
|
||||
// Let's see if the other device agreed to our proposed protocols.
|
||||
match channel.receive_json().await? {
|
||||
QrAuthMessage::LoginProtocolAccepted => (),
|
||||
QrAuthMessage::LoginFailure { reason, homeserver } => {
|
||||
return Err(QRCodeLoginError::LoginFailure { reason, homeserver });
|
||||
}
|
||||
message => {
|
||||
send_unexpected_message_error(&mut channel).await?;
|
||||
|
||||
return Err(QRCodeLoginError::UnexpectedMessage {
|
||||
expected: "m.login.protocol_accepted",
|
||||
received: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The OAuth 2.0 authorization server may or may not show this user code to
|
||||
// double check that we're talking to the right server. Let us display this, so
|
||||
// the other device can double check this as well.
|
||||
let user_code = auth_grant_response.user_code();
|
||||
state.set(LoginProgress::WaitingForToken { user_code: user_code.secret().to_owned() });
|
||||
|
||||
// Let's now wait for the access token to be provided to use by the OAuth 2.0
|
||||
// authorization server.
|
||||
trace!("Waiting for the OAuth 2.0 authorization server to give us the access token.");
|
||||
if let Err(e) = wait_for_tokens(&oauth, &server_metadata, &auth_grant_response).await {
|
||||
// If we received an error, and it's one of the ones we should report to the
|
||||
// other side, do so now.
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => {
|
||||
channel.send_json(QrAuthMessage::LoginDeclined).await?;
|
||||
}
|
||||
DeviceCodeErrorResponseType::ExpiredToken => {
|
||||
channel
|
||||
.send_json(QrAuthMessage::LoginFailure {
|
||||
reason: LoginFailureReason::AuthorizationExpired,
|
||||
homeserver: None,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
// We only received an access token from the OAuth 2.0 authorization server, we
|
||||
// have no clue who we are, so we need to figure out our user ID
|
||||
// now. TODO: This snippet is almost the same as the
|
||||
// OAuth::finish_login_method(), why is that method even a public
|
||||
// method and not called as part of the set session tokens method.
|
||||
trace!("Discovering our own user id.");
|
||||
let whoami_response = client.whoami().await.map_err(QRCodeLoginError::UserIdDiscovery)?;
|
||||
client
|
||||
.base_client()
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: whoami_response.user_id,
|
||||
device_id: OwnedDeviceId::from(device_id.to_base64()),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
Some(account),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| QRCodeLoginError::SessionTokens(error.into()))?;
|
||||
|
||||
client.oauth().enable_cross_process_lock().await?;
|
||||
|
||||
// Tell the existing device that we're logged in.
|
||||
trace!("Telling the existing device that we successfully logged in.");
|
||||
let message = QrAuthMessage::LoginSuccess;
|
||||
channel.send_json(&message).await?;
|
||||
|
||||
// Let's wait for the secrets bundle to be sent to us, otherwise we won't be a
|
||||
// fully E2EE enabled device.
|
||||
trace!("Waiting for the secrets bundle.");
|
||||
let bundle = match channel.receive_json().await? {
|
||||
QrAuthMessage::LoginSecrets(bundle) => bundle,
|
||||
QrAuthMessage::LoginFailure { reason, homeserver } => {
|
||||
return Err(QRCodeLoginError::LoginFailure { reason, homeserver });
|
||||
}
|
||||
message => {
|
||||
send_unexpected_message_error(&mut channel).await?;
|
||||
|
||||
return Err(QRCodeLoginError::UnexpectedMessage {
|
||||
expected: "m.login.secrets",
|
||||
received: message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Import the secrets bundle, this will allow us to sign the device keys with
|
||||
// the master key when we upload them.
|
||||
client.encryption().import_secrets_bundle(&bundle).await?;
|
||||
|
||||
// Upload the device keys, this will ensure that other devices see us as a fully
|
||||
// verified device ass soon as this method returns.
|
||||
client
|
||||
.encryption()
|
||||
.ensure_device_keys_upload()
|
||||
.await
|
||||
.map_err(QRCodeLoginError::DeviceKeyUpload)?;
|
||||
|
||||
// Run and wait for the E2EE initialization tasks, this will ensure that we
|
||||
// ourselves see us as verified and the recovery/backup states will
|
||||
// be known. If we did receive all the secrets in the secrets
|
||||
// bundle, then backups will be enabled after this step as well.
|
||||
client.encryption().spawn_initialization_task(None).await;
|
||||
client.encryption().wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
trace!("successfully logged in and enabled E2EE.");
|
||||
|
||||
// Tell our listener that we're done.
|
||||
state.set(LoginProgress::Done);
|
||||
|
||||
// And indeed, we are done with the login.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register the client with the OAuth 2.0 authorization server.
|
||||
///
|
||||
/// Returns the authorization server metadata.
|
||||
async fn register_client(
|
||||
oauth: &OAuth,
|
||||
registration_data: Option<&ClientRegistrationData>,
|
||||
) -> Result<AuthorizationServerMetadata, DeviceAuthorizationOAuthError> {
|
||||
let server_metadata = oauth.server_metadata().await.map_err(OAuthError::from)?;
|
||||
oauth.use_registration_data(&server_metadata, registration_data).await?;
|
||||
|
||||
Ok(server_metadata)
|
||||
}
|
||||
|
||||
async fn request_device_authorization(
|
||||
oauth: &OAuth,
|
||||
server_metadata: &AuthorizationServerMetadata,
|
||||
device_id: Curve25519PublicKey,
|
||||
) -> Result<StandardDeviceAuthorizationResponse, DeviceAuthorizationOAuthError> {
|
||||
let response = oauth
|
||||
.request_device_authorization(server_metadata, Some(device_id.to_base64().into()))
|
||||
.await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn wait_for_tokens(
|
||||
oauth: &OAuth,
|
||||
server_metadata: &AuthorizationServerMetadata,
|
||||
auth_response: &StandardDeviceAuthorizationResponse,
|
||||
) -> Result<(), DeviceAuthorizationOAuthError> {
|
||||
oauth.exchange_device_code(server_metadata, auth_response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Type telling us about the progress of the QR code login.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum LoginProgress {
|
||||
@@ -107,161 +290,17 @@ impl<'a> IntoFuture for LoginWithQrCode<'a> {
|
||||
// First things first, establish the secure channel. Since we're the one that
|
||||
// scanned the QR code, we're certain that the secure channel is
|
||||
// secure, under the assumption that we didn't scan the wrong QR code.
|
||||
let mut channel = self.establish_secure_channel().await?;
|
||||
let channel = self.establish_secure_channel().await?;
|
||||
|
||||
trace!("Established the secure channel.");
|
||||
|
||||
// The other side isn't yet sure that it's talking to the right device, show
|
||||
// a check code so they can confirm.
|
||||
let check_code = channel.check_code().to_owned();
|
||||
|
||||
self.state.set(LoginProgress::EstablishingSecureChannel { check_code });
|
||||
|
||||
// Register the client with the OAuth 2.0 authorization server.
|
||||
trace!("Registering the client with the OAuth 2.0 authorization server.");
|
||||
let server_metadata = self.register_client().await?;
|
||||
|
||||
// We want to use the Curve25519 public key for the device ID, so let's generate
|
||||
// a new vodozemac `Account` now.
|
||||
let account = vodozemac::olm::Account::new();
|
||||
let public_key = account.identity_keys().curve25519;
|
||||
let device_id = public_key;
|
||||
|
||||
// Let's tell the OAuth 2.0 authorization server that we want to log in using
|
||||
// the device authorization grant described in [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628).
|
||||
trace!("Requesting device authorization.");
|
||||
let auth_grant_response =
|
||||
self.request_device_authorization(&server_metadata, device_id).await?;
|
||||
|
||||
// Now we need to inform the other device of the login protocols we picked and
|
||||
// the URL they should use to log us in.
|
||||
trace!("Letting the existing device know about the device authorization grant.");
|
||||
let message = QrAuthMessage::authorization_grant_login_protocol(
|
||||
(&auth_grant_response).into(),
|
||||
device_id,
|
||||
);
|
||||
channel.send_json(&message).await?;
|
||||
|
||||
// Let's see if the other device agreed to our proposed protocols.
|
||||
match channel.receive_json().await? {
|
||||
QrAuthMessage::LoginProtocolAccepted => (),
|
||||
QrAuthMessage::LoginFailure { reason, homeserver } => {
|
||||
return Err(QRCodeLoginError::LoginFailure { reason, homeserver });
|
||||
}
|
||||
message => {
|
||||
send_unexpected_message_error(&mut channel).await?;
|
||||
|
||||
return Err(QRCodeLoginError::UnexpectedMessage {
|
||||
expected: "m.login.protocol_accepted",
|
||||
received: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The OAuth 2.0 authorization server may or may not show this user code to
|
||||
// double check that we're talking to the right server. Let us display this, so
|
||||
// the other device can double check this as well.
|
||||
let user_code = auth_grant_response.user_code();
|
||||
self.state
|
||||
.set(LoginProgress::WaitingForToken { user_code: user_code.secret().to_owned() });
|
||||
|
||||
// Let's now wait for the access token to be provided to use by the OAuth 2.0
|
||||
// authorization server.
|
||||
trace!("Waiting for the OAuth 2.0 authorization server to give us the access token.");
|
||||
if let Err(e) = self.wait_for_tokens(&server_metadata, &auth_grant_response).await {
|
||||
// If we received an error, and it's one of the ones we should report to the
|
||||
// other side, do so now.
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => {
|
||||
channel.send_json(QrAuthMessage::LoginDeclined).await?;
|
||||
}
|
||||
DeviceCodeErrorResponseType::ExpiredToken => {
|
||||
channel
|
||||
.send_json(QrAuthMessage::LoginFailure {
|
||||
reason: LoginFailureReason::AuthorizationExpired,
|
||||
homeserver: None,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
// We only received an access token from the OAuth 2.0 authorization server, we
|
||||
// have no clue who we are, so we need to figure out our user ID
|
||||
// now. TODO: This snippet is almost the same as the
|
||||
// OAuth::finish_login_method(), why is that method even a public
|
||||
// method and not called as part of the set session tokens method.
|
||||
trace!("Discovering our own user id.");
|
||||
let whoami_response =
|
||||
self.client.whoami().await.map_err(QRCodeLoginError::UserIdDiscovery)?;
|
||||
self.client
|
||||
.base_client()
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: whoami_response.user_id,
|
||||
device_id: OwnedDeviceId::from(device_id.to_base64()),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
Some(account),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| QRCodeLoginError::SessionTokens(error.into()))?;
|
||||
|
||||
self.client.oauth().enable_cross_process_lock().await?;
|
||||
|
||||
// Tell the existing device that we're logged in.
|
||||
trace!("Telling the existing device that we successfully logged in.");
|
||||
let message = QrAuthMessage::LoginSuccess;
|
||||
channel.send_json(&message).await?;
|
||||
|
||||
// Let's wait for the secrets bundle to be sent to us, otherwise we won't be a
|
||||
// fully E2EE enabled device.
|
||||
trace!("Waiting for the secrets bundle.");
|
||||
let bundle = match channel.receive_json().await? {
|
||||
QrAuthMessage::LoginSecrets(bundle) => bundle,
|
||||
QrAuthMessage::LoginFailure { reason, homeserver } => {
|
||||
return Err(QRCodeLoginError::LoginFailure { reason, homeserver });
|
||||
}
|
||||
message => {
|
||||
send_unexpected_message_error(&mut channel).await?;
|
||||
|
||||
return Err(QRCodeLoginError::UnexpectedMessage {
|
||||
expected: "m.login.secrets",
|
||||
received: message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Import the secrets bundle, this will allow us to sign the device keys with
|
||||
// the master key when we upload them.
|
||||
self.client.encryption().import_secrets_bundle(&bundle).await?;
|
||||
|
||||
// Upload the device keys, this will ensure that other devices see us as a fully
|
||||
// verified device ass soon as this method returns.
|
||||
self.client
|
||||
.encryption()
|
||||
.ensure_device_keys_upload()
|
||||
.await
|
||||
.map_err(QRCodeLoginError::DeviceKeyUpload)?;
|
||||
|
||||
// Run and wait for the E2EE initialization tasks, this will ensure that we
|
||||
// ourselves see us as verified and the recovery/backup states will
|
||||
// be known. If we did receive all the secrets in the secrets
|
||||
// bundle, then backups will be enabled after this step as well.
|
||||
self.client.encryption().spawn_initialization_task(None).await;
|
||||
self.client.encryption().wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
trace!("successfully logged in and enabled E2EE.");
|
||||
|
||||
// Tell our listener that we're done.
|
||||
self.state.set(LoginProgress::Done);
|
||||
|
||||
// And indeed, we are done with the login.
|
||||
Ok(())
|
||||
finish_login(self.client, channel, self.registration_data, self.state).await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -289,41 +328,6 @@ impl<'a> LoginWithQrCode<'a> {
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Register the client with the OAuth 2.0 authorization server.
|
||||
///
|
||||
/// Returns the authorization server metadata.
|
||||
async fn register_client(
|
||||
&self,
|
||||
) -> Result<AuthorizationServerMetadata, DeviceAuthorizationOAuthError> {
|
||||
let oauth = self.client.oauth();
|
||||
let server_metadata = oauth.server_metadata().await.map_err(OAuthError::from)?;
|
||||
oauth.use_registration_data(&server_metadata, self.registration_data).await?;
|
||||
|
||||
Ok(server_metadata)
|
||||
}
|
||||
|
||||
async fn request_device_authorization(
|
||||
&self,
|
||||
server_metadata: &AuthorizationServerMetadata,
|
||||
device_id: Curve25519PublicKey,
|
||||
) -> Result<StandardDeviceAuthorizationResponse, DeviceAuthorizationOAuthError> {
|
||||
let oauth = self.client.oauth();
|
||||
let response = oauth
|
||||
.request_device_authorization(server_metadata, Some(device_id.to_base64().into()))
|
||||
.await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn wait_for_tokens(
|
||||
&self,
|
||||
server_metadata: &AuthorizationServerMetadata,
|
||||
auth_response: &StandardDeviceAuthorizationResponse,
|
||||
) -> Result<(), DeviceAuthorizationOAuthError> {
|
||||
let oauth = self.client.oauth();
|
||||
oauth.exchange_device_code(server_metadata, auth_response).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_family = "wasm")))]
|
||||
|
||||
Reference in New Issue
Block a user