1use std::collections::BTreeMap;
8
9use mas_iana::jose::JsonWebSignatureAlg;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize, de::Error};
12use serde_with::skip_serializing_none;
13use ulid::Ulid;
14use url::Url;
15
16use crate::ConfigurationSection;
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
20pub struct UpstreamOAuth2Config {
21 pub providers: Vec<Provider>,
23}
24
25impl UpstreamOAuth2Config {
26 pub(crate) fn is_default(&self) -> bool {
28 self.providers.is_empty()
29 }
30}
31
32impl ConfigurationSection for UpstreamOAuth2Config {
33 const PATH: Option<&'static str> = Some("upstream_oauth2");
34
35 fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
36 for (index, provider) in self.providers.iter().enumerate() {
37 let annotate = |mut error: figment::Error| {
38 error.metadata = figment
39 .find_metadata(&format!("{root}.providers", root = Self::PATH.unwrap()))
40 .cloned();
41 error.profile = Some(figment::Profile::Default);
42 error.path = vec![
43 Self::PATH.unwrap().to_owned(),
44 "providers".to_owned(),
45 index.to_string(),
46 ];
47 Err(error)
48 };
49
50 if !matches!(provider.discovery_mode, DiscoveryMode::Disabled)
51 && provider.issuer.is_none()
52 {
53 return annotate(figment::Error::custom(
54 "The `issuer` field is required when discovery is enabled",
55 ));
56 }
57
58 match provider.token_endpoint_auth_method {
59 TokenAuthMethod::None
60 | TokenAuthMethod::PrivateKeyJwt
61 | TokenAuthMethod::SignInWithApple => {
62 if provider.client_secret.is_some() {
63 return annotate(figment::Error::custom(
64 "Unexpected field `client_secret` for the selected authentication method",
65 ));
66 }
67 }
68 TokenAuthMethod::ClientSecretBasic
69 | TokenAuthMethod::ClientSecretPost
70 | TokenAuthMethod::ClientSecretJwt => {
71 if provider.client_secret.is_none() {
72 return annotate(figment::Error::missing_field("client_secret"));
73 }
74 }
75 }
76
77 match provider.token_endpoint_auth_method {
78 TokenAuthMethod::None
79 | TokenAuthMethod::ClientSecretBasic
80 | TokenAuthMethod::ClientSecretPost
81 | TokenAuthMethod::SignInWithApple => {
82 if provider.token_endpoint_auth_signing_alg.is_some() {
83 return annotate(figment::Error::custom(
84 "Unexpected field `token_endpoint_auth_signing_alg` for the selected authentication method",
85 ));
86 }
87 }
88 TokenAuthMethod::ClientSecretJwt | TokenAuthMethod::PrivateKeyJwt => {
89 if provider.token_endpoint_auth_signing_alg.is_none() {
90 return annotate(figment::Error::missing_field(
91 "token_endpoint_auth_signing_alg",
92 ));
93 }
94 }
95 }
96
97 match provider.token_endpoint_auth_method {
98 TokenAuthMethod::SignInWithApple => {
99 if provider.sign_in_with_apple.is_none() {
100 return annotate(figment::Error::missing_field("sign_in_with_apple"));
101 }
102 }
103
104 _ => {
105 if provider.sign_in_with_apple.is_some() {
106 return annotate(figment::Error::custom(
107 "Unexpected field `sign_in_with_apple` for the selected authentication method",
108 ));
109 }
110 }
111 }
112 }
113
114 Ok(())
115 }
116}
117
118#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
120#[serde(rename_all = "snake_case")]
121pub enum ResponseMode {
122 Query,
125
126 FormPost,
131}
132
133#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
135#[serde(rename_all = "snake_case")]
136pub enum TokenAuthMethod {
137 None,
139
140 ClientSecretBasic,
143
144 ClientSecretPost,
147
148 ClientSecretJwt,
151
152 PrivateKeyJwt,
155
156 SignInWithApple,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
162#[serde(rename_all = "lowercase")]
163pub enum ImportAction {
164 #[default]
166 Ignore,
167
168 Suggest,
170
171 Force,
173
174 Require,
176}
177
178impl ImportAction {
179 #[allow(clippy::trivially_copy_pass_by_ref)]
180 const fn is_default(&self) -> bool {
181 matches!(self, ImportAction::Ignore)
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
187pub struct SubjectImportPreference {
188 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub template: Option<String>,
193}
194
195impl SubjectImportPreference {
196 const fn is_default(&self) -> bool {
197 self.template.is_none()
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
203pub struct LocalpartImportPreference {
204 #[serde(default, skip_serializing_if = "ImportAction::is_default")]
206 pub action: ImportAction,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub template: Option<String>,
213}
214
215impl LocalpartImportPreference {
216 const fn is_default(&self) -> bool {
217 self.action.is_default() && self.template.is_none()
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
223pub struct DisplaynameImportPreference {
224 #[serde(default, skip_serializing_if = "ImportAction::is_default")]
226 pub action: ImportAction,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub template: Option<String>,
233}
234
235impl DisplaynameImportPreference {
236 const fn is_default(&self) -> bool {
237 self.action.is_default() && self.template.is_none()
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
243pub struct EmailImportPreference {
244 #[serde(default, skip_serializing_if = "ImportAction::is_default")]
246 pub action: ImportAction,
247
248 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub template: Option<String>,
253}
254
255impl EmailImportPreference {
256 const fn is_default(&self) -> bool {
257 self.action.is_default() && self.template.is_none()
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
263pub struct AccountNameImportPreference {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub template: Option<String>,
270}
271
272impl AccountNameImportPreference {
273 const fn is_default(&self) -> bool {
274 self.template.is_none()
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
280pub struct ClaimsImports {
281 #[serde(default, skip_serializing_if = "SubjectImportPreference::is_default")]
283 pub subject: SubjectImportPreference,
284
285 #[serde(default, skip_serializing_if = "LocalpartImportPreference::is_default")]
287 pub localpart: LocalpartImportPreference,
288
289 #[serde(
291 default,
292 skip_serializing_if = "DisplaynameImportPreference::is_default"
293 )]
294 pub displayname: DisplaynameImportPreference,
295
296 #[serde(default, skip_serializing_if = "EmailImportPreference::is_default")]
299 pub email: EmailImportPreference,
300
301 #[serde(
303 default,
304 skip_serializing_if = "AccountNameImportPreference::is_default"
305 )]
306 pub account_name: AccountNameImportPreference,
307}
308
309impl ClaimsImports {
310 const fn is_default(&self) -> bool {
311 self.subject.is_default()
312 && self.localpart.is_default()
313 && self.displayname.is_default()
314 && self.email.is_default()
315 }
316}
317
318#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
320#[serde(rename_all = "snake_case")]
321pub enum DiscoveryMode {
322 #[default]
324 Oidc,
325
326 Insecure,
328
329 Disabled,
331}
332
333impl DiscoveryMode {
334 #[allow(clippy::trivially_copy_pass_by_ref)]
335 const fn is_default(&self) -> bool {
336 matches!(self, DiscoveryMode::Oidc)
337 }
338}
339
340#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
343#[serde(rename_all = "snake_case")]
344pub enum PkceMethod {
345 #[default]
349 Auto,
350
351 Always,
353
354 Never,
356}
357
358impl PkceMethod {
359 #[allow(clippy::trivially_copy_pass_by_ref)]
360 const fn is_default(&self) -> bool {
361 matches!(self, PkceMethod::Auto)
362 }
363}
364
365fn default_true() -> bool {
366 true
367}
368
369#[allow(clippy::trivially_copy_pass_by_ref)]
370fn is_default_true(value: &bool) -> bool {
371 *value
372}
373
374#[allow(clippy::ref_option)]
375fn is_signed_response_alg_default(signed_response_alg: &JsonWebSignatureAlg) -> bool {
376 *signed_response_alg == signed_response_alg_default()
377}
378
379#[allow(clippy::unnecessary_wraps)]
380fn signed_response_alg_default() -> JsonWebSignatureAlg {
381 JsonWebSignatureAlg::Rs256
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
385pub struct SignInWithApple {
386 pub private_key: String,
388
389 pub team_id: String,
391
392 pub key_id: String,
394}
395
396#[skip_serializing_none]
398#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
399pub struct Provider {
400 #[serde(default = "default_true", skip_serializing_if = "is_default_true")]
404 pub enabled: bool,
405
406 #[schemars(
408 with = "String",
409 regex(pattern = r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"),
410 description = "A ULID as per https://github.com/ulid/spec"
411 )]
412 pub id: Ulid,
413
414 #[serde(skip_serializing_if = "Option::is_none")]
418 pub issuer: Option<String>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub human_name: Option<String>,
423
424 #[serde(skip_serializing_if = "Option::is_none")]
436 pub brand_name: Option<String>,
437
438 pub client_id: String,
440
441 #[serde(skip_serializing_if = "Option::is_none")]
446 pub client_secret: Option<String>,
447
448 pub token_endpoint_auth_method: TokenAuthMethod,
450
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub sign_in_with_apple: Option<SignInWithApple>,
454
455 #[serde(skip_serializing_if = "Option::is_none")]
460 pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
461
462 #[serde(
467 default = "signed_response_alg_default",
468 skip_serializing_if = "is_signed_response_alg_default"
469 )]
470 pub id_token_signed_response_alg: JsonWebSignatureAlg,
471
472 pub scope: String,
474
475 #[serde(default, skip_serializing_if = "DiscoveryMode::is_default")]
480 pub discovery_mode: DiscoveryMode,
481
482 #[serde(default, skip_serializing_if = "PkceMethod::is_default")]
487 pub pkce_method: PkceMethod,
488
489 #[serde(default)]
495 pub fetch_userinfo: bool,
496
497 #[serde(skip_serializing_if = "Option::is_none")]
503 pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
504
505 #[serde(skip_serializing_if = "Option::is_none")]
509 pub authorization_endpoint: Option<Url>,
510
511 #[serde(skip_serializing_if = "Option::is_none")]
515 pub userinfo_endpoint: Option<Url>,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
521 pub token_endpoint: Option<Url>,
522
523 #[serde(skip_serializing_if = "Option::is_none")]
527 pub jwks_uri: Option<Url>,
528
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub response_mode: Option<ResponseMode>,
532
533 #[serde(default, skip_serializing_if = "ClaimsImports::is_default")]
536 pub claims_imports: ClaimsImports,
537
538 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
542 pub additional_authorization_parameters: BTreeMap<String, String>,
543
544 #[serde(skip_serializing_if = "Option::is_none")]
559 pub synapse_idp_id: Option<String>,
560}