mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-05-13 21:33:45 +00:00
281 lines
7.4 KiB
Rust
281 lines
7.4 KiB
Rust
use std::any::{Any, TypeId};
|
|
|
|
use conduwuit::{Err, Error, Result, err};
|
|
use http::StatusCode;
|
|
use ruma::{
|
|
OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
|
api::{
|
|
IncomingRequest,
|
|
auth_scheme::{
|
|
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
|
AuthScheme, NoAccessToken, NoAuthentication,
|
|
},
|
|
error::{ErrorKind, UnknownTokenErrorData},
|
|
federation::authentication::ServerSignatures,
|
|
},
|
|
assign,
|
|
};
|
|
use service::{
|
|
Services,
|
|
server_keys::{PubKeyMap, PubKeys},
|
|
users::AccessTokenStatus,
|
|
};
|
|
|
|
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct Auth {
|
|
pub(super) origin: Option<OwnedServerName>,
|
|
pub(super) sender_user: Option<OwnedUserId>,
|
|
pub(super) sender_device: Option<OwnedDeviceId>,
|
|
pub(super) appservice_info: Option<RegistrationInfo>,
|
|
}
|
|
|
|
pub(super) trait CheckAuth: AuthScheme {
|
|
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
incoming_request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
) -> impl Future<Output = Result<Auth>> + Send {
|
|
async move {
|
|
let route = TypeId::of::<R>();
|
|
|
|
let output = Self::extract_authentication(incoming_request).map_err(|err| {
|
|
err!(Request(Unauthorized(warn!(
|
|
"Failed to extract authorization: {}",
|
|
err.into()
|
|
))))
|
|
})?;
|
|
|
|
Self::verify(services, output, incoming_request, query, route).await
|
|
}
|
|
}
|
|
|
|
fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
route: TypeId,
|
|
) -> impl Future<Output = Result<Auth>> + Send;
|
|
}
|
|
|
|
impl CheckAuth for ServerSignatures {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
request: &hyper::Request<B>,
|
|
_query: AuthQueryParams,
|
|
_route: TypeId,
|
|
) -> Result<Auth> {
|
|
let destination = services.globals.server_name();
|
|
if output
|
|
.destination
|
|
.as_ref()
|
|
.is_some_and(|supplied_destination| supplied_destination != destination)
|
|
{
|
|
return Err!(Request(Unauthorized("Destination mismatch.")));
|
|
}
|
|
|
|
let key = services
|
|
.server_keys
|
|
.get_verify_key(&output.origin, &output.key)
|
|
.await
|
|
.map_err(|e| {
|
|
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {e}"))))
|
|
})?;
|
|
|
|
let keys: PubKeys = [(output.key.to_string(), key.key)].into();
|
|
let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into();
|
|
|
|
match output.verify_request(request, destination, &keys) {
|
|
| Ok(()) => Ok(Auth {
|
|
origin: Some(output.origin.clone()),
|
|
..Default::default()
|
|
}),
|
|
| Err(err) =>
|
|
Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for AccessToken {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
_request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
route: TypeId,
|
|
) -> Result<Auth> {
|
|
let (sender_user, sender_device, appservice_info) = {
|
|
if let Some((sender_user, sender_device, status)) =
|
|
services.users.find_from_token(&output).await
|
|
{
|
|
// If the token is expired we return a soft logout
|
|
if matches!(status, AccessTokenStatus::Expired) {
|
|
return Err(Error::Request(
|
|
ErrorKind::UnknownToken(
|
|
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
|
),
|
|
"This token has expired".into(),
|
|
StatusCode::UNAUTHORIZED,
|
|
));
|
|
}
|
|
|
|
// Locked users can only use /logout and /logout/all
|
|
if services
|
|
.users
|
|
.is_locked(&sender_user)
|
|
.await
|
|
.is_ok_and(std::convert::identity)
|
|
{
|
|
if !(route == TypeId::of::<ruma::api::client::session::logout::v3::Request>()
|
|
|| route
|
|
== TypeId::of::<ruma::api::client::session::logout_all::v3::Request>(
|
|
)) {
|
|
return Err!(Request(UserLocked("Your account is locked.")));
|
|
}
|
|
}
|
|
|
|
(Some(sender_user), Some(sender_device), None)
|
|
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await
|
|
{
|
|
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
|
|| {
|
|
UserId::parse_with_server_name(
|
|
appservice_info.registration.sender_localpart.as_str(),
|
|
services.globals.server_name(),
|
|
)
|
|
},
|
|
UserId::parse,
|
|
) else {
|
|
return Err!(Request(InvalidUsername("Username is invalid.")));
|
|
};
|
|
|
|
if !appservice_info.is_user_match(&sender_user) {
|
|
return Err!(Request(Exclusive("User is not in namespace.")));
|
|
}
|
|
|
|
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
|
// The device_id can be provided via `device_id` or
|
|
// `org.matrix.msc3202.device_id` query parameter.
|
|
let sender_device =
|
|
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
|
// Verify the device exists for this user
|
|
if services
|
|
.users
|
|
.get_device_metadata(&sender_user, device_id)
|
|
.await
|
|
.is_err()
|
|
{
|
|
return Err!(Request(Forbidden(
|
|
"Device does not exist for user or appservice cannot masquerade \
|
|
as this device."
|
|
)));
|
|
}
|
|
|
|
Some(device_id.to_owned())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
(Some(sender_user), sender_device, Some(appservice_info))
|
|
} else {
|
|
return Err(Error::Request(
|
|
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
|
"Invalid token".into(),
|
|
StatusCode::UNAUTHORIZED,
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(Auth {
|
|
sender_user,
|
|
sender_device,
|
|
appservice_info,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for AccessTokenOptional {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
route: TypeId,
|
|
) -> Result<Auth> {
|
|
match output {
|
|
| Some(token) =>
|
|
<AccessToken as CheckAuth>::verify(services, token, request, query, route).await,
|
|
| None => Ok(Auth::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for AppserviceToken {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
_request: &hyper::Request<B>,
|
|
_query: AuthQueryParams,
|
|
_route: TypeId,
|
|
) -> Result<Auth> {
|
|
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
|
|
return Err!(Request(Unauthorized("Invalid appservice token.")));
|
|
};
|
|
|
|
Ok(Auth {
|
|
appservice_info: Some(appservice_info),
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for AppserviceTokenOptional {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
output: Self::Output,
|
|
request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
route: TypeId,
|
|
) -> Result<Auth> {
|
|
match output {
|
|
| Some(token) =>
|
|
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
|
|
.await,
|
|
| None => Ok(Auth::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for NoAuthentication {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
_services: &Services,
|
|
_output: Self::Output,
|
|
_request: &hyper::Request<B>,
|
|
_query: AuthQueryParams,
|
|
_route: TypeId,
|
|
) -> Result<Auth> {
|
|
Ok(Auth::default())
|
|
}
|
|
}
|
|
|
|
impl CheckAuth for NoAccessToken {
|
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
|
services: &Services,
|
|
_output: Self::Output,
|
|
request: &hyper::Request<B>,
|
|
query: AuthQueryParams,
|
|
route: TypeId,
|
|
) -> Result<Auth> {
|
|
// We handle these the same as AccessTokenOptional
|
|
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
|
|
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err))))
|
|
})?;
|
|
|
|
<AccessTokenOptional as CheckAuth>::verify(services, token, request, query, route).await
|
|
}
|
|
}
|