Skip to main content

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}