protocol_droid/application/oauth/registration/ports.rs
1//! What a deployment can reach, and the ports that get it there.
2//!
3//! Two ways to become a registered client: RFC 7591 dynamic registration,
4//! where the client POSTs its metadata and the server issues a credential,
5//! and the Client ID Metadata Document, where the client publishes a
6//! document and asserts nothing. They differ in what they send and in what
7//! they establish, so they are two ports rather than one with a mode flag.
8//!
9//! [`Available`] is the separate question of whether either is wired up at
10//! all. It is an argument to [`plan_registration`](super::plan_registration)
11//! rather than something the handler reads off the identity, because the two
12//! fail independently — a client can publish a document to a URL that
13//! resolves nowhere, and a deployment can wire a dynamic registrar while its
14//! client has no metadata to present.
15
16use super::{ClientMetadata, RegistrarError};
17use crate::domain::oauth::registration::{ClientId, RegisteredClient};
18
19/// Registers a client dynamically through RFC 7591 §3.1.
20///
21/// A port of its own rather than a method on a wider trait, because a
22/// deployment may be able to do only one of the two mechanisms: a client that
23/// publishes a document and points at a server that implements only documents
24/// has no use for this, and an adapter that claimed it would be promising a
25/// capability it does not have.
26///
27/// Native `async fn`, so it is not object safe. That is the right trade:
28/// registration happens once per client rather than per request, and the call
29/// site takes `&impl DynamicRegistrar` for a zero-cost monomorphised call.
30///
31/// # Examples
32///
33/// ```
34/// use protocol_droid::application::oauth::registration::{
35/// ClientMetadata, DynamicRegistrar, RegistrarError,
36/// };
37/// use protocol_droid::domain::oauth::registration::{ClientId, RegisteredClient};
38///
39/// struct AlwaysTheSame;
40///
41/// impl DynamicRegistrar for AlwaysTheSame {
42/// async fn register_dynamic(
43/// &self,
44/// issuer: &str,
45/// _metadata: &ClientMetadata,
46/// ) -> Result<RegisteredClient, RegistrarError> {
47/// Ok(RegisteredClient::dynamic(
48/// issuer,
49/// ClientId::opaque("client-1"),
50/// None,
51/// )?)
52/// }
53/// }
54/// # Ok::<(), RegistrarError>(())
55/// ```
56pub trait DynamicRegistrar {
57 /// POST the metadata to the registration endpoint and read the credential
58 /// the server issues.
59 ///
60 /// # Errors
61 ///
62 /// [`RegistrarError`] if the request could not be completed, the server
63 /// refused it, or the response was not a usable registration response.
64 fn register_dynamic(
65 &self,
66 issuer: &str,
67 metadata: &ClientMetadata,
68 ) -> impl Future<Output = Result<RegisteredClient, RegistrarError>> + Send;
69}
70
71/// Fetches the Client ID Metadata Document a client published.
72///
73/// The counterpart to [`DynamicRegistrar`], and separate for the same reason.
74/// The two differ in what they establish as well as in what they send: this one
75/// yields no secret at all.
76///
77/// # Examples
78///
79/// ```
80/// use protocol_droid::application::oauth::registration::{
81/// MetadataDocumentLoader, RegistrarError,
82/// };
83/// use protocol_droid::domain::oauth::registration::{ClientId, RegisteredClient};
84///
85/// struct RefusesEverything;
86///
87/// impl MetadataDocumentLoader for RefusesEverything {
88/// async fn fetch_metadata_document(
89/// &self,
90/// _issuer: &str,
91/// _client_id: &ClientId,
92/// ) -> Result<RegisteredClient, RegistrarError> {
93/// Err(RegistrarError::Refused { status: 404 })
94/// }
95/// }
96/// # Ok::<(), RegistrarError>(())
97/// ```
98pub trait MetadataDocumentLoader {
99 /// Fetch the document published at `client_id` and verify what it claims.
100 ///
101 /// # Errors
102 ///
103 /// [`RegistrarError`] if the document could not be fetched, or
104 /// [`RegistrarError::Document`] if it does not claim the URL it was served
105 /// from. The two are distinct because the use case reacts differently.
106 fn fetch_metadata_document(
107 &self,
108 issuer: &str,
109 client_id: &ClientId,
110 ) -> impl Future<Output = Result<RegisteredClient, RegistrarError>> + Send;
111}
112
113/// What this deployment can actually reach.
114///
115/// A separate argument to [`plan_registration`](super::plan_registration) rather than something it reads
116/// off the identity, because the two fail independently: a client can publish
117/// a document to a URL that resolves nowhere, and a deployment can wire a
118/// dynamic registrar while its client has no metadata to present. Planning on
119/// one and discovering the other during execution is how a client ends up
120/// explaining a failure it could have refused to start.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct Available {
123 dynamic: bool,
124 metadata_document: bool,
125}
126
127impl Available {
128 /// Neither mechanism is reachable.
129 #[must_use]
130 pub const fn neither() -> Self {
131 Self {
132 dynamic: false,
133 metadata_document: false,
134 }
135 }
136
137 /// Only RFC 7591 dynamic registration.
138 #[must_use]
139 pub const fn dynamic_only() -> Self {
140 Self {
141 dynamic: true,
142 metadata_document: false,
143 }
144 }
145
146 /// Only Client ID Metadata Documents.
147 #[must_use]
148 pub const fn metadata_document_only() -> Self {
149 Self {
150 dynamic: false,
151 metadata_document: true,
152 }
153 }
154
155 /// Both, with a fallback available in either direction.
156 #[must_use]
157 pub const fn both() -> Self {
158 Self {
159 dynamic: true,
160 metadata_document: true,
161 }
162 }
163
164 /// Whether dynamic registration can be reached.
165 #[must_use]
166 pub const fn dynamic(self) -> bool {
167 self.dynamic
168 }
169
170 /// Whether a metadata document can be fetched.
171 #[must_use]
172 pub const fn metadata_document(self) -> bool {
173 self.metadata_document
174 }
175}