//! Module for [User-Interactive Authentication API][uiaa] types. //! //! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api use std::{borrow::Cow, fmt}; use bytes::BufMut; use ruma_common::{ api::{error::IntoHttpError, EndpointError, OutgoingResponse}, serde::{from_raw_json_value, JsonObject, StringEnum}, thirdparty::Medium, ClientSecret, OwnedSessionId, OwnedUserId, }; use serde::{ de::{self, DeserializeOwned}, Deserialize, Deserializer, Serialize, }; use serde_json::{ from_slice as from_json_slice, value::RawValue as RawJsonValue, Value as JsonValue, }; use crate::{ error::{Error as MatrixError, StandardErrorBody}, PrivOwnedStr, }; pub mod get_uiaa_fallback_page; mod user_serde; /// Information for one authentication stage. #[derive(Clone, Serialize)] #[non_exhaustive] #[serde(untagged)] pub enum AuthData { /// Password-based authentication (`m.login.password`). Password(Password), /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). ReCaptcha(ReCaptcha), /// Email-based authentication (`m.login.email.identity`). EmailIdentity(EmailIdentity), /// Phone number-based authentication (`m.login.msisdn`). Msisdn(Msisdn), /// Dummy authentication (`m.login.dummy`). Dummy(Dummy), /// Registration token-based authentication (`m.login.registration_token`). RegistrationToken(RegistrationToken), /// Fallback acknowledgement. FallbackAcknowledgement(FallbackAcknowledgement), #[doc(hidden)] _Custom(CustomAuthData), } impl AuthData { /// Creates a new `AuthData` with the given `auth_type` string, session and data. /// /// Prefer to use the public variants of `AuthData` where possible; this constructor is meant to /// be used for unsupported authentication types only and does not allow setting arbitrary /// data for supported ones. /// /// # Errors /// /// Returns an error if the `auth_type` is known and serialization of `data` to the /// corresponding `AuthData` variant fails. pub fn new( auth_type: &str, session: Option, data: JsonObject, ) -> serde_json::Result { fn deserialize_variant( session: Option, mut obj: JsonObject, ) -> serde_json::Result { if let Some(session) = session { obj.insert("session".into(), session.into()); } serde_json::from_value(JsonValue::Object(obj)) } Ok(match auth_type { "m.login.password" => Self::Password(deserialize_variant(session, data)?), "m.login.recaptcha" => Self::ReCaptcha(deserialize_variant(session, data)?), "m.login.email.identity" => Self::EmailIdentity(deserialize_variant(session, data)?), "m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?), "m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?), "m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?), _ => { Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data }) } }) } /// Creates a new `AuthData::FallbackAcknowledgement` with the given session key. pub fn fallback_acknowledgement(session: String) -> Self { Self::FallbackAcknowledgement(FallbackAcknowledgement::new(session)) } /// Returns the value of the `type` field, if it exists. pub fn auth_type(&self) -> Option { match self { Self::Password(_) => Some(AuthType::Password), Self::ReCaptcha(_) => Some(AuthType::ReCaptcha), Self::EmailIdentity(_) => Some(AuthType::EmailIdentity), Self::Msisdn(_) => Some(AuthType::Msisdn), Self::Dummy(_) => Some(AuthType::Dummy), Self::RegistrationToken(_) => Some(AuthType::RegistrationToken), Self::FallbackAcknowledgement(_) => None, Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))), } } /// Returns the value of the `session` field, if it exists. pub fn session(&self) -> Option<&str> { match self { Self::Password(x) => x.session.as_deref(), Self::ReCaptcha(x) => x.session.as_deref(), Self::EmailIdentity(x) => x.session.as_deref(), Self::Msisdn(x) => x.session.as_deref(), Self::Dummy(x) => x.session.as_deref(), Self::RegistrationToken(x) => x.session.as_deref(), Self::FallbackAcknowledgement(x) => Some(&x.session), Self::_Custom(x) => x.session.as_deref(), } } /// Returns the associated data. /// /// The returned JSON object won't contain the `type` and `session` fields, use /// [`.auth_type()`][Self::auth_type] / [`.session()`](Self::session) to access those. /// /// Prefer to use the public variants of `AuthData` where possible; this method is meant to be /// used for custom auth types only. pub fn data(&self) -> Cow<'_, JsonObject> { fn serialize(obj: T) -> JsonObject { match serde_json::to_value(obj).expect("auth data serialization to succeed") { JsonValue::Object(obj) => obj, _ => panic!("all auth data variants must serialize to objects"), } } match self { Self::Password(x) => Cow::Owned(serialize(Password { identifier: x.identifier.clone(), password: x.password.clone(), session: None, })), Self::ReCaptcha(x) => { Cow::Owned(serialize(ReCaptcha { response: x.response.clone(), session: None })) } Self::EmailIdentity(x) => Cow::Owned(serialize(EmailIdentity { thirdparty_id_creds: x.thirdparty_id_creds.clone(), session: None, })), Self::Msisdn(x) => Cow::Owned(serialize(Msisdn { thirdparty_id_creds: x.thirdparty_id_creds.clone(), session: None, })), Self::RegistrationToken(x) => { Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None })) } // Dummy and fallback acknowledgement have no associated data Self::Dummy(_) | Self::FallbackAcknowledgement(_) => Cow::Owned(JsonObject::default()), Self::_Custom(c) => Cow::Borrowed(&c.extra), } } } impl fmt::Debug for AuthData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Print `Password { .. }` instead of `Password(Password { .. })` match self { Self::Password(inner) => inner.fmt(f), Self::ReCaptcha(inner) => inner.fmt(f), Self::EmailIdentity(inner) => inner.fmt(f), Self::Msisdn(inner) => inner.fmt(f), Self::Dummy(inner) => inner.fmt(f), Self::RegistrationToken(inner) => inner.fmt(f), Self::FallbackAcknowledgement(inner) => inner.fmt(f), Self::_Custom(inner) => inner.fmt(f), } } } impl<'de> Deserialize<'de> for AuthData { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; #[derive(Deserialize)] struct ExtractType<'a> { #[serde(borrow, rename = "type")] auth_type: Option>, } let auth_type = serde_json::from_str::>(json.get()) .map_err(de::Error::custom)? .auth_type; match auth_type.as_deref() { Some("m.login.password") => from_raw_json_value(&json).map(Self::Password), Some("m.login.recaptcha") => from_raw_json_value(&json).map(Self::ReCaptcha), Some("m.login.email.identity") => from_raw_json_value(&json).map(Self::EmailIdentity), Some("m.login.msisdn") => from_raw_json_value(&json).map(Self::Msisdn), Some("m.login.dummy") => from_raw_json_value(&json).map(Self::Dummy), Some("m.login.registration_token") => { from_raw_json_value(&json).map(Self::RegistrationToken) } None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement), Some(_) => from_raw_json_value(&json).map(Self::_Custom), } } } /// The type of an authentication stage. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] pub enum AuthType { /// Password-based authentication (`m.login.password`). #[ruma_enum(rename = "m.login.password")] Password, /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). #[ruma_enum(rename = "m.login.recaptcha")] ReCaptcha, /// Email-based authentication (`m.login.email.identity`). #[ruma_enum(rename = "m.login.email.identity")] EmailIdentity, /// Phone number-based authentication (`m.login.msisdn`). #[ruma_enum(rename = "m.login.msisdn")] Msisdn, /// SSO-based authentication (`m.login.sso`). #[ruma_enum(rename = "m.login.sso")] Sso, /// Dummy authentication (`m.login.dummy`). #[ruma_enum(rename = "m.login.dummy")] Dummy, /// Registration token-based authentication (`m.login.registration_token`). #[ruma_enum(rename = "m.login.registration_token")] RegistrationToken, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Data for password-based UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#password-based #[derive(Clone, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.password")] pub struct Password { /// One of the user's identifiers. pub identifier: UserIdentifier, /// The plaintext password. pub password: String, /// The value of the session key given by the homeserver, if any. pub session: Option, } impl Password { /// Creates a new `Password` with the given identifier and password. pub fn new(identifier: UserIdentifier, password: String) -> Self { Self { identifier, password, session: None } } } impl fmt::Debug for Password { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { identifier, password: _, session } = self; f.debug_struct("Password") .field("identifier", identifier) .field("session", session) .finish_non_exhaustive() } } /// Data for ReCaptcha UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#google-recaptcha #[derive(Clone, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.recaptcha")] pub struct ReCaptcha { /// The captcha response. pub response: String, /// The value of the session key given by the homeserver, if any. pub session: Option, } impl ReCaptcha { /// Creates a new `ReCaptcha` with the given response string. pub fn new(response: String) -> Self { Self { response, session: None } } } impl fmt::Debug for ReCaptcha { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { response: _, session } = self; f.debug_struct("ReCaptcha").field("session", session).finish_non_exhaustive() } } /// Data for Email-based UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#email-based-identity--homeserver #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.email.identity")] pub struct EmailIdentity { /// Thirdparty identifier credentials. #[serde(rename = "threepid_creds")] pub thirdparty_id_creds: ThirdpartyIdCredentials, /// The value of the session key given by the homeserver, if any. pub session: Option, } /// Data for phone number-based UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#phone-numbermsisdn-based-identity--homeserver #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.msisdn")] pub struct Msisdn { /// Thirdparty identifier credentials. #[serde(rename = "threepid_creds")] pub thirdparty_id_creds: ThirdpartyIdCredentials, /// The value of the session key given by the homeserver, if any. pub session: Option, } /// Data for dummy UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#dummy-auth #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.dummy")] pub struct Dummy { /// The value of the session key given by the homeserver, if any. pub session: Option, } impl Dummy { /// Creates an empty `Dummy`. pub fn new() -> Self { Self::default() } } /// Data for registration token-based UIAA flow. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#token-authenticated-registration #[derive(Clone, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "m.login.registration_token")] pub struct RegistrationToken { /// The registration token. pub token: String, /// The value of the session key given by the homeserver, if any. pub session: Option, } impl RegistrationToken { /// Creates a new `RegistrationToken` with the given token. pub fn new(token: String) -> Self { Self { token, session: None } } } impl fmt::Debug for RegistrationToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { token: _, session } = self; f.debug_struct("RegistrationToken").field("session", session).finish_non_exhaustive() } } /// Data for UIAA fallback acknowledgement. /// /// See [the spec] for how to use this. /// /// [the spec]: https://spec.matrix.org/latest/client-server-api/#fallback #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct FallbackAcknowledgement { /// The value of the session key given by the homeserver. pub session: String, } impl FallbackAcknowledgement { /// Creates a new `FallbackAcknowledgement` with the given session key. pub fn new(session: String) -> Self { Self { session } } } #[doc(hidden)] #[derive(Clone, Deserialize, Serialize)] #[non_exhaustive] pub struct CustomAuthData { #[serde(rename = "type")] auth_type: String, session: Option, #[serde(flatten)] extra: JsonObject, } impl fmt::Debug for CustomAuthData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { auth_type, session, extra: _ } = self; f.debug_struct("CustomAuthData") .field("auth_type", auth_type) .field("session", session) .finish_non_exhaustive() } } /// Identification information for the user. #[derive(Clone, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum UserIdentifier { /// Either a fully qualified Matrix user ID, or just the localpart (as part of the 'identifier' /// field). UserIdOrLocalpart(String), /// An email address. Email { /// The email address. address: String, }, /// A phone number in the MSISDN format. Msisdn { /// The phone number according to the [E.164] numbering plan. /// /// [E.164]: https://www.itu.int/rec/T-REC-E.164-201011-I/en number: String, }, /// A phone number as a separate country code and phone number. /// /// The homeserver will be responsible for canonicalizing this to the MSISDN format. PhoneNumber { /// The country that the phone number is from. /// /// This is a two-letter uppercase [ISO-3166-1 alpha-2] country code. /// /// [ISO-3166-1 alpha-2]: https://www.iso.org/iso-3166-country-codes.html country: String, /// The phone number. phone: String, }, #[doc(hidden)] _CustomThirdParty(CustomThirdPartyId), } impl UserIdentifier { /// Creates a new `UserIdentifier` from the given third party identifier. pub fn third_party_id(medium: Medium, address: String) -> Self { match medium { Medium::Email => Self::Email { address }, Medium::Msisdn => Self::Msisdn { number: address }, _ => Self::_CustomThirdParty(CustomThirdPartyId { medium, address }), } } /// Get this `UserIdentifier` as a third party identifier if it is one. pub fn as_third_party_id(&self) -> Option<(&Medium, &str)> { match self { Self::Email { address } => Some((&Medium::Email, address)), Self::Msisdn { number } => Some((&Medium::Msisdn, number)), Self::_CustomThirdParty(CustomThirdPartyId { medium, address }) => { Some((medium, address)) } _ => None, } } } impl From for UserIdentifier { fn from(id: OwnedUserId) -> Self { Self::UserIdOrLocalpart(id.into()) } } impl From<&OwnedUserId> for UserIdentifier { fn from(id: &OwnedUserId) -> Self { Self::UserIdOrLocalpart(id.as_str().to_owned()) } } #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[non_exhaustive] pub struct CustomThirdPartyId { medium: Medium, address: String, } #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[non_exhaustive] pub struct IncomingCustomThirdPartyId { medium: Medium, address: String, } /// Credentials for third-party authentication (e.g. email / phone number). #[derive(Clone, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThirdpartyIdCredentials { /// Identity server session ID. pub sid: OwnedSessionId, /// Identity server client secret. pub client_secret: Box, /// Identity server URL. pub id_server: String, /// Identity server access token. pub id_access_token: String, } impl ThirdpartyIdCredentials { /// Creates a new `ThirdpartyIdCredentials` with the given session ID, client secret, identity /// server address and access token. pub fn new( sid: OwnedSessionId, client_secret: Box, id_server: String, id_access_token: String, ) -> Self { Self { sid, client_secret, id_server, id_access_token } } } impl fmt::Debug for ThirdpartyIdCredentials { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { sid, client_secret: _, id_server, id_access_token } = self; f.debug_struct("ThirdpartyIdCredentials") .field("sid", sid) .field("id_server", id_server) .field("id_access_token", id_access_token) .finish_non_exhaustive() } } /// Information about available authentication flows and status for User-Interactive Authenticiation /// API. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] 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. /// /// To create a `Box`, use `serde_json::value::to_raw_value`. pub params: Box, /// 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, } impl UiaaInfo { /// Creates a new `UiaaInfo` with the given flows and parameters. pub fn new(flows: Vec, params: Box) -> Self { Self { flows, completed: Vec::new(), params, session: None, auth_error: None } } } /// Description of steps required to authenticate via the User-Interactive Authentication API. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct AuthFlow { /// Ordered list of stages required to complete authentication. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub stages: Vec, } impl AuthFlow { /// Creates a new `AuthFlow` with the given stages. /// /// To create an empty `AuthFlow`, use `AuthFlow::default()`. pub fn new(stages: Vec) -> Self { Self { stages } } } /// Contains either a User-Interactive Authentication API response body or a Matrix error. #[derive(Clone, Debug)] #[allow(clippy::exhaustive_enums)] pub enum UiaaResponse { /// User-Interactive Authentication API response AuthResponse(UiaaInfo), /// Matrix error response MatrixError(MatrixError), } impl fmt::Display for UiaaResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AuthResponse(_) => write!(f, "User-Interactive Authentication required."), Self::MatrixError(err) => write!(f, "{err}"), } } } impl From for UiaaResponse { fn from(error: MatrixError) -> Self { Self::MatrixError(error) } } impl EndpointError for UiaaResponse { fn from_http_response>(response: http::Response) -> Self { if response.status() == http::StatusCode::UNAUTHORIZED { if let Ok(uiaa_info) = from_json_slice(response.body().as_ref()) { return Self::AuthResponse(uiaa_info); } } Self::MatrixError(MatrixError::from_http_response(response)) } } impl std::error::Error for UiaaResponse {} impl OutgoingResponse for UiaaResponse { fn try_into_http_response( self, ) -> Result, IntoHttpError> { match self { UiaaResponse::AuthResponse(authentication_info) => http::Response::builder() .header(http::header::CONTENT_TYPE, "application/json") .status(&http::StatusCode::UNAUTHORIZED) .body(ruma_common::serde::json_to_buf(&authentication_info)?) .map_err(Into::into), UiaaResponse::MatrixError(error) => error.try_into_http_response(), } } }