Skip to main content

gproxy_channel_api/
login.rs

1//! OAuth authorization-code login for channels (§14.5).
2//!
3//! [`Channel`](crate::channel::Channel) covers *per-request* upstream access and
4//! the silent `refresh_token` rotation; [`ChannelLogin`] is the orthogonal
5//! *first-time* credential acquisition — the interactive authcode + PKCE dance
6//! that mints the very secret a channel later refreshes. A channel may impl one,
7//! both, or neither (an API-key channel impls neither).
8//!
9//! Pure async + `serde_json` — no cipher, no persistence, no axum. Compiles on
10//! native AND wasm (the registry that holds these is used on every target); the
11//! admin HTTP endpoints that drive the flow are native-only. The dual
12//! `#[cfg_attr]` async_trait Send/?Send split mirrors [`Channel`](crate::Channel).
13
14use std::sync::Arc;
15
16use crate::error::ChannelError;
17use crate::transport::UpstreamClient;
18
19/// The output of [`ChannelLogin::authcode_start`]: where to send the user, and
20/// the redirect_uri the channel actually used.
21///
22/// `redirect_uri` is echoed back so the matching `complete` exchanges the code
23/// with the SAME value the authorize step advertised — OAuth requires them to
24/// match. A channel with a fixed redirect_uri ignores the caller's hint and
25/// returns its own here.
26pub struct AuthCodeStart {
27    pub authorize_url: String,
28    pub redirect_uri: String,
29    /// Opaque channel-owned state minted at start (e.g. a dynamically-registered
30    /// OAuth client's `client_id`/`client_secret`/`region` for AWS IdC) that the
31    /// matching `complete` must hand back to
32    /// [`authcode_exchange`](ChannelLogin::authcode_exchange). `None` for static
33    /// flows whose authorize URL needs no prior network call.
34    pub extra: Option<serde_json::Value>,
35}
36
37/// The output of [`ChannelLogin::device_start`]: the device + user codes and
38/// the URL the operator visits to authorize, plus the poll interval the
39/// provider asked for (seconds).
40pub struct DeviceInit {
41    pub device_code: String,
42    pub user_code: String,
43    pub verification_url: String,
44    pub interval_secs: u64,
45}
46
47/// One poll tick of a device-code login.
48#[derive(Debug)]
49pub enum DevicePoll {
50    /// The user has not finished authorizing yet — poll again after the
51    /// interval (covers both `authorization_pending` and `slow_down`).
52    Pending,
53    /// Authorized: the PLAINTEXT secret Value the caller seals + persists.
54    Ready(serde_json::Value),
55    /// The user denied access or the device code expired — abandon the flow.
56    Denied,
57}
58
59/// Inputs for starting an authorization-code login.
60pub struct AuthCodeStartCtx<'a> {
61    pub provider_settings: &'a serde_json::Value,
62    pub params: &'a serde_json::Value,
63    pub redirect_uri: &'a str,
64    pub state: &'a str,
65    pub pkce_challenge: &'a str,
66}
67
68/// Inputs for exchanging an authorization code.
69pub struct AuthCodeExchangeCtx<'a> {
70    pub provider_settings: &'a serde_json::Value,
71    pub code: &'a str,
72    pub verifier: &'a str,
73    pub redirect_uri: &'a str,
74    pub extra: Option<&'a serde_json::Value>,
75}
76
77/// Inputs for starting a device-code login.
78pub struct DeviceStartCtx<'a> {
79    pub provider_settings: &'a serde_json::Value,
80    pub params: &'a serde_json::Value,
81}
82
83/// Inputs for one device-code poll.
84pub struct DevicePollCtx<'a> {
85    pub provider_settings: &'a serde_json::Value,
86    pub device_code: &'a str,
87}
88
89/// Inputs for exchanging a browser session cookie.
90pub struct CookieExchangeCtx<'a> {
91    pub provider_settings: &'a serde_json::Value,
92    pub cookie: &'a str,
93}
94
95/// Interactive OAuth authorization-code (+PKCE) login for a channel.
96///
97/// Defaults make the trait opt-in: a channel that does not override returns
98/// `None` from [`authcode_start`](ChannelLogin::authcode_start) (no authcode
99/// flow) and `Unsupported` from
100/// [`authcode_exchange`](ChannelLogin::authcode_exchange).
101#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
102#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
103pub trait ChannelLogin: Send + Sync {
104    /// Build the provider authorize URL for an authcode+PKCE login. `Ok(None)`
105    /// means the channel has no authcode flow. An empty `redirect_uri` tells the
106    /// channel to use its own default (returned in [`AuthCodeStart`]).
107    ///
108    /// Async + client-bearing so a channel can do a pre-authorize round-trip
109    /// (e.g. AWS IdC dynamic client registration) before producing the URL.
110    /// `params` is opaque operator-supplied input (`auth_method`, `region`,
111    /// `start_url`, …); `{}` for the common static flow.
112    async fn authcode_start(
113        &self,
114        _client: &Arc<dyn UpstreamClient>,
115        _ctx: AuthCodeStartCtx<'_>,
116    ) -> Result<Option<AuthCodeStart>, ChannelError> {
117        Ok(None)
118    }
119
120    /// Exchange an authorization `code` (+ the PKCE `verifier`) for the
121    /// PLAINTEXT secret Value. `redirect_uri` MUST equal the one
122    /// [`authcode_start`](ChannelLogin::authcode_start) used. `extra` is the
123    /// `AuthCodeStart::extra` that start stashed (e.g. the registered IdC client
124    /// creds); `None` for static flows. The caller seals + persists the returned
125    /// Value (purity: the channel never touches cipher/persistence).
126    async fn authcode_exchange(
127        &self,
128        _client: &Arc<dyn UpstreamClient>,
129        _ctx: AuthCodeExchangeCtx<'_>,
130    ) -> Result<serde_json::Value, ChannelError> {
131        Err(ChannelError::Unsupported("authcode login"))
132    }
133
134    /// Begin a device-code login: ask the provider for a device + user code.
135    /// `None`-by-default channels return `Unsupported`. The caller stashes the
136    /// returned `device_code` server-side and polls
137    /// [`device_poll`](ChannelLogin::device_poll).
138    ///
139    /// `params` is opaque operator-supplied input (mirrors
140    /// [`authcode_start`](ChannelLogin::authcode_start)) — e.g.
141    /// `{"login_provider":"google"}` to pick the Kiro social provider; `{}` for
142    /// the channel default.
143    async fn device_start(
144        &self,
145        _client: &Arc<dyn UpstreamClient>,
146        _ctx: DeviceStartCtx<'_>,
147    ) -> Result<DeviceInit, ChannelError> {
148        Err(ChannelError::Unsupported("device login"))
149    }
150
151    /// Poll a pending device-code login with the `device_code` from
152    /// [`device_start`](ChannelLogin::device_start). Returns [`DevicePoll::Ready`]
153    /// with the PLAINTEXT secret
154    /// once authorized (the caller seals + persists), else `Pending`/`Denied`.
155    async fn device_poll(
156        &self,
157        _client: &Arc<dyn UpstreamClient>,
158        _ctx: DevicePollCtx<'_>,
159    ) -> Result<DevicePoll, ChannelError> {
160        Err(ChannelError::Unsupported("device login"))
161    }
162
163    /// Exchange a session `cookie` for the PLAINTEXT secret Value (the caller
164    /// seals + persists). For channels whose first-credential bootstrap is a
165    /// browser session cookie rather than an interactive OAuth dance.
166    async fn cookie_exchange(
167        &self,
168        _client: &Arc<dyn UpstreamClient>,
169        _ctx: CookieExchangeCtx<'_>,
170    ) -> Result<serde_json::Value, ChannelError> {
171        Err(ChannelError::Unsupported("cookie login"))
172    }
173}