From 1a64e42cfe03db40295400a185c5032e4a8a9044 Mon Sep 17 00:00:00 2001 From: strawberry Date: Tue, 2 Jul 2024 21:51:11 -0400 Subject: [PATCH] add /openid/request_token and /openid/userinfo routes heavily changed and improved by me Co-authored-by: mikoto Signed-off-by: strawberry --- conduwuit-example.toml | 7 ++++++ src/api/client/mod.rs | 2 ++ src/api/client/openid.rs | 41 ++++++++++++++++++++++++++++++++++ src/api/router/auth.rs | 26 +++++++++++++++++----- src/api/routes.rs | 2 ++ src/api/server/mod.rs | 2 ++ src/api/server/openid.rs | 16 +++++++++++++ src/core/config/mod.rs | 5 +++++ src/database/maps.rs | 1 + src/service/users/data.rs | 47 ++++++++++++++++++++++++++++++++++++++- src/service/users/mod.rs | 9 ++++++++ 11 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/api/client/openid.rs create mode 100644 src/api/server/openid.rs diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 9c6b7f0c..516a75a2 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -389,6 +389,13 @@ allow_profile_lookup_federation_requests = true # setting this to false may reduce startup time. #media_statup_check = true +# OpenID token expiration/TTL in seconds +# +# These are the OpenID tokens that are primarily used for Matrix account integrations, *not* OIDC/OpenID Connect/etc +# +# Defaults to 3600 (1 hour) +#openid_token_ttl = 3600 + ### Generic database options diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index 79088a5b..cf13cf7e 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -11,6 +11,7 @@ pub(super) mod keys; pub(super) mod media; pub(super) mod membership; pub(super) mod message; +pub(super) mod openid; pub(super) mod presence; pub(super) mod profile; pub(super) mod push; @@ -48,6 +49,7 @@ pub(super) use media::*; pub(super) use membership::*; pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id}; pub(super) use message::*; +pub(super) use openid::*; pub(super) use presence::*; pub(super) use profile::*; pub use profile::{update_all_rooms, update_avatar_url, update_displayname}; diff --git a/src/api/client/openid.rs b/src/api/client/openid.rs new file mode 100644 index 00000000..a1932052 --- /dev/null +++ b/src/api/client/openid.rs @@ -0,0 +1,41 @@ +use std::time::Duration; + +use conduit::utils; +use ruma::{ + api::client::{account, error::ErrorKind}, + authentication::TokenType, +}; + +use super::TOKEN_LENGTH; +use crate::{services, Error, Result, Ruma}; + +/// # `POST /_matrix/client/v3/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(crate) async fn create_openid_token_route( + body: Ruma, +) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if sender_user != &body.user_id { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Not allowed to request OpenID tokens on behalf of other users", + )); + } + + let access_token = utils::random_string(TOKEN_LENGTH); + + let expires_in = services() + .users + .create_openid_token(&body.user_id, &access_token)?; + + Ok(account::request_openid_token::v3::Response { + access_token, + token_type: TokenType::Bearer, + matrix_server_name: services().globals.config.server_name.clone(), + expires_in: Duration::from_secs(expires_in), + }) +} diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index f6dcc01f..da36c82c 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -76,12 +76,26 @@ pub(super) async fn auth( } match (metadata.authentication, token) { - (_, Token::Invalid) => Err(Error::BadRequest( - ErrorKind::UnknownToken { - soft_logout: false, - }, - "Unknown access token.", - )), + (_, Token::Invalid) => { + // OpenID endpoint uses a query param with the same name, drop this once query + // params for user auth are removed from the spec. This is required to make + // integration manager work. + if request.query.access_token.is_some() && request.parts.uri.path().contains("/openid/") { + Ok(Auth { + origin: None, + sender_user: None, + sender_device: None, + appservice_info: None, + }) + } else { + Err(Error::BadRequest( + ErrorKind::UnknownToken { + soft_logout: false, + }, + "Unknown access token.", + )) + } + }, (AuthScheme::AccessToken, Token::Appservice(info)) => Ok(auth_appservice(request, info)?), (AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken, Token::Appservice(info)) => { Ok(Auth { diff --git a/src/api/routes.rs b/src/api/routes.rs index a733ebe0..7510c6b9 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -39,6 +39,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(client::get_room_aliases_route) .ruma_route(client::get_filter_route) .ruma_route(client::create_filter_route) + .ruma_route(client::create_openid_token_route) .ruma_route(client::set_global_account_data_route) .ruma_route(client::set_room_account_data_route) .ruma_route(client::get_global_account_data_route) @@ -212,6 +213,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(server::get_profile_information_route) .ruma_route(server::get_keys_route) .ruma_route(server::claim_keys_route) + .ruma_route(server::get_openid_userinfo_route) .ruma_route(server::get_hierarchy_route) .ruma_route(server::well_known_server) .route("/_conduwuit/local_user_count", get(client::conduwuit_local_user_count)) diff --git a/src/api/server/mod.rs b/src/api/server/mod.rs index dce9c6ff..5d46a866 100644 --- a/src/api/server/mod.rs +++ b/src/api/server/mod.rs @@ -7,6 +7,7 @@ pub(super) mod invite; pub(super) mod key; pub(super) mod make_join; pub(super) mod make_leave; +pub(super) mod openid; pub(super) mod publicrooms; pub(super) mod query; pub(super) mod send; @@ -27,6 +28,7 @@ pub(super) use invite::*; pub(super) use key::*; pub(super) use make_join::*; pub(super) use make_leave::*; +pub(super) use openid::*; pub(super) use publicrooms::*; pub(super) use query::*; pub(super) use send::*; diff --git a/src/api/server/openid.rs b/src/api/server/openid.rs new file mode 100644 index 00000000..322c7343 --- /dev/null +++ b/src/api/server/openid.rs @@ -0,0 +1,16 @@ +use ruma::api::federation::openid::get_openid_userinfo; + +use crate::{services, Result, Ruma}; + +/// # `GET /_matrix/federation/v1/openid/userinfo` +/// +/// Get information about the user that generated the OpenID token. +pub(crate) 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)?, + )) +} diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 0b9cb299..b808f196 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -201,6 +201,8 @@ pub struct Config { pub query_trusted_key_servers_first: bool, #[serde(default = "default_log")] pub log: String, + #[serde(default = "default_openid_token_ttl")] + pub openid_token_ttl: u64, #[serde(default)] pub turn_username: String, #[serde(default)] @@ -671,6 +673,7 @@ impl fmt::Display for Config { "Query Trusted Key Servers First", &self.query_trusted_key_servers_first.to_string(), ), + ("OpenID Token TTL", &self.openid_token_ttl.to_string()), ( "TURN username", if self.turn_username.is_empty() { @@ -984,6 +987,8 @@ pub fn default_log() -> String { fn default_notification_push_path() -> String { "/_matrix/push/v1/notify".to_owned() } +fn default_openid_token_ttl() -> u64 { 60 * 60 } + fn default_turn_ttl() -> u64 { 60 * 60 * 24 } fn default_presence_idle_timeout_s() -> u64 { 5 * 60 } diff --git a/src/database/maps.rs b/src/database/maps.rs index 94e8ba99..1e09041c 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -91,6 +91,7 @@ pub const MAPS: &[&str] = &[ "userid_presenceid", "userid_selfsigningkeyid", "userid_usersigningkeyid", + "openidtoken_expiresatuserid", "userroomid_highlightcount", "userroomid_invitestate", "userroomid_joined", diff --git a/src/service/users/data.rs b/src/service/users/data.rs index cc40aa52..14aa8006 100644 --- a/src/service/users/data.rs +++ b/src/service/users/data.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, mem::size_of, sync::Arc}; -use conduit::{utils, warn, Error, Result}; +use conduit::{debug_info, utils, warn, Error, Result}; use database::{Database, Map}; use ruma::{ api::client::{device::Device, error::ErrorKind, filter::FilterDefinition}, @@ -28,6 +28,7 @@ pub struct Data { userid_masterkeyid: Arc, userid_selfsigningkeyid: Arc, userid_usersigningkeyid: Arc, + openidtoken_expiresatuserid: Arc, keychangeid_userid: Arc, todeviceid_events: Arc, userfilterid_filter: Arc, @@ -51,6 +52,7 @@ impl Data { userid_masterkeyid: db["userid_masterkeyid"].clone(), userid_selfsigningkeyid: db["userid_selfsigningkeyid"].clone(), userid_usersigningkeyid: db["userid_usersigningkeyid"].clone(), + openidtoken_expiresatuserid: db["openidtoken_expiresatuserid"].clone(), keychangeid_userid: db["keychangeid_userid"].clone(), todeviceid_events: db["todeviceid_events"].clone(), userfilterid_filter: db["userfilterid_filter"].clone(), @@ -920,6 +922,49 @@ impl Data { Ok(None) } } + + /// Creates an OpenID token, which can be used to prove that a user has + /// access to an account (primarily for integrations) + pub(super) fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result { + let expires_in = services().globals.config.openid_token_ttl; + let expires_at = utils::millis_since_unix_epoch().saturating_add(expires_in * 1000); + + 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(expires_in) + } + + /// Find out which user an OpenID access token belongs to. + pub(super) fn find_from_openid_token(&self, token: &str) -> Result { + let Some(value) = self.openidtoken_expiresatuserid.get(token.as_bytes())? else { + return Err(Error::BadRequest(ErrorKind::Unauthorized, "OpenID token is unrecognised")); + }; + + let (expires_at_bytes, user_bytes) = value.split_at(0_u64.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() { + debug_info!("OpenID token is expired, removing"); + self.openidtoken_expiresatuserid.remove(token.as_bytes())?; + + return Err(Error::BadRequest(ErrorKind::Unauthorized, "OpenID token is expired")); + } + + 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.")) + } } /// Will only return with Some(username) if the password was not empty and the diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index e7560436..8444d538 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -477,6 +477,15 @@ impl Service { pub fn get_filter(&self, user_id: &UserId, filter_id: &str) -> 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, token: &str) -> Result { + self.db.create_openid_token(user_id, token) + } + + /// 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