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}