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}