Skip to main content

protocol_droid/application/oauth/
environment.rs

1//! What the authorization flow needs from the outside world.
2//!
3//! The flow in [`authorizer`](super::authorizer) is decisions: which document to
4//! read next, which client to present, which scope to ask for. Everything it
5//! cannot decide by itself — fetching a URL, sending the user somewhere,
6//! trading a code for a token — it asks of an [`OAuthEnvironment`], so the
7//! flow runs unchanged against a test double, a headless agent, or a person
8//! with a browser.
9//!
10//! The port belongs to its consumer, here, and returns this module's errors:
11//! an adapter maps `reqwest` and the operating system's failures to them at
12//! the boundary, and nothing above names a third-party type.
13
14use core::future::Future;
15
16use crate::application::oauth::callback::AuthorizationCallback;
17use crate::application::oauth::redeem::TokenEndpoint;
18
19/// A response read in full.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Fetched {
22    /// The HTTP status.
23    pub status: u16,
24    /// The body, capped by the adapter.
25    pub body: Vec<u8>,
26}
27
28impl Fetched {
29    /// Whether the status is `2xx`.
30    #[must_use]
31    pub const fn is_success(&self) -> bool {
32        self.status >= 200 && self.status < 300
33    }
34}
35
36/// Why the environment could not do what it was asked.
37#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
38pub enum EnvironmentError {
39    /// The network, or the other side, failed.
40    #[error("{0}")]
41    Unavailable(String),
42    /// The user, or whatever stands in for them, declined or could not be
43    /// reached.
44    #[error("the authorization was not completed: {0}")]
45    Declined(String),
46}
47
48/// The outside world, as the authorization flow sees it.
49pub trait OAuthEnvironment: Send + Sync + 'static {
50    /// Where authorization codes are redeemed, bound to one endpoint.
51    type Token: TokenEndpoint + Send + Sync;
52
53    /// A token endpoint at `url`.
54    fn token_endpoint(&self, url: &str) -> Self::Token;
55
56    /// `GET`s `url` without following redirects.
57    ///
58    /// # Errors
59    ///
60    /// [`EnvironmentError::Unavailable`] when no response was read.
61    fn get(&self, url: &str) -> impl Future<Output = Result<Fetched, EnvironmentError>> + Send;
62
63    /// `POST`s `body` to `url` as `application/json`.
64    ///
65    /// # Errors
66    ///
67    /// [`EnvironmentError::Unavailable`] when no response was read.
68    fn post_json(
69        &self,
70        url: &str,
71        body: &str,
72    ) -> impl Future<Output = Result<Fetched, EnvironmentError>> + Send;
73
74    /// Sends the user to `authorization_url` and returns where they came back.
75    ///
76    /// A browser adapter opens the URL and waits for the loopback redirect; a
77    /// headless one asks an authorization server that approves by itself and
78    /// reads the `Location` it answers with.
79    ///
80    /// # Errors
81    ///
82    /// [`EnvironmentError::Declined`] when no callback arrived.
83    fn authorize(
84        &self,
85        authorization_url: &str,
86    ) -> impl Future<Output = Result<AuthorizationCallback, EnvironmentError>> + Send;
87}