Skip to main content

auth_framework/
client.rs

1//! OAuth 2.0 client types — canonical definitions (RFC 6749 §2.1).
2//!
3//! [`ClientType`] and [`ClientConfig`] are the single source of truth for client
4//! classification used throughout the OAuth 2.0/2.1 stack (domain layer, server
5//! layer, and storage layer).  All other modules import these types rather than
6//! defining their own copies.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// OAuth 2.0 client classification (RFC 6749 §2.1).
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ClientType {
15    /// Confidential clients can securely store their credentials (server-side apps).
16    Confidential,
17    /// Public clients cannot securely store credentials (SPAs, native apps).
18    Public,
19}
20
21/// Full configuration record for a registered OAuth 2.0 client.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ClientConfig {
24    /// Unique client identifier
25    pub client_id: String,
26    /// Client secret — only present for `Confidential` clients
27    pub client_secret: Option<String>,
28    /// RFC 6749 §2.1 client classification
29    pub client_type: ClientType,
30    /// Authorised redirect URIs
31    pub redirect_uris: crate::types::RedirectUris,
32    /// Scopes this client is allowed to request
33    pub authorized_scopes: crate::types::Scopes,
34    /// Grant types this client is allowed to use
35    pub authorized_grant_types: crate::types::GrantTypes,
36    /// Response types this client is allowed to use
37    pub authorized_response_types: crate::types::ResponseTypes,
38    /// Human-readable display name
39    pub client_name: Option<String>,
40    /// Optional description
41    pub client_description: Option<String>,
42    /// Arbitrary metadata (e.g. logo_uri, tos_uri, contacts, …)
43    pub metadata: HashMap<String, serde_json::Value>,
44}
45
46impl Default for ClientConfig {
47    fn default() -> Self {
48        Self {
49            client_id: Uuid::new_v4().to_string(),
50            client_secret: None,
51            client_type: ClientType::Public,
52            redirect_uris: crate::types::RedirectUris::empty(),
53            authorized_scopes: crate::types::Scopes::from(vec!["read".to_string()]),
54            authorized_grant_types: crate::types::GrantTypes::from(vec![
55                "authorization_code".to_string(),
56            ]),
57            authorized_response_types: crate::types::ResponseTypes::from(vec!["code".to_string()]),
58            client_name: None,
59            client_description: None,
60            metadata: HashMap::new(),
61        }
62    }
63}
64
65/// Builder for creating `ClientConfig` instances with fluent API.
66///
67/// Reduces cognitive load when registering OAuth clients with many optional fields.
68/// Required fields are set in `new()`, optional fields via builder methods.
69///
70/// # Example
71///
72/// ```rust
73/// use auth_framework::client::{ClientConfig, ClientType};
74/// use auth_framework::types::{RedirectUris, Scopes, GrantTypes, ResponseTypes};
75///
76/// let config = ClientConfig::builder("client123", ClientType::Confidential)
77///     .client_secret("secret456")
78///     .redirect_uris(RedirectUris::new(vec!["https://example.com/callback".to_string()]))
79///     .authorized_scopes(Scopes::new(vec!["read".to_string(), "write".to_string()]))
80///     .client_name("My App")
81///     .build();
82/// ```
83#[derive(Debug, Clone)]
84pub struct ClientConfigBuilder {
85    client_id: String,
86    client_secret: Option<String>,
87    client_type: ClientType,
88    redirect_uris: crate::types::RedirectUris,
89    authorized_scopes: crate::types::Scopes,
90    authorized_grant_types: crate::types::GrantTypes,
91    authorized_response_types: crate::types::ResponseTypes,
92    client_name: Option<String>,
93    client_description: Option<String>,
94    metadata: HashMap<String, serde_json::Value>,
95}
96
97impl ClientConfigBuilder {
98    /// Create a new builder with required fields.
99    ///
100    /// Sets sensible defaults for optional fields:
101    /// - `client_secret`: None
102    /// - `redirect_uris`: empty
103    /// - `authorized_scopes`: ["read"]
104    /// - `authorized_grant_types`: ["authorization_code"]
105    /// - `authorized_response_types`: ["code"]
106    /// - `client_name`, `client_description`: None
107    /// - `metadata`: empty
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
113    ///
114    /// let builder = ClientConfigBuilder::new("my-app", ClientType::Public);
115    /// let config = builder.build();
116    /// assert_eq!(config.client_id, "my-app");
117    /// ```
118    pub fn new(client_id: impl Into<String>, client_type: ClientType) -> Self {
119        Self {
120            client_id: client_id.into(),
121            client_secret: None,
122            client_type,
123            redirect_uris: crate::types::RedirectUris::empty(),
124            authorized_scopes: crate::types::Scopes::from(vec!["read".to_string()]),
125            authorized_grant_types: crate::types::GrantTypes::from(vec![
126                "authorization_code".to_string(),
127            ]),
128            authorized_response_types: crate::types::ResponseTypes::from(vec!["code".to_string()]),
129            client_name: None,
130            client_description: None,
131            metadata: HashMap::new(),
132        }
133    }
134
135    /// Set the client secret.
136    ///
137    /// # Example
138    ///
139    /// ```rust
140    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
141    ///
142    /// let config = ClientConfigBuilder::new("app", ClientType::Confidential)
143    ///     .client_secret("s3cret")
144    ///     .build();
145    /// assert_eq!(config.client_secret.unwrap(), "s3cret");
146    /// ```
147    pub fn client_secret(mut self, client_secret: impl Into<String>) -> Self {
148        self.client_secret = Some(client_secret.into());
149        self
150    }
151
152    /// Set the authorized redirect URIs.
153    ///
154    /// # Example
155    ///
156    /// ```rust
157    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
158    /// use auth_framework::types::RedirectUris;
159    ///
160    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
161    ///     .redirect_uris(RedirectUris::new(vec!["https://example.com/cb".into()]))
162    ///     .build();
163    /// ```
164    pub fn redirect_uris(mut self, redirect_uris: crate::types::RedirectUris) -> Self {
165        self.redirect_uris = redirect_uris;
166        self
167    }
168
169    /// Set the authorized scopes.
170    ///
171    /// # Example
172    ///
173    /// ```rust
174    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
175    /// use auth_framework::types::Scopes;
176    ///
177    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
178    ///     .authorized_scopes(Scopes::new(vec!["read".into(), "write".into()]))
179    ///     .build();
180    /// ```
181    pub fn authorized_scopes(mut self, authorized_scopes: crate::types::Scopes) -> Self {
182        self.authorized_scopes = authorized_scopes;
183        self
184    }
185
186    /// Set the authorized grant types.
187    ///
188    /// # Example
189    ///
190    /// ```rust
191    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
192    /// use auth_framework::types::GrantTypes;
193    ///
194    /// let config = ClientConfigBuilder::new("app", ClientType::Confidential)
195    ///     .authorized_grant_types(GrantTypes::new(vec!["client_credentials".into()]))
196    ///     .build();
197    /// ```
198    pub fn authorized_grant_types(
199        mut self,
200        authorized_grant_types: crate::types::GrantTypes,
201    ) -> Self {
202        self.authorized_grant_types = authorized_grant_types;
203        self
204    }
205
206    /// Set the authorized response types.
207    ///
208    /// # Example
209    ///
210    /// ```rust
211    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
212    /// use auth_framework::types::ResponseTypes;
213    ///
214    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
215    ///     .authorized_response_types(ResponseTypes::new(vec!["code".into()]))
216    ///     .build();
217    /// ```
218    pub fn authorized_response_types(
219        mut self,
220        authorized_response_types: crate::types::ResponseTypes,
221    ) -> Self {
222        self.authorized_response_types = authorized_response_types;
223        self
224    }
225
226    /// Set the client name.
227    ///
228    /// # Example
229    ///
230    /// ```rust
231    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
232    ///
233    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
234    ///     .client_name("My Application")
235    ///     .build();
236    /// assert_eq!(config.client_name.unwrap(), "My Application");
237    /// ```
238    pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
239        self.client_name = Some(client_name.into());
240        self
241    }
242
243    /// Set the client description.
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
249    ///
250    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
251    ///     .client_description("OAuth 2.0 demo app")
252    ///     .build();
253    /// assert_eq!(config.client_description.unwrap(), "OAuth 2.0 demo app");
254    /// ```
255    pub fn client_description(mut self, client_description: impl Into<String>) -> Self {
256        self.client_description = Some(client_description.into());
257        self
258    }
259
260    /// Set the metadata.
261    ///
262    /// # Example
263    ///
264    /// ```rust
265    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
266    /// use std::collections::HashMap;
267    ///
268    /// let mut meta = HashMap::new();
269    /// meta.insert("logo_uri".into(), serde_json::json!("https://example.com/logo.png"));
270    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
271    ///     .metadata(meta)
272    ///     .build();
273    /// assert!(config.metadata.contains_key("logo_uri"));
274    /// ```
275    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
276        self.metadata = metadata;
277        self
278    }
279
280    /// Add a metadata key-value pair.
281    ///
282    /// # Example
283    ///
284    /// ```rust
285    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
286    ///
287    /// let config = ClientConfigBuilder::new("app", ClientType::Public)
288    ///     .with_metadata("tos_uri", serde_json::json!("https://example.com/tos"))
289    ///     .build();
290    /// assert!(config.metadata.contains_key("tos_uri"));
291    /// ```
292    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
293        self.metadata.insert(key.into(), value);
294        self
295    }
296
297    /// Build the `ClientConfig` instance.
298    ///
299    /// # Example
300    ///
301    /// ```rust
302    /// use auth_framework::client::{ClientConfigBuilder, ClientType};
303    ///
304    /// let config = ClientConfigBuilder::new("app", ClientType::Public).build();
305    /// assert_eq!(config.client_id, "app");
306    /// ```
307    pub fn build(self) -> ClientConfig {
308        ClientConfig {
309            client_id: self.client_id,
310            client_secret: self.client_secret,
311            client_type: self.client_type,
312            redirect_uris: self.redirect_uris,
313            authorized_scopes: self.authorized_scopes,
314            authorized_grant_types: self.authorized_grant_types,
315            authorized_response_types: self.authorized_response_types,
316            client_name: self.client_name,
317            client_description: self.client_description,
318            metadata: self.metadata,
319        }
320    }
321}
322
323impl ClientConfig {
324    /// Start building a `ClientConfig` with fluent setters.
325    ///
326    /// # Example
327    ///
328    /// ```rust
329    /// use auth_framework::client::{ClientConfig, ClientType};
330    ///
331    /// let config = ClientConfig::builder("client123", ClientType::Public)
332    ///     .client_name("My SPA")
333    ///     .build();
334    /// ```
335    pub fn builder(client_id: impl Into<String>, client_type: ClientType) -> ClientConfigBuilder {
336        ClientConfigBuilder::new(client_id, client_type)
337    }
338}