diff --git a/src/api/client_server/mod.rs b/src/api/client_server/mod.rs index afe5181e..a35d7a98 100644 --- a/src/api/client_server/mod.rs +++ b/src/api/client_server/mod.rs @@ -11,6 +11,7 @@ mod keys; mod media; mod membership; mod message; +mod openid; mod presence; mod profile; mod push; @@ -47,6 +48,7 @@ pub use keys::*; pub use media::*; pub use membership::*; pub use message::*; +pub use openid::*; pub use presence::*; pub use profile::*; pub use push::*; diff --git a/src/api/client_server/openid.rs b/src/api/client_server/openid.rs new file mode 100644 index 00000000..42160410 --- /dev/null +++ b/src/api/client_server/openid.rs @@ -0,0 +1,23 @@ +use std::time::Duration; + +use ruma::{api::client::account, authentication::TokenType}; + +use crate::{services, Result, Ruma}; + +/// # `POST /_matrix/client/r0/user/{userId}/openid/request_token` +/// +/// Request an OpenID token to verify identity with third-party services. +/// +/// - The token generated is only valid for the OpenID API. +pub async fn create_openid_token_route( + body: Ruma, +) -> Result { + let (access_token, expires_in) = services().users.create_openid_token(&body.user_id)?; + + Ok(account::request_openid_token::v3::Response { + access_token, + token_type: TokenType::Bearer, + matrix_server_name: services().globals.server_name().to_owned(), + expires_in: Duration::from_secs(expires_in), + }) +} diff --git a/src/api/ruma_wrapper/axum.rs b/src/api/ruma_wrapper/axum.rs index a56ee359..f5ef6050 100644 --- a/src/api/ruma_wrapper/axum.rs +++ b/src/api/ruma_wrapper/axum.rs @@ -102,10 +102,15 @@ where let (sender_user, sender_device, sender_servername, appservice_info) = match (metadata.authentication, token) { (_, Token::Invalid) => { - return Err(Error::BadRequest( - ErrorKind::UnknownToken { soft_logout: false }, - "Unknown access token.", - )) + // OpenID endpoint uses a query param with the same name, drop this once query params for user auth are removed from the spec + if query_params.access_token.is_some() { + (None, None, None, None) + } else { + return Err(Error::BadRequest( + ErrorKind::UnknownToken { soft_logout: false }, + "Unknown access token.", + )); + } } (AuthScheme::AccessToken, Token::Appservice(info)) => { let user_id = query_params diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 6ca352b8..13a6d648 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -24,6 +24,7 @@ use ruma::{ event::{get_event, get_missing_events, get_room_state, get_room_state_ids}, keys::{claim_keys, get_keys}, membership::{create_invite, create_join_event, prepare_join_event}, + openid::get_openid_userinfo, query::{get_profile_information, get_room_information}, transactions::{ edu::{DeviceListUpdateContent, DirectDeviceContent, Edu, SigningKeyUpdateContent}, @@ -1914,6 +1915,25 @@ pub async fn claim_keys_route( }) } +/// # `GET /_matrix/federation/v1/openid/userinfo` +/// +/// Get information about the user that generated the OpenID token. +pub async fn get_openid_userinfo_route( + body: Ruma, +) -> Result { + Ok(get_openid_userinfo::v1::Response::new( + services() + .users + .find_from_openid_token(&body.access_token)? + .ok_or_else(|| { + Error::BadRequest( + ErrorKind::Unauthorized, + "OpenID token has expired or does not exist.", + ) + })?, + )) +} + /// # `GET /.well-known/matrix/server` /// /// Returns the federation server discovery information. diff --git a/src/config/mod.rs b/src/config/mod.rs index 652b3a4c..378ab929 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -47,6 +47,8 @@ pub struct Config { #[serde(default = "false_fn")] pub allow_registration: bool, pub registration_token: Option, + #[serde(default = "default_openid_token_ttl")] + pub openid_token_ttl: u64, #[serde(default = "true_fn")] pub allow_encryption: bool, #[serde(default = "false_fn")] @@ -302,6 +304,10 @@ fn default_turn_ttl() -> u64 { 60 * 60 * 24 } +fn default_openid_token_ttl() -> u64 { + 60 * 60 +} + // I know, it's a great name pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 diff --git a/src/database/key_value/users.rs b/src/database/key_value/users.rs index 0e6db83a..63321a40 100644 --- a/src/database/key_value/users.rs +++ b/src/database/key_value/users.rs @@ -11,6 +11,7 @@ use ruma::{ use tracing::warn; use crate::{ + api::client_server::TOKEN_LENGTH, database::KeyValueDatabase, service::{self, users::clean_signatures}, services, utils, Error, Result, @@ -943,6 +944,52 @@ impl service::users::Data for KeyValueDatabase { Ok(None) } } + + // Creates an OpenID token, which can be used to prove that a user has access to an account (primarily for integrations) + fn create_openid_token(&self, user_id: &UserId) -> Result<(String, u64)> { + let token = utils::random_string(TOKEN_LENGTH); + + let expires_in = services().globals.config.openid_token_ttl; + let expires_at = utils::millis_since_unix_epoch() + .checked_add(expires_in * 1000) + .expect("time is valid"); + + let mut value = expires_at.to_be_bytes().to_vec(); + value.extend_from_slice(user_id.as_bytes()); + + self.openidtoken_expiresatuserid + .insert(token.as_bytes(), value.as_slice())?; + + Ok((token, expires_in)) + } + + /// Find out which user an OpenID access token belongs to. + fn find_from_openid_token(&self, token: &str) -> Result> { + let Some(value) = self.openidtoken_expiresatuserid.get(token.as_bytes())? else { + return Ok(None); + }; + let (expires_at_bytes, user_bytes) = value.split_at(0u64.to_be_bytes().len()); + + let expires_at = u64::from_be_bytes( + expires_at_bytes + .try_into() + .map_err(|_| Error::bad_database("expires_at in openid_userid is invalid u64."))?, + ); + + if expires_at < utils::millis_since_unix_epoch() { + self.openidtoken_expiresatuserid.remove(token.as_bytes())?; + + return Ok(None); + } + + Some( + UserId::parse(utils::string_from_bytes(user_bytes).map_err(|_| { + Error::bad_database("User ID in openid_userid is invalid unicode.") + })?) + .map_err(|_| Error::bad_database("User ID in openid_userid is invalid.")), + ) + .transpose() + } } impl KeyValueDatabase {} diff --git a/src/database/mod.rs b/src/database/mod.rs index 8d1b1913..f4740ff4 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -57,6 +57,7 @@ pub struct KeyValueDatabase { pub(super) userid_masterkeyid: Arc, pub(super) userid_selfsigningkeyid: Arc, pub(super) userid_usersigningkeyid: Arc, + pub(super) openidtoken_expiresatuserid: Arc, // expiresatuserid = expiresat + userid pub(super) userfilterid_filter: Arc, // UserFilterId = UserId + FilterId @@ -290,6 +291,7 @@ impl KeyValueDatabase { userid_masterkeyid: builder.open_tree("userid_masterkeyid")?, userid_selfsigningkeyid: builder.open_tree("userid_selfsigningkeyid")?, userid_usersigningkeyid: builder.open_tree("userid_usersigningkeyid")?, + openidtoken_expiresatuserid: builder.open_tree("openidtoken_expiresatuserid")?, userfilterid_filter: builder.open_tree("userfilterid_filter")?, todeviceid_events: builder.open_tree("todeviceid_events")?, diff --git a/src/main.rs b/src/main.rs index 5d60a6bf..6eeff9a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -277,6 +277,7 @@ fn routes(config: &Config) -> Router { .ruma_route(client_server::get_room_aliases_route) .ruma_route(client_server::get_filter_route) .ruma_route(client_server::create_filter_route) + .ruma_route(client_server::create_openid_token_route) .ruma_route(client_server::set_global_account_data_route) .ruma_route(client_server::set_room_account_data_route) .ruma_route(client_server::get_global_account_data_route) @@ -431,6 +432,7 @@ fn routes(config: &Config) -> Router { .ruma_route(server_server::get_profile_information_route) .ruma_route(server_server::get_keys_route) .ruma_route(server_server::claim_keys_route) + .ruma_route(server_server::get_openid_userinfo_route) .ruma_route(server_server::well_known_server) } else { router diff --git a/src/service/users/data.rs b/src/service/users/data.rs index ddf941e3..4566c36d 100644 --- a/src/service/users/data.rs +++ b/src/service/users/data.rs @@ -211,4 +211,10 @@ pub trait Data: Send + Sync { fn create_filter(&self, user_id: &UserId, filter: &FilterDefinition) -> Result; fn get_filter(&self, user_id: &UserId, filter_id: &str) -> Result>; + + // Creates an OpenID token, which can be used to prove that a user has access to an account (primarily for integrations) + fn create_openid_token(&self, user_id: &UserId) -> Result<(String, u64)>; + + /// Find out which user an OpenID access token belongs to. + fn find_from_openid_token(&self, token: &str) -> Result>; } diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 91331667..c3799586 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -598,6 +598,16 @@ impl Service { ) -> Result> { self.db.get_filter(user_id, filter_id) } + + // Creates an OpenID token, which can be used to prove that a user has access to an account (primarily for integrations) + pub fn create_openid_token(&self, user_id: &UserId) -> Result<(String, u64)> { + self.db.create_openid_token(user_id) + } + + /// Find out which user an OpenID access token belongs to. + pub fn find_from_openid_token(&self, token: &str) -> Result> { + self.db.find_from_openid_token(token) + } } /// Ensure that a user only sees signatures from themselves and the target user