From 548ff03f30b69106568f00cda971f7a39bdc7d7f Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:37:57 +0200 Subject: [PATCH] OAuth resource indicators: Accept `imap`, `smtp`, `pop3` and `sieve` as valid resource indicators for OAuth access tokens --- CHANGELOG.md | 13 ++++++- crates/http/src/auth/oauth/auth.rs | 62 +++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e36a543..fe197e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,23 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.16.13] - 2026-07-XX + +If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. + +## Added + +## Changed + +## Fixed +- OAuth resource indicators: Accept `imap`, `smtp`, `pop3` and `sieve` as valid resource indicators for OAuth access tokens. + ## [0.16.12] - 2026-07-06 If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. ## Added -- DKIM2 implementation ([draft-ietf-dkim-dkim2-spec-03](https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-spec/)). +- DKIM2 implementation ([draft-ietf-dkim-dkim2-spec-04](https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-spec/)). - DMARCbis implementation: - Domain-based Message Authentication, Reporting, and Conformance (DMARC) ([RFC 9989](https://datatracker.ietf.org/doc/html/rfc9989)) - DMARC Aggregate Reporting ([RFC 9990](https://datatracker.ietf.org/doc/html/rfc9990)) diff --git a/crates/http/src/auth/oauth/auth.rs b/crates/http/src/auth/oauth/auth.rs index 4f0e58bc..11d1616c 100644 --- a/crates/http/src/auth/oauth/auth.rs +++ b/crates/http/src/auth/oauth/auth.rs @@ -225,10 +225,30 @@ impl OAuthApiHandler for Server { // Validate Resource Indicators (RFC 8707) for resource in &resource { - if !is_known_resource(&self.core.network.http.url_https, 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("Unknown resource indicator.")); + .details(format!("Unknown resource indicator: {}", resource))); } } @@ -585,8 +605,38 @@ fn grant_scope(requested: Option<&str>, registered_mask: u64) -> Option (!granted.is_empty()).then_some(granted) } -fn is_known_resource(base_url: &str, uri: &str) -> bool { - let base = base_url.trim_end_matches('/'); - uri.strip_prefix(base) - .is_some_and(|rest| rest.is_empty() || rest.starts_with('/')) +fn is_known_resource<'x>(hostnames: impl IntoIterator, 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(|(h, _)| h.as_bytes().iter().all(|c| c.is_ascii_digit())) + .map_or(authority, |(h, _)| h); + + supported + && hostnames + .into_iter() + .any(|hostname| host.eq_ignore_ascii_case(hostname)) }