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}