utoipa/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, iter};
7
8use serde::{Deserialize, Serialize};
9
10use super::{builder, extensions::Extensions};
11
12/// OpenAPI [security requirement][security] object.
13///
14/// Security requirement holds list of required [`SecurityScheme`] *names* and possible *scopes* required
15/// to execute the operation. They can be defined in [`#[utoipa::path(...)]`][path] or in `#[openapi(...)]`
16/// of [`OpenApi`][openapi].
17///
18/// Applying the security requirement to [`OpenApi`][openapi] will make it globally
19/// available to all operations. When applied to specific [`#[utoipa::path(...)]`][path] will only
20/// make the security requirements available for that operation. Only one of the requirements must be
21/// satisfied.
22///
23/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
24/// [path]: ../../attr.path.html
25/// [openapi]: ../../derive.OpenApi.html
26#[non_exhaustive]
27#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
28#[cfg_attr(feature = "debug", derive(Debug))]
29pub struct SecurityRequirement {
30    #[serde(flatten)]
31    value: BTreeMap<String, Vec<String>>,
32}
33
34impl SecurityRequirement {
35    /// Construct a new [`SecurityRequirement`].
36    ///
37    /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
38    /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
39    /// Scopes must match to the ones defined in [`SecurityScheme`].
40    ///
41    /// # Examples
42    ///
43    /// Create new security requirement with scopes.
44    /// ```rust
45    /// # use utoipa::openapi::security::SecurityRequirement;
46    /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
47    /// ```
48    ///
49    /// You can also create an empty security requirement with `Default::default()`.
50    /// ```rust
51    /// # use utoipa::openapi::security::SecurityRequirement;
52    /// SecurityRequirement::default();
53    /// ```
54    ///
55    /// If you have more than one name in the security requirement you can use
56    /// [`SecurityRequirement::add`].
57    pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
58        name: N,
59        scopes: S,
60    ) -> Self {
61        Self {
62            value: BTreeMap::from_iter(iter::once_with(|| {
63                (
64                    Into::<String>::into(name),
65                    scopes
66                        .into_iter()
67                        .map(|scope| Into::<String>::into(scope))
68                        .collect::<Vec<_>>(),
69                )
70            })),
71        }
72    }
73
74    /// Allows to add multiple names to security requirement.
75    ///
76    /// Accepts name for the security requirement which must match to the name of available [`SecurityScheme`].
77    /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
78    /// Scopes must match to the ones defined in [`SecurityScheme`].
79    ///
80    /// # Examples
81    ///
82    /// Make both API keys required:
83    /// ```rust
84    /// # use utoipa::openapi::security::{SecurityRequirement, HttpAuthScheme, HttpBuilder, SecurityScheme};
85    /// # use utoipa::{openapi, Modify, OpenApi};
86    /// # use serde::Serialize;
87    /// #[derive(Debug, Serialize)]
88    /// struct Foo;
89    ///
90    /// impl Modify for Foo {
91    ///     fn modify(&self, openapi: &mut openapi::OpenApi) {
92    ///         if let Some(schema) = openapi.components.as_mut() {
93    ///             schema.add_security_scheme(
94    ///                 "api_key1",
95    ///                 SecurityScheme::Http(
96    ///                     HttpBuilder::new()
97    ///                         .scheme(HttpAuthScheme::Bearer)
98    ///                         .bearer_format("JWT")
99    ///                         .build(),
100    ///                 ),
101    ///             );
102    ///             schema.add_security_scheme(
103    ///                 "api_key2",
104    ///                 SecurityScheme::Http(
105    ///                     HttpBuilder::new()
106    ///                         .scheme(HttpAuthScheme::Bearer)
107    ///                         .bearer_format("JWT")
108    ///                         .build(),
109    ///                 ),
110    ///             );
111    ///         }
112    ///     }
113    /// }
114    ///
115    /// #[derive(Default, OpenApi)]
116    /// #[openapi(
117    ///     modifiers(&Foo),
118    ///     security(
119    ///         ("api_key1" = ["edit:items", "read:items"], "api_key2" = ["edit:items", "read:items"]),
120    ///     )
121    /// )]
122    /// struct ApiDoc;
123    /// ```
124    pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
125        mut self,
126        name: N,
127        scopes: S,
128    ) -> Self {
129        self.value.insert(
130            Into::<String>::into(name),
131            scopes.into_iter().map(Into::<String>::into).collect(),
132        );
133
134        self
135    }
136}
137
138/// OpenAPI [security scheme][security] for path operations.
139///
140/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
141///
142/// # Examples
143///
144/// Create implicit oauth2 flow security schema for path operations.
145/// ```rust
146/// # use utoipa::openapi::security::{SecurityScheme, OAuth2, Implicit, Flow, Scopes};
147/// SecurityScheme::OAuth2(
148///     OAuth2::with_description([Flow::Implicit(
149///         Implicit::new(
150///             "https://localhost/auth/dialog",
151///             Scopes::from_iter([
152///                 ("edit:items", "edit my items"),
153///                 ("read:items", "read my items")
154///             ]),
155///         ),
156///     )], "my oauth2 flow")
157/// );
158/// ```
159///
160/// Create JWT header authentication.
161/// ```rust
162/// # use utoipa::openapi::security::{SecurityScheme, HttpAuthScheme, HttpBuilder};
163/// SecurityScheme::Http(
164///     HttpBuilder::new().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build()
165/// );
166/// ```
167#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
168#[serde(tag = "type", rename_all = "camelCase")]
169#[cfg_attr(feature = "debug", derive(Debug))]
170pub enum SecurityScheme {
171    /// Oauth flow authentication.
172    #[serde(rename = "oauth2")]
173    OAuth2(OAuth2),
174    /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*.
175    ApiKey(ApiKey),
176    /// Http authentication such as *`bearer`* or *`basic`*.
177    Http(Http),
178    /// Open id connect url to discover OAuth2 configuration values.
179    OpenIdConnect(OpenIdConnect),
180    /// Authentication is done via client side certificate.
181    ///
182    /// OpenApi 3.1 type
183    #[serde(rename = "mutualTLS")]
184    MutualTls {
185        #[allow(missing_docs)]
186        #[serde(skip_serializing_if = "Option::is_none")]
187        description: Option<String>,
188        /// Optional extensions "x-something".
189        #[serde(skip_serializing_if = "Option::is_none", flatten)]
190        extensions: Option<Extensions>,
191    },
192}
193
194/// Api key authentication [`SecurityScheme`].
195#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
196#[serde(tag = "in", rename_all = "lowercase")]
197#[cfg_attr(feature = "debug", derive(Debug))]
198pub enum ApiKey {
199    /// Create api key which is placed in HTTP header.
200    Header(ApiKeyValue),
201    /// Create api key which is placed in query parameters.
202    Query(ApiKeyValue),
203    /// Create api key which is placed in cookie value.
204    Cookie(ApiKeyValue),
205}
206
207/// Value object for [`ApiKey`].
208#[non_exhaustive]
209#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
210#[cfg_attr(feature = "debug", derive(Debug))]
211pub struct ApiKeyValue {
212    /// Name of the [`ApiKey`] parameter.
213    pub name: String,
214
215    /// Description of the the [`ApiKey`] [`SecurityScheme`]. Supports markdown syntax.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub description: Option<String>,
218
219    /// Optional extensions "x-something".
220    #[serde(skip_serializing_if = "Option::is_none", flatten)]
221    pub extensions: Option<Extensions>,
222}
223
224impl ApiKeyValue {
225    /// Constructs new api key value.
226    ///
227    /// # Examples
228    ///
229    /// Create new api key security schema with name `api_key`.
230    /// ```rust
231    /// # use utoipa::openapi::security::ApiKeyValue;
232    /// let api_key = ApiKeyValue::new("api_key");
233    /// ```
234    pub fn new<S: Into<String>>(name: S) -> Self {
235        Self {
236            name: name.into(),
237            description: None,
238            extensions: Default::default(),
239        }
240    }
241
242    /// Construct a new api key with optional description supporting markdown syntax.
243    ///
244    /// # Examples
245    ///
246    /// Create new api key security schema with name `api_key` with description.
247    /// ```rust
248    /// # use utoipa::openapi::security::ApiKeyValue;
249    /// let api_key = ApiKeyValue::with_description("api_key", "my api_key token");
250    /// ```
251    pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
252        Self {
253            name: name.into(),
254            description: Some(description.into()),
255            extensions: Default::default(),
256        }
257    }
258}
259
260builder! {
261    HttpBuilder;
262
263    /// Http authentication [`SecurityScheme`] builder.
264    ///
265    /// Methods can be chained to configure _bearer_format_ or to add _description_.
266    #[non_exhaustive]
267    #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
268    #[serde(rename_all = "camelCase")]
269    #[cfg_attr(feature = "debug", derive(Debug))]
270    pub struct Http {
271        /// Http authorization scheme in HTTP `Authorization` header value.
272        pub scheme: HttpAuthScheme,
273
274        /// Optional hint to client how the bearer token is formatted. Valid only with [`HttpAuthScheme::Bearer`].
275        #[serde(skip_serializing_if = "Option::is_none")]
276        pub bearer_format: Option<String>,
277
278        /// Optional description of [`Http`] [`SecurityScheme`] supporting markdown syntax.
279        #[serde(skip_serializing_if = "Option::is_none")]
280        pub description: Option<String>,
281
282        /// Optional extensions "x-something".
283        #[serde(skip_serializing_if = "Option::is_none", flatten)]
284        pub extensions: Option<Extensions>,
285    }
286}
287
288impl Http {
289    /// Create new http authentication security schema.
290    ///
291    /// Accepts one argument which defines the scheme of the http authentication.
292    ///
293    /// # Examples
294    ///
295    /// Create http security schema with basic authentication.
296    /// ```rust
297    /// # use utoipa::openapi::security::{SecurityScheme, Http, HttpAuthScheme};
298    /// SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
299    /// ```
300    pub fn new(scheme: HttpAuthScheme) -> Self {
301        Self {
302            scheme,
303            bearer_format: None,
304            description: None,
305            extensions: Default::default(),
306        }
307    }
308}
309
310impl HttpBuilder {
311    /// Add or change http authentication scheme used.
312    ///
313    /// # Examples
314    ///
315    /// Create new [`Http`] [`SecurityScheme`] via [`HttpBuilder`].
316    /// ```rust
317    /// # use utoipa::openapi::security::{HttpBuilder, HttpAuthScheme};
318    /// let http = HttpBuilder::new().scheme(HttpAuthScheme::Basic).build();
319    /// ```
320    pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
321        self.scheme = scheme;
322
323        self
324    }
325    /// Add or change informative bearer format for http security schema.
326    ///
327    /// This is only applicable to [`HttpAuthScheme::Bearer`].
328    ///
329    /// # Examples
330    ///
331    /// Add JTW bearer format for security schema.
332    /// ```rust
333    /// # use utoipa::openapi::security::{HttpBuilder, HttpAuthScheme};
334    /// HttpBuilder::new().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build();
335    /// ```
336    pub fn bearer_format<S: Into<String>>(mut self, bearer_format: S) -> Self {
337        if self.scheme == HttpAuthScheme::Bearer {
338            self.bearer_format = Some(bearer_format.into());
339        }
340
341        self
342    }
343
344    /// Add or change optional description supporting markdown syntax.
345    pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
346        self.description = description.map(|description| description.into());
347
348        self
349    }
350}
351
352/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1).
353///
354/// Types are maintained at <https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml>.
355#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
356#[cfg_attr(feature = "debug", derive(Debug))]
357#[serde(rename_all = "lowercase")]
358#[allow(missing_docs)]
359pub enum HttpAuthScheme {
360    Basic,
361    Bearer,
362    Digest,
363    Hoba,
364    Mutual,
365    Negotiate,
366    OAuth,
367    #[serde(rename = "scram-sha-1")]
368    ScramSha1,
369    #[serde(rename = "scram-sha-256")]
370    ScramSha256,
371    Vapid,
372}
373
374impl Default for HttpAuthScheme {
375    fn default() -> Self {
376        Self::Basic
377    }
378}
379
380/// Open id connect [`SecurityScheme`].
381#[non_exhaustive]
382#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
383#[serde(rename_all = "camelCase")]
384#[cfg_attr(feature = "debug", derive(Debug))]
385pub struct OpenIdConnect {
386    /// Url of the [`OpenIdConnect`] to discover OAuth2 connect values.
387    pub open_id_connect_url: String,
388
389    /// Description of [`OpenIdConnect`] [`SecurityScheme`] supporting markdown syntax.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub description: Option<String>,
392
393    /// Optional extensions "x-something".
394    #[serde(skip_serializing_if = "Option::is_none", flatten)]
395    pub extensions: Option<Extensions>,
396}
397
398impl OpenIdConnect {
399    /// Construct a new open id connect security schema.
400    ///
401    /// # Examples
402    ///
403    /// ```rust
404    /// # use utoipa::openapi::security::OpenIdConnect;
405    /// OpenIdConnect::new("https://localhost/openid");
406    /// ```
407    pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
408        Self {
409            open_id_connect_url: open_id_connect_url.into(),
410            description: None,
411            extensions: Default::default(),
412        }
413    }
414
415    /// Construct a new [`OpenIdConnect`] [`SecurityScheme`] with optional description
416    /// supporting markdown syntax.
417    ///
418    /// # Examples
419    ///
420    /// ```rust
421    /// # use utoipa::openapi::security::OpenIdConnect;
422    /// OpenIdConnect::with_description("https://localhost/openid", "my pet api open id connect");
423    /// ```
424    pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
425        Self {
426            open_id_connect_url: open_id_connect_url.into(),
427            description: Some(description.into()),
428            extensions: Default::default(),
429        }
430    }
431}
432
433/// OAuth2 [`Flow`] configuration for [`SecurityScheme`].
434#[non_exhaustive]
435#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
436#[cfg_attr(feature = "debug", derive(Debug))]
437pub struct OAuth2 {
438    /// Map of supported OAuth2 flows.
439    pub flows: BTreeMap<String, Flow>,
440
441    /// Optional description for the [`OAuth2`] [`Flow`] [`SecurityScheme`].
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub description: Option<String>,
444
445    /// Optional extensions "x-something".
446    #[serde(skip_serializing_if = "Option::is_none", flatten)]
447    pub extensions: Option<Extensions>,
448}
449
450impl OAuth2 {
451    /// Construct a new OAuth2 security schema configuration object.
452    ///
453    /// Oauth flow accepts slice of [`Flow`] configuration objects and can be optionally provided with description.
454    ///
455    /// # Examples
456    ///
457    /// Create new OAuth2 flow with multiple authentication flows.
458    /// ```rust
459    /// # use utoipa::openapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
460    /// OAuth2::new([Flow::Password(
461    ///     Password::with_refresh_url(
462    ///         "https://localhost/oauth/token",
463    ///         Scopes::from_iter([
464    ///             ("edit:items", "edit my items"),
465    ///             ("read:items", "read my items")
466    ///         ]),
467    ///         "https://localhost/refresh/token"
468    ///     )),
469    ///     Flow::AuthorizationCode(
470    ///         AuthorizationCode::new(
471    ///         "https://localhost/authorization/token",
472    ///         "https://localhost/token/url",
473    ///         Scopes::from_iter([
474    ///             ("edit:items", "edit my items"),
475    ///             ("read:items", "read my items")
476    ///         ])),
477    ///    ),
478    /// ]);
479    /// ```
480    pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
481        Self {
482            flows: BTreeMap::from_iter(
483                flows
484                    .into_iter()
485                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
486            ),
487            extensions: None,
488            description: None,
489        }
490    }
491
492    /// Construct a new OAuth2 flow with optional description supporting markdown syntax.
493    ///
494    /// # Examples
495    ///
496    /// Create new OAuth2 flow with multiple authentication flows with description.
497    /// ```rust
498    /// # use utoipa::openapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
499    /// OAuth2::with_description([Flow::Password(
500    ///     Password::with_refresh_url(
501    ///         "https://localhost/oauth/token",
502    ///         Scopes::from_iter([
503    ///             ("edit:items", "edit my items"),
504    ///             ("read:items", "read my items")
505    ///         ]),
506    ///         "https://localhost/refresh/token"
507    ///     )),
508    ///     Flow::AuthorizationCode(
509    ///         AuthorizationCode::new(
510    ///         "https://localhost/authorization/token",
511    ///         "https://localhost/token/url",
512    ///         Scopes::from_iter([
513    ///             ("edit:items", "edit my items"),
514    ///             ("read:items", "read my items")
515    ///         ])
516    ///      ),
517    ///    ),
518    /// ], "my oauth2 flow");
519    /// ```
520    pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
521        flows: I,
522        description: S,
523    ) -> Self {
524        Self {
525            flows: BTreeMap::from_iter(
526                flows
527                    .into_iter()
528                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
529            ),
530            extensions: None,
531            description: Some(description.into()),
532        }
533    }
534}
535
536/// [`OAuth2`] flow configuration object.
537///
538/// See more details at <https://spec.openapis.org/oas/latest.html#oauth-flows-object>.
539#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
540#[serde(untagged)]
541#[cfg_attr(feature = "debug", derive(Debug))]
542pub enum Flow {
543    /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details.
544    ///
545    /// Soon to be deprecated by <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics>.
546    Implicit(Implicit),
547    /// Define password [`Flow`] type. See [`Password::new`] for usage details.
548    Password(Password),
549    /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details.
550    ClientCredentials(ClientCredentials),
551    /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details.
552    AuthorizationCode(AuthorizationCode),
553}
554
555impl Flow {
556    fn get_type_as_str(&self) -> &str {
557        match self {
558            Self::Implicit(_) => "implicit",
559            Self::Password(_) => "password",
560            Self::ClientCredentials(_) => "clientCredentials",
561            Self::AuthorizationCode(_) => "authorizationCode",
562        }
563    }
564}
565
566/// Implicit [`Flow`] configuration for [`OAuth2`].
567#[non_exhaustive]
568#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
569#[serde(rename_all = "camelCase")]
570#[cfg_attr(feature = "debug", derive(Debug))]
571pub struct Implicit {
572    /// Authorization token url for the flow.
573    pub authorization_url: String,
574
575    /// Optional refresh token url for the flow.
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub refresh_url: Option<String>,
578
579    /// Scopes required by the flow.
580    #[serde(flatten)]
581    pub scopes: Scopes,
582
583    /// Optional extensions "x-something".
584    #[serde(skip_serializing_if = "Option::is_none", flatten)]
585    pub extensions: Option<Extensions>,
586}
587
588impl Implicit {
589    /// Construct a new implicit oauth2 flow.
590    ///
591    /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can
592    /// also be an empty map.
593    ///
594    /// # Examples
595    ///
596    /// Create new implicit flow with scopes.
597    /// ```rust
598    /// # use utoipa::openapi::security::{Implicit, Scopes};
599    /// Implicit::new(
600    ///     "https://localhost/auth/dialog",
601    ///     Scopes::from_iter([
602    ///         ("edit:items", "edit my items"),
603    ///         ("read:items", "read my items")
604    ///     ]),
605    /// );
606    /// ```
607    ///
608    /// Create new implicit flow without any scopes.
609    /// ```rust
610    /// # use utoipa::openapi::security::{Implicit, Scopes};
611    /// Implicit::new(
612    ///     "https://localhost/auth/dialog",
613    ///     Scopes::new(),
614    /// );
615    /// ```
616    pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
617        Self {
618            authorization_url: authorization_url.into(),
619            refresh_url: None,
620            scopes,
621            extensions: Default::default(),
622        }
623    }
624
625    /// Construct a new implicit oauth2 flow with refresh url for getting refresh tokens.
626    ///
627    /// This is essentially same as [`Implicit::new`] but allows defining `refresh_url` for the [`Implicit`]
628    /// oauth2 flow.
629    ///
630    /// # Examples
631    ///
632    /// Create a new implicit oauth2 flow with refresh token.
633    /// ```rust
634    /// # use utoipa::openapi::security::{Implicit, Scopes};
635    /// Implicit::with_refresh_url(
636    ///     "https://localhost/auth/dialog",
637    ///     Scopes::new(),
638    ///     "https://localhost/refresh-token"
639    /// );
640    /// ```
641    pub fn with_refresh_url<S: Into<String>>(
642        authorization_url: S,
643        scopes: Scopes,
644        refresh_url: S,
645    ) -> Self {
646        Self {
647            authorization_url: authorization_url.into(),
648            refresh_url: Some(refresh_url.into()),
649            scopes,
650            extensions: Default::default(),
651        }
652    }
653}
654
655/// Authorization code [`Flow`] configuration for [`OAuth2`].
656#[non_exhaustive]
657#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
658#[serde(rename_all = "camelCase")]
659#[cfg_attr(feature = "debug", derive(Debug))]
660pub struct AuthorizationCode {
661    /// Url for authorization token.
662    pub authorization_url: String,
663    /// Token url for the flow.
664    pub token_url: String,
665
666    /// Optional refresh token url for the flow.
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub refresh_url: Option<String>,
669
670    /// Scopes required by the flow.
671    #[serde(flatten)]
672    pub scopes: Scopes,
673
674    /// Optional extensions "x-something".
675    #[serde(skip_serializing_if = "Option::is_none", flatten)]
676    pub extensions: Option<Extensions>,
677}
678
679impl AuthorizationCode {
680    /// Construct a new authorization code oauth flow.
681    ///
682    /// Accepts three arguments: one which is authorization url, two a token url and
683    /// three a map of scopes for oauth flow.
684    ///
685    /// # Examples
686    ///
687    /// Create new authorization code flow with scopes.
688    /// ```rust
689    /// # use utoipa::openapi::security::{AuthorizationCode, Scopes};
690    /// AuthorizationCode::new(
691    ///     "https://localhost/auth/dialog",
692    ///     "https://localhost/token",
693    ///     Scopes::from_iter([
694    ///         ("edit:items", "edit my items"),
695    ///         ("read:items", "read my items")
696    ///     ]),
697    /// );
698    /// ```
699    ///
700    /// Create new authorization code flow without any scopes.
701    /// ```rust
702    /// # use utoipa::openapi::security::{AuthorizationCode, Scopes};
703    /// AuthorizationCode::new(
704    ///     "https://localhost/auth/dialog",
705    ///     "https://localhost/token",
706    ///     Scopes::new(),
707    /// );
708    /// ```
709    pub fn new<A: Into<String>, T: Into<String>>(
710        authorization_url: A,
711        token_url: T,
712        scopes: Scopes,
713    ) -> Self {
714        Self {
715            authorization_url: authorization_url.into(),
716            token_url: token_url.into(),
717            refresh_url: None,
718            scopes,
719            extensions: Default::default(),
720        }
721    }
722
723    /// Construct a new  [`AuthorizationCode`] OAuth2 flow with additional refresh token url.
724    ///
725    /// This is essentially same as [`AuthorizationCode::new`] but allows defining extra parameter `refresh_url`
726    /// for fetching refresh token.
727    ///
728    /// # Examples
729    ///
730    /// Create [`AuthorizationCode`] OAuth2 flow with refresh url.
731    /// ```rust
732    /// # use utoipa::openapi::security::{AuthorizationCode, Scopes};
733    /// AuthorizationCode::with_refresh_url(
734    ///     "https://localhost/auth/dialog",
735    ///     "https://localhost/token",
736    ///     Scopes::new(),
737    ///     "https://localhost/refresh-token"
738    /// );
739    /// ```
740    pub fn with_refresh_url<S: Into<String>>(
741        authorization_url: S,
742        token_url: S,
743        scopes: Scopes,
744        refresh_url: S,
745    ) -> Self {
746        Self {
747            authorization_url: authorization_url.into(),
748            token_url: token_url.into(),
749            refresh_url: Some(refresh_url.into()),
750            scopes,
751            extensions: Default::default(),
752        }
753    }
754}
755
756/// Password [`Flow`] configuration for [`OAuth2`].
757#[non_exhaustive]
758#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
759#[serde(rename_all = "camelCase")]
760#[cfg_attr(feature = "debug", derive(Debug))]
761pub struct Password {
762    /// Token url for this OAuth2 flow. OAuth2 standard requires TLS.
763    pub token_url: String,
764
765    /// Optional refresh token url.
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub refresh_url: Option<String>,
768
769    /// Scopes required by the flow.
770    #[serde(flatten)]
771    pub scopes: Scopes,
772
773    /// Optional extensions "x-something".
774    #[serde(skip_serializing_if = "Option::is_none", flatten)]
775    pub extensions: Option<Extensions>,
776}
777
778impl Password {
779    /// Construct a new password oauth flow.
780    ///
781    /// Accepts two arguments: one which is a token url and
782    /// two a map of scopes for oauth flow.
783    ///
784    /// # Examples
785    ///
786    /// Create new password flow with scopes.
787    /// ```rust
788    /// # use utoipa::openapi::security::{Password, Scopes};
789    /// Password::new(
790    ///     "https://localhost/token",
791    ///     Scopes::from_iter([
792    ///         ("edit:items", "edit my items"),
793    ///         ("read:items", "read my items")
794    ///     ]),
795    /// );
796    /// ```
797    ///
798    /// Create new password flow without any scopes.
799    /// ```rust
800    /// # use utoipa::openapi::security::{Password, Scopes};
801    /// Password::new(
802    ///     "https://localhost/token",
803    ///     Scopes::new(),
804    /// );
805    /// ```
806    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
807        Self {
808            token_url: token_url.into(),
809            refresh_url: None,
810            scopes,
811            extensions: Default::default(),
812        }
813    }
814
815    /// Construct a new password oauth flow with additional refresh url.
816    ///
817    /// This is essentially same as [`Password::new`] but allows defining third parameter for `refresh_url`
818    /// for fetching refresh tokens.
819    ///
820    /// # Examples
821    ///
822    /// Create new password flow with refresh url.
823    /// ```rust
824    /// # use utoipa::openapi::security::{Password, Scopes};
825    /// Password::with_refresh_url(
826    ///     "https://localhost/token",
827    ///     Scopes::from_iter([
828    ///         ("edit:items", "edit my items"),
829    ///         ("read:items", "read my items")
830    ///     ]),
831    ///     "https://localhost/refres-token"
832    /// );
833    /// ```
834    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
835        Self {
836            token_url: token_url.into(),
837            refresh_url: Some(refresh_url.into()),
838            scopes,
839            extensions: Default::default(),
840        }
841    }
842}
843
844/// Client credentials [`Flow`] configuration for [`OAuth2`].
845#[non_exhaustive]
846#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
847#[serde(rename_all = "camelCase")]
848#[cfg_attr(feature = "debug", derive(Debug))]
849pub struct ClientCredentials {
850    /// Token url used for [`ClientCredentials`] flow. OAuth2 standard requires TLS.
851    pub token_url: String,
852
853    /// Optional refresh token url.
854    #[serde(skip_serializing_if = "Option::is_none")]
855    pub refresh_url: Option<String>,
856
857    /// Scopes required by the flow.
858    #[serde(flatten)]
859    pub scopes: Scopes,
860
861    /// Optional extensions "x-something".
862    #[serde(skip_serializing_if = "Option::is_none", flatten)]
863    pub extensions: Option<Extensions>,
864}
865
866impl ClientCredentials {
867    /// Construct a new client credentials oauth flow.
868    ///
869    /// Accepts two arguments: one which is a token url and
870    /// two a map of scopes for oauth flow.
871    ///
872    /// # Examples
873    ///
874    /// Create new client credentials flow with scopes.
875    /// ```rust
876    /// # use utoipa::openapi::security::{ClientCredentials, Scopes};
877    /// ClientCredentials::new(
878    ///     "https://localhost/token",
879    ///     Scopes::from_iter([
880    ///         ("edit:items", "edit my items"),
881    ///         ("read:items", "read my items")
882    ///     ]),
883    /// );
884    /// ```
885    ///
886    /// Create new client credentials flow without any scopes.
887    /// ```rust
888    /// # use utoipa::openapi::security::{ClientCredentials, Scopes};
889    /// ClientCredentials::new(
890    ///     "https://localhost/token",
891    ///     Scopes::new(),
892    /// );
893    /// ```
894    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
895        Self {
896            token_url: token_url.into(),
897            refresh_url: None,
898            scopes,
899            extensions: Default::default(),
900        }
901    }
902
903    /// Construct a new client credentials oauth flow with additional refresh url.
904    ///
905    /// This is essentially same as [`ClientCredentials::new`] but allows defining third parameter for
906    /// `refresh_url`.
907    ///
908    /// # Examples
909    ///
910    /// Create new client credentials for with refresh url.
911    /// ```rust
912    /// # use utoipa::openapi::security::{ClientCredentials, Scopes};
913    /// ClientCredentials::with_refresh_url(
914    ///     "https://localhost/token",
915    ///     Scopes::from_iter([
916    ///         ("edit:items", "edit my items"),
917    ///         ("read:items", "read my items")
918    ///     ]),
919    ///     "https://localhost/refresh-url"
920    /// );
921    /// ```
922    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
923        Self {
924            token_url: token_url.into(),
925            refresh_url: Some(refresh_url.into()),
926            scopes,
927            extensions: Default::default(),
928        }
929    }
930}
931
932/// [`OAuth2`] flow scopes object defines required permissions for oauth flow.
933///
934/// Scopes must be given to oauth2 flow but depending on need one of few initialization methods
935/// could be used.
936///
937/// * Create empty map of scopes you can use [`Scopes::new`].
938/// * Create map with only one scope you can use [`Scopes::one`].
939/// * Create multiple scopes from iterator with [`Scopes::from_iter`].
940///
941/// # Examples
942///
943/// Create empty map of scopes.
944/// ```rust
945/// # use utoipa::openapi::security::Scopes;
946/// let scopes = Scopes::new();
947/// ```
948///
949/// Create [`Scopes`] holding one scope.
950/// ```rust
951/// # use utoipa::openapi::security::Scopes;
952/// let scopes = Scopes::one("edit:item", "edit pets");
953/// ```
954///
955/// Create map of scopes from iterator.
956/// ```rust
957/// # use utoipa::openapi::security::Scopes;
958/// let scopes = Scopes::from_iter([
959///     ("edit:items", "edit my items"),
960///     ("read:items", "read my items")
961/// ]);
962/// ```
963#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
964#[cfg_attr(feature = "debug", derive(Debug))]
965pub struct Scopes {
966    scopes: BTreeMap<String, String>,
967}
968
969impl Scopes {
970    /// Construct new [`Scopes`] with empty map of scopes. This is useful if oauth flow does not need
971    /// any permission scopes.
972    ///
973    /// # Examples
974    ///
975    /// Create empty map of scopes.
976    /// ```rust
977    /// # use utoipa::openapi::security::Scopes;
978    /// let scopes = Scopes::new();
979    /// ```
980    pub fn new() -> Self {
981        Self {
982            ..Default::default()
983        }
984    }
985
986    /// Construct new [`Scopes`] with holding one scope.
987    ///
988    /// * `scope` Is be the permission required.
989    /// * `description` Short description about the permission.
990    ///
991    /// # Examples
992    ///
993    /// Create map of scopes with one scope item.
994    /// ```rust
995    /// # use utoipa::openapi::security::Scopes;
996    /// let scopes = Scopes::one("edit:item", "edit items");
997    /// ```
998    pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
999        Self {
1000            scopes: BTreeMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
1001        }
1002    }
1003}
1004
1005impl<I> FromIterator<(I, I)> for Scopes
1006where
1007    I: Into<String>,
1008{
1009    fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
1010        Self {
1011            scopes: iter
1012                .into_iter()
1013                .map(|(key, value)| (key.into(), value.into()))
1014                .collect(),
1015        }
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022
1023    macro_rules! test_fn {
1024        ($name:ident: $schema:expr; $expected:literal) => {
1025            #[test]
1026            fn $name() {
1027                let value = serde_json::to_value($schema).unwrap();
1028                let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
1029
1030                assert_eq!(
1031                    value,
1032                    expected_value,
1033                    "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
1034                    stringify!($name),
1035                    value,
1036                    expected_value
1037                );
1038
1039                println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
1040            }
1041        };
1042    }
1043
1044    test_fn! {
1045    security_scheme_correct_http_bearer_json:
1046    SecurityScheme::Http(
1047        HttpBuilder::new().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build()
1048    );
1049    r###"{
1050  "type": "http",
1051  "scheme": "bearer",
1052  "bearerFormat": "JWT"
1053}"###
1054    }
1055
1056    test_fn! {
1057        security_scheme_correct_basic_auth:
1058        SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
1059        r###"{
1060  "type": "http",
1061  "scheme": "basic"
1062}"###
1063    }
1064
1065    test_fn! {
1066        security_scheme_correct_digest_auth:
1067        SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
1068        r###"{
1069  "type": "http",
1070  "scheme": "digest"
1071}"###
1072    }
1073
1074    test_fn! {
1075        security_scheme_correct_hoba_auth:
1076        SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
1077        r###"{
1078  "type": "http",
1079  "scheme": "hoba"
1080}"###
1081    }
1082
1083    test_fn! {
1084        security_scheme_correct_mutual_auth:
1085        SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
1086        r###"{
1087  "type": "http",
1088  "scheme": "mutual"
1089}"###
1090    }
1091
1092    test_fn! {
1093        security_scheme_correct_negotiate_auth:
1094        SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
1095        r###"{
1096  "type": "http",
1097  "scheme": "negotiate"
1098}"###
1099    }
1100
1101    test_fn! {
1102        security_scheme_correct_oauth_auth:
1103        SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
1104        r###"{
1105  "type": "http",
1106  "scheme": "oauth"
1107}"###
1108    }
1109
1110    test_fn! {
1111        security_scheme_correct_scram_sha1_auth:
1112        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
1113        r###"{
1114  "type": "http",
1115  "scheme": "scram-sha-1"
1116}"###
1117    }
1118
1119    test_fn! {
1120        security_scheme_correct_scram_sha256_auth:
1121        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
1122        r###"{
1123  "type": "http",
1124  "scheme": "scram-sha-256"
1125}"###
1126    }
1127
1128    test_fn! {
1129        security_scheme_correct_api_key_cookie_auth:
1130        SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
1131        r###"{
1132  "type": "apiKey",
1133  "name": "api_key",
1134  "in": "cookie"
1135}"###
1136    }
1137
1138    test_fn! {
1139        security_scheme_correct_api_key_header_auth:
1140        SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("api_key")));
1141        r###"{
1142  "type": "apiKey",
1143  "name": "api_key",
1144  "in": "header"
1145}"###
1146    }
1147
1148    test_fn! {
1149        security_scheme_correct_api_key_query_auth:
1150        SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
1151        r###"{
1152  "type": "apiKey",
1153  "name": "api_key",
1154  "in": "query"
1155}"###
1156    }
1157
1158    test_fn! {
1159        security_scheme_correct_open_id_connect_auth:
1160        SecurityScheme::OpenIdConnect(OpenIdConnect::new("https://localhost/openid"));
1161        r###"{
1162  "type": "openIdConnect",
1163  "openIdConnectUrl": "https://localhost/openid"
1164}"###
1165    }
1166
1167    test_fn! {
1168        security_scheme_correct_oauth2_implicit:
1169        SecurityScheme::OAuth2(
1170            OAuth2::with_description([Flow::Implicit(
1171                Implicit::new(
1172                    "https://localhost/auth/dialog",
1173                    Scopes::from_iter([
1174                        ("edit:items", "edit my items"),
1175                        ("read:items", "read my items")
1176                    ]),
1177                ),
1178            )], "my oauth2 flow")
1179        );
1180        r###"{
1181  "type": "oauth2",
1182  "flows": {
1183    "implicit": {
1184      "authorizationUrl": "https://localhost/auth/dialog",
1185      "scopes": {
1186        "edit:items": "edit my items",
1187        "read:items": "read my items"
1188      }
1189    }
1190  },
1191  "description": "my oauth2 flow"
1192}"###
1193    }
1194
1195    test_fn! {
1196        security_scheme_correct_oauth2_password:
1197        SecurityScheme::OAuth2(
1198            OAuth2::with_description([Flow::Password(
1199                Password::with_refresh_url(
1200                    "https://localhost/oauth/token",
1201                    Scopes::from_iter([
1202                        ("edit:items", "edit my items"),
1203                        ("read:items", "read my items")
1204                    ]),
1205                    "https://localhost/refresh/token"
1206                ),
1207            )], "my oauth2 flow")
1208        );
1209        r###"{
1210  "type": "oauth2",
1211  "flows": {
1212    "password": {
1213      "tokenUrl": "https://localhost/oauth/token",
1214      "refreshUrl": "https://localhost/refresh/token",
1215      "scopes": {
1216        "edit:items": "edit my items",
1217        "read:items": "read my items"
1218      }
1219    }
1220  },
1221  "description": "my oauth2 flow"
1222}"###
1223    }
1224
1225    test_fn! {
1226        security_scheme_correct_oauth2_client_credentials:
1227        SecurityScheme::OAuth2(
1228            OAuth2::new([Flow::ClientCredentials(
1229                ClientCredentials::with_refresh_url(
1230                    "https://localhost/oauth/token",
1231                    Scopes::from_iter([
1232                        ("edit:items", "edit my items"),
1233                        ("read:items", "read my items")
1234                    ]),
1235                    "https://localhost/refresh/token"
1236                ),
1237            )])
1238        );
1239        r###"{
1240  "type": "oauth2",
1241  "flows": {
1242    "clientCredentials": {
1243      "tokenUrl": "https://localhost/oauth/token",
1244      "refreshUrl": "https://localhost/refresh/token",
1245      "scopes": {
1246        "edit:items": "edit my items",
1247        "read:items": "read my items"
1248      }
1249    }
1250  }
1251}"###
1252    }
1253
1254    test_fn! {
1255        security_scheme_correct_oauth2_authorization_code:
1256        SecurityScheme::OAuth2(
1257            OAuth2::new([Flow::AuthorizationCode(
1258                AuthorizationCode::with_refresh_url(
1259                    "https://localhost/authorization/token",
1260                    "https://localhost/token/url",
1261                    Scopes::from_iter([
1262                        ("edit:items", "edit my items"),
1263                        ("read:items", "read my items")
1264                    ]),
1265                    "https://localhost/refresh/token"
1266                ),
1267            )])
1268        );
1269        r###"{
1270  "type": "oauth2",
1271  "flows": {
1272    "authorizationCode": {
1273      "authorizationUrl": "https://localhost/authorization/token",
1274      "tokenUrl": "https://localhost/token/url",
1275      "refreshUrl": "https://localhost/refresh/token",
1276      "scopes": {
1277        "edit:items": "edit my items",
1278        "read:items": "read my items"
1279      }
1280    }
1281  }
1282}"###
1283    }
1284
1285    test_fn! {
1286        security_scheme_correct_oauth2_authorization_code_no_scopes:
1287        SecurityScheme::OAuth2(
1288            OAuth2::new([Flow::AuthorizationCode(
1289                AuthorizationCode::with_refresh_url(
1290                    "https://localhost/authorization/token",
1291                    "https://localhost/token/url",
1292                    Scopes::new(),
1293                    "https://localhost/refresh/token"
1294                ),
1295            )])
1296        );
1297        r###"{
1298  "type": "oauth2",
1299  "flows": {
1300    "authorizationCode": {
1301      "authorizationUrl": "https://localhost/authorization/token",
1302      "tokenUrl": "https://localhost/token/url",
1303      "refreshUrl": "https://localhost/refresh/token",
1304      "scopes": {}
1305    }
1306  }
1307}"###
1308    }
1309
1310    test_fn! {
1311        security_scheme_correct_mutual_tls:
1312        SecurityScheme::MutualTls {
1313            description: Some(String::from("authorization is performed with client side certificate")),
1314            extensions: None,
1315        };
1316        r###"{
1317  "type": "mutualTLS",
1318  "description": "authorization is performed with client side certificate"
1319}"###
1320    }
1321}