mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-18 00:01:24 +01:00
OpenID routes
Cherry-picked from https://gitlab.com/famedly/conduit/-/merge_requests/681. Co-Authored-By: Matthias Ahouansou <matthias@ahouansou.cz>
This commit is contained in:
parent
6cb7896e17
commit
2e57497bc8
11 changed files with 161 additions and 6 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
doc-valid-idents = [
|
doc-valid-idents = [
|
||||||
"SemVer",
|
"SemVer",
|
||||||
|
"OpenID",
|
||||||
"..",
|
"..",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ mod keys;
|
||||||
mod media;
|
mod media;
|
||||||
mod membership;
|
mod membership;
|
||||||
mod message;
|
mod message;
|
||||||
|
mod openid;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod push;
|
mod push;
|
||||||
mod read_marker;
|
mod read_marker;
|
||||||
|
|
@ -45,6 +46,7 @@ pub(crate) use keys::*;
|
||||||
pub(crate) use media::*;
|
pub(crate) use media::*;
|
||||||
pub(crate) use membership::*;
|
pub(crate) use membership::*;
|
||||||
pub(crate) use message::*;
|
pub(crate) use message::*;
|
||||||
|
pub(crate) use openid::*;
|
||||||
pub(crate) use profile::*;
|
pub(crate) use profile::*;
|
||||||
pub(crate) use push::*;
|
pub(crate) use push::*;
|
||||||
pub(crate) use read_marker::*;
|
pub(crate) use read_marker::*;
|
||||||
|
|
|
||||||
24
src/api/client_server/openid.rs
Normal file
24
src/api/client_server/openid.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use ruma::{api::client::account, authentication::TokenType};
|
||||||
|
|
||||||
|
use crate::{services, Ar, Ra, Result};
|
||||||
|
|
||||||
|
/// # `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(crate) async fn create_openid_token_route(
|
||||||
|
body: Ar<account::request_openid_token::v3::Request>,
|
||||||
|
) -> Result<Ra<account::request_openid_token::v3::Response>> {
|
||||||
|
let (access_token, expires_in) =
|
||||||
|
services().users.create_openid_token(&body.user_id)?;
|
||||||
|
|
||||||
|
Ok(Ra(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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
@ -117,12 +117,19 @@ async fn ar_from_request_inner(
|
||||||
let (sender_user, sender_device, sender_servername, appservice_info) =
|
let (sender_user, sender_device, sender_servername, appservice_info) =
|
||||||
match (metadata.authentication, token) {
|
match (metadata.authentication, token) {
|
||||||
(_, Token::Invalid) => {
|
(_, 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
|
||||||
|
if query_params.access_token.is_some() {
|
||||||
|
(None, None, None, None)
|
||||||
|
} else {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
ErrorKind::UnknownToken {
|
ErrorKind::UnknownToken {
|
||||||
soft_logout: false,
|
soft_logout: false,
|
||||||
},
|
},
|
||||||
"Unknown access token.",
|
"Unknown access token.",
|
||||||
))
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(AuthScheme::AccessToken, Token::Appservice(info)) => {
|
(AuthScheme::AccessToken, Token::Appservice(info)) => {
|
||||||
let user_id = query_params
|
let user_id = query_params
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ use ruma::{
|
||||||
membership::{
|
membership::{
|
||||||
create_invite, create_join_event, prepare_join_event,
|
create_invite, create_join_event, prepare_join_event,
|
||||||
},
|
},
|
||||||
|
openid::get_openid_userinfo,
|
||||||
query::{get_profile_information, get_room_information},
|
query::{get_profile_information, get_room_information},
|
||||||
transactions::{
|
transactions::{
|
||||||
edu::{
|
edu::{
|
||||||
|
|
@ -2076,6 +2077,25 @@ pub(crate) async fn claim_keys_route(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # `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: Ar<get_openid_userinfo::v1::Request>,
|
||||||
|
) -> Result<Ra<get_openid_userinfo::v1::Response>> {
|
||||||
|
Ok(Ra(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 /_matrix/federation/v1/media/download/{mediaId}`
|
/// # `GET /_matrix/federation/v1/media/download/{mediaId}`
|
||||||
///
|
///
|
||||||
/// Downloads media owned by a remote homeserver.
|
/// Downloads media owned by a remote homeserver.
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,7 @@ fn client_routes() -> Router {
|
||||||
.ruma_route(c2s::get_room_aliases_route)
|
.ruma_route(c2s::get_room_aliases_route)
|
||||||
.ruma_route(c2s::get_filter_route)
|
.ruma_route(c2s::get_filter_route)
|
||||||
.ruma_route(c2s::create_filter_route)
|
.ruma_route(c2s::create_filter_route)
|
||||||
|
.ruma_route(c2s::create_openid_token_route)
|
||||||
.ruma_route(c2s::set_global_account_data_route)
|
.ruma_route(c2s::set_global_account_data_route)
|
||||||
.ruma_route(c2s::set_room_account_data_route)
|
.ruma_route(c2s::set_room_account_data_route)
|
||||||
.ruma_route(c2s::get_global_account_data_route)
|
.ruma_route(c2s::get_global_account_data_route)
|
||||||
|
|
@ -651,6 +652,7 @@ fn federation_routes(config: &Config) -> Router {
|
||||||
.ruma_route(s2s::get_profile_information_route)
|
.ruma_route(s2s::get_profile_information_route)
|
||||||
.ruma_route(s2s::get_keys_route)
|
.ruma_route(s2s::get_keys_route)
|
||||||
.ruma_route(s2s::claim_keys_route)
|
.ruma_route(s2s::claim_keys_route)
|
||||||
|
.ruma_route(s2s::get_openid_userinfo_route)
|
||||||
.ruma_route(s2s::media_download_route)
|
.ruma_route(s2s::media_download_route)
|
||||||
.ruma_route(s2s::media_thumbnail_route)
|
.ruma_route(s2s::media_thumbnail_route)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ pub(crate) struct Config {
|
||||||
#[serde(default = "false_fn")]
|
#[serde(default = "false_fn")]
|
||||||
pub(crate) allow_registration: bool,
|
pub(crate) allow_registration: bool,
|
||||||
pub(crate) registration_token: Option<String>,
|
pub(crate) registration_token: Option<String>,
|
||||||
|
#[serde(default = "default_openid_token_ttl")]
|
||||||
|
pub(crate) openid_token_ttl: u64,
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
pub(crate) allow_encryption: bool,
|
pub(crate) allow_encryption: bool,
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
|
|
@ -491,6 +493,10 @@ fn default_max_request_size() -> UInt {
|
||||||
(20_u32 * 1024 * 1024).into()
|
(20_u32 * 1024 * 1024).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_openid_token_ttl() -> u64 {
|
||||||
|
60 * 60
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tracing_filter() -> EnvFilterClone {
|
pub(crate) fn default_tracing_filter() -> EnvFilterClone {
|
||||||
"info,ruma_state_res=warn"
|
"info,ruma_state_res=warn"
|
||||||
.parse()
|
.parse()
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ pub(crate) struct KeyValueDatabase {
|
||||||
pub(super) userid_selfsigningkeyid: Arc<dyn KvTree>,
|
pub(super) userid_selfsigningkeyid: Arc<dyn KvTree>,
|
||||||
pub(super) userid_usersigningkeyid: Arc<dyn KvTree>,
|
pub(super) userid_usersigningkeyid: Arc<dyn KvTree>,
|
||||||
|
|
||||||
|
// ExpiresAtUserId = ExpiresAt + UserId
|
||||||
|
pub(super) openidtoken_expiresatuserid: Arc<dyn KvTree>,
|
||||||
|
|
||||||
// UserFilterId = UserId + FilterId
|
// UserFilterId = UserId + FilterId
|
||||||
pub(super) userfilterid_filter: Arc<dyn KvTree>,
|
pub(super) userfilterid_filter: Arc<dyn KvTree>,
|
||||||
|
|
||||||
|
|
@ -346,6 +349,8 @@ impl KeyValueDatabase {
|
||||||
.open_tree("userid_selfsigningkeyid")?,
|
.open_tree("userid_selfsigningkeyid")?,
|
||||||
userid_usersigningkeyid: builder
|
userid_usersigningkeyid: builder
|
||||||
.open_tree("userid_usersigningkeyid")?,
|
.open_tree("userid_usersigningkeyid")?,
|
||||||
|
openidtoken_expiresatuserid: builder
|
||||||
|
.open_tree("openidtoken_expiresatuserid")?,
|
||||||
userfilterid_filter: builder.open_tree("userfilterid_filter")?,
|
userfilterid_filter: builder.open_tree("userfilterid_filter")?,
|
||||||
todeviceid_events: builder.open_tree("todeviceid_events")?,
|
todeviceid_events: builder.open_tree("todeviceid_events")?,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use ruma::{
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
api::client_server::TOKEN_LENGTH,
|
||||||
database::KeyValueDatabase,
|
database::KeyValueDatabase,
|
||||||
service::{self, users::clean_signatures},
|
service::{self, users::clean_signatures},
|
||||||
services, utils, Error, Result,
|
services, utils, Error, Result,
|
||||||
|
|
@ -1085,6 +1086,66 @@ impl service::users::Data for KeyValueDatabase {
|
||||||
Ok(None)
|
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<Option<OwnedUserId>> {
|
||||||
|
let Some(value) =
|
||||||
|
self.openidtoken_expiresatuserid.get(token.as_bytes())?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
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() {
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will only return with Some(username) if the password was not empty and the
|
/// Will only return with Some(username) if the password was not empty and the
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,23 @@ impl Service {
|
||||||
) -> Result<Option<FilterDefinition>> {
|
) -> Result<Option<FilterDefinition>> {
|
||||||
self.db.get_filter(user_id, filter_id)
|
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(crate) 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(crate) fn find_from_openid_token(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<Option<OwnedUserId>> {
|
||||||
|
self.db.find_from_openid_token(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure that a user only sees signatures from themselves and the target user
|
/// Ensure that a user only sees signatures from themselves and the target user
|
||||||
|
|
|
||||||
|
|
@ -256,4 +256,14 @@ pub(crate) trait Data: Send + Sync {
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
filter_id: &str,
|
filter_id: &str,
|
||||||
) -> Result<Option<FilterDefinition>>;
|
) -> Result<Option<FilterDefinition>>;
|
||||||
|
|
||||||
|
// 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<Option<OwnedUserId>>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue