Skip to content

Commit 05f055e

Browse files
refactor(webauthn): narrow RelatedOriginsHttpClient error to WellKnownFetchError
The trait's old return type was the full RelatedOriginsError, but four of its five variants (UnexpectedContentType, MalformedJson, MalformedDocument, NoMatchingOrigin) are produced inside validate_related_origins after the fetch returns. Implementers had no reason to ever emit them. Introduce WellKnownFetchError with the variants a fetcher can actually emit (Transport, Status, BodyTooLarge, NotSupported) and let RelatedOriginsError wrap it via a Fetch variant with #[from]. The reqwest client now distinguishes non-200 status from transport faults and from body-cap hits without stringifying everything. Also drops RelatedOriginsError::kind(); the two debug! call sites switch to logging the Display form of the error directly.
1 parent f0e136b commit 05f055e

6 files changed

Lines changed: 62 additions & 58 deletions

File tree

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON, GetAssertionRequestPars
133133
if let Err(err) =
134134
validate_related_origins(&request_origin.origin, &parsed, psl, http).await
135135
{
136-
debug!(rp_id = %parsed.0, kind = err.kind(), "Related-origins validation failed");
136+
debug!(rp_id = %parsed.0, %err, "Related-origins validation failed");
137137
return Err(GetAssertionRequestParsingError::MismatchingRelyingPartyId(
138138
parsed.0,
139139
effective_rp_id.to_string(),
@@ -591,7 +591,7 @@ mod tests {
591591

592592
use crate::ops::webauthn::psl::MockPublicSuffixList;
593593
use crate::ops::webauthn::related_origins::{
594-
RelatedOriginsError, RelatedOriginsHttpClient, WellKnownResponse,
594+
RelatedOriginsHttpClient, WellKnownFetchError, WellKnownResponse,
595595
};
596596
use crate::ops::webauthn::{GetAssertionRequest, NoRelatedOriginsClient, RequestOrigin};
597597
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
@@ -601,7 +601,7 @@ mod tests {
601601
/// Test-only HTTP client backed by a fixed response. `panicking` proves the
602602
/// suffix-check short-circuit by failing the test if the fetch is invoked.
603603
struct MockHttpClient {
604-
response: Option<Result<WellKnownResponse, RelatedOriginsError>>,
604+
response: Option<Result<WellKnownResponse, WellKnownFetchError>>,
605605
}
606606

607607
impl MockHttpClient {
@@ -614,7 +614,7 @@ mod tests {
614614
}
615615
}
616616

617-
fn err(e: RelatedOriginsError) -> Self {
617+
fn err(e: WellKnownFetchError) -> Self {
618618
Self {
619619
response: Some(Err(e)),
620620
}
@@ -630,7 +630,7 @@ mod tests {
630630
async fn fetch_well_known(
631631
&self,
632632
_: &RelyingPartyId,
633-
) -> Result<WellKnownResponse, RelatedOriginsError> {
633+
) -> Result<WellKnownResponse, WellKnownFetchError> {
634634
match &self.response {
635635
Some(r) => r.clone(),
636636
None => panic!("fetch_well_known should not be called"),
@@ -839,7 +839,7 @@ mod tests {
839839
async fn related_origins_fetch_error_keeps_mismatch_error() {
840840
let request_origin: RequestOrigin = "https://app.example.org".parse().unwrap();
841841
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""example.com""#);
842-
let http = MockHttpClient::err(RelatedOriginsError::FetchFailed("simulated".into()));
842+
let http = MockHttpClient::err(WellKnownFetchError::Transport("simulated".into()));
843843

844844
let result = GetAssertionRequest::from_json(
845845
&request_origin,

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ impl FromIdlModel<PublicKeyCredentialCreationOptionsJSON, MakeCredentialRequestP
383383
if let Err(err) =
384384
validate_related_origins(&request_origin.origin, &rp_id, psl, http).await
385385
{
386-
debug!(rp_id = %rp_id.0, kind = err.kind(), "Related-origins validation failed");
386+
debug!(rp_id = %rp_id.0, %err, "Related-origins validation failed");
387387
return Err(
388388
MakeCredentialRequestParsingError::MismatchingRelyingPartyId(
389389
rp_id.0,
@@ -657,7 +657,7 @@ mod tests {
657657

658658
use crate::ops::webauthn::psl::MockPublicSuffixList;
659659
use crate::ops::webauthn::related_origins::{
660-
RelatedOriginsError, RelatedOriginsHttpClient, WellKnownResponse,
660+
RelatedOriginsHttpClient, WellKnownFetchError, WellKnownResponse,
661661
};
662662
use crate::ops::webauthn::{MakeCredentialRequest, NoRelatedOriginsClient, RequestOrigin};
663663
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
@@ -667,7 +667,7 @@ mod tests {
667667
/// Test-only HTTP client backed by a fixed response. `panicking` proves the
668668
/// suffix-check short-circuit by failing the test if the fetch is invoked.
669669
struct MockHttpClient {
670-
response: Option<Result<WellKnownResponse, RelatedOriginsError>>,
670+
response: Option<Result<WellKnownResponse, WellKnownFetchError>>,
671671
}
672672

673673
impl MockHttpClient {
@@ -680,7 +680,7 @@ mod tests {
680680
}
681681
}
682682

683-
fn err(e: RelatedOriginsError) -> Self {
683+
fn err(e: WellKnownFetchError) -> Self {
684684
Self {
685685
response: Some(Err(e)),
686686
}
@@ -696,7 +696,7 @@ mod tests {
696696
async fn fetch_well_known(
697697
&self,
698698
_: &RelyingPartyId,
699-
) -> Result<WellKnownResponse, RelatedOriginsError> {
699+
) -> Result<WellKnownResponse, WellKnownFetchError> {
700700
match &self.response {
701701
Some(r) => r.clone(),
702702
None => panic!("fetch_well_known should not be called"),
@@ -1130,7 +1130,7 @@ mod tests {
11301130
"rp",
11311131
r#"{"id": "example.com", "name": "example.com"}"#,
11321132
);
1133-
let http = MockHttpClient::err(RelatedOriginsError::FetchFailed("simulated".into()));
1133+
let http = MockHttpClient::err(WellKnownFetchError::Transport("simulated".into()));
11341134

11351135
let result = MakeCredentialRequest::from_json(
11361136
&request_origin,

libwebauthn/src/ops/webauthn/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub use psl::{
3838
};
3939
pub use related_origins::{
4040
validate_related_origins, NoRelatedOriginsClient, RelatedOriginsError,
41-
RelatedOriginsHttpClient, WellKnownResponse,
41+
RelatedOriginsHttpClient, WellKnownFetchError, WellKnownResponse,
4242
};
4343
#[cfg(feature = "related-origins-client")]
4444
pub use related_origins::{HttpPolicy, ReqwestRelatedOriginsClient};

libwebauthn/src/ops/webauthn/related_origins/http.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use futures::StreamExt;
88
use reqwest::redirect::Policy;
99
use reqwest::{Client, StatusCode};
1010

11-
use super::{RelatedOriginsError, RelatedOriginsHttpClient, WellKnownResponse};
11+
use super::{RelatedOriginsHttpClient, WellKnownFetchError, WellKnownResponse};
1212
use crate::ops::webauthn::idl::rpid::RelyingPartyId;
1313

1414
#[derive(Debug, Clone)]
@@ -35,11 +35,11 @@ pub struct ReqwestRelatedOriginsClient {
3535
}
3636

3737
impl ReqwestRelatedOriginsClient {
38-
pub fn new() -> Result<Self, RelatedOriginsError> {
38+
pub fn new() -> Result<Self, WellKnownFetchError> {
3939
Self::with_policy(HttpPolicy::default())
4040
}
4141

42-
pub fn with_policy(policy: HttpPolicy) -> Result<Self, RelatedOriginsError> {
42+
pub fn with_policy(policy: HttpPolicy) -> Result<Self, WellKnownFetchError> {
4343
let max_redirects = policy.max_redirects;
4444
let redirect_policy = Policy::custom(move |attempt| {
4545
if attempt.previous().len() >= max_redirects {
@@ -58,7 +58,7 @@ impl ReqwestRelatedOriginsClient {
5858
.referer(false)
5959
.timeout(policy.request_timeout)
6060
.build()
61-
.map_err(|e| RelatedOriginsError::FetchFailed(e.to_string()))?;
61+
.map_err(|e| WellKnownFetchError::Transport(e.to_string()))?;
6262
Ok(Self {
6363
client,
6464
max_body_bytes: policy.max_body_bytes,
@@ -71,19 +71,16 @@ impl RelatedOriginsHttpClient for ReqwestRelatedOriginsClient {
7171
async fn fetch_well_known(
7272
&self,
7373
rp_id: &RelyingPartyId,
74-
) -> Result<WellKnownResponse, RelatedOriginsError> {
74+
) -> Result<WellKnownResponse, WellKnownFetchError> {
7575
let url = format!("https://{}/.well-known/webauthn", rp_id.0);
7676
let response = self
7777
.client
7878
.get(&url)
7979
.send()
8080
.await
81-
.map_err(|e| RelatedOriginsError::FetchFailed(e.to_string()))?;
81+
.map_err(|e| WellKnownFetchError::Transport(e.to_string()))?;
8282
if response.status() != StatusCode::OK {
83-
return Err(RelatedOriginsError::FetchFailed(format!(
84-
"status {}",
85-
response.status()
86-
)));
83+
return Err(WellKnownFetchError::Status(response.status().as_u16()));
8784
}
8885
let content_type = response
8986
.headers()
@@ -94,11 +91,9 @@ impl RelatedOriginsHttpClient for ReqwestRelatedOriginsClient {
9491
let mut body = Vec::with_capacity(8 * 1024);
9592
let mut stream = response.bytes_stream();
9693
while let Some(chunk) = stream.next().await {
97-
let chunk = chunk.map_err(|e| RelatedOriginsError::FetchFailed(e.to_string()))?;
94+
let chunk = chunk.map_err(|e| WellKnownFetchError::Transport(e.to_string()))?;
9895
if body.len() + chunk.len() > self.max_body_bytes {
99-
return Err(RelatedOriginsError::FetchFailed(
100-
"body exceeded size cap".into(),
101-
));
96+
return Err(WellKnownFetchError::BodyTooLarge);
10297
}
10398
body.extend_from_slice(&chunk);
10499
}

libwebauthn/src/ops/webauthn/related_origins/mod.rs

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,41 @@ pub struct WellKnownResponse {
3232
/// Fetcher for `https://{rp_id}/.well-known/webauthn`, per WebAuthn L3 §5.11.1
3333
/// step 2. Implementations MUST send no credentials, no Referer, refuse
3434
/// non-`https://` redirects, cap the body size, and bound the request duration.
35-
/// Implementations MUST return `Err(FetchFailed)` for any status code other
36-
/// than 200 (after following redirects). Implementations MUST report the wire
37-
/// `Content-Type` header value unmodified (or `None` if absent) and MUST NOT
38-
/// synthesise an `application/json` content type for non-JSON responses.
35+
/// Implementations MUST return `Err(WellKnownFetchError::Status)` for any
36+
/// status code other than 200 (after following redirects). Implementations MUST
37+
/// report the wire `Content-Type` header value unmodified (or `None` if absent)
38+
/// and MUST NOT synthesise an `application/json` content type for non-JSON
39+
/// responses.
3940
#[async_trait]
4041
pub trait RelatedOriginsHttpClient: Send + Sync {
4142
async fn fetch_well_known(
4243
&self,
4344
rp_id: &RelyingPartyId,
44-
) -> Result<WellKnownResponse, RelatedOriginsError>;
45+
) -> Result<WellKnownResponse, WellKnownFetchError>;
46+
}
47+
48+
/// Failure modes for [`RelatedOriginsHttpClient::fetch_well_known`].
49+
#[derive(thiserror::Error, Debug, Clone)]
50+
pub enum WellKnownFetchError {
51+
/// Transport-level failure: TLS, DNS, timeout, rejected redirect, body
52+
/// stream interrupt, client build error, etc.
53+
#[error("transport error: {0}")]
54+
Transport(String),
55+
/// Endpoint replied with a non-200 status (after following redirects).
56+
#[error("HTTP status {0}")]
57+
Status(u16),
58+
/// Body exceeded the implementation's configured size cap before completion.
59+
#[error("body exceeded configured size cap")]
60+
BodyTooLarge,
61+
/// Implementation does not perform fetches (see [`NoRelatedOriginsClient`]).
62+
#[error("client does not support related-origin fetches")]
63+
NotSupported,
4564
}
4665

4766
#[derive(thiserror::Error, Debug, Clone)]
4867
pub enum RelatedOriginsError {
4968
#[error("well-known fetch failed: {0}")]
50-
FetchFailed(String),
69+
Fetch(#[from] WellKnownFetchError),
5170
#[error("unexpected content type: {0:?}")]
5271
UnexpectedContentType(Option<String>),
5372
/// Step 2.b: body did not decode as JSON.
@@ -60,19 +79,6 @@ pub enum RelatedOriginsError {
6079
NoMatchingOrigin,
6180
}
6281

63-
impl RelatedOriginsError {
64-
/// Log-safe variant discriminant; `Debug`/`Display` may carry reqwest/serde text with IPs or body snippets.
65-
pub fn kind(&self) -> &'static str {
66-
match self {
67-
RelatedOriginsError::FetchFailed(_) => "fetch_failed",
68-
RelatedOriginsError::UnexpectedContentType(_) => "unexpected_content_type",
69-
RelatedOriginsError::MalformedJson(_) => "malformed_json",
70-
RelatedOriginsError::MalformedDocument(_) => "malformed_document",
71-
RelatedOriginsError::NoMatchingOrigin => "no_matching_origin",
72-
}
73-
}
74-
}
75-
7682
pub type RelatedOriginsResult = Result<(), RelatedOriginsError>;
7783

7884
#[derive(Debug, Deserialize)]
@@ -192,10 +198,8 @@ impl RelatedOriginsHttpClient for NoRelatedOriginsClient {
192198
async fn fetch_well_known(
193199
&self,
194200
_: &RelyingPartyId,
195-
) -> Result<WellKnownResponse, RelatedOriginsError> {
196-
Err(RelatedOriginsError::FetchFailed(
197-
"this client does not support related origin requests".into(),
198-
))
201+
) -> Result<WellKnownResponse, WellKnownFetchError> {
202+
Err(WellKnownFetchError::NotSupported)
199203
}
200204
}
201205

@@ -205,15 +209,15 @@ mod tests {
205209
use super::*;
206210

207211
struct MockClient {
208-
response: Result<WellKnownResponse, RelatedOriginsError>,
212+
response: Result<WellKnownResponse, WellKnownFetchError>,
209213
}
210214

211215
#[async_trait]
212216
impl RelatedOriginsHttpClient for MockClient {
213217
async fn fetch_well_known(
214218
&self,
215219
_: &RelyingPartyId,
216-
) -> Result<WellKnownResponse, RelatedOriginsError> {
220+
) -> Result<WellKnownResponse, WellKnownFetchError> {
217221
self.response.clone()
218222
}
219223
}
@@ -670,9 +674,9 @@ mod tests {
670674
}
671675

672676
#[tokio::test]
673-
async fn fetch_error_propagates_as_fetch_failed() {
677+
async fn fetch_error_propagates_as_fetch() {
674678
let http = MockClient {
675-
response: Err(RelatedOriginsError::FetchFailed("simulated".into())),
679+
response: Err(WellKnownFetchError::Transport("simulated".into())),
676680
};
677681
let res = validate_related_origins(
678682
&caller("https://example.com"),
@@ -681,7 +685,12 @@ mod tests {
681685
&http,
682686
)
683687
.await;
684-
assert!(matches!(res, Err(RelatedOriginsError::FetchFailed(_))));
688+
assert!(matches!(
689+
res,
690+
Err(RelatedOriginsError::Fetch(WellKnownFetchError::Transport(
691+
_
692+
)))
693+
));
685694
}
686695

687696
#[tokio::test]

libwebauthn/tests/related_origins.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
use async_trait::async_trait;
77

88
use libwebauthn::ops::webauthn::{
9-
GetAssertionRequest, MakeCredentialRequest, PublicSuffixList, RelatedOriginsError,
10-
RelatedOriginsHttpClient, RelyingPartyId, RequestOrigin, WebAuthnIDL, WellKnownResponse,
9+
GetAssertionRequest, MakeCredentialRequest, PublicSuffixList, RelatedOriginsHttpClient,
10+
RelyingPartyId, RequestOrigin, WebAuthnIDL, WellKnownFetchError, WellKnownResponse,
1111
};
1212

1313
const KNOWN_SUFFIXES: &[&str] = &["com", "org"];
@@ -39,7 +39,7 @@ impl RelatedOriginsHttpClient for StaticHttp {
3939
async fn fetch_well_known(
4040
&self,
4141
_: &RelyingPartyId,
42-
) -> Result<WellKnownResponse, RelatedOriginsError> {
42+
) -> Result<WellKnownResponse, WellKnownFetchError> {
4343
Ok(WellKnownResponse {
4444
content_type: Some("application/json".into()),
4545
body: self.body.as_bytes().to_vec(),

0 commit comments

Comments
 (0)