/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ErrorType; use crate::auth::authenticate::Authenticator; use common::{ Server, auth::{ BuildAccessToken, oauth::{ client_id::{ClientMeta, decode_client_id, encode_client_id, scopes_to_mask}, registration::{ ClientRegistrationError, ClientRegistrationRequest, ClientRegistrationResponse, TokenEndpointAuthMethod, redirect_uri_matches, validate_grant_metadata, validate_redirect_uri, }, }, }, }; use directory::core::secret::{hash_secret, verify_secret_hash}; use http_proto::{request::fetch_body, *}; use hyper::StatusCode; use registry::schema::{ enums::{PasswordHashAlgorithm, Permission}, prelude::{ObjectType, Property, UTCDateTime}, structs::OAuthClient, }; use std::future::Future; use store::{ rand::{RngExt, distr::Alphanumeric, rng}, registry::write::{RegistryWrite, RegistryWriteResult}, write::now, }; 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> + Send; fn validate_client_registration( &self, client_id: &str, redirect_uri: Option<&str>, account_id: u32, ) -> impl Future>> + Send; fn verify_client_secret( &self, client_id: &str, client_secret: Option<&str>, ) -> impl Future>> + Send; } impl ClientRegistrationHandler for Server { async fn handle_oauth_registration_request( &self, req: &mut HttpRequest, session: HttpSessionData, ) -> trc::Result { // Parse request let body = fetch_body(req, 20 * 1024, session.session_id).await; let request = serde_json::from_slice::( body.as_deref().unwrap_or_default(), ) .map_err(|err| { trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) })?; // Validate redirect URIs and grant metadata (RFC 7591 + OAuth Public Clients profile) if request.redirect_uris.is_empty() { return Ok(registration_error( ClientRegistrationError::invalid_redirect_uri( "At least one redirect URI is required.", ), )); } for uri in &request.redirect_uris { if let Err(err) = validate_redirect_uri(uri) { return Ok(registration_error(err)); } } if let Err(err) = validate_grant_metadata(&request) { return Ok(registration_error(err)); } let is_public = matches!( request.token_endpoint_auth_method, None | Some(TokenEndpointAuthMethod::None) ); if is_public { // Public client: issue a stateless, self-describing client id with no database write if self.core.oauth.allow_anonymous_client_registration { self.is_http_anonymous_request_allowed(session.remote_ip) .await?; } else { let (_, access_token) = self.authenticate_headers(req, &session).await?; access_token.enforce_permission(Permission::OAuthClientRegistration)?; } let client_id = encode_client_id( self.core.oauth.oauth_key.as_bytes(), &ClientMeta { redirect_uris: request.redirect_uris.clone(), scope_mask: scopes_to_mask(request.scope.as_deref().unwrap_or_default()), client_name: request.client_name.clone(), }, ) .map_err(|err| { trc::AuthEvent::Error .into_err() .details("Failed to encode client id.") .reason(err) .caused_by(trc::location!()) })?; trc::event!( Auth(AuthEvent::ClientRegistration), Id = client_id.clone(), RemoteIp = session.remote_ip ); return Ok(JsonResponse::with_status( StatusCode::CREATED, ClientRegistrationResponse { client_id_issued_at: Some(now()), client_id, request, ..Default::default() }, ) .no_cache() .into_http_response()); } // Confidential client: authenticate and persist the registration let (_, access_token) = self.authenticate_headers(req, &session).await?; access_token.enforce_permission(Permission::OAuthClientRegistration)?; let tenant_id = access_token.tenant_id(); // Generate client ID let client_id = rng() .sample_iter(Alphanumeric) .take(20) .map(|ch| char::from(ch.to_ascii_lowercase())) .collect::(); // Generate client secret let client_secret = rng() .sample_iter(Alphanumeric) .take(48) .map(char::from) .collect::(); let secret_hash = hash_secret( PasswordHashAlgorithm::Argon2id, client_secret.clone().into_bytes(), ) .await .caused_by(trc::location!())?; // inbuxa: AU-1.10: a client registering itself let result = inbuxa_features::audit::scope::system( "oauth-registration", 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(), secret: Some(secret_hash), created_at: UTCDateTime::now(), ..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::with_status( StatusCode::CREATED, ClientRegistrationResponse { client_id, client_secret: Some(client_secret), client_id_issued_at: Some(now()), client_secret_expires_at: Some(0), 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> { // Stateless client ids are self-describing and validated at the authorization endpoint if decode_client_id(self.core.oauth.oauth_key.as_bytes(), client_id).is_some() { return Ok(None); } 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::(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| redirect_uri_matches(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 })) } async fn verify_client_secret( &self, client_id: &str, client_secret: Option<&str>, ) -> trc::Result> { // Stateless and unregistered clients have no secret to verify if decode_client_id(self.core.oauth.oauth_key.as_bytes(), client_id).is_some() { return Ok(None); } let Some(client_id) = self .registry() .primary_key( ObjectType::OAuthClient.into(), Property::ClientId, client_id.as_bytes().to_vec(), ) .await? else { return Ok(None); }; let Some(client) = self .registry() .object::(client_id.id()) .await .caused_by(trc::location!())? else { return Ok(None); }; match client.secret.as_deref() { Some(hash) if !hash.is_empty() => match client_secret { Some(secret) if verify_secret_hash(hash, secret.as_bytes()) .await .caused_by(trc::location!())? => { Ok(None) } _ => Ok(Some(ErrorType::InvalidClient)), }, _ => Ok(None), } } } fn registration_error(error: ClientRegistrationError) -> HttpResponse { JsonResponse::with_status(StatusCode::BAD_REQUEST, error) .no_cache() .into_http_response() }