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    /// RFC 7009 revocation endpoint, served by `revoke::revoke` and mounted at
9    /// `/revoke` by both `routes::router` and `routes::bearer_only_router`.
10    /// Stays an `Option` so a consumer assembling this metadata without that
11    /// route can omit the capability rather than advertise an endpoint that
12    /// 404s.
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub revocation_endpoint: Option<String>,
15    pub registration_endpoint: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub native_callback_endpoint: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub native_poll_endpoint: Option<String>,
20    pub jwks_uri: String,
21    pub response_types_supported: Vec<String>,
22    pub scopes_supported: Vec<String>,
23    pub grant_types_supported: Vec<String>,
24    pub code_challenge_methods_supported: Vec<String>,
25    pub token_endpoint_auth_methods_supported: Vec<String>,
26    #[serde(skip_serializing_if = "Vec::is_empty")]
27    pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
28    /// RFC 9207 §2.3 — MUST be `true` whenever the authorization server includes
29    /// the `iss` parameter in authorization responses (soma-auth always does, in
30    /// `authorize::callback`). Always emitted, never conditional.
31    pub authorization_response_iss_parameter_supported: bool,
32    /// Advertises OAuth Client ID Metadata Document support at `/authorize`
33    /// (see `crate::cimd`). Always `true` — soma-auth supports CIMD
34    /// unconditionally alongside DCR.
35    pub client_id_metadata_document_supported: bool,
36    #[serde(skip_serializing_if = "Vec::is_empty")]
37    pub authorization_grant_profiles_supported: Vec<String>,
38}
39
40/// Query params for `GET /native/callback` and `GET /native/poll` — the
41/// RFC 8252 §7.1-style native-app flow where the *server* hosts the OAuth
42/// redirect_uri (a real HTTPS URL, not a client-run loopback listener) and
43/// the desktop client polls for the resulting code by `state`.
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub struct NativePollQuery {
46    pub state: String,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct NativePollResponse {
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub code: Option<String>,
53}
54
55/// A native-flow authorization code, stored server-side keyed by `state`
56/// until the polling client retrieves it (`take_native_authorization_result`
57/// is a one-shot read-and-delete).
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct NativeAuthorizationResultRow {
60    pub state: String,
61    pub code: String,
62    pub created_at: i64,
63    pub expires_at: i64,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ProtectedResourceMetadata {
68    pub resource: String,
69    pub authorization_servers: Vec<String>,
70    pub scopes_supported: Vec<String>,
71    pub bearer_methods_supported: Vec<String>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
75pub struct ClientRegistrationRequest {
76    pub redirect_uris: Vec<String>,
77    /// OIDC / RFC 7591 client application type ("web" or "native"). Optional on
78    /// the wire; defaults to "web" (the OIDC default) when omitted. The MCP draft
79    /// (2026-07-28) asks clients to specify this during DCR to avoid OIDC
80    /// redirect-URI conflicts.
81    #[serde(default)]
82    pub application_type: Option<String>,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86pub struct ClientRegistrationResponse {
87    pub client_id: String,
88    pub redirect_uris: Vec<String>,
89    pub token_endpoint_auth_method: String,
90    pub application_type: String,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
94pub struct AuthorizeQuery {
95    #[serde(default)]
96    pub response_type: String,
97    pub client_id: String,
98    pub redirect_uri: String,
99    pub state: String,
100    #[serde(default)]
101    pub resource: Option<String>,
102    #[serde(default)]
103    pub scope: String,
104    #[serde(default)]
105    pub provider: Option<String>,
106    pub code_challenge: String,
107    pub code_challenge_method: String,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
111pub struct CallbackQuery {
112    pub state: String,
113    pub code: String,
114}
115
116#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
117pub struct BrowserLoginQuery {
118    #[serde(default)]
119    pub return_to: Option<String>,
120    #[serde(default)]
121    pub provider: Option<String>,
122}
123
124#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub struct TokenRequest {
126    pub grant_type: String,
127    #[serde(default)]
128    pub code: Option<String>,
129    #[serde(default)]
130    pub client_id: Option<String>,
131    #[serde(default)]
132    pub resource: Option<String>,
133    #[serde(default)]
134    pub redirect_uri: Option<String>,
135    #[serde(default)]
136    pub code_verifier: Option<String>,
137    #[serde(default)]
138    pub refresh_token: Option<String>,
139    #[serde(default)]
140    pub client_secret: Option<String>,
141    #[serde(default)]
142    pub scope: Option<String>,
143    #[serde(default)]
144    pub client_assertion_type: Option<String>,
145    #[serde(default)]
146    pub client_assertion: Option<String>,
147    #[serde(default)]
148    pub assertion: Option<String>,
149}
150
151#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
152pub struct RevocationRequest {
153    pub token: String,
154    #[serde(default)]
155    pub token_type_hint: Option<String>,
156    #[serde(default)]
157    pub client_id: Option<String>,
158    #[serde(default)]
159    pub client_secret: Option<String>,
160    #[serde(default)]
161    pub client_assertion_type: Option<String>,
162    #[serde(default)]
163    pub client_assertion: Option<String>,
164}
165
166#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
167pub struct TokenResponse {
168    pub access_token: String,
169    pub token_type: String,
170    pub expires_in: u64,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub refresh_token: Option<String>,
173    pub scope: String,
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177pub struct RegisteredClient {
178    pub client_id: String,
179    pub redirect_uris: Vec<String>,
180    pub created_at: i64,
181    #[serde(default = "default_token_endpoint_auth_method")]
182    pub token_endpoint_auth_method: String,
183    #[serde(default)]
184    pub jwks: Option<serde_json::Value>,
185}
186
187fn default_token_endpoint_auth_method() -> String {
188    "none".to_string()
189}
190
191#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192pub struct AuthorizationRequestRow {
193    pub state: String,
194    pub client_id: String,
195    pub redirect_uri: String,
196    pub client_state: String,
197    pub resource: String,
198    pub scope: String,
199    pub provider: String,
200    pub provider_code_verifier: String,
201    pub code_challenge: String,
202    pub code_challenge_method: String,
203    pub created_at: i64,
204    pub expires_at: i64,
205    /// The client's `token_endpoint_auth_method` as resolved at `/authorize`,
206    /// carried forward onto the authorization code this request becomes.
207    /// See [`AuthorizationCodeRow::token_endpoint_auth_method`].
208    pub token_endpoint_auth_method: Option<String>,
209}
210
211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212pub struct AuthorizationCodeRow {
213    pub code: String,
214    pub client_id: String,
215    pub subject: String,
216    pub redirect_uri: String,
217    pub resource: String,
218    pub scope: String,
219    pub provider: String,
220    pub code_challenge: String,
221    pub code_challenge_method: String,
222    pub provider_refresh_token: Option<String>,
223    pub created_at: i64,
224    pub expires_at: i64,
225    /// The `token_endpoint_auth_method` this grant was issued under, recorded
226    /// when `/authorize` resolved the client.
227    ///
228    /// This is the *contract* of the grant, not a live view of the client: a
229    /// client that later changes its declared method does not retroactively
230    /// change grants that were already issued under the old one.
231    ///
232    /// `None` means "unknown" — either a row written before schema v5, or a
233    /// client that could not be resolved at issuance time. `/token` then
234    /// resolves the client exactly as it did before this field existed. It
235    /// deliberately does **not** mean `"none"`: defaulting an unknown row to
236    /// public would silently downgrade a confidential client.
237    pub token_endpoint_auth_method: Option<String>,
238}
239
240#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct RefreshTokenRow {
242    pub refresh_token: String,
243    pub client_id: String,
244    pub subject: String,
245    pub resource: String,
246    pub scope: String,
247    pub provider: String,
248    pub provider_refresh_token: Option<String>,
249    pub created_at: i64,
250    pub expires_at: i64,
251    /// The `token_endpoint_auth_method` this grant was issued under, inherited
252    /// from the authorization code that minted it and preserved across every
253    /// refresh and rotation. See
254    /// [`AuthorizationCodeRow::token_endpoint_auth_method`].
255    pub token_endpoint_auth_method: Option<String>,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
259pub struct BrowserSessionRow {
260    pub session_id: String,
261    pub subject: String,
262    pub email: Option<String>,
263    pub csrf_token: String,
264    pub created_at: i64,
265    pub expires_at: i64,
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub struct BrowserLoginStateRow {
270    pub state: String,
271    pub return_to: String,
272    pub provider: String,
273    pub provider_code_verifier: String,
274    pub created_at: i64,
275    pub expires_at: i64,
276}
277
278/// Persisted upstream OAuth credential row.
279///
280/// The encrypted `token_blob` is `chacha20poly1305(token_response_json)` sealed with a
281/// fresh 12-byte nonce per write. `access_token_expires_at` is denormalized for cheap
282/// pruning in `cleanup_expired`. `refresh_token_present` enables dropping access-only
283/// stale rows while keeping rows that still have a refresh token for re-use (SEC-9).
284///
285/// `Debug` is implemented manually with redaction — never derive it.
286#[derive(Clone)]
287pub struct UpstreamOauthCredentialRow {
288    pub upstream_name: String,
289    pub subject: String,
290    /// Canonical authorization-server issuer that minted these credentials.
291    /// Empty values are legacy rows and are never reusable.
292    pub issuer: String,
293    pub client_id: String,
294    pub granted_scopes_json: String,
295    pub token_blob: Vec<u8>,
296    pub token_blob_nonce: Vec<u8>,
297    pub token_received_at: i64,
298    pub access_token_expires_at: i64,
299    pub refresh_token_present: bool,
300}
301
302impl std::fmt::Debug for UpstreamOauthCredentialRow {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        f.debug_struct("UpstreamOauthCredentialRow")
305            .field("upstream_name", &self.upstream_name)
306            .field("subject", &"<redacted>")
307            .field("issuer", &self.issuer)
308            .field("client_id", &self.client_id)
309            .field("granted_scopes_json", &self.granted_scopes_json)
310            .field("token_blob", &"<redacted>")
311            .field("token_blob_nonce", &"<redacted>")
312            .field("token_received_at", &self.token_received_at)
313            .field("access_token_expires_at", &self.access_token_expires_at)
314            .field("refresh_token_present", &self.refresh_token_present)
315            .finish()
316    }
317}
318
319/// Short-lived upstream OAuth state row. Holds the CSRF token and PKCE verifier
320/// between `/authorize` redirect and `/callback` redemption.
321///
322/// `expires_at - created_at` MUST NOT exceed 600 seconds. The persistence helper
323/// rejects violations.
324///
325/// `Debug` is implemented manually with redaction — never derive it (`pkce_verifier`
326/// is sensitive).
327#[derive(Clone)]
328pub struct UpstreamOauthStateRow {
329    pub upstream_name: String,
330    pub subject: String,
331    pub csrf_token: String,
332    pub pkce_verifier: String,
333    /// RFC 9207 issuer recorded when the authorization request was created.
334    pub expected_issuer: Option<String>,
335    /// Whether the authorization server advertised mandatory issuer responses.
336    pub require_issuer: bool,
337    /// JSON-encoded scopes requested in this authorization round.
338    pub requested_scopes_json: String,
339    pub created_at: i64,
340    pub expires_at: i64,
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub struct UpstreamOauthDynamicClientRow {
345    pub client_id: String,
346    /// Canonical authorization-server issuer that registered this client.
347    /// Empty values are legacy rows and are never reusable.
348    pub issuer: String,
349}
350
351/// A row from the `allowed_users` table.
352///
353/// Email is always stored and returned in lowercase. `added_by` is the subject
354/// of the admin who added the entry. Never log `email` directly — use
355/// `util::fingerprint(email)` for safe diagnostic output.
356#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
357pub struct AllowedUserRow {
358    pub email: String,
359    pub added_by: String,
360    pub created_at: i64,
361}
362
363impl std::fmt::Debug for UpstreamOauthStateRow {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        f.debug_struct("UpstreamOauthStateRow")
366            .field("upstream_name", &self.upstream_name)
367            .field("subject", &"<redacted>")
368            .field("csrf_token", &"<redacted>")
369            .field("pkce_verifier", &"<redacted>")
370            .field("expected_issuer", &self.expected_issuer)
371            .field("require_issuer", &self.require_issuer)
372            .field("requested_scopes_json", &self.requested_scopes_json)
373            .field("created_at", &self.created_at)
374            .field("expires_at", &self.expires_at)
375            .finish()
376    }
377}