1use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
22pub struct ClientMetadata {
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub redirect_uris: Vec<String>,
27
28 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub application_type: Option<String>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub token_endpoint_auth_method: Option<String>,
43
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub grant_types: Vec<String>,
47
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub response_types: Vec<String>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub client_name: Option<String>,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub client_uri: Option<String>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub logo_uri: Option<String>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub scope: Option<String>,
67
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
70 pub contacts: Vec<String>,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub tos_uri: Option<String>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub policy_uri: Option<String>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub jwks_uri: Option<String>,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub jwks: Option<serde_json::Value>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub software_id: Option<String>,
93
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub software_version: Option<String>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub software_statement: Option<String>,
102
103 #[serde(flatten)]
106 pub additional_fields: HashMap<String, serde_json::Value>,
107}
108
109impl ClientMetadata {
110 pub fn new() -> Self {
113 Self {
114 grant_types: vec!["authorization_code".into()],
115 response_types: vec!["code".into()],
116 ..Self::default()
117 }
118 }
119
120 pub fn with_redirect_uris<I, S>(mut self, uris: I) -> Self
122 where
123 I: IntoIterator<Item = S>,
124 S: Into<String>,
125 {
126 self.redirect_uris = uris.into_iter().map(Into::into).collect();
127 self
128 }
129
130 pub fn with_application_type(mut self, application_type: impl Into<String>) -> Self {
133 self.application_type = Some(application_type.into());
134 self
135 }
136
137 pub fn with_token_endpoint_auth_method(mut self, method: impl Into<String>) -> Self {
139 self.token_endpoint_auth_method = Some(method.into());
140 self
141 }
142
143 pub fn with_grant_types<I, S>(mut self, grant_types: I) -> Self
152 where
153 I: IntoIterator<Item = S>,
154 S: Into<String>,
155 {
156 self.grant_types = grant_types.into_iter().map(Into::into).collect();
157 if !self
158 .grant_types
159 .iter()
160 .any(|grant| grant == "authorization_code" || grant == "implicit")
161 {
162 self.response_types.clear();
163 }
164 self
165 }
166
167 pub fn with_response_types<I, S>(mut self, response_types: I) -> Self
169 where
170 I: IntoIterator<Item = S>,
171 S: Into<String>,
172 {
173 self.response_types = response_types.into_iter().map(Into::into).collect();
174 self
175 }
176
177 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
179 self.client_name = Some(name.into());
180 self
181 }
182
183 pub fn with_client_uri(mut self, uri: impl Into<String>) -> Self {
185 self.client_uri = Some(uri.into());
186 self
187 }
188
189 pub fn with_logo_uri(mut self, uri: impl Into<String>) -> Self {
191 self.logo_uri = Some(uri.into());
192 self
193 }
194
195 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
198 where
199 I: IntoIterator<Item = S>,
200 S: Into<String>,
201 {
202 let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
203 self.scope = Some(scopes.join(" "));
204 self
205 }
206
207 pub fn with_contacts<I, S>(mut self, contacts: I) -> Self
209 where
210 I: IntoIterator<Item = S>,
211 S: Into<String>,
212 {
213 self.contacts = contacts.into_iter().map(Into::into).collect();
214 self
215 }
216
217 pub fn with_tos_uri(mut self, uri: impl Into<String>) -> Self {
219 self.tos_uri = Some(uri.into());
220 self
221 }
222
223 pub fn with_policy_uri(mut self, uri: impl Into<String>) -> Self {
225 self.policy_uri = Some(uri.into());
226 self
227 }
228
229 pub fn with_jwks_uri(mut self, uri: impl Into<String>) -> Self {
231 self.jwks_uri = Some(uri.into());
232 self
233 }
234
235 pub fn with_jwks(mut self, jwks: impl Into<serde_json::Value>) -> Self {
237 self.jwks = Some(jwks.into());
238 self
239 }
240
241 pub fn with_software_id(mut self, id: impl Into<String>) -> Self {
243 self.software_id = Some(id.into());
244 self
245 }
246
247 pub fn with_software_version(mut self, version: impl Into<String>) -> Self {
249 self.software_version = Some(version.into());
250 self
251 }
252
253 pub fn with_software_statement(mut self, jwt: impl Into<String>) -> Self {
255 self.software_statement = Some(jwt.into());
256 self
257 }
258
259 pub fn with_additional_field(
261 mut self,
262 name: impl Into<String>,
263 value: impl Into<serde_json::Value>,
264 ) -> Self {
265 self.additional_fields.insert(name.into(), value.into());
266 self
267 }
268}
269
270#[derive(Clone, PartialEq, Serialize, Deserialize)]
280pub struct ClientRegistrationResponse {
281 pub client_id: String,
283
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub client_secret: Option<String>,
287
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub client_id_issued_at: Option<u64>,
291
292 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub client_secret_expires_at: Option<u64>,
296
297 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub registration_access_token: Option<String>,
300
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub registration_client_uri: Option<String>,
304
305 #[serde(flatten)]
307 pub metadata: ClientMetadata,
308}
309
310impl std::fmt::Debug for ClientRegistrationResponse {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("ClientRegistrationResponse")
315 .field("client_id", &self.client_id)
316 .field(
317 "client_secret",
318 &self.client_secret.as_ref().map(|_| "[redacted]"),
319 )
320 .field("client_id_issued_at", &self.client_id_issued_at)
321 .field("client_secret_expires_at", &self.client_secret_expires_at)
322 .field(
323 "registration_access_token",
324 &self
325 .registration_access_token
326 .as_ref()
327 .map(|_| "[redacted]"),
328 )
329 .field("registration_client_uri", &self.registration_client_uri)
330 .field("metadata", &self.metadata)
331 .finish()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use serde_json::json;
339
340 #[test]
341 fn it_prefills_the_oauth21_profile() {
342 let metadata = ClientMetadata::new();
343 assert_eq!(metadata.grant_types, ["authorization_code"]);
344 assert_eq!(metadata.response_types, ["code"]);
345 }
346
347 #[test]
348 fn it_drops_response_types_for_non_redirect_grants() {
349 let metadata = ClientMetadata::new().with_grant_types(["client_credentials"]);
350 assert!(metadata.response_types.is_empty());
351 let json = serde_json::to_value(&metadata).unwrap();
353 assert_eq!(json, json!({ "grant_types": ["client_credentials"] }));
354
355 let metadata =
357 ClientMetadata::new().with_grant_types(["authorization_code", "refresh_token"]);
358 assert_eq!(metadata.response_types, ["code"]);
359
360 let metadata = ClientMetadata::new()
362 .with_grant_types(["urn:example:custom"])
363 .with_response_types(["custom"]);
364 assert_eq!(metadata.response_types, ["custom"]);
365 }
366
367 #[test]
368 fn it_serializes_only_populated_fields() {
369 let metadata = ClientMetadata::new()
370 .with_redirect_uris(["https://app.example.com/callback"])
371 .with_client_name("My App")
372 .with_scopes(["read", "write"]);
373 let json = serde_json::to_value(&metadata).unwrap();
374 assert_eq!(
375 json,
376 json!({
377 "redirect_uris": ["https://app.example.com/callback"],
378 "grant_types": ["authorization_code"],
379 "response_types": ["code"],
380 "client_name": "My App",
381 "scope": "read write"
382 })
383 );
384 }
385
386 #[test]
387 fn it_preserves_extension_and_localized_fields() {
388 let document = json!({
389 "redirect_uris": ["https://app.example.com/callback"],
390 "client_name": "My App",
391 "client_name#ja-JP": "マイアプリ",
392 "backchannel_logout_uri": "https://app.example.com/logout"
393 });
394 let metadata: ClientMetadata = serde_json::from_value(document.clone()).unwrap();
395 assert_eq!(
396 metadata.additional_fields["client_name#ja-JP"],
397 json!("マイアプリ")
398 );
399 assert_eq!(
400 metadata.additional_fields["backchannel_logout_uri"],
401 json!("https://app.example.com/logout")
402 );
403 assert_eq!(serde_json::to_value(&metadata).unwrap(), document);
405 }
406
407 #[test]
408 fn it_round_trips_the_application_type() {
409 let metadata = ClientMetadata::new()
410 .with_redirect_uris(["http://127.0.0.1:8080/callback"])
411 .with_application_type("native");
412 assert_eq!(metadata.application_type.as_deref(), Some("native"));
413
414 let json = serde_json::to_value(&metadata).unwrap();
415 assert_eq!(json["application_type"], json!("native"));
416 assert!(!metadata.additional_fields.contains_key("application_type"));
418
419 let parsed: ClientMetadata = serde_json::from_value(json).unwrap();
420 assert_eq!(parsed, metadata);
421
422 let json = serde_json::to_value(ClientMetadata::new()).unwrap();
424 assert!(json.get("application_type").is_none());
425 }
426
427 #[test]
428 fn it_deserializes_a_registration_response() {
429 let response: ClientRegistrationResponse = serde_json::from_value(json!({
430 "client_id": "s6BhdRkqt3",
431 "client_secret": "cf136dc3c1fc93f31185e5885805d",
432 "client_id_issued_at": 2893256800u64,
433 "client_secret_expires_at": 0,
434 "registration_access_token": "this.is.an.access.token",
435 "registration_client_uri": "https://server.example.com/register/s6BhdRkqt3",
436 "redirect_uris": ["https://client.example.org/callback"],
437 "grant_types": ["authorization_code", "refresh_token"],
438 "client_name": "My Example Client",
439 "token_endpoint_auth_method": "client_secret_basic"
440 }))
441 .unwrap();
442
443 assert_eq!(response.client_id, "s6BhdRkqt3");
444 assert_eq!(response.client_secret_expires_at, Some(0));
445 assert_eq!(
446 response.metadata.redirect_uris,
447 ["https://client.example.org/callback"]
448 );
449 assert_eq!(
450 response.metadata.token_endpoint_auth_method.as_deref(),
451 Some("client_secret_basic")
452 );
453 }
454
455 #[test]
456 fn it_requires_a_client_id_in_the_response() {
457 let result = serde_json::from_value::<ClientRegistrationResponse>(json!({
458 "client_secret": "secret"
459 }));
460 assert!(result.is_err());
461 }
462
463 #[test]
464 fn it_redacts_credentials_in_debug_output() {
465 let response: ClientRegistrationResponse = serde_json::from_value(json!({
466 "client_id": "s6BhdRkqt3",
467 "client_secret": "s3cret-value",
468 "registration_access_token": "management-token"
469 }))
470 .unwrap();
471 let debug = format!("{response:?}");
472 assert!(debug.contains("s6BhdRkqt3"));
473 assert!(!debug.contains("s3cret-value"));
474 assert!(!debug.contains("management-token"));
475 assert!(debug.contains("[redacted]"));
476 }
477}