Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{HttpAuthCache, Server, auth::AuthRequest, network::limiter::InFlight};
use directory::Credentials;
use http_proto::{HttpRequest, HttpSessionData};
use hyper::header;
use mail_parser::decoders::base64::base64_decode;
use std::future::Future;
use std::time::{Duration, Instant};
pub trait Authenticator: Sync + Send {
fn authenticate_headers(
&self,
req: &HttpRequest,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<(Option<InFlight>, AccessToken)>> + Send;
}
impl Authenticator for Server {
async fn authenticate_headers(
&self,
req: &HttpRequest,
session: &HttpSessionData,
) -> trc::Result<(Option<InFlight>, AccessToken)> {
if let Some((mechanism, token)) = req.authorization() {
// 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() {
let access_token = AccessToken::renew(
self.access_token(http_cache.account_id).await?,
http_cache.credential_id,
session.remote_ip,
)?;
if access_token.revision() == http_cache.revision {
// Enforce authenticated rate limit
return self
.is_http_authenticated_request_allowed(&access_token, session.remote_ip)
.await
.map(|in_flight| (in_flight, access_token));
}
}
// If the revision is not valid, remove the cached credentials
self.inner.cache.http_auth.remove(token);
}
let credentials = if mechanism.eq_ignore_ascii_case("basic") {
// Decode the base64 encoded credentials
decode_plain_auth(token).ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
.details("Failed to decode Basic auth request.")
.id(token.to_string())
.caused_by(trc::location!())
})?
} else if mechanism.eq_ignore_ascii_case("bearer") {
// Enforce anonymous rate limit
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
Credentials::Bearer {
username: None,
token: token.to_string(),
}
} else {
// Enforce anonymous rate limit
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
return Err(trc::AuthEvent::Error
.into_err()
.reason("Unsupported authentication mechanism.")
.details(token.to_string())
.caused_by(trc::location!()));
};
// Authenticate
let access_token = self
.authenticate(&AuthRequest::from_credentials(
credentials,
session.session_id,
session.remote_ip,
))
.await?;
// Cache credentials
self.inner.cache.http_auth.insert(
token.into(),
HttpAuthCache {
account_id: access_token.account_id(),
revision: access_token.revision(),
credential_id: access_token.credential_id(),
expires: Instant::now()
+ Duration::from_secs(self.core.oauth.oauth_expiry_token),
},
);
// Enforce authenticated rate limit
self.is_http_authenticated_request_allowed(&access_token, session.remote_ip)
.await
.map(|in_flight| (in_flight, access_token))
} else {
// Enforce anonymous rate limit
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
Err(trc::AuthEvent::Failed
.into_err()
.details("Missing Authorization header.")
.caused_by(trc::location!()))
}
}
}
pub trait HttpHeaders {
fn authorization(&self) -> Option<(&str, &str)>;
fn authorization_basic(&self) -> Option<&str>;
}
impl HttpHeaders for HttpRequest {
fn authorization(&self) -> Option<(&str, &str)> {
self.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.split_once(' ').map(|(l, t)| (l, t.trim())))
}
fn authorization_basic(&self) -> Option<&str> {
self.authorization().and_then(|(l, t)| {
if l.eq_ignore_ascii_case("basic") {
Some(t)
} else {
None
}
})
}
}
fn decode_plain_auth(token: &str) -> Option<Credentials> {
base64_decode(token.as_bytes())
.and_then(|token| String::from_utf8(token).ok())
.and_then(|token| {
token
.split_once(':')
.map(|(login, secret)| Credentials::Basic {
username: login.trim().to_lowercase(),
secret: secret.to_string(),
mfa_token: None,
})
})
}
+9
View File
@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod authenticate;
pub mod oauth;
pub mod permissions;
+649
View File
@@ -0,0 +1,649 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode, PkceCodeChallenge};
use crate::auth::oauth::{
OAuthStatus, openid::OpenIdHandler, registration::ClientRegistrationHandler,
};
use common::{
KV_OAUTH, Server,
auth::{
AuthRequest,
authentication::UsernameParts,
oauth::{
CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, SUPPORTED_SCOPES, USER_CODE_ALPHABET,
USER_CODE_LEN,
client_id::{decode_client_id, scopes_to_mask},
registration::redirect_uri_matches,
},
},
};
use directory::Credentials;
use http_proto::*;
use std::future::Future;
use store::{
Serialize,
dispatch::lookup::KeyValue,
write::{Archive, Archiver},
};
use store::{
rand::{
RngExt,
distr::{Alphanumeric, StandardUniform},
rng,
},
write::AlignedBytes,
};
use trc::AddContext;
#[derive(Debug, serde::Serialize)]
pub struct ProtectedResourceMetadata {
pub resource: String,
pub authorization_servers: [String; 1],
pub scopes_supported: &'static [&'static str],
pub bearer_methods_supported: &'static [&'static str],
}
#[derive(Debug, serde::Serialize)]
pub struct OAuthMetadata {
pub issuer: String,
pub token_endpoint: String,
pub authorization_endpoint: String,
pub device_authorization_endpoint: String,
pub registration_endpoint: String,
pub introspection_endpoint: String,
pub grant_types_supported: &'static [&'static str],
pub response_types_supported: &'static [&'static str],
pub scopes_supported: &'static [&'static str],
pub token_endpoint_auth_methods_supported: &'static [&'static str],
pub code_challenge_methods_supported: &'static [&'static str],
pub authorization_response_iss_parameter_supported: bool,
}
pub trait OAuthApiHandler: Sync + Send {
fn handle_discover_request(
&self,
session: &HttpSessionData,
account_name: &str,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_login_request(
&self,
session: &HttpSessionData,
body: Vec<u8>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_device_auth(
&self,
req: &mut HttpRequest,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_oauth_metadata(&self) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_oauth_protected_resource(
&self,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum LoginRequest {
#[serde(rename_all = "camelCase")]
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>,
#[serde(default)]
scope: Option<String>,
#[serde(default)]
code_challenge: Option<String>,
#[serde(default)]
code_challenge_method: Option<String>,
#[serde(default)]
state: Option<String>,
#[serde(default)]
resource: Vec<String>,
},
#[serde(rename_all = "camelCase")]
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 {
Authenticated { client_code: String, iss: String },
Verified,
MfaRequired,
Failure,
}
impl OAuthApiHandler for Server {
async fn handle_discover_request(
&self,
session: &HttpSessionData,
account_name: &str,
) -> trc::Result<HttpResponse> {
let username = UsernameParts::new(account_name.trim());
let auth_as = username.auth_as();
let is_recovery_admin = self
.registry()
.recovery_admin()
.is_some_and(|(user, _)| user.trim().eq_ignore_ascii_case(auth_as.address()));
if !is_recovery_admin
&& let Some(domain_name) = auth_as.domain().filter(|domain| !domain.is_empty())
&& let Some(endpoint) = self
.get_directory_for_domain(domain_name)
.await?
.and_then(|directory| directory.oidc_discovery_document())
{
Ok(JsonResponse::new(&endpoint.document)
.no_cache()
.into_http_response())
} else {
self.handle_oidc_metadata(!session.is_tls).await
}
}
async fn handle_login_request(
&self,
session: &HttpSessionData,
body: Vec<u8>,
) -> trc::Result<HttpResponse> {
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 {
LoginRequest::AuthCode {
account_name,
account_secret,
mfa_token,
client_id,
redirect_uri,
nonce,
scope,
code_challenge,
code_challenge_method,
resource,
..
} => {
// Validate clientId
if client_id.len() > CLIENT_ID_MAX_LEN {
return Err(trc::AuthEvent::Error
.into_err()
.details("Client ID is too long."));
} else if redirect_uri
.as_ref()
.is_some_and(|uri| uri.starts_with("http://"))
{
#[cfg(not(feature = "dev_mode"))]
if !self.registry().is_recovery_mode() && code_challenge.is_none() {
return Err(trc::AuthEvent::Error
.into_err()
.details("Redirect URI must be HTTPS."));
}
}
// Resolve the client and validate the redirect URI against the registration.
// Stateless client ids are self-describing; otherwise fall back to the registry.
let redirect_uri = redirect_uri.ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
.details("A redirect URI is required.")
})?;
let stateless_client =
decode_client_id(self.core.oauth.oauth_key.as_bytes(), &client_id);
let granted_scope = match &stateless_client {
Some(meta) => {
if !meta
.redirect_uris
.iter()
.any(|uri| redirect_uri_matches(uri, &redirect_uri))
{
return Err(trc::AuthEvent::Error
.into_err()
.details("Redirect URI does not match the client registration."));
}
grant_scope(scope.as_deref(), meta.scope_mask)
}
None => scope,
};
// Validate Resource Indicators (RFC 8707)
for resource in &resource {
if !is_known_resource(
[self.core.network.server_name.as_str()]
.into_iter()
.chain(
self.core
.network
.info
.services
.values()
.filter_map(|v| v.hostname.as_deref()),
)
.chain(
self.core
.network
.info
.mxs
.iter()
.filter_map(|mx| mx.hostname.as_deref()),
),
resource,
) {
return Err(trc::AuthEvent::Error
.into_err()
.details(format!("Unknown resource indicator: {}", resource)));
}
}
// Parse and validate PKCE challenge (RFC 7636).
let pkce_challenge = match code_challenge {
Some(challenge) => match code_challenge_method.as_deref().unwrap_or("plain") {
"S256" => PkceCodeChallenge::S256(challenge),
"plain" if stateless_client.is_none() => {
PkceCodeChallenge::Plain(challenge)
}
_ => {
return Err(trc::AuthEvent::Error
.into_err()
.details("Unsupported PKCE code_challenge_method."));
}
},
None => {
if stateless_client.is_some() {
return Err(trc::AuthEvent::Error.into_err().details(
"A PKCE code_challenge with the S256 method is required.",
));
}
PkceCodeChallenge::None
}
};
// 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) => {
// Registry-backed clients are validated once the account is known
if stateless_client.is_none()
&& self
.validate_client_registration(
&client_id,
Some(redirect_uri.as_str()),
access_token.account_id(),
)
.await?
.is_some()
{
return Err(trc::AuthEvent::Error
.into_err()
.details("Invalid client registration."));
}
// 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,
code_challenge: pkce_challenge,
scope: granted_scope,
resources: resource,
})
.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?;
LoginResponse::Authenticated {
client_code,
iss: self.core.network.http.url_https.clone(),
}
}
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);
}
},
}
}
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(
KV_OAUTH,
code.as_bytes(),
))
.await?
{
let oauth = auth_code_
.unarchive::<OAuthCode>()
.caused_by(trc::location!())?;
if oauth.status == OAuthStatus::Pending {
// 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(),
code_challenge: PkceCodeChallenge::None,
scope: oauth.scope.as_ref().map(|s| s.to_string()),
resources: oauth
.resources
.iter()
.map(|s| s.to_string())
.collect(),
};
// 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?;
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);
}
},
}
}
}
result
}
};
Ok(JsonResponse::new(response).no_cache().into_http_response())
}
async fn handle_device_auth(
&self,
req: &mut HttpRequest,
session: &HttpSessionData,
) -> trc::Result<HttpResponse> {
// Parse form
let mut form_data = FormData::from_request(req, MAX_POST_LEN, session.session_id).await?;
let client_id = form_data
.remove("client_id")
.filter(|client_id| client_id.len() <= CLIENT_ID_MAX_LEN)
.ok_or_else(|| {
trc::ResourceEvent::BadParameters
.into_err()
.details("Client ID is missing.")
})?;
let nonce = form_data.remove("nonce");
let scope = form_data
.remove("scope")
.and_then(|scope| grant_scope(Some(&scope), u64::MAX));
// Generate device code
let device_code = rng()
.sample_iter(Alphanumeric)
.take(DEVICE_CODE_LEN)
.map(char::from)
.collect::<String>();
// Generate user code
let mut user_code = String::with_capacity(USER_CODE_LEN + 1);
for (pos, ch) in rng()
.sample_iter(StandardUniform)
.take(USER_CODE_LEN)
.map(|v: u64| char::from(USER_CODE_ALPHABET[v as usize % USER_CODE_ALPHABET.len()]))
.enumerate()
{
if pos == USER_CODE_LEN / 2 {
user_code.push('-');
}
user_code.push(ch);
}
// Add OAuth status
let oauth_code = Archiver::new(OAuthCode {
status: OAuthStatus::Pending,
account_id: u32::MAX,
client_id,
nonce,
params: device_code.clone(),
code_challenge: PkceCodeChallenge::None,
scope,
resources: Vec::new(),
})
.untrusted()
.serialize()
.caused_by(trc::location!())?;
// Insert device code
self.in_memory_store()
.key_set(
KeyValue::with_prefix(KV_OAUTH, device_code.as_bytes(), oauth_code.clone())
.expires(self.core.oauth.oauth_expiry_user_code),
)
.await?;
// Insert user code
self.in_memory_store()
.key_set(
KeyValue::with_prefix(KV_OAUTH, user_code.as_bytes(), oauth_code)
.expires(self.core.oauth.oauth_expiry_user_code),
)
.await?;
// Build response
let base_url = &self.core.network.http.url_https;
Ok(JsonResponse::new(DeviceAuthResponse {
verification_uri: format!("{base_url}/device"),
verification_uri_complete: format!("{base_url}/device/?code={user_code}"),
device_code,
user_code,
expires_in: self.core.oauth.oauth_expiry_user_code,
interval: 5,
})
.no_cache()
.into_http_response())
}
async fn handle_oauth_metadata(&self) -> trc::Result<HttpResponse> {
let base_url = &self.core.network.http.url_https;
Ok(JsonResponse::new(OAuthMetadata {
authorization_endpoint: format!("{base_url}/login",),
token_endpoint: format!("{base_url}/auth/token"),
device_authorization_endpoint: format!("{base_url}/auth/device"),
introspection_endpoint: format!("{base_url}/auth/introspect"),
registration_endpoint: format!("{base_url}/auth/register"),
grant_types_supported: &[
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
],
response_types_supported: &["code"],
scopes_supported: SUPPORTED_SCOPES,
token_endpoint_auth_methods_supported: &[
"none",
"client_secret_post",
"client_secret_basic",
],
code_challenge_methods_supported: &["S256"],
authorization_response_iss_parameter_supported: true,
issuer: base_url.to_string(),
})
.into_http_response()
.with_cors_unrestricted())
}
async fn handle_oauth_protected_resource(&self) -> trc::Result<HttpResponse> {
let base_url = &self.core.network.http.url_https;
Ok(JsonResponse::new(ProtectedResourceMetadata {
resource: base_url.to_string(),
authorization_servers: [base_url.to_string()],
scopes_supported: SUPPORTED_SCOPES,
bearer_methods_supported: &["header"],
})
.into_http_response()
.with_cors_unrestricted())
}
}
fn grant_scope(requested: Option<&str>, registered_mask: u64) -> Option<String> {
let mut granted = String::new();
for scope in requested.unwrap_or_default().split_ascii_whitespace() {
let bit = scopes_to_mask(scope);
if bit != 0 && registered_mask & bit == bit {
if !granted.is_empty() {
granted.push(' ');
}
granted.push_str(scope);
}
}
(!granted.is_empty()).then_some(granted)
}
fn is_known_resource<'x>(hostnames: impl IntoIterator<Item = &'x str>, uri: &str) -> bool {
let Some((scheme, rest)) = uri.split_once("://") else {
return false;
};
let supported = hashify::tiny_map!(scheme.as_bytes(),
b"http" => true,
b"https" => true,
b"smtp" => true,
b"smtps" => true,
b"imap" => true,
b"imaps" => true,
b"pop3" => true,
b"pop3s" => true,
b"caldav" => true,
b"caldavs" => true,
b"webdav" => true,
b"webdavs" => true,
b"carddav" => true,
b"carddavs" => true,
b"sieve" => true,
b"sieves" => true
)
.unwrap_or(false);
let authority = rest.split_once('/').map_or(rest, |(auth, _)| auth);
let host = authority
.rsplit_once(':')
.filter(|(_, port)| !port.is_empty() && port.as_bytes().iter().all(|c| c.is_ascii_digit()))
.map_or(authority, |(host, _)| host);
supported
&& hostnames
.into_iter()
.any(|hostname| host.eq_ignore_ascii_case(hostname))
}
+242
View File
@@ -0,0 +1,242 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use http_proto::{HttpRequest, request::fetch_body};
use hyper::header::CONTENT_TYPE;
use serde::{Deserialize, Serialize};
use utils::map::vec_map::VecMap;
pub mod auth;
pub mod openid;
pub mod registration;
pub mod token;
#[derive(
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
Copy,
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
)]
#[rkyv(compare(PartialEq))]
pub enum OAuthStatus {
Authorized,
TokenIssued,
Pending,
}
const MAX_POST_LEN: usize = 2048;
pub struct OAuth {
pub key: String,
pub expiry_user_code: u64,
pub expiry_auth_code: u64,
pub expiry_token: u64,
pub expiry_refresh_token: u64,
pub expiry_refresh_token_renew: u64,
pub max_auth_attempts: u32,
pub metadata: String,
}
#[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug)]
pub struct OAuthCode {
pub status: OAuthStatus,
pub account_id: u32,
pub client_id: String,
pub nonce: Option<String>,
pub params: String,
pub code_challenge: PkceCodeChallenge,
pub scope: Option<String>,
pub resources: Vec<String>,
}
#[derive(
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
)]
#[rkyv(compare(PartialEq))]
pub enum PkceCodeChallenge {
None,
S256(String),
Plain(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceAuthGet {
code: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceAuthPost {
code: Option<String>,
email: Option<String>,
password: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceAuthRequest {
client_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceAuthResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CodeAuthRequest {
response_type: String,
client_id: String,
redirect_uri: String,
scope: Option<String>,
state: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CodeAuthForm {
code: String,
email: Option<String>,
password: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenRequest {
pub grant_type: String,
pub code: Option<String>,
pub device_code: Option<String>,
pub client_id: Option<String>,
pub refresh_token: Option<String>,
pub redirect_uri: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum TokenResponse {
Granted(OAuthResponse),
Error { error: ErrorType },
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct OAuthResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ErrorType {
#[serde(rename = "invalid_grant")]
InvalidGrant,
#[serde(rename = "invalid_client")]
InvalidClient,
#[serde(rename = "invalid_scope")]
InvalidScope,
#[serde(rename = "invalid_request")]
InvalidRequest,
#[serde(rename = "unauthorized_client")]
UnauthorizedClient,
#[serde(rename = "unsupported_grant_type")]
UnsupportedGrantType,
#[serde(rename = "authorization_pending")]
AuthorizationPending,
#[serde(rename = "slow_down")]
SlowDown,
#[serde(rename = "access_denied")]
AccessDenied,
#[serde(rename = "expired_token")]
ExpiredToken,
}
impl TokenResponse {
pub fn error(error: ErrorType) -> Self {
TokenResponse::Error { error }
}
pub fn is_error(&self) -> bool {
matches!(self, TokenResponse::Error { .. })
}
}
#[derive(Debug)]
pub struct FormData {
fields: VecMap<String, String>,
}
impl FormData {
pub async fn from_request(
req: &mut HttpRequest,
max_len: usize,
session_id: u64,
) -> trc::Result<Self> {
match (
req.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.and_then(|val| val.parse::<mime::Mime>().ok()),
fetch_body(req, max_len, session_id).await,
) {
(Some(content_type), Some(body)) => {
let mut fields = VecMap::new();
if let Some(boundary) = content_type.get_param(mime::BOUNDARY) {
for mut field in
form_data::FormData::new(&body[..], boundary.as_str()).flatten()
{
let value = String::from_utf8_lossy(&field.bytes().unwrap_or_default())
.into_owned();
fields.append(field.name, value);
}
} else {
for (key, value) in http_proto::form_urlencoded::parse(&body) {
fields.append(key.into_owned(), value.into_owned());
}
}
Ok(FormData { fields })
}
_ => Err(trc::ResourceEvent::BadParameters
.into_err()
.details("Invalid post request")),
}
}
pub fn get(&self, key: &str) -> Option<&str> {
self.fields.get(key).map(|v| v.as_str())
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.fields.remove(key)
}
pub fn has_field(&self, key: &str) -> bool {
self.fields.get(key).is_some_and(|v| !v.is_empty())
}
pub fn fields(&self) -> impl Iterator<Item = (&String, &String)> {
self.fields.iter()
}
}
+113
View File
@@ -0,0 +1,113 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::oauth::SUPPORTED_SCOPES, auth::oauth::oidc::Userinfo};
use http_proto::*;
use serde::Serialize;
use std::future::Future;
#[derive(Debug, Serialize)]
pub struct OpenIdMetadata {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub jwks_uri: String,
pub registration_endpoint: String,
pub device_authorization_endpoint: String,
pub scopes_supported: &'static [&'static str],
pub response_types_supported: &'static [&'static str],
pub subject_types_supported: &'static [&'static str],
pub grant_types_supported: &'static [&'static str],
pub token_endpoint_auth_methods_supported: &'static [&'static str],
pub id_token_signing_alg_values_supported: &'static [&'static str],
pub claims_supported: &'static [&'static str],
pub code_challenge_methods_supported: &'static [&'static str],
pub authorization_response_iss_parameter_supported: bool,
}
pub trait OpenIdHandler: Sync + Send {
fn handle_userinfo_request(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_oidc_metadata(
&self,
strip_base_url: bool,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl OpenIdHandler for Server {
async fn handle_userinfo_request(&self, account_id: u32) -> trc::Result<HttpResponse> {
let account = self.account(account_id).await?;
Ok(JsonResponse::new(Userinfo {
sub: Some(account_id.to_string()),
name: account.description().map(|d| d.to_string()),
preferred_username: Some(account.name().to_string()),
email: account.name().to_string().into(),
email_verified: true,
..Default::default()
})
.no_cache()
.into_http_response())
}
async fn handle_oidc_metadata(&self, strip_base_url: bool) -> trc::Result<HttpResponse> {
let base_url = if strip_base_url {
#[cfg(feature = "dev_mode")]
{
"http://127.0.0.1:8080"
}
#[cfg(not(feature = "dev_mode"))]
{
""
}
} else {
&self.core.network.http.url_https
};
Ok(JsonResponse::new(OpenIdMetadata {
authorization_endpoint: format!("{base_url}/login",),
token_endpoint: format!("{base_url}/auth/token"),
userinfo_endpoint: format!("{base_url}/auth/userinfo"),
jwks_uri: format!("{base_url}/auth/jwks.json"),
registration_endpoint: format!("{base_url}/auth/register"),
device_authorization_endpoint: format!("{base_url}/auth/device"),
response_types_supported: &["code"],
grant_types_supported: &[
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
],
scopes_supported: SUPPORTED_SCOPES,
subject_types_supported: &["public"],
token_endpoint_auth_methods_supported: &[
"none",
"client_secret_post",
"client_secret_basic",
],
id_token_signing_alg_values_supported: &[
"RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "HS256",
"HS384", "HS512",
],
claims_supported: &[
"sub",
"name",
"preferred_username",
"email",
"email_verified",
],
code_challenge_methods_supported: &["S256"],
authorization_response_iss_parameter_supported: true,
issuer: base_url.to_string(),
})
.into_http_response()
.with_cors_unrestricted())
}
}
+336
View File
@@ -0,0 +1,336 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
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<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;
fn verify_client_secret(
&self,
client_id: &str,
client_secret: Option<&str>,
) -> 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> {
// 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)
})?;
// 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::<String>();
// Generate client secret
let client_secret = rng()
.sample_iter(Alphanumeric)
.take(48)
.map(char::from)
.collect::<String>();
let secret_hash = hash_secret(
PasswordHashAlgorithm::Argon2id,
client_secret.clone().into_bytes(),
)
.await
.caused_by(trc::location!())?;
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(),
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<Option<ErrorType>> {
// 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::<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| 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<Option<ErrorType>> {
// 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::<OAuthClient>(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()
}
+439
View File
@@ -0,0 +1,439 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ArchivedOAuthStatus, ArchivedPkceCodeChallenge, ErrorType, FormData, MAX_POST_LEN, OAuthCode,
OAuthResponse, OAuthStatus, TokenResponse, registration::ClientRegistrationHandler,
};
use crate::auth::authenticate::HttpHeaders;
use base64::{
Engine,
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
};
use common::{
KV_OAUTH, Server,
auth::{
AccessToken,
oauth::{GrantType, oidc::StandardClaims},
},
};
use http_proto::*;
use hyper::StatusCode;
use sha2::{Digest, Sha256};
use std::{borrow::Cow, future::Future};
use store::{
dispatch::lookup::KeyValue,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
pub trait TokenHandler: Sync + Send {
fn handle_token_request(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_token_introspect(
&self,
req: &mut HttpRequest,
access_token: &AccessToken,
session_id: u64,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
#[allow(clippy::too_many_arguments)]
fn issue_token(
&self,
account_id: u32,
client_id: &str,
issuer: String,
nonce: Option<String>,
scope: Option<String>,
with_refresh_token: bool,
with_id_token: bool,
) -> impl Future<Output = trc::Result<OAuthResponse>> + Send;
}
impl TokenHandler for Server {
// Token endpoint
async fn handle_token_request(
&self,
req: &mut HttpRequest,
session: HttpSessionData,
) -> trc::Result<HttpResponse> {
// Parse form
let params = FormData::from_request(req, MAX_POST_LEN, session.session_id).await?;
let grant_type = params.get("grant_type").unwrap_or_default();
let (client_id_cred, client_secret_cred) = client_credentials(req, &params);
let mut response = TokenResponse::error(ErrorType::InvalidGrant);
let issuer = self.core.network.http.url_https.to_string();
if grant_type.eq_ignore_ascii_case("authorization_code") {
response = if let (Some(code), Some(client_id), Some(redirect_uri)) = (
params.get("code"),
client_id_cred.as_deref(),
params.get("redirect_uri"),
) {
// Obtain code
match self
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(KeyValue::<()>::build_key(
KV_OAUTH,
code.as_bytes(),
))
.await?
{
Some(auth_code_) => {
let oauth = auth_code_
.unarchive::<OAuthCode>()
.caused_by(trc::location!())?;
if client_id != oauth.client_id || redirect_uri != oauth.params {
TokenResponse::error(ErrorType::InvalidClient)
} else if !verify_pkce(&oauth.code_challenge, params.get("code_verifier")) {
TokenResponse::error(ErrorType::InvalidGrant)
} else if oauth.status == OAuthStatus::Authorized {
// Validate client id
if let Some(error) = self
.validate_client_registration(
client_id,
redirect_uri.into(),
oauth.account_id.into(),
)
.await?
{
TokenResponse::error(error)
} else if let Some(error) = self
.verify_client_secret(client_id, client_secret_cred.as_deref())
.await?
{
TokenResponse::error(error)
} else {
// Mark this token as issued
self.in_memory_store()
.key_delete(KeyValue::<()>::build_key(
KV_OAUTH,
code.as_bytes(),
))
.await?;
// Issue token
self.issue_token(
oauth.account_id.into(),
&oauth.client_id,
issuer,
oauth.nonce.as_ref().map(|s| s.as_str().into()),
oauth.scope.as_ref().map(|s| s.as_str().into()),
true,
true,
)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
}
} else {
TokenResponse::error(ErrorType::InvalidGrant)
}
}
None => TokenResponse::error(ErrorType::AccessDenied),
}
} else {
TokenResponse::error(ErrorType::InvalidClient)
};
} else if grant_type.eq_ignore_ascii_case("urn:ietf:params:oauth:grant-type:device_code") {
response = TokenResponse::error(ErrorType::ExpiredToken);
if let (Some(device_code), Some(client_id)) =
(params.get("device_code"), params.get("client_id"))
{
// Obtain code
if let Some(auth_code_) = self
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(KeyValue::<()>::build_key(
KV_OAUTH,
device_code.as_bytes(),
))
.await?
{
let oauth = auth_code_
.unarchive::<OAuthCode>()
.caused_by(trc::location!())?;
response = if oauth.client_id != client_id {
TokenResponse::error(ErrorType::InvalidClient)
} else {
match oauth.status {
ArchivedOAuthStatus::Authorized => {
if let Some(error) = self
.validate_client_registration(
client_id,
None,
oauth.account_id.into(),
)
.await?
{
TokenResponse::error(error)
} else {
// Mark this token as issued
self.in_memory_store()
.key_delete(KeyValue::<()>::build_key(
KV_OAUTH,
device_code.as_bytes(),
))
.await?;
// Issue token
self.issue_token(
oauth.account_id.into(),
&oauth.client_id,
issuer,
oauth.nonce.as_ref().map(|s| s.as_str().into()),
oauth.scope.as_ref().map(|s| s.as_str().into()),
true,
true,
)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
}
}
ArchivedOAuthStatus::Pending => {
TokenResponse::error(ErrorType::AuthorizationPending)
}
ArchivedOAuthStatus::TokenIssued => {
TokenResponse::error(ErrorType::ExpiredToken)
}
}
};
}
}
} else if grant_type.eq_ignore_ascii_case("refresh_token") {
if let Some(refresh_token) = params.get("refresh_token") {
if let Some(client_id) = client_id_cred.as_deref()
&& let Some(error) = self
.verify_client_secret(client_id, client_secret_cred.as_deref())
.await?
{
return Ok(JsonResponse::with_status(
StatusCode::BAD_REQUEST,
TokenResponse::error(error),
)
.into_http_response());
}
response = match self
.validate_access_token(GrantType::RefreshToken.into(), refresh_token)
.await
{
Ok(token_info) => self
.issue_token(
token_info.account_id,
"",
issuer,
None,
None,
token_info.expires_in
<= self.core.oauth.oauth_expiry_refresh_token_renew,
false,
)
.await
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?,
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to validate refresh token")
.span_id(session.session_id)
);
TokenResponse::error(ErrorType::InvalidGrant)
}
};
} else {
response = TokenResponse::error(ErrorType::InvalidRequest);
}
}
Ok(JsonResponse::with_status(
if response.is_error() {
StatusCode::BAD_REQUEST
} else {
StatusCode::OK
},
response,
)
.into_http_response())
}
async fn handle_token_introspect(
&self,
req: &mut HttpRequest,
access_token: &AccessToken,
session_id: u64,
) -> trc::Result<HttpResponse> {
// Parse token
let token = FormData::from_request(req, 1024, session_id)
.await?
.remove("token")
.ok_or_else(|| {
trc::ResourceEvent::BadParameters
.into_err()
.details("Client ID is missing.")
})?;
self.introspect_access_token(&token, access_token)
.await
.map(|response| JsonResponse::new(response).no_cache().into_http_response())
}
async fn issue_token(
&self,
account_id: u32,
client_id: &str,
issuer: String,
nonce: Option<String>,
scope: Option<String>,
with_refresh_token: bool,
with_id_token: bool,
) -> trc::Result<OAuthResponse> {
let credential_version = self
.access_token(account_id)
.await
.caused_by(trc::location!())?
.credential_version();
let account = self.account(account_id).await.caused_by(trc::location!())?;
let account_name = account.name();
Ok(OAuthResponse {
access_token: self
.encode_access_token(
GrantType::AccessToken,
account_id,
account_name,
self.core.oauth.oauth_expiry_token,
None,
credential_version.into(),
)
.await?,
token_type: "bearer".to_string(),
expires_in: self.core.oauth.oauth_expiry_token,
refresh_token: if with_refresh_token {
self.encode_access_token(
GrantType::RefreshToken,
account_id,
account_name,
self.core.oauth.oauth_expiry_refresh_token,
None,
credential_version.into(),
)
.await?
.into()
} else {
None
},
id_token: if with_id_token {
match self.issue_id_token(
account_id.to_string(),
issuer,
client_id,
StandardClaims {
nonce,
preferred_username: account.name().to_string().into(),
email: account.name().to_string().into(),
description: account.description().map(|d| d.to_string()),
},
) {
Ok(id_token) => Some(id_token),
Err(err) => {
trc::error!(err);
None
}
}
} else {
None
},
scope,
})
}
}
fn client_credentials<'x>(
req: &'x HttpRequest,
params: &'x FormData,
) -> (Option<Cow<'x, str>>, Option<Cow<'x, str>>) {
let mut client_id = params.get("client_id").map(Cow::Borrowed);
let mut client_secret = params.get("client_secret").map(Cow::Borrowed);
if (client_id.is_none() || client_secret.is_none())
&& let Some((id, secret)) = req
.authorization_basic()
.and_then(|token| STANDARD.decode(token).ok())
.and_then(|bytes| String::from_utf8(bytes).ok())
.and_then(|creds| {
creds
.split_once(':')
.map(|(id, secret)| (id.to_string(), secret.to_string()))
})
{
if client_id.is_none() {
client_id = Some(Cow::Owned(id));
}
if client_secret.is_none() {
client_secret = Some(Cow::Owned(secret));
}
}
(client_id, client_secret)
}
fn verify_pkce(stored: &ArchivedPkceCodeChallenge, verifier: Option<&str>) -> bool {
let is_valid_pkce_challenge = |challenge: &str| {
(43..=128).contains(&challenge.len())
&& challenge
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~'))
};
let constant_time_eq = |a: &[u8], b: &[u8]| {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
};
match (stored, verifier) {
(ArchivedPkceCodeChallenge::None, None) => true,
(ArchivedPkceCodeChallenge::Plain(expected), Some(verifier))
if is_valid_pkce_challenge(verifier) =>
{
constant_time_eq(expected.as_bytes(), verifier.as_bytes())
}
(ArchivedPkceCodeChallenge::S256(expected), Some(verifier))
if is_valid_pkce_challenge(verifier) =>
{
let digest = Sha256::digest(verifier.as_bytes());
let computed = URL_SAFE_NO_PAD.encode(digest);
constant_time_eq(expected.as_bytes(), computed.as_bytes())
}
_ => false,
}
}
+130
View File
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
Server,
auth::{AccessToken, RECOVERY_ADMIN_ID, permissions::PermissionsListBuilder},
};
use http_proto::{HttpResponse, JsonResponse, ToHttpResponse};
use registry::{
schema::enums::{Locale, Permission},
types::EnumImpl,
};
use serde::Serialize;
use utils::DomainPart;
#[derive(Debug, Clone, Serialize)]
pub struct Account {
pub permissions: Vec<Permission>,
pub edition: &'static str,
pub locale: Locale,
}
pub trait AccountApiHandler: Sync + Send {
fn handle_account_request(
&self,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl AccountApiHandler for Server {
async fn handle_account_request(
&self,
access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
#[cfg(not(feature = "enterprise"))]
let edition = "oss";
let account_info = self.account_info(access_token.account_id()).await?;
let is_external_directory = if let Some(domain_name) = account_info.name().try_domain_part()
&& self.get_directory_for_domain(domain_name).await?.is_some()
{
true
} else {
false
};
let is_recovery_admin = access_token.account_id() == RECOVERY_ADMIN_ID;
let permissions = if !self.registry().is_bootstrap_mode()
&& let Some(scope) = access_token.access_scope()
{
let mut permissions = scope.permissions.clone();
for p in [
Permission::SysDmarcInternalReportUpdate,
Permission::SysDmarcInternalReportCreate,
Permission::SysDmarcExternalReportUpdate,
Permission::SysDmarcExternalReportCreate,
Permission::SysTlsInternalReportUpdate,
Permission::SysTlsInternalReportCreate,
Permission::SysTlsExternalReportUpdate,
Permission::SysTlsExternalReportCreate,
Permission::SysArfExternalReportCreate,
Permission::SysArfExternalReportUpdate,
Permission::SysQueuedMessageCreate,
Permission::SysLogCreate,
Permission::SysLogDestroy,
Permission::SysLogUpdate,
Permission::SysClusterNodeCreate,
Permission::SysClusterNodeUpdate,
Permission::SysClusterNodeDestroy,
Permission::SysBootstrapGet,
Permission::SysBootstrapUpdate,
] {
permissions.clear(p.to_id() as usize);
}
if !self.core.groupware.allow_directory_query {
for p in [
Permission::JmapPrincipalQuery,
Permission::JmapPrincipalQueryChanges,
Permission::JmapPrincipalGet,
Permission::JmapPrincipalGetAvailability,
Permission::JmapPrincipalChanges,
] {
permissions.clear(p.to_id() as usize);
}
}
if is_external_directory || is_recovery_admin {
permissions.clear(Permission::SysAccountPasswordGet.to_id() as usize);
permissions.clear(Permission::SysAccountPasswordUpdate.to_id() as usize);
}
if is_recovery_admin {
for p in [
Permission::SysAccountSettingsGet,
Permission::SysAccountSettingsUpdate,
Permission::SysApiKeyCreate,
Permission::SysApiKeyUpdate,
Permission::SysApiKeyDestroy,
Permission::SysApiKeyQuery,
Permission::SysApiKeyGet,
Permission::SysAppPasswordCreate,
Permission::SysAppPasswordUpdate,
Permission::SysAppPasswordDestroy,
Permission::SysAppPasswordQuery,
Permission::SysAppPasswordGet,
] {
permissions.clear(p.to_id() as usize);
}
}
permissions.build_permissions_list()
} else if self.registry().is_bootstrap_mode() {
vec![Permission::SysBootstrapGet, Permission::SysBootstrapUpdate]
} else {
Vec::new()
};
Ok(JsonResponse::new(Account {
permissions,
edition,
locale: account_info.locale(),
})
.into_http_response())
}
}