OAuth resource indicators: Accept imap, smtp, pop3 and sieve as valid resource indicators for OAuth access tokens

This commit is contained in:
Maurus Decimus
2026-07-06 15:37:57 +02:00
parent d368b53ca1
commit 548ff03f30
2 changed files with 68 additions and 7 deletions

View File

@@ -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/). 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 ## [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. 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 ## 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: - DMARCbis implementation:
- Domain-based Message Authentication, Reporting, and Conformance (DMARC) ([RFC 9989](https://datatracker.ietf.org/doc/html/rfc9989)) - 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)) - DMARC Aggregate Reporting ([RFC 9990](https://datatracker.ietf.org/doc/html/rfc9990))

View File

@@ -225,10 +225,30 @@ impl OAuthApiHandler for Server {
// Validate Resource Indicators (RFC 8707) // Validate Resource Indicators (RFC 8707)
for resource in &resource { 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 return Err(trc::AuthEvent::Error
.into_err() .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<String>
(!granted.is_empty()).then_some(granted) (!granted.is_empty()).then_some(granted)
} }
fn is_known_resource(base_url: &str, uri: &str) -> bool { fn is_known_resource<'x>(hostnames: impl IntoIterator<Item = &'x str>, uri: &str) -> bool {
let base = base_url.trim_end_matches('/'); let Some((scheme, rest)) = uri.split_once("://") else {
uri.strip_prefix(base) return false;
.is_some_and(|rest| rest.is_empty() || rest.starts_with('/')) };
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))
} }