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}