Kévin Commaille 90cef5a50b client-api: Add support for refresh tokens
According to MSC2918
2022-06-11 19:31:09 +02:00

487 lines
17 KiB
Rust

//! `POST /_matrix/client/*/login`
pub mod v3 {
//! `/v3/` ([spec])
//!
//! [spec]: https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login
#[cfg(feature = "unstable-msc2918")]
use std::time::Duration;
use ruma_common::{
api::ruma_api,
serde::{Incoming, JsonObject},
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId,
};
use serde::{
de::{self, DeserializeOwned},
Deserialize, Deserializer, Serialize,
};
use serde_json::Value as JsonValue;
use crate::uiaa::{IncomingUserIdentifier, UserIdentifier};
ruma_api! {
metadata: {
description: "Login to the homeserver.",
method: POST,
name: "login",
r0_path: "/_matrix/client/r0/login",
stable_path: "/_matrix/client/v3/login",
rate_limited: true,
authentication: None,
added: 1.0,
}
request: {
/// The authentication mechanism.
#[serde(flatten)]
pub login_info: LoginInfo<'a>,
/// ID of the client device
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<&'a DeviceId>,
/// A display name to assign to the newly-created device.
///
/// Ignored if `device_id` corresponds to a known device.
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_device_display_name: Option<&'a str>,
/// If set to `true`, the client supports refresh tokens.
#[cfg(feature = "unstable-msc2918")]
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
rename = "org.matrix.msc2918.refresh_token",
alias = "refresh_token",
)]
pub refresh_token: bool,
}
response: {
/// The fully-qualified Matrix ID that has been registered.
pub user_id: OwnedUserId,
/// An access token for the account.
pub access_token: String,
/// The hostname of the homeserver on which the account has been registered.
///
/// Deprecated: Clients should instead use the `user_id.server_name()`
/// method if they require it.
#[serde(skip_serializing_if = "Option::is_none")]
pub home_server: Option<OwnedServerName>,
/// ID of the logged-in device.
///
/// Will be the same as the corresponding parameter in the request, if one was
/// specified.
pub device_id: OwnedDeviceId,
/// Client configuration provided by the server.
///
/// If present, clients SHOULD use the provided object to reconfigure themselves.
#[serde(skip_serializing_if = "Option::is_none")]
pub well_known: Option<DiscoveryInfo>,
/// A refresh token for the account.
///
/// This token can be used to obtain a new access token when it expires by calling the
/// `/refresh` endpoint.
#[cfg(feature = "unstable-msc2918")]
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
/// The lifetime of the access token, in milliseconds.
///
/// Once the access token has expired, a new access token can be obtained by using the
/// provided refresh token. If no refresh token is provided, the client will need to
/// re-login to obtain a new access token.
///
/// If this is `None`, the client can assume that the access token will not expire.
#[cfg(feature = "unstable-msc2918")]
#[serde(
with = "ruma_common::serde::duration::opt_ms",
default,
skip_serializing_if = "Option::is_none",
rename = "expires_in_ms",
)]
pub expires_in: Option<Duration>,
}
error: crate::Error
}
impl<'a> Request<'a> {
/// Creates a new `Request` with the given login info.
pub fn new(login_info: LoginInfo<'a>) -> Self {
Self {
login_info,
device_id: None,
initial_device_display_name: None,
#[cfg(feature = "unstable-msc2918")]
refresh_token: false,
}
}
}
impl Response {
/// Creates a new `Response` with the given user ID, access token and device ID.
pub fn new(user_id: OwnedUserId, access_token: String, device_id: OwnedDeviceId) -> Self {
Self {
user_id,
access_token,
home_server: None,
device_id,
well_known: None,
#[cfg(feature = "unstable-msc2918")]
refresh_token: None,
#[cfg(feature = "unstable-msc2918")]
expires_in: None,
}
}
}
/// The authentication mechanism.
///
/// To construct the custom `LoginInfo` variant you first have to construct
/// [`IncomingLoginInfo::new`] and then call [`IncomingLoginInfo::to_outgoing`] on it.
#[derive(Clone, Debug, Incoming, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[incoming_derive(!Deserialize)]
#[serde(untagged)]
pub enum LoginInfo<'a> {
/// An identifier and password are supplied to authenticate.
Password(Password<'a>),
/// Token-based login.
Token(Token<'a>),
/// Application Service-specific login.
ApplicationService(ApplicationService<'a>),
#[doc(hidden)]
_Custom(CustomLoginInfo<'a>),
}
impl IncomingLoginInfo {
/// Creates a new `IncomingLoginInfo` with the given `login_type` string, session and data.
///
/// Prefer to use the public variants of `IncomingLoginInfo` where possible; this
/// constructor is meant be used for unsupported authentication mechanisms only and
/// does not allow setting arbitrary data for supported ones.
///
/// # Errors
///
/// Returns an error if the `login_type` is known and serialization of `data` to the
/// corresponding `IncomingLoginInfo` variant fails.
pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
Ok(match login_type {
"m.login.password" => {
Self::Password(serde_json::from_value(JsonValue::Object(data))?)
}
"m.login.token" => Self::Token(serde_json::from_value(JsonValue::Object(data))?),
"m.login.application_service" => {
Self::ApplicationService(serde_json::from_value(JsonValue::Object(data))?)
}
_ => Self::_Custom(IncomingCustomLoginInfo {
login_type: login_type.into(),
extra: data,
}),
})
}
/// Convert `IncomingLoginInfo` to `LoginInfo`.
pub fn to_outgoing(&self) -> LoginInfo<'_> {
match self {
Self::Password(a) => LoginInfo::Password(a.to_outgoing()),
Self::Token(a) => LoginInfo::Token(a.to_outgoing()),
Self::ApplicationService(a) => LoginInfo::ApplicationService(a.to_outgoing()),
Self::_Custom(a) => LoginInfo::_Custom(CustomLoginInfo {
login_type: &a.login_type,
extra: &a.extra,
}),
}
}
}
impl<'de> Deserialize<'de> for IncomingLoginInfo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
fn from_json_value<T: DeserializeOwned, E: de::Error>(val: JsonValue) -> Result<T, E> {
serde_json::from_value(val).map_err(E::custom)
}
// FIXME: Would be better to use serde_json::value::RawValue, but that would require
// implementing Deserialize manually for Request, bc. `#[serde(flatten)]` breaks things.
let json = JsonValue::deserialize(deserializer)?;
let login_type =
json["type"].as_str().ok_or_else(|| de::Error::missing_field("type"))?;
match login_type {
"m.login.password" => from_json_value(json).map(Self::Password),
"m.login.token" => from_json_value(json).map(Self::Token),
_ => from_json_value(json).map(Self::_Custom),
}
}
}
/// An identifier and password to supply as authentication.
#[derive(Clone, Debug, Incoming, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.password")]
pub struct Password<'a> {
/// Identification information for the user.
pub identifier: UserIdentifier<'a>,
/// The password.
pub password: &'a str,
}
impl<'a> Password<'a> {
/// Creates a new `Password` with the given identifier and password.
pub fn new(identifier: UserIdentifier<'a>, password: &'a str) -> Self {
Self { identifier, password }
}
}
impl IncomingPassword {
/// Convert `IncomingPassword` to `Password`.
fn to_outgoing(&self) -> Password<'_> {
Password { identifier: self.identifier.to_outgoing(), password: &self.password }
}
}
/// A token to supply as authentication.
#[derive(Clone, Debug, Incoming, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.token")]
pub struct Token<'a> {
/// The token.
pub token: &'a str,
}
impl<'a> Token<'a> {
/// Creates a new `Token` with the given token.
pub fn new(token: &'a str) -> Self {
Self { token }
}
}
impl IncomingToken {
/// Convert `IncomingToken` to `Token`.
fn to_outgoing(&self) -> Token<'_> {
Token { token: &self.token }
}
}
/// An identifier to supply for Application Service authentication.
#[derive(Clone, Debug, Incoming, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.application_service")]
pub struct ApplicationService<'a> {
/// Identification information for the user.
pub identifier: UserIdentifier<'a>,
}
impl<'a> ApplicationService<'a> {
/// Creates a new `ApplicationService` with the given identifier.
pub fn new(identifier: UserIdentifier<'a>) -> Self {
Self { identifier }
}
}
impl IncomingApplicationService {
/// Convert `IncomingApplicationService` to `ApplicationService`.
fn to_outgoing(&self) -> ApplicationService<'_> {
ApplicationService { identifier: self.identifier.to_outgoing() }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct CustomLoginInfo<'a> {
#[serde(rename = "type")]
login_type: &'a str,
#[serde(flatten)]
extra: &'a JsonObject,
}
#[doc(hidden)]
#[derive(Clone, Debug, Deserialize)]
#[non_exhaustive]
pub struct IncomingCustomLoginInfo {
#[serde(rename = "type")]
login_type: String,
#[serde(flatten)]
extra: JsonObject,
}
/// Client configuration provided by the server.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct DiscoveryInfo {
/// Information about the homeserver to connect to.
#[serde(rename = "m.homeserver")]
pub homeserver: HomeserverInfo,
/// Information about the identity server to connect to.
#[serde(rename = "m.identity_server")]
pub identity_server: Option<IdentityServerInfo>,
}
impl DiscoveryInfo {
/// Create a new `DiscoveryInfo` with the given homeserver.
pub fn new(homeserver: HomeserverInfo) -> Self {
Self { homeserver, identity_server: None }
}
}
/// Information about the homeserver to connect to.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct HomeserverInfo {
/// The base URL for the homeserver for client-server connections.
pub base_url: String,
}
impl HomeserverInfo {
/// Create a new `HomeserverInfo` with the given base url.
pub fn new(base_url: String) -> Self {
Self { base_url }
}
}
/// Information about the identity server to connect to.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct IdentityServerInfo {
/// The base URL for the identity server for client-server connections.
pub base_url: String,
}
impl IdentityServerInfo {
/// Create a new `IdentityServerInfo` with the given base url.
pub fn new(base_url: String) -> Self {
Self { base_url }
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use serde_json::{from_value as from_json_value, json};
use super::{IncomingLoginInfo, IncomingToken};
use crate::uiaa::IncomingUserIdentifier;
#[test]
fn deserialize_login_type() {
let login = assert_matches!(
from_json_value(json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "cheeky_monkey"
},
"password": "ilovebananas"
}))
.unwrap(),
IncomingLoginInfo::Password(login) => login
);
let user = assert_matches!(
login.identifier,
IncomingUserIdentifier::UserIdOrLocalpart(user) => user
);
assert_eq!(user, "cheeky_monkey");
assert_eq!(login.password, "ilovebananas");
let token = assert_matches!(
from_json_value(json!({
"type": "m.login.token",
"token": "1234567890abcdef"
}))
.unwrap(),
IncomingLoginInfo::Token(IncomingToken { token }) => token
);
assert_eq!(token, "1234567890abcdef");
}
#[test]
#[cfg(feature = "client")]
fn serialize_login_request_body() {
use ruma_common::{
api::{MatrixVersion, OutgoingRequest, SendAccessToken},
thirdparty::Medium,
};
use serde_json::Value as JsonValue;
use super::{LoginInfo, Password, Request, Token};
use crate::uiaa::UserIdentifier;
let req: http::Request<Vec<u8>> = Request {
login_info: LoginInfo::Token(Token { token: "0xdeadbeef" }),
device_id: None,
initial_device_display_name: Some("test"),
#[cfg(feature = "unstable-msc2918")]
refresh_token: false,
}
.try_into_http_request(
"https://homeserver.tld",
SendAccessToken::None,
&[MatrixVersion::V1_1],
)
.unwrap();
let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
assert_eq!(
req_body_value,
json!({
"type": "m.login.token",
"token": "0xdeadbeef",
"initial_device_display_name": "test",
})
);
let req: http::Request<Vec<u8>> = Request {
login_info: LoginInfo::Password(Password {
identifier: UserIdentifier::ThirdPartyId {
address: "hello@example.com",
medium: Medium::Email,
},
password: "deadbeef",
}),
device_id: None,
initial_device_display_name: Some("test"),
#[cfg(feature = "unstable-msc2918")]
refresh_token: false,
}
.try_into_http_request(
"https://homeserver.tld",
SendAccessToken::None,
&[MatrixVersion::V1_1],
)
.unwrap();
let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
assert_eq!(
req_body_value,
json!({
"identifier": {
"type": "m.id.thirdparty",
"medium": "email",
"address": "hello@example.com"
},
"type": "m.login.password",
"password": "deadbeef",
"initial_device_display_name": "test",
})
);
}
}
}