Skip to main content

soma_auth/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4pub struct AuthorizationServerMetadata {
5    pub issuer: String,
6    pub authorization_endpoint: String,
7    pub token_endpoint: String,
8    pub registration_endpoint: String,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub native_callback_endpoint: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub native_poll_endpoint: Option<String>,
13    pub jwks_uri: String,
14    pub response_types_supported: Vec<String>,
15    pub grant_types_supported: Vec<String>,
16    pub code_challenge_methods_supported: Vec<String>,
17    pub token_endpoint_auth_methods_supported: Vec<String>,
18    /// RFC 9207 §2.3 — MUST be `true` whenever the authorization server includes
19    /// the `iss` parameter in authorization responses (soma-auth always does, in
20    /// `authorize::callback`). Always emitted, never conditional.
21    pub authorization_response_iss_parameter_supported: bool,
22    /// Advertises OAuth Client ID Metadata Document support at `/authorize`
23    /// (see `crate::cimd`). Always `true` — soma-auth supports CIMD
24    /// unconditionally alongside DCR.
25    pub client_id_metadata_document_supported: bool,
26}
27
28/// Query params for `GET /native/callback` and `GET /native/poll` — the
29/// RFC 8252 §7.1-style native-app flow where the *server* hosts the OAuth
30/// redirect_uri (a real HTTPS URL, not a client-run loopback listener) and
31/// the desktop client polls for the resulting code by `state`.
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct NativePollQuery {
34    pub state: String,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct NativePollResponse {
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub code: Option<String>,
41}
42
43/// A native-flow authorization code, stored server-side keyed by `state`
44/// until the polling client retrieves it (`take_native_authorization_result`
45/// is a one-shot read-and-delete).
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub struct NativeAuthorizationResultRow {
48    pub state: String,
49    pub code: String,
50    pub created_at: i64,
51    pub expires_at: i64,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55pub struct ProtectedResourceMetadata {
56    pub resource: String,
57    pub authorization_servers: Vec<String>,
58    pub scopes_supported: Vec<String>,
59    pub bearer_methods_supported: Vec<String>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63pub struct ClientRegistrationRequest {
64    pub redirect_uris: Vec<String>,
65    /// OIDC / RFC 7591 client application type ("web" or "native"). Optional on
66    /// the wire; defaults to "web" (the OIDC default) when omitted. The MCP draft
67    /// (2026-07-28) asks clients to specify this during DCR to avoid OIDC
68    /// redirect-URI conflicts.
69    #[serde(default)]
70    pub application_type: Option<String>,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ClientRegistrationResponse {
75    pub client_id: String,
76    pub redirect_uris: Vec<String>,
77    pub token_endpoint_auth_method: String,
78    pub application_type: String,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct AuthorizeQuery {
83    #[serde(default)]
84    pub response_type: String,
85    pub client_id: String,
86    pub redirect_uri: String,
87    pub state: String,
88    #[serde(default)]
89    pub resource: Option<String>,
90    #[serde(default)]
91    pub scope: String,
92    #[serde(default)]
93    pub provider: Option<String>,
94    pub code_challenge: String,
95    pub code_challenge_method: String,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct CallbackQuery {
100    pub state: String,
101    pub code: String,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
105pub struct BrowserLoginQuery {
106    #[serde(default)]
107    pub return_to: Option<String>,
108    #[serde(default)]
109    pub provider: Option<String>,
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub struct TokenRequest {
114    pub grant_type: String,
115    #[serde(default)]
116    pub code: Option<String>,
117    #[serde(default)]
118    pub client_id: Option<String>,
119    #[serde(default)]
120    pub resource: Option<String>,
121    #[serde(default)]
122    pub redirect_uri: Option<String>,
123    #[serde(default)]
124    pub code_verifier: Option<String>,
125    #[serde(default)]
126    pub refresh_token: Option<String>,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
130pub struct TokenResponse {
131    pub access_token: String,
132    pub token_type: String,
133    pub expires_in: u64,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub refresh_token: Option<String>,
136    pub scope: String,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140pub struct RegisteredClient {
141    pub client_id: String,
142    pub redirect_uris: Vec<String>,
143    pub created_at: i64,
144}
145
146#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub struct AuthorizationRequestRow {
148    pub state: String,
149    pub client_id: String,
150    pub redirect_uri: String,
151    pub client_state: String,
152    pub resource: String,
153    pub scope: String,
154    pub provider: String,
155    pub provider_code_verifier: String,
156    pub code_challenge: String,
157    pub code_challenge_method: String,
158    pub created_at: i64,
159    pub expires_at: i64,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163pub struct AuthorizationCodeRow {
164    pub code: String,
165    pub client_id: String,
166    pub subject: String,
167    pub redirect_uri: String,
168    pub resource: String,
169    pub scope: String,
170    pub provider: String,
171    pub code_challenge: String,
172    pub code_challenge_method: String,
173    pub provider_refresh_token: Option<String>,
174    pub created_at: i64,
175    pub expires_at: i64,
176}
177
178#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
179pub struct RefreshTokenRow {
180    pub refresh_token: String,
181    pub client_id: String,
182    pub subject: String,
183    pub resource: String,
184    pub scope: String,
185    pub provider: String,
186    pub provider_refresh_token: Option<String>,
187    pub created_at: i64,
188    pub expires_at: i64,
189}
190
191#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192pub struct BrowserSessionRow {
193    pub session_id: String,
194    pub subject: String,
195    pub email: Option<String>,
196    pub csrf_token: String,
197    pub created_at: i64,
198    pub expires_at: i64,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct BrowserLoginStateRow {
203    pub state: String,
204    pub return_to: String,
205    pub provider: String,
206    pub provider_code_verifier: String,
207    pub created_at: i64,
208    pub expires_at: i64,
209}
210
211/// Persisted upstream OAuth credential row.
212///
213/// The encrypted `token_blob` is `chacha20poly1305(token_response_json)` sealed with a
214/// fresh 12-byte nonce per write. `access_token_expires_at` is denormalized for cheap
215/// pruning in `cleanup_expired`. `refresh_token_present` enables dropping access-only
216/// stale rows while keeping rows that still have a refresh token for re-use (SEC-9).
217///
218/// `Debug` is implemented manually with redaction — never derive it.
219#[derive(Clone)]
220pub struct UpstreamOauthCredentialRow {
221    pub upstream_name: String,
222    pub subject: String,
223    pub client_id: String,
224    pub granted_scopes_json: String,
225    pub token_blob: Vec<u8>,
226    pub token_blob_nonce: Vec<u8>,
227    pub token_received_at: i64,
228    pub access_token_expires_at: i64,
229    pub refresh_token_present: bool,
230}
231
232impl std::fmt::Debug for UpstreamOauthCredentialRow {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        f.debug_struct("UpstreamOauthCredentialRow")
235            .field("upstream_name", &self.upstream_name)
236            .field("subject", &"<redacted>")
237            .field("client_id", &self.client_id)
238            .field("granted_scopes_json", &self.granted_scopes_json)
239            .field("token_blob", &"<redacted>")
240            .field("token_blob_nonce", &"<redacted>")
241            .field("token_received_at", &self.token_received_at)
242            .field("access_token_expires_at", &self.access_token_expires_at)
243            .field("refresh_token_present", &self.refresh_token_present)
244            .finish()
245    }
246}
247
248/// Short-lived upstream OAuth state row. Holds the CSRF token and PKCE verifier
249/// between `/authorize` redirect and `/callback` redemption.
250///
251/// `expires_at - created_at` MUST NOT exceed 600 seconds. The persistence helper
252/// rejects violations.
253///
254/// `Debug` is implemented manually with redaction — never derive it (`pkce_verifier`
255/// is sensitive).
256#[derive(Clone)]
257pub struct UpstreamOauthStateRow {
258    pub upstream_name: String,
259    pub subject: String,
260    pub csrf_token: String,
261    pub pkce_verifier: String,
262    pub created_at: i64,
263    pub expires_at: i64,
264}
265
266/// A row from the `allowed_users` table.
267///
268/// Email is always stored and returned in lowercase. `added_by` is the subject
269/// of the admin who added the entry. Never log `email` directly — use
270/// `util::fingerprint(email)` for safe diagnostic output.
271#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
272pub struct AllowedUserRow {
273    pub email: String,
274    pub added_by: String,
275    pub created_at: i64,
276}
277
278impl std::fmt::Debug for UpstreamOauthStateRow {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        f.debug_struct("UpstreamOauthStateRow")
281            .field("upstream_name", &self.upstream_name)
282            .field("subject", &"<redacted>")
283            .field("csrf_token", &"<redacted>")
284            .field("pkce_verifier", &"<redacted>")
285            .field("created_at", &self.created_at)
286            .field("expires_at", &self.expires_at)
287            .finish()
288    }
289}