salvo_oapi/openapi/security.rs
1//! Implements [OpenAPI Security Schema][security] types.
2//!
3//! Refer to [`SecurityScheme`] for usage and more details.
4//!
5//! [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
6use std::collections::BTreeMap;
7use std::iter;
8
9use serde::{Deserialize, Deserializer, Serialize};
10
11use crate::PropMap;
12
13/// OpenAPI [security requirement][security] object.
14///
15/// Security requirement holds list of required [`SecurityScheme`] *names* and possible *scopes*
16/// required to execute the operation. They can be defined in
17/// [`#[salvo_oapi::endpoint(...)]`][endpoint].
18///
19/// Applying the security requirement to [`OpenApi`][openapi] will make it globally
20/// available to all operations. When applied to specific [`#[salvo_oapi::endpoint(...)]`][endpoint]
21/// will only make the security requirements available for that operation. Only one of the
22/// requirements must be satisfied.
23///
24/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
25/// [endpoint]: ../../attr.endpoint.html
26/// [openapi]: ../../derive.OpenApi.html
27#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, Default, Clone, PartialEq, Eq)]
28pub struct SecurityRequirement {
29 #[serde(flatten)]
30 pub(crate) value: BTreeMap<String, Vec<String>>,
31}
32
33impl SecurityRequirement {
34 /// Construct a new [`SecurityRequirement`]
35 ///
36 /// Accepts name for the security requirement which must match to the name of available
37 /// [`SecurityScheme`]. Second parameter is [`IntoIterator`] of [`Into<String>`] scopes
38 /// needed by the [`SecurityRequirement`]. Scopes must match to the ones defined in
39 /// [`SecurityScheme`].
40 ///
41 /// As of OpenAPI 3.2 the name may instead be the URI of a Security Scheme Object. A name
42 /// identical to a component name is always resolved as a component name, so to reference a
43 /// scheme by a single-segment relative URI that collides with a component name, prefix it
44 /// with `./` (e.g. `./foo`). No such resolution is performed here — the name is stored
45 /// verbatim.
46 ///
47 /// # Examples
48 ///
49 /// Creates a new security requirement with scopes.
50 /// ```
51 /// # use salvo_oapi::security::SecurityRequirement;
52 /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
53 /// ```
54 ///
55 /// Reference a security scheme by URI (OpenAPI 3.2).
56 /// ```
57 /// # use salvo_oapi::security::SecurityRequirement;
58 /// SecurityRequirement::new("https://example.com/schemes.json#/oauth", ["read:items"]);
59 /// ```
60 ///
61 /// You can also create an empty security requirement with `Default::default()`.
62 /// ```
63 /// # use salvo_oapi::security::SecurityRequirement;
64 /// SecurityRequirement::default();
65 /// ```
66 #[must_use]
67 pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
68 name: N,
69 scopes: S,
70 ) -> Self {
71 Self {
72 value: BTreeMap::from_iter(iter::once_with(|| {
73 (
74 Into::<String>::into(name),
75 scopes
76 .into_iter()
77 .map(|scope| Into::<String>::into(scope))
78 .collect::<Vec<_>>(),
79 )
80 })),
81 }
82 }
83
84 /// Check if the security requirement is empty.
85 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.value.is_empty()
88 }
89
90 /// Allows to add multiple names to security requirement.
91 ///
92 /// Accepts name for the security requirement which must match to the name of available
93 /// [`SecurityScheme`]. Second parameter is [`IntoIterator`] of [`Into<String>`] scopes
94 /// needed by the [`SecurityRequirement`]. Scopes must match to the ones defined in
95 /// [`SecurityScheme`].
96 #[must_use]
97 pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
98 mut self,
99 name: N,
100 scopes: S,
101 ) -> Self {
102 self.value.insert(
103 Into::<String>::into(name),
104 scopes.into_iter().map(Into::<String>::into).collect(),
105 );
106
107 self
108 }
109}
110
111/// OpenAPI [security scheme][security] for path operations.
112///
113/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
114///
115/// # Examples
116///
117/// Create implicit OAuth2 flow security schema for path operations.
118/// ```
119/// # use salvo_oapi::security::{SecurityScheme, OAuth2, Implicit, Flow, Scopes};
120/// SecurityScheme::OAuth2(OAuth2::with_description(
121/// [Flow::Implicit(Implicit::new(
122/// "https://localhost/auth/dialog",
123/// Scopes::from_iter([
124/// ("edit:items", "edit my items"),
125/// ("read:items", "read my items"),
126/// ]),
127/// ))],
128/// "my oauth2 flow",
129/// ));
130/// ```
131///
132/// Create JWT header authentication.
133/// ```
134/// # use salvo_oapi::security::{SecurityScheme, HttpAuthScheme, Http};
135/// SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
136/// ```
137#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
138#[serde(tag = "type", rename_all = "camelCase")]
139pub enum SecurityScheme {
140 /// OAuth flow authentication.
141 #[serde(rename = "oauth2")]
142 OAuth2(OAuth2),
143 /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*.
144 ApiKey(ApiKey),
145 /// Http authentication such as *`bearer`* or *`basic`*.
146 Http(Http),
147 /// OpenID Connect URL to discover OAuth2 configuration values.
148 OpenIdConnect(OpenIdConnect),
149 /// Authentication is done via client side certificate.
150 ///
151 /// OpenApi 3.1 type
152 #[serde(rename = "mutualTLS")]
153 MutualTls {
154 /// Description information.
155 #[serde(skip_serializing_if = "Option::is_none")]
156 description: Option<String>,
157 /// Declares this security scheme deprecated. Added in OpenAPI 3.2.
158 #[serde(skip_serializing_if = "Option::is_none", default)]
159 deprecated: Option<bool>,
160 },
161}
162impl From<OAuth2> for SecurityScheme {
163 fn from(oauth2: OAuth2) -> Self {
164 Self::OAuth2(oauth2)
165 }
166}
167impl From<ApiKey> for SecurityScheme {
168 fn from(api_key: ApiKey) -> Self {
169 Self::ApiKey(api_key)
170 }
171}
172impl From<OpenIdConnect> for SecurityScheme {
173 fn from(open_id_connect: OpenIdConnect) -> Self {
174 Self::OpenIdConnect(open_id_connect)
175 }
176}
177
178/// Api key authentication [`SecurityScheme`].
179#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
180#[serde(tag = "in", rename_all = "lowercase")]
181pub enum ApiKey {
182 /// Create api key which is placed in HTTP header.
183 Header(ApiKeyValue),
184 /// Create api key which is placed in query parameters.
185 Query(ApiKeyValue),
186 /// Create api key which is placed in cookie value.
187 Cookie(ApiKeyValue),
188}
189
190/// Value object for [`ApiKey`].
191#[non_exhaustive]
192#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
193pub struct ApiKeyValue {
194 /// Name of the [`ApiKey`] parameter.
195 pub name: String,
196
197 /// Description of the [`ApiKey`] [`SecurityScheme`]. Supports markdown syntax.
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub description: Option<String>,
200
201 /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub deprecated: Option<bool>,
204
205 /// Optional extensions "x-something"
206 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
207 pub extensions: PropMap<String, serde_json::Value>,
208}
209
210impl ApiKeyValue {
211 /// Constructs new api key value.
212 ///
213 /// # Examples
214 ///
215 /// Creates a new API key security schema named `api_key`.
216 /// ```
217 /// # use salvo_oapi::security::ApiKeyValue;
218 /// let api_key = ApiKeyValue::new("api_key");
219 /// ```
220 pub fn new<S: Into<String>>(name: S) -> Self {
221 Self {
222 name: name.into(),
223 description: None,
224 deprecated: None,
225 extensions: Default::default(),
226 }
227 }
228
229 /// Construct a new api key with optional description supporting markdown syntax.
230 ///
231 /// # Examples
232 ///
233 /// Creates a new API key security schema named `api_key` with a description.
234 /// ```
235 /// # use salvo_oapi::security::ApiKeyValue;
236 /// let api_key = ApiKeyValue::with_description("api_key", "my api_key token");
237 /// ```
238 pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
239 Self {
240 name: name.into(),
241 description: Some(description.into()),
242 deprecated: None,
243 extensions: Default::default(),
244 }
245 }
246
247 /// Mark this [`ApiKeyValue`] deprecated. Requires OpenAPI 3.2.
248 #[must_use]
249 pub fn deprecated(mut self, deprecated: bool) -> Self {
250 self.deprecated = Some(deprecated);
251 self
252 }
253
254 /// Add openapi extensions (`x-something`) for [`ApiKeyValue`].
255 #[must_use]
256 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
257 self.extensions = extensions;
258 self
259 }
260}
261
262/// Http authentication [`SecurityScheme`] builder.
263///
264/// Methods can be chained to configure _bearer_format_ or to add _description_.
265#[non_exhaustive]
266#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
267#[serde(rename_all = "camelCase")]
268pub struct Http {
269 /// Http authorization scheme in HTTP `Authorization` header value.
270 pub scheme: HttpAuthScheme,
271
272 /// Optional hint to client how the bearer token is formatted. Valid only with
273 /// [`HttpAuthScheme::Bearer`].
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub bearer_format: Option<String>,
276
277 /// Optional description of [`Http`] [`SecurityScheme`] supporting markdown syntax.
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub description: Option<String>,
280
281 /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub deprecated: Option<bool>,
284
285 /// Optional extensions "x-something"
286 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
287 pub extensions: PropMap<String, serde_json::Value>,
288}
289
290impl Http {
291 /// Creates a new HTTP authentication security schema.
292 ///
293 /// Accepts one argument which defines the scheme of the http authentication.
294 ///
295 /// # Examples
296 ///
297 /// Create http security schema with basic authentication.
298 /// ```
299 /// # use salvo_oapi::security::{SecurityScheme, Http, HttpAuthScheme};
300 /// SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
301 /// ```
302 #[must_use]
303 pub fn new(scheme: HttpAuthScheme) -> Self {
304 Self {
305 scheme,
306 bearer_format: None,
307 description: None,
308 deprecated: None,
309 extensions: Default::default(),
310 }
311 }
312 /// Add or change http authentication scheme used.
313 #[must_use]
314 pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
315 self.scheme = scheme;
316
317 self
318 }
319 /// Add or change informative bearer format for http security schema.
320 ///
321 /// This is only applicable to [`HttpAuthScheme::Bearer`].
322 ///
323 /// # Examples
324 ///
325 /// Add JTW bearer format for security schema.
326 /// ```
327 /// # use salvo_oapi::security::{Http, HttpAuthScheme};
328 /// Http::new(HttpAuthScheme::Bearer).bearer_format("JWT");
329 /// ```
330 #[must_use]
331 pub fn bearer_format<S: Into<String>>(mut self, bearer_format: S) -> Self {
332 if self.scheme == HttpAuthScheme::Bearer {
333 self.bearer_format = Some(bearer_format.into());
334 }
335
336 self
337 }
338
339 /// Add or change optional description supporting markdown syntax.
340 #[must_use]
341 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
342 self.description = Some(description.into());
343
344 self
345 }
346
347 /// Mark this [`Http`] security scheme deprecated. Requires OpenAPI 3.2.
348 #[must_use]
349 pub fn deprecated(mut self, deprecated: bool) -> Self {
350 self.deprecated = Some(deprecated);
351 self
352 }
353
354 /// Add openapi extensions (`x-something`) for [`Http`].
355 #[must_use]
356 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
357 self.extensions = extensions;
358 self
359 }
360}
361
362/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1).
363///
364/// Types are maintained at <https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml>.
365#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
366#[serde(rename_all = "lowercase")]
367pub enum HttpAuthScheme {
368 /// Basic authentication scheme.
369 #[default]
370 Basic,
371 /// Bearer authentication scheme.
372 Bearer,
373 /// Digest authentication scheme.
374 Digest,
375 /// HOBA authentication scheme.
376 Hoba,
377 /// Mutual authentication scheme.
378 Mutual,
379 /// Negotiate authentication scheme.
380 Negotiate,
381 /// OAuth authentication scheme.
382 OAuth,
383 /// ScramSha1 authentication scheme.
384 #[serde(rename = "scram-sha-1")]
385 ScramSha1,
386 /// ScramSha256 authentication scheme.
387 #[serde(rename = "scram-sha-256")]
388 ScramSha256,
389 /// Vapid authentication scheme.
390 Vapid,
391}
392
393/// OpenID Connect [`SecurityScheme`].
394#[non_exhaustive]
395#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct OpenIdConnect {
398 /// Url of the [`OpenIdConnect`] to discover OAuth2 connect values.
399 pub open_id_connect_url: String,
400
401 /// Description of [`OpenIdConnect`] [`SecurityScheme`] supporting markdown syntax.
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub description: Option<String>,
404
405 /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub deprecated: Option<bool>,
408
409 /// Optional extensions "x-something"
410 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
411 pub extensions: PropMap<String, serde_json::Value>,
412}
413
414impl OpenIdConnect {
415 /// Construct a new open id connect security schema.
416 ///
417 /// # Examples
418 ///
419 /// ```
420 /// # use salvo_oapi::security::OpenIdConnect;
421 /// OpenIdConnect::new("https://localhost/openid");
422 /// ```
423 pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
424 Self {
425 open_id_connect_url: open_id_connect_url.into(),
426 description: None,
427 deprecated: None,
428 extensions: Default::default(),
429 }
430 }
431
432 /// Construct a new [`OpenIdConnect`] [`SecurityScheme`] with optional description
433 /// supporting markdown syntax.
434 ///
435 /// # Examples
436 ///
437 /// ```
438 /// # use salvo_oapi::security::OpenIdConnect;
439 /// OpenIdConnect::with_description("https://localhost/openid", "my pet api open id connect");
440 /// ```
441 pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
442 Self {
443 open_id_connect_url: open_id_connect_url.into(),
444 description: Some(description.into()),
445 deprecated: None,
446 extensions: Default::default(),
447 }
448 }
449
450 /// Mark this [`OpenIdConnect`] security scheme deprecated. Requires OpenAPI 3.2.
451 #[must_use]
452 pub fn deprecated(mut self, deprecated: bool) -> Self {
453 self.deprecated = Some(deprecated);
454 self
455 }
456
457 /// Add openapi extensions (`x-something`) for [`OpenIdConnect`].
458 #[must_use]
459 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
460 self.extensions = extensions;
461 self
462 }
463}
464
465/// OAuth2 [`Flow`] configuration for [`SecurityScheme`].
466#[non_exhaustive]
467#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
468#[serde(rename_all = "camelCase")]
469pub struct OAuth2 {
470 /// Map of supported OAuth2 flows, keyed by the flow name defined by the
471 /// [OAuth Flows Object][flows].
472 ///
473 /// [flows]: https://spec.openapis.org/oas/v3.2.0.html#oauth-flows-object
474 #[serde(deserialize_with = "deserialize_flows")]
475 pub flows: PropMap<String, Flow>,
476
477 /// Optional description for the [`OAuth2`] [`Flow`] [`SecurityScheme`].
478 #[serde(skip_serializing_if = "Option::is_none")]
479 pub description: Option<String>,
480
481 /// URL to the OAuth2 authorization server metadata
482 /// ([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414)). TLS is required.
483 /// Added in OpenAPI 3.2.
484 #[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
485 pub oauth2_metadata_url: Option<String>,
486
487 /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
488 #[serde(skip_serializing_if = "Option::is_none")]
489 pub deprecated: Option<bool>,
490
491 /// Optional extensions "x-something"
492 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
493 pub extensions: PropMap<String, serde_json::Value>,
494}
495
496impl OAuth2 {
497 /// Construct a new OAuth2 security schema configuration object.
498 ///
499 /// OAuth flow accepts a slice of [`Flow`] configuration objects and can be optionally provided
500 /// with description.
501 ///
502 /// # Examples
503 ///
504 /// Creates a new OAuth2 flow with multiple authentication flows.
505 /// ```
506 /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
507 /// OAuth2::new([
508 /// Flow::Password(Password::with_refresh_url(
509 /// "https://localhost/oauth/token",
510 /// Scopes::from_iter([
511 /// ("edit:items", "edit my items"),
512 /// ("read:items", "read my items"),
513 /// ]),
514 /// "https://localhost/refresh/token",
515 /// )),
516 /// Flow::AuthorizationCode(AuthorizationCode::new(
517 /// "https://localhost/authorization/token",
518 /// "https://localhost/token/url",
519 /// Scopes::from_iter([
520 /// ("edit:items", "edit my items"),
521 /// ("read:items", "read my items"),
522 /// ]),
523 /// )),
524 /// ]);
525 /// ```
526 pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
527 Self {
528 flows: PropMap::from_iter(
529 flows
530 .into_iter()
531 .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
532 ),
533 description: None,
534 oauth2_metadata_url: None,
535 deprecated: None,
536 extensions: Default::default(),
537 }
538 }
539
540 /// Construct a new OAuth2 flow with optional description supporting markdown syntax.
541 ///
542 /// # Examples
543 ///
544 /// Creates a new OAuth2 flow with multiple authentication flows and a description.
545 /// ```
546 /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
547 /// OAuth2::with_description(
548 /// [
549 /// Flow::Password(Password::with_refresh_url(
550 /// "https://localhost/oauth/token",
551 /// Scopes::from_iter([
552 /// ("edit:items", "edit my items"),
553 /// ("read:items", "read my items"),
554 /// ]),
555 /// "https://localhost/refresh/token",
556 /// )),
557 /// Flow::AuthorizationCode(AuthorizationCode::new(
558 /// "https://localhost/authorization/token",
559 /// "https://localhost/token/url",
560 /// Scopes::from_iter([
561 /// ("edit:items", "edit my items"),
562 /// ("read:items", "read my items"),
563 /// ]),
564 /// )),
565 /// ],
566 /// "my oauth2 flow",
567 /// );
568 /// ```
569 pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
570 flows: I,
571 description: S,
572 ) -> Self {
573 Self {
574 flows: PropMap::from_iter(
575 flows
576 .into_iter()
577 .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
578 ),
579 description: Some(description.into()),
580 oauth2_metadata_url: None,
581 deprecated: None,
582 extensions: Default::default(),
583 }
584 }
585
586 /// Set the OAuth2 authorization server metadata URL. Requires OpenAPI 3.2.
587 #[must_use]
588 pub fn oauth2_metadata_url<S: Into<String>>(mut self, oauth2_metadata_url: S) -> Self {
589 self.oauth2_metadata_url = Some(oauth2_metadata_url.into());
590 self
591 }
592
593 /// Mark this [`OAuth2`] security scheme deprecated. Requires OpenAPI 3.2.
594 #[must_use]
595 pub fn deprecated(mut self, deprecated: bool) -> Self {
596 self.deprecated = Some(deprecated);
597 self
598 }
599}
600
601/// Deserialize the [`OAuth2::flows`] map by dispatching on the flow name rather than relying on
602/// [`Flow`]'s untagged representation.
603///
604/// Several flows are structurally indistinguishable — `password` and `clientCredentials` have
605/// identical fields, and `deviceAuthorization` is a superset of both — so an untagged match would
606/// resolve them by declaration order and pick the wrong variant. The map key names the flow, so
607/// use it.
608fn deserialize_flows<'de, D>(deserializer: D) -> Result<PropMap<String, Flow>, D::Error>
609where
610 D: Deserializer<'de>,
611{
612 fn flow<T, E>(name: &str, value: serde_json::Value) -> Result<T, E>
613 where
614 T: serde::de::DeserializeOwned,
615 E: serde::de::Error,
616 {
617 serde_json::from_value(value)
618 .map_err(|e| E::custom(format!("invalid `{name}` oauth2 flow: {e}")))
619 }
620
621 let raw = PropMap::<String, serde_json::Value>::deserialize(deserializer)?;
622 let mut flows = PropMap::new();
623 for (key, value) in raw {
624 let parsed = match &*key {
625 "implicit" => Flow::Implicit(flow(&key, value)?),
626 "password" => Flow::Password(flow(&key, value)?),
627 "clientCredentials" => Flow::ClientCredentials(flow(&key, value)?),
628 "authorizationCode" => Flow::AuthorizationCode(flow(&key, value)?),
629 "deviceAuthorization" => Flow::DeviceAuthorization(flow(&key, value)?),
630 // Unknown flow name: fall back to matching on shape.
631 _ => flow(&key, value)?,
632 };
633 flows.insert(key, parsed);
634 }
635 Ok(flows)
636}
637
638/// [`OAuth2`] flow configuration object.
639///
640///
641/// See more details at <https://spec.openapis.org/oas/latest.html#oauth-flows-object>.
642#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
643#[serde(untagged)]
644pub enum Flow {
645 /// Define device authorization [`Flow`] type. See [`DeviceAuthorization::new`] for usage
646 /// details. Added in OpenAPI 3.2.
647 ///
648 /// Declared first because this enum is untagged: the other variants ignore the unknown
649 /// `deviceAuthorizationUrl` key, so a device flow reached through a bare `Flow`
650 /// deserialization would otherwise be misparsed as [`Flow::Password`]. Inside an
651 /// [`OAuth2`] document the flow name decides the variant — see [`deserialize_flows`].
652 DeviceAuthorization(DeviceAuthorization),
653 /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details.
654 ///
655 /// Soon to be deprecated by <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics>.
656 Implicit(Implicit),
657 /// Define password [`Flow`] type. See [`Password::new`] for usage details.
658 Password(Password),
659 /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details.
660 ClientCredentials(ClientCredentials),
661 /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details.
662 AuthorizationCode(AuthorizationCode),
663}
664
665impl Flow {
666 fn get_type_as_str(&self) -> &str {
667 match self {
668 Self::DeviceAuthorization(_) => "deviceAuthorization",
669 Self::Implicit(_) => "implicit",
670 Self::Password(_) => "password",
671 Self::ClientCredentials(_) => "clientCredentials",
672 Self::AuthorizationCode(_) => "authorizationCode",
673 }
674 }
675}
676
677/// Implicit [`Flow`] configuration for [`OAuth2`].
678#[non_exhaustive]
679#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
680#[serde(rename_all = "camelCase")]
681pub struct Implicit {
682 /// Authorization token url for the flow.
683 pub authorization_url: String,
684
685 /// Optional refresh token url for the flow.
686 #[serde(skip_serializing_if = "Option::is_none")]
687 pub refresh_url: Option<String>,
688
689 /// Scopes required by the flow.
690 #[serde(flatten)]
691 pub scopes: Scopes,
692
693 /// Optional extensions "x-something"
694 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
695 pub extensions: PropMap<String, serde_json::Value>,
696}
697
698impl Implicit {
699 /// Construct a new implicit oauth2 flow.
700 ///
701 /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can
702 /// also be an empty map.
703 ///
704 /// # Examples
705 ///
706 /// Creates a new implicit flow with scopes.
707 /// ```
708 /// # use salvo_oapi::security::{Implicit, Scopes};
709 /// Implicit::new(
710 /// "https://localhost/auth/dialog",
711 /// Scopes::from_iter([
712 /// ("edit:items", "edit my items"),
713 /// ("read:items", "read my items"),
714 /// ]),
715 /// );
716 /// ```
717 ///
718 /// Creates a new implicit flow without any scopes.
719 /// ```
720 /// # use salvo_oapi::security::{Implicit, Scopes};
721 /// Implicit::new("https://localhost/auth/dialog", Scopes::new());
722 /// ```
723 pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
724 Self {
725 authorization_url: authorization_url.into(),
726 refresh_url: None,
727 scopes,
728 extensions: Default::default(),
729 }
730 }
731
732 /// Construct a new implicit oauth2 flow with refresh url for getting refresh tokens.
733 ///
734 /// This is essentially same as [`Implicit::new`] but allows defining `refresh_url` for the
735 /// [`Implicit`] oauth2 flow.
736 ///
737 /// # Examples
738 ///
739 /// Create a new implicit OAuth2 flow with refresh token.
740 /// ```
741 /// # use salvo_oapi::security::{Implicit, Scopes};
742 /// Implicit::with_refresh_url(
743 /// "https://localhost/auth/dialog",
744 /// Scopes::new(),
745 /// "https://localhost/refresh-token",
746 /// );
747 /// ```
748 pub fn with_refresh_url<S: Into<String>>(
749 authorization_url: S,
750 scopes: Scopes,
751 refresh_url: S,
752 ) -> Self {
753 Self {
754 authorization_url: authorization_url.into(),
755 refresh_url: Some(refresh_url.into()),
756 scopes,
757 extensions: Default::default(),
758 }
759 }
760
761 /// Add openapi extensions (`x-something`) for [`Implicit`].
762 #[must_use]
763 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
764 self.extensions = extensions;
765 self
766 }
767}
768
769/// Authorization code [`Flow`] configuration for [`OAuth2`].
770#[non_exhaustive]
771#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
772#[serde(rename_all = "camelCase")]
773pub struct AuthorizationCode {
774 /// Url for authorization token.
775 pub authorization_url: String,
776 /// Token url for the flow.
777 pub token_url: String,
778
779 /// Optional refresh token url for the flow.
780 #[serde(skip_serializing_if = "Option::is_none")]
781 pub refresh_url: Option<String>,
782
783 /// Scopes required by the flow.
784 #[serde(flatten)]
785 pub scopes: Scopes,
786
787 /// Optional extensions "x-something"
788 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
789 pub extensions: PropMap<String, serde_json::Value>,
790}
791
792impl AuthorizationCode {
793 /// Construct a new authorization code OAuth flow.
794 ///
795 /// Accepts three arguments: one which is authorization url, two a token url and
796 /// three, a map of scopes for the OAuth flow.
797 ///
798 /// # Examples
799 ///
800 /// Creates a new authorization code flow with scopes.
801 /// ```
802 /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
803 /// AuthorizationCode::new(
804 /// "https://localhost/auth/dialog",
805 /// "https://localhost/token",
806 /// Scopes::from_iter([
807 /// ("edit:items", "edit my items"),
808 /// ("read:items", "read my items"),
809 /// ]),
810 /// );
811 /// ```
812 ///
813 /// Creates a new authorization code flow without any scopes.
814 /// ```
815 /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
816 /// AuthorizationCode::new(
817 /// "https://localhost/auth/dialog",
818 /// "https://localhost/token",
819 /// Scopes::new(),
820 /// );
821 /// ```
822 pub fn new<A: Into<String>, T: Into<String>>(
823 authorization_url: A,
824 token_url: T,
825 scopes: Scopes,
826 ) -> Self {
827 Self {
828 authorization_url: authorization_url.into(),
829 token_url: token_url.into(),
830 refresh_url: None,
831 scopes,
832 extensions: Default::default(),
833 }
834 }
835
836 /// Construct a new [`AuthorizationCode`] OAuth2 flow with additional refresh token url.
837 ///
838 /// This is essentially same as [`AuthorizationCode::new`] but allows defining extra parameter
839 /// `refresh_url` for fetching refresh token.
840 ///
841 /// # Examples
842 ///
843 /// Create [`AuthorizationCode`] OAuth2 flow with refresh url.
844 /// ```
845 /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
846 /// AuthorizationCode::with_refresh_url(
847 /// "https://localhost/auth/dialog",
848 /// "https://localhost/token",
849 /// Scopes::new(),
850 /// "https://localhost/refresh-token",
851 /// );
852 /// ```
853 pub fn with_refresh_url<S: Into<String>>(
854 authorization_url: S,
855 token_url: S,
856 scopes: Scopes,
857 refresh_url: S,
858 ) -> Self {
859 Self {
860 authorization_url: authorization_url.into(),
861 token_url: token_url.into(),
862 refresh_url: Some(refresh_url.into()),
863 scopes,
864 extensions: Default::default(),
865 }
866 }
867
868 /// Add openapi extensions (`x-something`) for [`AuthorizationCode`].
869 #[must_use]
870 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
871 self.extensions = extensions;
872 self
873 }
874}
875
876/// Password [`Flow`] configuration for [`OAuth2`].
877#[non_exhaustive]
878#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
879#[serde(rename_all = "camelCase")]
880pub struct Password {
881 /// Token url for this OAuth2 flow. OAuth2 standard requires TLS.
882 pub token_url: String,
883
884 /// Optional refresh token url.
885 #[serde(skip_serializing_if = "Option::is_none")]
886 pub refresh_url: Option<String>,
887
888 /// Scopes required by the flow.
889 #[serde(flatten)]
890 pub scopes: Scopes,
891
892 /// Optional extensions "x-something"
893 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
894 pub extensions: PropMap<String, serde_json::Value>,
895}
896
897impl Password {
898 /// Construct a new password OAuth flow.
899 ///
900 /// Accepts two arguments: one which is a token url and
901 /// two, a map of scopes for the OAuth flow.
902 ///
903 /// # Examples
904 ///
905 /// Creates a new password flow with scopes.
906 /// ```
907 /// # use salvo_oapi::security::{Password, Scopes};
908 /// Password::new(
909 /// "https://localhost/token",
910 /// Scopes::from_iter([
911 /// ("edit:items", "edit my items"),
912 /// ("read:items", "read my items"),
913 /// ]),
914 /// );
915 /// ```
916 ///
917 /// Creates a new password flow without any scopes.
918 /// ```
919 /// # use salvo_oapi::security::{Password, Scopes};
920 /// Password::new("https://localhost/token", Scopes::new());
921 /// ```
922 pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
923 Self {
924 token_url: token_url.into(),
925 refresh_url: None,
926 scopes,
927 extensions: Default::default(),
928 }
929 }
930
931 /// Construct a new password OAuth flow with an additional refresh URL.
932 ///
933 /// This is essentially same as [`Password::new`] but allows defining third parameter for
934 /// `refresh_url` for fetching refresh tokens.
935 ///
936 /// # Examples
937 ///
938 /// Creates a new password flow with a refresh URL.
939 /// ```
940 /// # use salvo_oapi::security::{Password, Scopes};
941 /// Password::with_refresh_url(
942 /// "https://localhost/token",
943 /// Scopes::from_iter([
944 /// ("edit:items", "edit my items"),
945 /// ("read:items", "read my items"),
946 /// ]),
947 /// "https://localhost/refres-token",
948 /// );
949 /// ```
950 pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
951 Self {
952 token_url: token_url.into(),
953 refresh_url: Some(refresh_url.into()),
954 scopes,
955 extensions: Default::default(),
956 }
957 }
958
959 /// Add openapi extensions (`x-something`) for [`Password`].
960 #[must_use]
961 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
962 self.extensions = extensions;
963 self
964 }
965}
966
967/// Client credentials [`Flow`] configuration for [`OAuth2`].
968#[non_exhaustive]
969#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
970#[serde(rename_all = "camelCase")]
971pub struct ClientCredentials {
972 /// Token url used for [`ClientCredentials`] flow. OAuth2 standard requires TLS.
973 pub token_url: String,
974
975 /// Optional refresh token url.
976 #[serde(skip_serializing_if = "Option::is_none")]
977 pub refresh_url: Option<String>,
978
979 /// Scopes required by the flow.
980 #[serde(flatten)]
981 pub scopes: Scopes,
982
983 /// Optional extensions "x-something"
984 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
985 pub extensions: PropMap<String, serde_json::Value>,
986}
987
988impl ClientCredentials {
989 /// Construct a new client credentials OAuth flow.
990 ///
991 /// Accepts two arguments: one which is a token url and
992 /// two, a map of scopes for the OAuth flow.
993 ///
994 /// # Examples
995 ///
996 /// Creates a new client credentials flow with scopes.
997 /// ```
998 /// # use salvo_oapi::security::{ClientCredentials, Scopes};
999 /// ClientCredentials::new(
1000 /// "https://localhost/token",
1001 /// Scopes::from_iter([
1002 /// ("edit:items", "edit my items"),
1003 /// ("read:items", "read my items"),
1004 /// ]),
1005 /// );
1006 /// ```
1007 ///
1008 /// Creates a new client credentials flow without any scopes.
1009 /// ```
1010 /// # use salvo_oapi::security::{ClientCredentials, Scopes};
1011 /// ClientCredentials::new("https://localhost/token", Scopes::new());
1012 /// ```
1013 pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
1014 Self {
1015 token_url: token_url.into(),
1016 refresh_url: None,
1017 scopes,
1018 extensions: Default::default(),
1019 }
1020 }
1021
1022 /// Construct a new client credentials OAuth flow with an additional refresh URL.
1023 ///
1024 /// This is essentially same as [`ClientCredentials::new`] but allows defining third parameter
1025 /// for `refresh_url`.
1026 ///
1027 /// # Examples
1028 ///
1029 /// Creates a new client credentials flow with a refresh URL.
1030 /// ```
1031 /// # use salvo_oapi::security::{ClientCredentials, Scopes};
1032 /// ClientCredentials::with_refresh_url(
1033 /// "https://localhost/token",
1034 /// Scopes::from_iter([
1035 /// ("edit:items", "edit my items"),
1036 /// ("read:items", "read my items"),
1037 /// ]),
1038 /// "https://localhost/refresh-url",
1039 /// );
1040 /// ```
1041 pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
1042 Self {
1043 token_url: token_url.into(),
1044 refresh_url: Some(refresh_url.into()),
1045 scopes,
1046 extensions: Default::default(),
1047 }
1048 }
1049
1050 /// Add openapi extensions (`x-something`) for [`ClientCredentials`].
1051 #[must_use]
1052 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
1053 self.extensions = extensions;
1054 self
1055 }
1056}
1057
1058/// Device authorization [`Flow`] configuration for [`OAuth2`], as defined by
1059/// [RFC8628](https://tools.ietf.org/html/rfc8628). Added in OpenAPI 3.2.
1060#[non_exhaustive]
1061#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1062#[serde(rename_all = "camelCase")]
1063pub struct DeviceAuthorization {
1064 /// Device authorization url for this flow. OAuth2 standard requires TLS.
1065 pub device_authorization_url: String,
1066
1067 /// Token url for this flow. OAuth2 standard requires TLS.
1068 pub token_url: String,
1069
1070 /// Optional refresh token url.
1071 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub refresh_url: Option<String>,
1073
1074 /// Scopes required by the flow.
1075 #[serde(flatten)]
1076 pub scopes: Scopes,
1077
1078 /// Optional extensions "x-something"
1079 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
1080 pub extensions: PropMap<String, serde_json::Value>,
1081}
1082
1083impl DeviceAuthorization {
1084 /// Construct a new device authorization OAuth flow.
1085 ///
1086 /// # Examples
1087 ///
1088 /// ```
1089 /// # use salvo_oapi::security::{DeviceAuthorization, Scopes};
1090 /// DeviceAuthorization::new(
1091 /// "https://localhost/device_authorization",
1092 /// "https://localhost/token",
1093 /// Scopes::from_iter([("edit:items", "edit my items")]),
1094 /// );
1095 /// ```
1096 pub fn new<S: Into<String>>(device_authorization_url: S, token_url: S, scopes: Scopes) -> Self {
1097 Self {
1098 device_authorization_url: device_authorization_url.into(),
1099 token_url: token_url.into(),
1100 refresh_url: None,
1101 scopes,
1102 extensions: Default::default(),
1103 }
1104 }
1105
1106 /// Construct a new device authorization OAuth flow with an additional refresh URL.
1107 ///
1108 /// # Examples
1109 ///
1110 /// ```
1111 /// # use salvo_oapi::security::{DeviceAuthorization, Scopes};
1112 /// DeviceAuthorization::with_refresh_url(
1113 /// "https://localhost/device_authorization",
1114 /// "https://localhost/token",
1115 /// Scopes::new(),
1116 /// "https://localhost/refresh-token",
1117 /// );
1118 /// ```
1119 pub fn with_refresh_url<S: Into<String>>(
1120 device_authorization_url: S,
1121 token_url: S,
1122 scopes: Scopes,
1123 refresh_url: S,
1124 ) -> Self {
1125 Self {
1126 device_authorization_url: device_authorization_url.into(),
1127 token_url: token_url.into(),
1128 refresh_url: Some(refresh_url.into()),
1129 scopes,
1130 extensions: Default::default(),
1131 }
1132 }
1133
1134 /// Add openapi extensions (`x-something`) for [`DeviceAuthorization`].
1135 #[must_use]
1136 pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
1137 self.extensions = extensions;
1138 self
1139 }
1140}
1141
1142/// [`OAuth2`] flow scopes object defines required permissions for an OAuth flow.
1143///
1144/// Scopes must be given to oauth2 flow but depending on need one of few initialization methods
1145/// could be used.
1146///
1147/// * Create empty map of scopes you can use [`Scopes::new`].
1148/// * Create map with only one scope you can use [`Scopes::one`].
1149/// * Create multiple scopes from iterator with [`Scopes::from_iter`].
1150///
1151/// # Examples
1152///
1153/// Create empty map of scopes.
1154/// ```
1155/// # use salvo_oapi::security::Scopes;
1156/// let scopes = Scopes::new();
1157/// ```
1158///
1159/// Create [`Scopes`] holding one scope.
1160/// ```
1161/// # use salvo_oapi::security::Scopes;
1162/// let scopes = Scopes::one("edit:item", "edit pets");
1163/// ```
1164///
1165/// Create map of scopes from iterator.
1166/// ```
1167/// # use salvo_oapi::security::Scopes;
1168/// let scopes = Scopes::from_iter([
1169/// ("edit:items", "edit my items"),
1170/// ("read:items", "read my items"),
1171/// ]);
1172/// ```
1173#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1174pub struct Scopes {
1175 scopes: PropMap<String, String>,
1176}
1177
1178impl Scopes {
1179 /// Construct new [`Scopes`] with empty map of scopes. This is useful if an OAuth flow does not
1180 /// need any permission scopes.
1181 ///
1182 /// # Examples
1183 ///
1184 /// Create empty map of scopes.
1185 /// ```
1186 /// # use salvo_oapi::security::Scopes;
1187 /// let scopes = Scopes::new();
1188 /// ```
1189 #[must_use]
1190 pub fn new() -> Self {
1191 Default::default()
1192 }
1193
1194 /// Construct new [`Scopes`] with holding one scope.
1195 ///
1196 /// * `scope` Is be the permission required.
1197 /// * `description` Short description about the permission.
1198 ///
1199 /// # Examples
1200 ///
1201 /// Create map of scopes with one scope item.
1202 /// ```
1203 /// # use salvo_oapi::security::Scopes;
1204 /// let scopes = Scopes::one("edit:item", "edit items");
1205 /// ```
1206 #[must_use]
1207 pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
1208 Self {
1209 scopes: PropMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
1210 }
1211 }
1212}
1213
1214impl<I> FromIterator<(I, I)> for Scopes
1215where
1216 I: Into<String>,
1217{
1218 fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
1219 Self {
1220 scopes: iter
1221 .into_iter()
1222 .map(|(key, value)| (key.into(), value.into()))
1223 .collect(),
1224 }
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231
1232 macro_rules! test_fn {
1233 ($name:ident : $schema:expr; $expected:literal) => {
1234 #[test]
1235 fn $name() {
1236 let value = serde_json::to_value($schema).unwrap();
1237 let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
1238
1239 assert_eq!(
1240 value,
1241 expected_value,
1242 "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
1243 stringify!($name),
1244 value,
1245 expected_value
1246 );
1247
1248 println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
1249 }
1250 };
1251 }
1252
1253 test_fn! {
1254 security_scheme_correct_default_http_auth:
1255 SecurityScheme::Http(Http::new(HttpAuthScheme::default()));
1256 r###"{
1257 "type": "http",
1258 "scheme": "basic"
1259}"###
1260 }
1261
1262 test_fn! {
1263 security_scheme_correct_http_bearer_json:
1264 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
1265 r###"{
1266 "type": "http",
1267 "scheme": "bearer",
1268 "bearerFormat": "JWT"
1269}"###
1270 }
1271
1272 test_fn! {
1273 security_scheme_correct_basic_auth:
1274 SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
1275 r###"{
1276 "type": "http",
1277 "scheme": "basic"
1278}"###
1279 }
1280
1281 test_fn! {
1282 security_scheme_correct_basic_auth_change_to_digest_auth_with_description:
1283 SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).scheme(HttpAuthScheme::Digest).description(String::from("digest auth")));
1284 r###"{
1285 "type": "http",
1286 "scheme": "digest",
1287 "description": "digest auth"
1288}"###
1289 }
1290
1291 test_fn! {
1292 security_scheme_correct_digest_auth:
1293 SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
1294 r###"{
1295 "type": "http",
1296 "scheme": "digest"
1297}"###
1298 }
1299
1300 test_fn! {
1301 security_scheme_correct_hoba_auth:
1302 SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
1303 r###"{
1304 "type": "http",
1305 "scheme": "hoba"
1306}"###
1307 }
1308
1309 test_fn! {
1310 security_scheme_correct_mutual_auth:
1311 SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
1312 r###"{
1313 "type": "http",
1314 "scheme": "mutual"
1315}"###
1316 }
1317
1318 test_fn! {
1319 security_scheme_correct_negotiate_auth:
1320 SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
1321 r###"{
1322 "type": "http",
1323 "scheme": "negotiate"
1324}"###
1325 }
1326
1327 test_fn! {
1328 security_scheme_correct_oauth_auth:
1329 SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
1330 r###"{
1331 "type": "http",
1332 "scheme": "oauth"
1333}"###
1334 }
1335
1336 test_fn! {
1337 security_scheme_correct_scram_sha1_auth:
1338 SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
1339 r###"{
1340 "type": "http",
1341 "scheme": "scram-sha-1"
1342}"###
1343 }
1344
1345 test_fn! {
1346 security_scheme_correct_scram_sha256_auth:
1347 SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
1348 r###"{
1349 "type": "http",
1350 "scheme": "scram-sha-256"
1351}"###
1352 }
1353
1354 test_fn! {
1355 security_scheme_correct_api_key_cookie_auth:
1356 SecurityScheme::from(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
1357 r###"{
1358 "type": "apiKey",
1359 "name": "api_key",
1360 "in": "cookie"
1361}"###
1362 }
1363
1364 test_fn! {
1365 security_scheme_correct_api_key_header_auth:
1366 SecurityScheme::from(ApiKey::Header(ApiKeyValue::new("api_key")));
1367 r###"{
1368 "type": "apiKey",
1369 "name": "api_key",
1370 "in": "header"
1371}"###
1372 }
1373
1374 test_fn! {
1375 security_scheme_correct_api_key_query_auth:
1376 SecurityScheme::from(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
1377 r###"{
1378 "type": "apiKey",
1379 "name": "api_key",
1380 "in": "query"
1381}"###
1382 }
1383
1384 test_fn! {
1385 security_scheme_correct_api_key_query_auth_with_description:
1386 SecurityScheme::from(ApiKey::Query(ApiKeyValue::with_description(String::from("api_key"), String::from("my api_key"))));
1387 r###"{
1388 "type": "apiKey",
1389 "name": "api_key",
1390 "description": "my api_key",
1391 "in": "query"
1392}"###
1393 }
1394
1395 test_fn! {
1396 security_scheme_correct_open_id_connect_auth:
1397 SecurityScheme::from(OpenIdConnect::new("https://localhost/openid"));
1398 r###"{
1399 "type": "openIdConnect",
1400 "openIdConnectUrl": "https://localhost/openid"
1401}"###
1402 }
1403
1404 test_fn! {
1405 security_scheme_correct_open_id_connect_auth_with_description:
1406 SecurityScheme::from(OpenIdConnect::with_description("https://localhost/openid", "OpenIdConnect auth"));
1407 r###"{
1408 "type": "openIdConnect",
1409 "openIdConnectUrl": "https://localhost/openid",
1410 "description": "OpenIdConnect auth"
1411}"###
1412 }
1413
1414 test_fn! {
1415 security_scheme_correct_oauth2_implicit:
1416 SecurityScheme::from(
1417 OAuth2::with_description([Flow::Implicit(
1418 Implicit::new(
1419 "https://localhost/auth/dialog",
1420 Scopes::from_iter([
1421 ("edit:items", "edit my items"),
1422 ("read:items", "read my items")
1423 ]),
1424 ),
1425 )], "my oauth2 flow")
1426 );
1427 r###"{
1428 "type": "oauth2",
1429 "flows": {
1430 "implicit": {
1431 "authorizationUrl": "https://localhost/auth/dialog",
1432 "scopes": {
1433 "edit:items": "edit my items",
1434 "read:items": "read my items"
1435 }
1436 }
1437 },
1438 "description": "my oauth2 flow"
1439}"###
1440 }
1441
1442 test_fn! {
1443 security_scheme_correct_oauth2_implicit_with_refresh_url:
1444 SecurityScheme::from(
1445 OAuth2::with_description([Flow::Implicit(
1446 Implicit::with_refresh_url(
1447 "https://localhost/auth/dialog",
1448 Scopes::from_iter([
1449 ("edit:items", "edit my items"),
1450 ("read:items", "read my items")
1451 ]),
1452 "https://localhost/refresh-token"
1453 ),
1454 )], "my oauth2 flow")
1455 );
1456 r###"{
1457 "type": "oauth2",
1458 "flows": {
1459 "implicit": {
1460 "authorizationUrl": "https://localhost/auth/dialog",
1461 "refreshUrl": "https://localhost/refresh-token",
1462 "scopes": {
1463 "edit:items": "edit my items",
1464 "read:items": "read my items"
1465 }
1466 }
1467 },
1468 "description": "my oauth2 flow"
1469}"###
1470 }
1471
1472 test_fn! {
1473 security_scheme_correct_oauth2_password:
1474 SecurityScheme::OAuth2(
1475 OAuth2::with_description([Flow::Password(
1476 Password::new(
1477 "https://localhost/oauth/token",
1478 Scopes::from_iter([
1479 ("edit:items", "edit my items"),
1480 ("read:items", "read my items")
1481 ])
1482 ),
1483 )], "my oauth2 flow")
1484 );
1485 r###"{
1486 "type": "oauth2",
1487 "flows": {
1488 "password": {
1489 "tokenUrl": "https://localhost/oauth/token",
1490 "scopes": {
1491 "edit:items": "edit my items",
1492 "read:items": "read my items"
1493 }
1494 }
1495 },
1496 "description": "my oauth2 flow"
1497}"###
1498 }
1499
1500 test_fn! {
1501 security_scheme_correct_oauth2_password_with_refresh_url:
1502 SecurityScheme::OAuth2(
1503 OAuth2::with_description([Flow::Password(
1504 Password::with_refresh_url(
1505 "https://localhost/oauth/token",
1506 Scopes::from_iter([
1507 ("edit:items", "edit my items"),
1508 ("read:items", "read my items")
1509 ]),
1510 "https://localhost/refresh/token"
1511 ),
1512 )], "my oauth2 flow")
1513 );
1514 r###"{
1515 "type": "oauth2",
1516 "flows": {
1517 "password": {
1518 "tokenUrl": "https://localhost/oauth/token",
1519 "refreshUrl": "https://localhost/refresh/token",
1520 "scopes": {
1521 "edit:items": "edit my items",
1522 "read:items": "read my items"
1523 }
1524 }
1525 },
1526 "description": "my oauth2 flow"
1527}"###
1528 }
1529
1530 test_fn! {
1531 security_scheme_correct_oauth2_client_credentials:
1532 SecurityScheme::OAuth2(
1533 OAuth2::new([Flow::ClientCredentials(
1534 ClientCredentials::new(
1535 "https://localhost/oauth/token",
1536 Scopes::from_iter([
1537 ("edit:items", "edit my items"),
1538 ("read:items", "read my items")
1539 ])
1540 ),
1541 )])
1542 );
1543 r###"{
1544 "type": "oauth2",
1545 "flows": {
1546 "clientCredentials": {
1547 "tokenUrl": "https://localhost/oauth/token",
1548 "scopes": {
1549 "edit:items": "edit my items",
1550 "read:items": "read my items"
1551 }
1552 }
1553 }
1554}"###
1555 }
1556
1557 test_fn! {
1558 security_scheme_correct_oauth2_client_credentials_with_refresh_url:
1559 SecurityScheme::OAuth2(
1560 OAuth2::new([Flow::ClientCredentials(
1561 ClientCredentials::with_refresh_url(
1562 "https://localhost/oauth/token",
1563 Scopes::from_iter([
1564 ("edit:items", "edit my items"),
1565 ("read:items", "read my items")
1566 ]),
1567 "https://localhost/refresh/token"
1568 ),
1569 )])
1570 );
1571 r###"{
1572 "type": "oauth2",
1573 "flows": {
1574 "clientCredentials": {
1575 "tokenUrl": "https://localhost/oauth/token",
1576 "refreshUrl": "https://localhost/refresh/token",
1577 "scopes": {
1578 "edit:items": "edit my items",
1579 "read:items": "read my items"
1580 }
1581 }
1582 }
1583}"###
1584 }
1585
1586 test_fn! {
1587 security_scheme_correct_oauth2_authorization_code:
1588 SecurityScheme::OAuth2(
1589 OAuth2::new([Flow::AuthorizationCode(
1590 AuthorizationCode::with_refresh_url(
1591 "https://localhost/authorization/token",
1592 "https://localhost/token/url",
1593 Scopes::from_iter([
1594 ("edit:items", "edit my items"),
1595 ("read:items", "read my items")
1596 ]),
1597 "https://localhost/refresh/token"
1598 ),
1599 )])
1600 );
1601 r###"{
1602 "type": "oauth2",
1603 "flows": {
1604 "authorizationCode": {
1605 "authorizationUrl": "https://localhost/authorization/token",
1606 "tokenUrl": "https://localhost/token/url",
1607 "refreshUrl": "https://localhost/refresh/token",
1608 "scopes": {
1609 "edit:items": "edit my items",
1610 "read:items": "read my items"
1611 }
1612 }
1613 }
1614}"###
1615 }
1616
1617 test_fn! {
1618 security_scheme_correct_oauth2_authorization_code_no_scopes:
1619 SecurityScheme::OAuth2(
1620 OAuth2::new([Flow::AuthorizationCode(
1621 AuthorizationCode::new(
1622 "https://localhost/authorization/token",
1623 "https://localhost/token/url",
1624 Scopes::new()
1625 ),
1626 )])
1627 );
1628 r###"{
1629 "type": "oauth2",
1630 "flows": {
1631 "authorizationCode": {
1632 "authorizationUrl": "https://localhost/authorization/token",
1633 "tokenUrl": "https://localhost/token/url",
1634 "scopes": {}
1635 }
1636 }
1637}"###
1638 }
1639
1640 test_fn! {
1641 security_scheme_correct_oauth2_authorization_code_one_scopes:
1642 SecurityScheme::OAuth2(
1643 OAuth2::new([Flow::AuthorizationCode(
1644 AuthorizationCode::new(
1645 "https://localhost/authorization/token",
1646 "https://localhost/token/url",
1647 Scopes::one("edit:items", "edit my items")
1648 ),
1649 )])
1650 );
1651 r###"{
1652 "type": "oauth2",
1653 "flows": {
1654 "authorizationCode": {
1655 "authorizationUrl": "https://localhost/authorization/token",
1656 "tokenUrl": "https://localhost/token/url",
1657 "scopes": {
1658 "edit:items": "edit my items"
1659 }
1660 }
1661 }
1662}"###
1663 }
1664
1665 test_fn! {
1666 security_scheme_correct_mutual_tls:
1667 SecurityScheme::MutualTls {
1668 description: Some(String::from("authorization is performed with client side certificate")),
1669 deprecated: None
1670 };
1671 r###"{
1672 "type": "mutualTLS",
1673 "description": "authorization is performed with client side certificate"
1674}"###
1675 }
1676
1677 #[test]
1678 fn security_requirement_accepts_uri_references() {
1679 // OpenAPI 3.2 allows a requirement name to be a URI, a `./`-prefixed relative
1680 // reference disambiguating a component-name collision, or a plain component name.
1681 let requirement = SecurityRequirement::new("api_key", Vec::<String>::new())
1682 .add("./foo", ["read:items"])
1683 .add("https://example.com/schemes.json#/oauth", ["write:items"]);
1684
1685 let value = serde_json::to_value(&requirement).expect("serialize");
1686 assert_eq!(
1687 value,
1688 serde_json::json!({
1689 "api_key": [],
1690 "./foo": ["read:items"],
1691 "https://example.com/schemes.json#/oauth": ["write:items"]
1692 })
1693 );
1694
1695 let parsed: SecurityRequirement = serde_json::from_value(value).expect("deserialize");
1696 assert_eq!(parsed, requirement);
1697 }
1698
1699 #[test]
1700 fn device_authorization_flow_round_trips_under_its_own_key() {
1701 let scheme = SecurityScheme::OAuth2(OAuth2::new([Flow::DeviceAuthorization(
1702 DeviceAuthorization::with_refresh_url(
1703 "https://localhost/device_authorization",
1704 "https://localhost/token",
1705 Scopes::one("edit:items", "edit my items"),
1706 "https://localhost/refresh",
1707 ),
1708 )]));
1709
1710 let value = serde_json::to_value(&scheme).expect("serialize");
1711 assert_eq!(
1712 value,
1713 serde_json::json!({
1714 "type": "oauth2",
1715 "flows": {
1716 "deviceAuthorization": {
1717 "deviceAuthorizationUrl": "https://localhost/device_authorization",
1718 "tokenUrl": "https://localhost/token",
1719 "refreshUrl": "https://localhost/refresh",
1720 "scopes": { "edit:items": "edit my items" }
1721 }
1722 }
1723 })
1724 );
1725
1726 // `Flow` is untagged, so the device flow must not be swallowed by `Password`, whose
1727 // fields are a subset of it.
1728 let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1729 assert_eq!(parsed, scheme);
1730 }
1731
1732 #[test]
1733 fn oauth2_metadata_url_and_deprecated_serialize() {
1734 let scheme = SecurityScheme::OAuth2(
1735 OAuth2::new([Flow::ClientCredentials(ClientCredentials::new(
1736 "https://localhost/token",
1737 Scopes::new(),
1738 ))])
1739 .oauth2_metadata_url("https://localhost/.well-known/oauth-authorization-server")
1740 .deprecated(true),
1741 );
1742
1743 let value = serde_json::to_value(&scheme).expect("serialize");
1744 assert_eq!(
1745 value["oauth2MetadataUrl"],
1746 serde_json::json!("https://localhost/.well-known/oauth-authorization-server")
1747 );
1748 assert_eq!(value["deprecated"], serde_json::json!(true));
1749
1750 let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1751 assert_eq!(parsed, scheme);
1752 }
1753
1754 #[test]
1755 fn deprecated_is_available_on_every_scheme_kind() {
1756 for scheme in [
1757 SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).deprecated(true)),
1758 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("api_key").deprecated(true))),
1759 SecurityScheme::OpenIdConnect(
1760 OpenIdConnect::new("https://localhost/openid").deprecated(true),
1761 ),
1762 SecurityScheme::MutualTls {
1763 description: None,
1764 deprecated: Some(true),
1765 },
1766 ] {
1767 let value = serde_json::to_value(&scheme).expect("serialize");
1768 assert_eq!(
1769 value["deprecated"],
1770 serde_json::json!(true),
1771 "deprecated missing from {value}"
1772 );
1773 let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1774 assert_eq!(parsed, scheme);
1775 }
1776 }
1777}