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}