protocol_droid/adapters/secondary/oauth/registration/metadata_document.rs
1//! Fetching a Client ID Metadata Document: the client publishes its identity
2//! at a URL and the authorization server reads it.
3//!
4//! # Why the fetched document is not trusted
5//!
6//! This is the part of client registration where a failure is silent and
7//! upstream. Dynamic registration hands back a credential the server minted;
8//! a metadata document is something the *client* wrote and anyone on the path
9//! could have rewritten. If a document served at
10//! `https://client.example.com/metadata.json` claims
11//! `"client_id": "https://attacker.example.com/metadata.json"`, then every
12//! authorization server that fetches it sees a document that looks complete and
13//! identifies the wrong party — and nothing downstream can detect that,
14//! because nothing downstream re-reads the URL.
15//!
16//! So the check is here, and it is the one the draft requires: the document's
17//! `client_id` must be the URL it was served from. That is
18//! [`ClientId::verify_metadata_document`], and a document that fails it
19//! surfaces as [`RegistrarError::Document`]. The use case treats that failure
20//! differently from a fetch failure — it does **not** fall back to dynamic
21//! registration — because a document that is present and wrong is a forgery,
22//! and falling back would let an impersonated client quietly register a fresh
23//! identity rather than surface the problem.
24//!
25//! # Why no secret is produced
26//!
27//! The draft is explicit that no shared secret can be established for this
28//! form. There is no code path here that could produce one: the only
29//! constructor used is [`RegisteredClient::from_metadata_document`], which
30//! takes no secret parameter to get wrong.
31
32use serde::Deserialize;
33
34use crate::application::oauth::registration::{MetadataDocumentLoader, RegistrarError};
35use crate::domain::oauth::registration::{ClientId, RegisteredClient};
36
37/// The most this module will read from one metadata document.
38///
39/// A document is a handful of URLs and short strings. The cap is here so a
40/// server that will not stop sending cannot turn one fetch into an allocation
41/// of whatever size it likes — and the URL being fetched is one the client
42/// chose, so the server is only half-trusted at best.
43const MAX_DOCUMENT_BYTES: usize = 64 * 1024;
44
45/// Fetches published Client ID Metadata Documents.
46#[derive(Debug, Clone)]
47pub struct PublishedDocumentLoader {
48 http: reqwest::Client,
49}
50
51impl PublishedDocumentLoader {
52 /// A registrar with no configuration.
53 #[must_use]
54 pub fn new() -> Self {
55 Self {
56 // Built once for the same reason as every other adapter here: a
57 // per-request TLS handshake and a fresh pool are not something to
58 // pay during registration.
59 http: reqwest::Client::new(),
60 }
61 }
62
63 /// Replaces the HTTP client, for a caller that needs a timeout, a proxy or
64 /// custom TLS settings.
65 #[must_use]
66 pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
67 self.http = http;
68 self
69 }
70}
71
72impl Default for PublishedDocumentLoader {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl MetadataDocumentLoader for PublishedDocumentLoader {
79 async fn fetch_metadata_document(
80 &self,
81 issuer: &str,
82 client_id: &ClientId,
83 ) -> Result<RegisteredClient, RegistrarError> {
84 // RFC 7591 §3.1 and CIMD both use GET, and both expect
85 // `application/json`. `ClientId::metadata_url` already refused
86 // anything that is not `https`, so this GET cannot be redirected by a
87 // caller into fetching an arbitrary scheme.
88 let response = self
89 .http
90 .get(client_id.as_str())
91 .send()
92 .await
93 .map_err(|failure| RegistrarError::Unavailable(failure.to_string()))?;
94
95 let status = response.status();
96 if !status.is_success() {
97 return Err(RegistrarError::Refused {
98 status: status.as_u16(),
99 });
100 }
101
102 let body = read_capped(response).await?;
103 document(issuer, client_id, &body)
104 }
105}
106
107/// Turn a fetched document into a client, after checking what it claims.
108///
109/// Pure, and separate from the HTTP above for the same reason
110/// [`claims_from`](crate::adapters::secondary::oauth::introspection) is: the
111/// check the whole module exists for lives in how a document is *read*, and it
112/// is worth testing without a socket in the way.
113///
114/// # Errors
115///
116/// [`RegistrarError::Malformed`] if the body is not a metadata document, or
117/// [`RegistrarError::Document`] if the document's `client_id` is not the URL it
118/// was served from. The two stay distinct because the use case reacts
119/// differently: a malformed document can fall back to dynamic registration, a
120/// document that is present and wrong cannot.
121fn document(
122 issuer: &str,
123 client_id: &ClientId,
124 body: &[u8],
125) -> Result<RegisteredClient, RegistrarError> {
126 let document: Document = serde_json::from_slice(body)
127 .map_err(|failure| RegistrarError::Malformed(failure.to_string()))?;
128
129 // The check the whole module exists for. Nothing downstream can re-detect
130 // a forged document, because nothing downstream re-reads the URL.
131 client_id
132 .verify_metadata_document(&document.client_id)
133 .map_err(RegistrarError::Document)?;
134
135 Ok(RegisteredClient::from_metadata_document(
136 issuer,
137 client_id.clone(),
138 ))
139}
140
141/// The document as it arrives.
142///
143/// Private, and separate from anything in `application/`: this is a wire shape
144/// with one field this crate reads and a great many it does not. Unknown
145/// fields are ignored here rather than preserved, because the document is
146/// already published — the client holds the authoritative copy, and this crate
147/// is a reader of it, not a relay.
148#[derive(Debug, Deserialize)]
149struct Document {
150 /// The identifier the document claims. Verified against the URL it came
151 /// from before anything is built from it.
152 client_id: String,
153}
154
155/// Reads a response body, stopping if it grows past [`MAX_DOCUMENT_BYTES`].
156async fn read_capped(mut response: reqwest::Response) -> Result<Vec<u8>, RegistrarError> {
157 let mut body: Vec<u8> = Vec::new();
158 while let Some(chunk) = response
159 .chunk()
160 .await
161 .map_err(|failure| RegistrarError::Unavailable(failure.to_string()))?
162 {
163 if body.len().saturating_add(chunk.len()) > MAX_DOCUMENT_BYTES {
164 return Err(RegistrarError::Malformed(format!(
165 "the metadata document exceeded {MAX_DOCUMENT_BYTES} bytes",
166 )));
167 }
168 body.extend_from_slice(&chunk);
169 }
170 Ok(body)
171}