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