From 0d080a7ffad2703df401afbe572608317fe8c31d Mon Sep 17 00:00:00 2001 From: iinuwa Date: Sun, 19 Apr 2020 15:09:45 -0500 Subject: [PATCH] Add UIAA error types --- CHANGELOG.md | 10 +- src/error.rs | 2 + src/r0.rs | 1 + src/r0/account.rs | 40 ---- src/r0/account/add_3pid.rs | 31 +++ src/r0/account/change_password.rs | 6 +- src/r0/account/deactivate.rs | 8 +- src/r0/account/register.rs | 6 +- src/r0/device/delete_device.rs | 7 +- src/r0/device/delete_devices.rs | 7 +- src/r0/uiaa.rs | 363 ++++++++++++++++++++++++++++++ 11 files changed, 423 insertions(+), 58 deletions(-) create mode 100644 src/r0/account/add_3pid.rs create mode 100644 src/r0/uiaa.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4ca735..99d0b4fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,13 @@ Breaking changes: * Add `server_name` parameter to `r0::join::join_room_by_id_or_alias` -* Add `auth_parameters` to `r0::account::AuthenticationData` +* Modify `r0::account::AuthenticationData`: + - Rename to `AuthData` + - Change to an enum to facilitate fallback auth acknowledgements + - Add `auth_parameters` field + - Move to `r0::uiaa` module * Add `room_network` parameter to `r0::directory::get_public_rooms_filtered` to - represent `include_all_networks` and `third_party_instance_id` Matrix fields. + represent `include_all_networks` and `third_party_instance_id` Matrix fields * Update `r0::account::register` endpoint: * Remove `bind_email` request field (removed in r0.6.0) * Remove `inhibit_login` request field, make `access_token` and `device_id` response fields optional (added in r0.4.0) @@ -19,7 +23,7 @@ Breaking changes: Improvements: -* Add types for User-Interactive Authentication API: `r0::account::{UserInteractiveAuthenticationInfo, AuthenticationFlow}` +* Add types for User-Interactive Authentication API: `r0::uiaa::{AuthFlow, UiaaInfo, UiaaResponse}` * Add missing serde attributes to `get_content_thumbnail` query parameters # 0.7.2 diff --git a/src/error.rs b/src/error.rs index 999f40aa..0a5ce361 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; /// An enum for the error kind. Items may contain additional information. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(tag = "errcode")] +#[cfg_attr(test, derive(PartialEq))] pub enum ErrorKind { /// M_FORBIDDEN #[serde(rename = "M_FORBIDDEN")] @@ -98,6 +99,7 @@ pub enum ErrorKind { /// A Matrix Error without a status code #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] pub struct ErrorBody { /// A value which can be used to handle an error message #[serde(flatten)] diff --git a/src/r0.rs b/src/r0.rs index 9ed2405f..3a761cbc 100644 --- a/src/r0.rs +++ b/src/r0.rs @@ -30,5 +30,6 @@ pub mod sync; pub mod tag; pub mod thirdparty; pub mod typing; +pub mod uiaa; pub mod user_directory; pub mod voip; diff --git a/src/r0/account.rs b/src/r0/account.rs index 6598d76d..c015ab2f 100644 --- a/src/r0/account.rs +++ b/src/r0/account.rs @@ -17,48 +17,8 @@ pub mod unbind_3pid; pub mod whoami; -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; -/// Additional authentication information for the user-interactive authentication API. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AuthenticationData { - /// The login type that the client is attempting to complete. - #[serde(rename = "type")] - pub kind: String, - /// The value of the session key given by the homeserver. - pub session: Option, - /// Parameters submitted for a particular authentication stage. - #[serde(flatten)] - pub auth_parameters: BTreeMap, -} - -/// Information about available authentication flows and status for -/// User-Interactive Authenticiation API. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UserInteractiveAuthenticationInfo { - /// List of authentication flows available for this endpoint. - pub flows: Vec, - /// List of stages in the current flow completed by the client. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub completed: Vec, - /// Authentication parameters required for the client to complete authentication. - pub params: serde_json::Value, - /// Session key for client to use to complete authentication. - #[serde(skip_serializing_if = "Option::is_none")] - pub session: Option, -} - -/// Description of steps required to authenticate via the User-Interactive -/// Authentication API. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AuthenticationFlow { - /// Ordered list of stages required to complete authentication. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stages: Vec, -} - /// Additional authentication information for requestToken endpoints. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdentityServerInfo { diff --git a/src/r0/account/add_3pid.rs b/src/r0/account/add_3pid.rs new file mode 100644 index 00000000..adef2d45 --- /dev/null +++ b/src/r0/account/add_3pid.rs @@ -0,0 +1,31 @@ +//! [POST /_matrix/client/r0/account/3pid/add](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-add) + +use ruma_api::ruma_api; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Add contact information to a user's account", + method: POST, + name: "add_3pid", + path: "/_matrix/client/r0/account/3pid/add", + rate_limited: true, + requires_authentication: true, + } + + request { + /// Additional information for the User-Interactive Authentication API. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + /// Client-generated secret string used to protect this session. + pub client_secret: String, + /// The session identifier given by the identity server. + pub sid: String, + } + + response {} + + error: UiaaResponse +} + diff --git a/src/r0/account/change_password.rs b/src/r0/account/change_password.rs index abb83411..6eb5f045 100644 --- a/src/r0/account/change_password.rs +++ b/src/r0/account/change_password.rs @@ -2,7 +2,7 @@ use ruma_api::ruma_api; -use super::AuthenticationData; +use crate::r0::uiaa::{AuthData, UiaaResponse}; ruma_api! { metadata { @@ -18,10 +18,10 @@ ruma_api! { /// The new password for the account. pub new_password: String, /// Additional authentication information for the user-interactive authentication API. - pub auth: Option, + pub auth: Option, } response {} - error: crate::Error + error: UiaaResponse } diff --git a/src/r0/account/deactivate.rs b/src/r0/account/deactivate.rs index 33af8c3a..5a21a071 100644 --- a/src/r0/account/deactivate.rs +++ b/src/r0/account/deactivate.rs @@ -2,7 +2,9 @@ use ruma_api::ruma_api; -use super::{AuthenticationData, ThirdPartyIdRemovalStatus}; +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +use super::ThirdPartyIdRemovalStatus; ruma_api! { metadata { @@ -17,7 +19,7 @@ ruma_api! { request { /// Additional authentication information for the user-interactive authentication API. #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, /// Identity server from which to unbind the user's third party /// identifier. #[serde(skip_serializing_if = "Option::is_none")] @@ -29,5 +31,5 @@ ruma_api! { pub id_server_unbind_result: ThirdPartyIdRemovalStatus, } - error: crate::Error + error: UiaaResponse } diff --git a/src/r0/account/register.rs b/src/r0/account/register.rs index f517f4ca..27046397 100644 --- a/src/r0/account/register.rs +++ b/src/r0/account/register.rs @@ -4,7 +4,7 @@ use ruma_api::ruma_api; use ruma_identifiers::{DeviceId, UserId}; use serde::{Deserialize, Serialize}; -use super::AuthenticationData; +use crate::r0::uiaa::{AuthData, UiaaResponse}; ruma_api! { metadata { @@ -46,7 +46,7 @@ ruma_api! { /// It should be left empty, or omitted, unless an earlier call returned an response /// with status code 401. #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, /// Kind of account to register /// /// Defaults to `User` if omitted. @@ -73,7 +73,7 @@ ruma_api! { pub device_id: Option, } - error: crate::Error + error: UiaaResponse } /// The kind of account being registered. diff --git a/src/r0/device/delete_device.rs b/src/r0/device/delete_device.rs index b99b8592..2f5f33f0 100644 --- a/src/r0/device/delete_device.rs +++ b/src/r0/device/delete_device.rs @@ -1,9 +1,10 @@ //! [DELETE /_matrix/client/r0/devices/{deviceId}](https://matrix.org/docs/spec/client_server/r0.6.0#delete-matrix-client-r0-devices-deviceid) -use crate::r0::account::AuthenticationData; use ruma_api::ruma_api; use ruma_identifiers::DeviceId; +use crate::r0::uiaa::{AuthData, UiaaResponse}; + ruma_api! { metadata { description: "Delete a device for authenticated user.", @@ -20,10 +21,10 @@ ruma_api! { pub device_id: DeviceId, /// Additional authentication information for the user-interactive authentication API. #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, } response {} - error: crate::Error + error: UiaaResponse } diff --git a/src/r0/device/delete_devices.rs b/src/r0/device/delete_devices.rs index ef74419a..d8f35b6d 100644 --- a/src/r0/device/delete_devices.rs +++ b/src/r0/device/delete_devices.rs @@ -1,9 +1,10 @@ //! [POST /_matrix/client/r0/delete_devices](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-delete-devices) -use crate::r0::account::AuthenticationData; use ruma_api::ruma_api; use ruma_identifiers::DeviceId; +use crate::r0::uiaa::{AuthData, UiaaResponse}; + ruma_api! { metadata { description: "Delete specified devices.", @@ -20,10 +21,10 @@ ruma_api! { /// Additional authentication information for the user-interactive authentication API. #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, } response {} - error: crate::Error + error: UiaaResponse } diff --git a/src/r0/uiaa.rs b/src/r0/uiaa.rs new file mode 100644 index 00000000..bc6ac20b --- /dev/null +++ b/src/r0/uiaa.rs @@ -0,0 +1,363 @@ +//! Module for User-Interactive Authentication API types. + +use std::collections::BTreeMap; + +use ruma_api::{error::ResponseDeserializationError, EndpointError}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +use crate::error::{Error as MatrixError, ErrorBody}; + +/// Additional authentication information for the user-interactive authentication API. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +#[cfg_attr(test, derive(PartialEq))] +pub enum AuthData { + /// Used for sending UIAA authentication requests to the homeserver directly + /// from the client. + DirectRequest { + /// The login type that the client is attempting to complete. + #[serde(rename = "type")] + kind: String, + /// The value of the session key given by the homeserver. + #[serde(skip_serializing_if = "Option::is_none")] + session: Option, + /// Parameters submitted for a particular authentication stage. + #[serde(flatten)] + auth_parameters: BTreeMap, + }, + /// Used by the client to acknowledge that the user has completed a UIAA + /// stage through the fallback method. + FallbackAcknowledgement { + /// The value of the session key given by the homeserver. + session: String, + }, +} + +/// Information about available authentication flows and status for +/// User-Interactive Authenticiation API. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct UiaaInfo { + /// List of authentication flows available for this endpoint. + pub flows: Vec, + /// List of stages in the current flow completed by the client. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + /// Authentication parameters required for the client to complete authentication. + pub params: JsonValue, + /// Session key for client to use to complete authentication. + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + /// Authentication-related errors for previous request returned by homeserver. + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub auth_error: Option, +} + +/// Description of steps required to authenticate via the User-Interactive +/// Authentication API. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct AuthFlow { + /// Ordered list of stages required to complete authentication. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +/// Contains either a User-Interactive Authentication API response body or a +/// Matrix error. +#[derive(Clone, Debug)] +pub enum UiaaResponse { + /// User-Interactive Authentication API response + AuthResponse(UiaaInfo), + /// Matrix error response + MatrixError(MatrixError), +} + +impl From for UiaaResponse { + fn from(error: MatrixError) -> Self { + Self::MatrixError(error) + } +} + +impl EndpointError for UiaaResponse { + fn try_from_response( + response: http::Response>, + ) -> Result { + if response.status() == http::StatusCode::UNAUTHORIZED { + if let Ok(authentication_info) = serde_json::from_slice::(response.body()) { + return Ok(UiaaResponse::AuthResponse(authentication_info)); + } + } + + MatrixError::try_from_response(response).map(From::from) + } +} + +impl From for http::Response> { + fn from(uiaa_response: UiaaResponse) -> http::Response> { + match uiaa_response { + UiaaResponse::AuthResponse(authentication_info) => http::Response::builder() + .header(http::header::CONTENT_TYPE, "application/json") + .status(&http::StatusCode::UNAUTHORIZED) + .body(serde_json::to_vec(&authentication_info).unwrap()) + .unwrap(), + UiaaResponse::MatrixError(error) => http::Response::from(error), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use ruma_api::EndpointError; + use serde_json::{ + from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, + }; + + use super::{AuthData, AuthFlow, UiaaInfo, UiaaResponse}; + use crate::error::{ErrorBody, ErrorKind}; + + #[test] + fn test_serialize_authentication_data_direct_request() { + let mut auth_parameters = BTreeMap::new(); + auth_parameters.insert( + "example_credential".into(), + JsonValue::String("verypoorsharedsecret".into()), + ); + let authentication_data = AuthData::DirectRequest { + kind: "example.type.foo".to_string(), + session: Some("ZXY000".to_string()), + auth_parameters, + }; + + assert_eq!( + json!({ "type": "example.type.foo", "session": "ZXY000", "example_credential": "verypoorsharedsecret" }), + to_json_value(authentication_data).unwrap() + ); + } + + #[test] + fn test_deserialize_authentication_data_direct_request() { + let mut auth_parameters = BTreeMap::new(); + auth_parameters.insert( + "example_credential".into(), + JsonValue::String("verypoorsharedsecret".into()), + ); + let authentication_data = AuthData::DirectRequest { + kind: "example.type.foo".into(), + session: Some("opaque_session_id".to_string()), + auth_parameters, + }; + let json = json!({ "type": "example.type.foo", "session": "opaque_session_id", "example_credential": "verypoorsharedsecret", }); + + assert_eq!( + from_json_value::(json).unwrap(), + authentication_data + ); + } + + #[test] + fn test_serialize_authentication_data_fallback() { + let authentication_data = AuthData::FallbackAcknowledgement { + session: "ZXY000".to_string(), + }; + + assert_eq!( + json!({ "session": "ZXY000" }), + to_json_value(authentication_data).unwrap() + ); + } + + #[test] + fn test_deserialize_authentication_data_fallback() { + let authentication_data = AuthData::FallbackAcknowledgement { + session: "opaque_session_id".to_string(), + }; + let json = json!({ "session": "opaque_session_id" }); + + assert_eq!( + from_json_value::(json).unwrap(), + authentication_data + ); + } + + #[test] + fn test_serialize_uiaa_info() { + let params = json!({ + "example.type.baz": { + "example_key": "foobar" + } + }); + + let uiaa_info = UiaaInfo { + flows: vec![AuthFlow { + stages: vec!["m.login.password".to_string(), "m.login.dummy".to_string()], + }], + completed: vec!["m.login.password".to_string()], + params, + session: None, + auth_error: None, + }; + + let json = json!({ + "flows": [{ "stages": ["m.login.password", "m.login.dummy"] }], + "completed": ["m.login.password"], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + } + }); + assert_eq!(to_json_value(uiaa_info).unwrap(), json); + } + + #[test] + fn test_deserialize_uiaa_info() { + let json = json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": [ "example.type.foo" ], + "flows": [ + { + "stages": [ "example.type.foo", "example.type.bar" ] + }, + { + "stages": [ "example.type.foo", "example.type.baz" ] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" + }); + + let uiaa_info = UiaaInfo { + auth_error: Some(ErrorBody { + kind: ErrorKind::Forbidden, + message: "Invalid password".to_string(), + }), + completed: vec!["example.type.foo".to_string()], + flows: vec![ + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.bar".to_string(), + ], + }, + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.baz".to_string(), + ], + }, + ], + params: json!({ + "example.type.baz": { + "example_key": "foobar" + } + }), + session: Some("xxxxxx".to_string()), + }; + assert_eq!(from_json_value::(json).unwrap(), uiaa_info); + } + + #[test] + fn test_try_uiaa_response_into_http_response() { + let params = json!({ + "example.type.baz": { + "example_key": "foobar" + } + }); + + let uiaa_info = UiaaInfo { + flows: vec![AuthFlow { + stages: vec!["m.login.password".to_string(), "m.login.dummy".to_string()], + }], + completed: vec!["m.login.password".to_string()], + params, + session: None, + auth_error: None, + }; + let uiaa_response: http::Response> = + UiaaResponse::AuthResponse(uiaa_info.clone()).into(); + + assert_eq!( + serde_json::from_slice::(uiaa_response.body()).unwrap(), + uiaa_info, + ); + assert_eq!( + uiaa_response.status(), + http::status::StatusCode::UNAUTHORIZED + ); + } + + #[test] + fn test_try_uiaa_response_from_http_response() { + let json = serde_json::to_string(&json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": [ "example.type.foo" ], + "flows": [ + { + "stages": [ "example.type.foo", "example.type.bar" ] + }, + { + "stages": [ "example.type.foo", "example.type.baz" ] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" + })) + .unwrap(); + let http_response = http::Response::builder() + .status(http::StatusCode::UNAUTHORIZED) + .body(json.into()) + .unwrap(); + + let uiaa_info = UiaaInfo { + auth_error: Some(ErrorBody { + kind: ErrorKind::Forbidden, + message: "Invalid password".to_string(), + }), + completed: vec!["example.type.foo".to_string()], + flows: vec![ + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.bar".to_string(), + ], + }, + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.baz".to_string(), + ], + }, + ], + params: json!({ + "example.type.baz": { + "example_key": "foobar" + } + }), + session: Some("xxxxxx".to_string()), + }; + + let parsed_uiaa_info = match UiaaResponse::try_from_response(http_response) { + Ok(auth_response) => match auth_response { + UiaaResponse::AuthResponse(uiaa_info) => Some(uiaa_info), + _ => None, + }, + _ => None, + }; + + assert_eq!(parsed_uiaa_info, Some(uiaa_info)); + } +}