Files
Stalwart/crates/http/src/auth/oauth/registration.rs
2026-04-24 19:58:03 +02:00

183 lines
5.6 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::ErrorType;
use crate::auth::authenticate::Authenticator;
use common::{
Server,
auth::{
BuildAccessToken,
oauth::registration::{ClientRegistrationRequest, ClientRegistrationResponse},
},
};
use http_proto::{request::fetch_body, *};
use registry::schema::{
enums::Permission,
prelude::{ObjectType, Property},
structs::OAuthClient,
};
use std::future::Future;
use store::{
rand::{Rng, distr::Alphanumeric, rng},
registry::write::{RegistryWrite, RegistryWriteResult},
};
use trc::{AddContext, AuthEvent};
use types::id::Id;
pub trait ClientRegistrationHandler: Sync + Send {
fn handle_oauth_registration_request(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn validate_client_registration(
&self,
client_id: &str,
redirect_uri: Option<&str>,
account_id: u32,
) -> impl Future<Output = trc::Result<Option<ErrorType>>> + Send;
}
impl ClientRegistrationHandler for Server {
async fn handle_oauth_registration_request(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
) -> trc::Result<HttpResponse> {
let tenant_id = if !self.core.oauth.allow_anonymous_client_registration {
// Authenticate request
let (_, access_token) = self.authenticate_headers(req, &session).await?;
// Validate permissions
access_token.enforce_permission(Permission::OAuthClientRegistration)?;
access_token.tenant_id()
} else {
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
None
};
// Parse request
let body = fetch_body(req, 20 * 1024, session.session_id).await;
let request = serde_json::from_slice::<ClientRegistrationRequest>(
body.as_deref().unwrap_or_default(),
)
.map_err(|err| {
trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
})?;
// Generate client ID
let client_id = rng()
.sample_iter(Alphanumeric)
.take(20)
.map(|ch| char::from(ch.to_ascii_lowercase()))
.collect::<String>();
let result = self
.registry()
.write(RegistryWrite::insert(
&OAuthClient {
client_id: client_id.clone(),
description: request.client_name.clone(),
contacts: request.contacts.clone().into(),
member_tenant_id: tenant_id.map(|id| Id::new(id as u64)),
redirect_uris: request.redirect_uris.clone().into(),
logo: request.logo_uri.clone(),
..Default::default()
}
.into(),
))
.await
.caused_by(trc::location!())?;
if !matches!(result, RegistryWriteResult::Success(_)) {
return Err(trc::StoreEvent::UnexpectedError
.into_err()
.details("Failed to register OAuth client.")
.reason(result.to_string())
.caused_by(trc::location!()));
}
trc::event!(
Auth(AuthEvent::ClientRegistration),
Id = client_id.to_string(),
RemoteIp = session.remote_ip
);
Ok(JsonResponse::new(ClientRegistrationResponse {
client_id,
request,
..Default::default()
})
.no_cache()
.into_http_response())
}
async fn validate_client_registration(
&self,
client_id: &str,
redirect_uri: Option<&str>,
account_id: u32,
) -> trc::Result<Option<ErrorType>> {
if !self.core.oauth.require_client_authentication {
return Ok(None);
}
// Fetch client registration
let found_registration = if let Some(client_id) = self
.registry()
.primary_key(
ObjectType::OAuthClient.into(),
Property::ClientId,
client_id.as_bytes().to_vec(),
)
.await?
{
if let Some(redirect_uri) = redirect_uri {
let client = self
.registry()
.object::<OAuthClient>(client_id.id())
.await?
.ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.details("OAuth client not found.")
.caused_by(trc::location!())
.ctx(trc::Key::Id, client_id.id().id())
})?;
if client.redirect_uris.iter().any(|uri| uri == redirect_uri) {
return Ok(None);
}
} else {
// Device flow does not require a redirect URI
return Ok(None);
}
true
} else {
false
};
// Check if the account is allowed to override client registration
if self
.access_token(account_id)
.await
.caused_by(trc::location!())?
.build()
.has_permission(Permission::OAuthClientOverride)
{
return Ok(None);
}
Ok(Some(if found_registration {
ErrorType::InvalidClient
} else {
ErrorType::InvalidRequest
}))
}
}