Registry testing - part 5

This commit is contained in:
mdecimus
2026-03-15 18:01:12 +01:00
parent 4b5688fd57
commit e0105bc43b
46 changed files with 1764 additions and 710 deletions

View File

@@ -31,7 +31,7 @@ impl Authenticator for Server {
// Check if the credentials are cached
if let Some(http_cache) = self.inner.cache.http_auth.get(token) {
// Make sure the revision is still valid
if http_cache.expires <= Instant::now() {
if http_cache.expires > Instant::now() {
let access_token = AccessToken::renew(
self.access_token(http_cache.account_id).await?,
http_cache.credential_id,
@@ -152,6 +152,7 @@ fn decode_plain_auth(token: &str) -> Option<Credentials> {
.map(|(login, secret)| Credentials::Basic {
username: login.trim().to_lowercase(),
secret: secret.to_string(),
mfa_token: None,
})
})
}

View File

@@ -4,18 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode, OAuthCodeRequest};
use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode};
use crate::auth::oauth::OAuthStatus;
use common::{
KV_OAUTH, Server,
auth::{
AccessToken,
AuthRequest,
oauth::{CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, USER_CODE_ALPHABET, USER_CODE_LEN},
},
};
use directory::Credentials;
use http_proto::*;
use serde::Deserialize;
use serde_json::json;
use std::future::Future;
use store::{
Serialize,
@@ -31,8 +30,9 @@ use store::{
write::AlignedBytes,
};
use trc::AddContext;
use utils::DomainPart;
#[derive(Debug, serde::Serialize, Deserialize)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OAuthMetadata {
pub issuer: String,
pub token_endpoint: String,
@@ -46,10 +46,10 @@ pub struct OAuthMetadata {
}
pub trait OAuthApiHandler: Sync + Send {
fn handle_oauth_api_request(
fn handle_login_request(
&self,
access_token: &AccessToken,
body: Option<Vec<u8>>,
session: HttpSessionData,
body: Vec<u8>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_device_auth(
@@ -65,20 +65,78 @@ pub trait OAuthApiHandler: Sync + Send {
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum LoginRequest {
Discovery {
account_name: String,
},
AuthCode {
account_name: String,
account_secret: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
mfa_token: Option<String>,
client_id: String,
#[serde(default)]
redirect_uri: Option<String>,
#[serde(default)]
nonce: Option<String>,
},
AuthDevice {
account_name: String,
account_secret: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
mfa_token: Option<String>,
code: String,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum LoginResponse {
Local,
External { endpoint: String },
Authenticated { client_code: String },
Verified,
MfaRequired,
Failure,
}
impl OAuthApiHandler for Server {
async fn handle_oauth_api_request(
async fn handle_login_request(
&self,
access_token: &AccessToken,
body: Option<Vec<u8>>,
session: HttpSessionData,
body: Vec<u8>,
) -> trc::Result<HttpResponse> {
let request =
serde_json::from_slice::<OAuthCodeRequest>(body.as_deref().unwrap_or_default())
.map_err(|err| {
trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
})?;
let request = serde_json::from_slice::<LoginRequest>(&body).map_err(|err| {
trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
})?;
let response = match request {
OAuthCodeRequest::Code {
LoginRequest::Discovery { account_name } => {
let account_name = account_name.trim().to_lowercase();
if let Some(domain_name) = account_name.try_domain_part() {
if let Some(endpoint) = self
.get_directory_for_domain(domain_name)
.await?
.and_then(|directory| directory.oidc_authorization_endpoint())
{
LoginResponse::External { endpoint }
} else {
LoginResponse::Local
}
} else {
LoginResponse::Local
}
}
LoginRequest::AuthCode {
account_name,
account_secret,
mfa_token,
client_id,
redirect_uri,
nonce,
@@ -97,56 +155,76 @@ impl OAuthApiHandler for Server {
.details("Redirect URI must be HTTPS."));
}
// Generate client code
let client_code = rng()
.sample_iter(Alphanumeric)
.take(DEVICE_CODE_LEN)
.map(char::from)
.collect::<String>();
// Authenticate
match self
.authenticate(&AuthRequest {
credentials: Credentials::Basic {
username: account_name,
secret: account_secret,
mfa_token,
},
session_id: session.session_id,
remote_ip: session.remote_ip,
})
.await
{
Ok(access_token) => {
// Generate client code
let client_code = rng()
.sample_iter(Alphanumeric)
.take(DEVICE_CODE_LEN)
.map(char::from)
.collect::<String>();
// Serialize OAuth code
let value = Archiver::new(OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.account_id(),
client_id,
nonce,
params: redirect_uri.unwrap_or_default(),
})
.untrusted()
.serialize()
.caused_by(trc::location!())?;
// Serialize OAuth code
let value = Archiver::new(OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.account_id(),
client_id,
nonce,
params: redirect_uri.unwrap_or_default(),
})
.untrusted()
.serialize()
.caused_by(trc::location!())?;
// Insert client code
self.in_memory_store()
.key_set(
KeyValue::with_prefix(KV_OAUTH, client_code.as_bytes(), value)
.expires(self.core.oauth.oauth_expiry_auth_code),
)
.await?;
// Insert client code
self.in_memory_store()
.key_set(
KeyValue::with_prefix(KV_OAUTH, client_code.as_bytes(), value)
.expires(self.core.oauth.oauth_expiry_auth_code),
)
.await?;
#[cfg(not(feature = "enterprise"))]
let is_enterprise = false;
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
let is_enterprise = self.core.is_enterprise_edition();
// SPDX-SnippetEnd
json!({
"data": {
"code": client_code,
"permissions": access_token.permissions(),
"version": env!("CARGO_PKG_VERSION"),
"isEnterprise": is_enterprise,
LoginResponse::Authenticated { client_code }
}
Err(err) => match *err.as_ref() {
trc::EventType::Auth(trc::AuthEvent::MfaRequired) => {
trc::error!(err.span_id(session.session_id));
LoginResponse::MfaRequired
}
trc::EventType::Auth(_) => {
trc::error!(err.span_id(session.session_id));
LoginResponse::Failure
}
trc::EventType::Security(_) => {
trc::error!(err.span_id(session.session_id));
LoginResponse::Failure
}
_ => {
return Err(err);
}
},
})
}
}
OAuthCodeRequest::Device { code } => {
let mut success = false;
LoginRequest::AuthDevice {
account_name,
account_secret,
mfa_token,
code,
} => {
// Obtain code
let mut result = LoginResponse::Failure;
if let Some(auth_code_) = self
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(KeyValue::<()>::build_key(
@@ -159,40 +237,75 @@ impl OAuthApiHandler for Server {
.unarchive::<OAuthCode>()
.caused_by(trc::location!())?;
if oauth.status == OAuthStatus::Pending {
let new_oauth_code = OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.account_id(),
client_id: oauth.client_id.to_string(),
nonce: oauth.nonce.as_ref().map(|s| s.to_string()),
params: Default::default(),
};
success = true;
// Authenticate
match self
.authenticate(&AuthRequest {
credentials: Credentials::Basic {
username: account_name,
secret: account_secret,
mfa_token,
},
session_id: session.session_id,
remote_ip: session.remote_ip,
})
.await
{
Ok(access_token) => {
let new_oauth_code = OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.account_id(),
client_id: oauth.client_id.to_string(),
nonce: oauth.nonce.as_ref().map(|s| s.to_string()),
params: Default::default(),
};
// Delete issued user code
self.in_memory_store()
.key_delete(KeyValue::<()>::build_key(KV_OAUTH, code.as_bytes()))
.await?;
// Delete issued user code
self.in_memory_store()
.key_delete(KeyValue::<()>::build_key(
KV_OAUTH,
code.as_bytes(),
))
.await?;
// Update device code status
self.in_memory_store()
.key_set(
KeyValue::with_prefix(
KV_OAUTH,
oauth.params.as_bytes(),
Archiver::new(new_oauth_code)
.untrusted()
.serialize()
.caused_by(trc::location!())?,
)
.expires(self.core.oauth.oauth_expiry_auth_code),
)
.await?;
// Update device code status
self.in_memory_store()
.key_set(
KeyValue::with_prefix(
KV_OAUTH,
oauth.params.as_bytes(),
Archiver::new(new_oauth_code)
.untrusted()
.serialize()
.caused_by(trc::location!())?,
)
.expires(self.core.oauth.oauth_expiry_auth_code),
)
.await?;
result = LoginResponse::Verified;
}
Err(err) => match *err.as_ref() {
trc::EventType::Auth(trc::AuthEvent::MfaRequired) => {
trc::error!(err.span_id(session.session_id));
result = LoginResponse::MfaRequired;
}
trc::EventType::Auth(_) => {
trc::error!(err.span_id(session.session_id));
result = LoginResponse::Failure;
}
trc::EventType::Security(_) => {
trc::error!(err.span_id(session.session_id));
result = LoginResponse::Failure;
}
_ => {
return Err(err);
}
},
}
}
}
json!({
"data": success,
})
result
}
};
@@ -257,6 +370,8 @@ impl OAuthApiHandler for Server {
)
.await?;
let c = println!("Expires in: {}", self.core.oauth.oauth_expiry_user_code);
// Insert user code
self.in_memory_store()
.key_set(

View File

@@ -152,21 +152,6 @@ pub enum ErrorType {
ExpiredToken,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum OAuthCodeRequest {
Code {
client_id: String,
redirect_uri: Option<String>,
#[serde(default)]
nonce: Option<String>,
},
Device {
code: String,
},
}
impl TokenResponse {
pub fn error(error: ErrorType) -> Self {
TokenResponse::Error { error }

View File

@@ -14,18 +14,15 @@ use common::{
},
};
use http_proto::{request::fetch_body, *};
use registry::{
schema::{
enums::Permission,
prelude::{ObjectType, Property},
structs::OAuthClient,
},
types::datetime::UTCDateTime,
use registry::schema::{
enums::Permission,
prelude::{ObjectType, Property},
structs::OAuthClient,
};
use std::future::Future;
use store::{
rand::{Rng, distr::Alphanumeric, rng},
registry::{RegistryQuery, write::RegistryWrite},
registry::write::{RegistryWrite, RegistryWriteResult},
};
use trc::{AddContext, AuthEvent};
use types::id::Id;
@@ -79,11 +76,11 @@ impl ClientRegistrationHandler for Server {
.map(|ch| char::from(ch.to_ascii_lowercase()))
.collect::<String>();
self.registry()
let result = self
.registry()
.write(RegistryWrite::insert(
&OAuthClient {
client_id: client_id.clone(),
created_at: UTCDateTime::now(),
description: request.client_name.clone(),
contacts: request.contacts.clone().into(),
member_tenant_id: tenant_id.map(|id| Id::new(id as u64)),
@@ -96,6 +93,14 @@ impl ClientRegistrationHandler for Server {
.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(),
@@ -124,23 +129,24 @@ impl ClientRegistrationHandler for Server {
// Fetch client registration
let found_registration = if let Some(client_id) = self
.registry()
.query::<Vec<Id>>(
RegistryQuery::new(ObjectType::OAuthClient).equal(Property::ClientId, client_id),
.primary_key(
ObjectType::OAuthClient.into(),
Property::ClientId,
client_id.as_bytes().to_vec(),
)
.await?
.first()
{
if let Some(redirect_uri) = redirect_uri {
let client = self
.registry()
.object::<OAuthClient>(*client_id)
.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())
.ctx(trc::Key::Id, client_id.id().id())
})?;
if client.redirect_uris.iter().any(|uri| uri == redirect_uri) {
return Ok(None);