Repository: juhaku/utoipa
Pull request #35 (2022-03-02 21:19:44)

Add Security Requirement implementation
* Add SecurityRequirement implementation
* Add SecurityRequirement and other security related documentation
* Add unit tests for security requirement macro attributes


Patch:
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index 7923d38..d76f4f5 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -83,6 +83,23 @@ impl OpenApi {
         self
     }
 
+    /// Add list of [`SecurityRequirement`]s that are globally available for all operations.
+    pub fn with_securities<I: IntoIterator<Item = SecurityRequirement>>(
+        mut self,
+        securities: I,
+    ) -> Self {
+        self.security = Some(securities.into_iter().collect());
+
+        self
+    }
+
+    /// Add [`SecurityRequirement`] that is globally available for all operations.
+    pub fn with_security(mut self, security: SecurityRequirement) -> Self {
+        self.security.as_mut().unwrap().push(security);
+
+        self
+    }
+
     #[cfg(feature = "serde_json")]
     pub fn to_json(&self) -> Result<String, serde_json::Error> {
         serde_json::to_string(self)
diff --git a/src/openapi/path.rs b/src/openapi/path.rs
index 2fb3358..40a31b4 100644
--- a/src/openapi/path.rs
+++ b/src/openapi/path.rs
@@ -272,6 +272,7 @@ impl Operation {
         self
     }
 
+    /// Add list of [`SecurityRequirement`]s that are globally available for all operations.
     pub fn with_securities<I: IntoIterator<Item = SecurityRequirement>>(
         mut self,
         securities: I,
@@ -281,6 +282,7 @@ impl Operation {
         self
     }
 
+    /// Add [`SecurityRequirement`] that is globally available for all operations.
     pub fn with_security(mut self, security: SecurityRequirement) -> Self {
         self.security.as_mut().unwrap().push(security);
 
diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs
index 649fedb..fed6b71 100644
--- a/src/openapi/schema.rs
+++ b/src/openapi/schema.rs
@@ -33,6 +33,12 @@ impl Components {
         self
     }
 
+    /// Add [`SecuritySchema`] to [`Components`] while consuming self.
+    ///
+    /// Accepts two arguments where first is the name of the [`SecuritySchema`]. This is later when
+    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the [`SecuritySchema`].
+    ///
+    /// [requirement]: ../security/struct.SecurityRequirement.html
     pub fn with_security_schemas<N: Into<String>, S: Into<SecuritySchema>>(
         mut self,
         name: N,
@@ -44,6 +50,13 @@ impl Components {
         self
     }
 
+    /// Add [`SecuritySchema`] to [`Components`] **without** consuming self. Use this when you want to
+    /// update existing [`Components`] object.
+    ///
+    /// Accepts two arguments where first is the name of the [`SecuritySchema`]. This is later when
+    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the [`SecuritySchema`].
+    ///
+    /// [requirement]: ../security/struct.SecurityRequirement.html
     pub fn add_security_schema<N: Into<String>, S: Into<SecuritySchema>>(
         &mut self,
         name: N,
@@ -53,6 +66,13 @@ impl Components {
             .insert(name.into(), security_schema.into());
     }
 
+    /// Add iterator of [`SecuritySchema`]s to [`Components`] **without** consuming self. Use this
+    /// when you want to update existing [`Components`] object.
+    ///
+    /// Accepts two arguments where first is the name of the [`SecuritySchema`]. This is later when
+    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the [`SecuritySchema`].
+    ///
+    /// [requirement]: ../security/struct.SecurityRequirement.html
     pub fn add_security_schemas_from_iter<
         I: IntoIterator<Item = (N, S)>,
         N: Into<String>,
diff --git a/src/openapi/security.rs b/src/openapi/security.rs
index 1b71c26..fe4cf47 100644
--- a/src/openapi/security.rs
+++ b/src/openapi/security.rs
@@ -1,10 +1,24 @@
 //! OpenAPI schema's security components implementations.
 //!
 //! Refer to [`SecuritySchema`] for usage and more details.
-use std::collections::HashMap;
+use std::{collections::HashMap, iter};
 
 use serde::{Deserialize, Serialize};
 
+/// OpenAPI [security requirment][security] object.
+///
+/// Security requirement holds list of required [`SecuritySchema`] *names* and possible *scopes* required
+/// to execute the operation. They can be defined in [`#[utoipa::path(...)]`][path] or in `#[openapi(...)]`
+/// of [`OpenApi`][openapi].
+///
+/// Applying the security requirement to [`OpenApi`][openapi] will make it globally
+/// available to all operations. When applied to specific [`#[utoipa::path(...)]`][path] will only
+/// make the security requirements available for that operation. Only one of the requirements must be
+/// satisfied.
+///
+/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
+/// [path]: ../../attr.path.html
+/// [openapi]: ../../derive.OpenApi.html
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Debug, Default, Clone)]
 pub struct SecurityRequirement {
@@ -13,14 +27,46 @@ pub struct SecurityRequirement {
 }
 
 impl SecurityRequirement {
-    pub fn new() -> Self {
-        Default::default()
+    /// Construct a new [`SecurityRequirement`]
+    ///
+    /// Accepts name for the security requirement which must match to the name of available [`SecuritySchema`].
+    /// Second parameter is [`IntoIterator`] of [`Into<String>`] scopes needed by the [`SecurityRequirement`].
+    /// Scopes must match to the ones defined in [`SecuritySchema`].
+    ///
+    /// # Examples
+    ///
+    /// Create new security requirement with scopes.
+    /// ```rust
+    /// # use utoipa::openapi::security::SecurityRequirement;
+    /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
+    /// ```
+    ///
+    /// You can also create an empty security requirement with `Default::default()`.
+    /// ```rust
+    /// # use utoipa::openapi::security::SecurityRequirement;
+    /// SecurityRequirement::default();
+    /// ```
+    pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
+        name: N,
+        scopes: S,
+    ) -> Self {
+        Self {
+            value: HashMap::from_iter(iter::once_with(|| {
+                (
+                    Into::<String>::into(name),
+                    scopes
+                        .into_iter()
+                        .map(|scope| Into::<String>::into(scope))
+                        .collect::<Vec<_>>(),
+                )
+            })),
+        }
     }
 }
 
-/// Defines OpenAPI security schema that the path operations can use.
+/// OpenAPI [security schema][security] for path operations.
 ///
-/// See more details at <https://spec.openapis.org/oas/latest.html#security-scheme-object>.
+/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
 ///
 /// # Examples
 ///
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 678ff3d..e07ec71 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -12,7 +12,7 @@ use syn::{
 use crate::{
     component_type::{ComponentFormat, ComponentType},
     doc_comment::CommentAttributes,
-    Deprecated, ValueArray,
+    Array, Deprecated,
 };
 
 use self::attr::{ComponentAttr, Enum, NamedField, UnnamedFieldStruct};
@@ -275,7 +275,8 @@ impl ToTokens for SimpleEnum<'_> {
             .iter()
             .filter(|variant| matches!(variant.fields, Fields::Unit))
             .map(|variant| variant.ident.to_string())
-            .collect::<ValueArray<String>>();
+            .collect::<Array<String>>()
+            .into_referenced_array();
 
         tokens.extend(quote! {
             utoipa::openapi::Property::new(utoipa::openapi::ComponentType::String)
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index b30e083..d7c9543 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -31,6 +31,7 @@ mod doc_comment;
 mod ext;
 mod openapi;
 mod path;
+mod security_requirement;
 
 use crate::path::{Path, PathAttr, PathOperation};
 
@@ -166,6 +167,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
 ///   the performed request.
 /// * **responses** Slice of responses the endpoint is going to possibly return to the caller.
 /// * **params** Slice of params that the endpoint accepts.
+/// * **security** List of [`SecurityRequirement`][security]s local to the path operation.
 ///
 /// # Request Body Attributes
 ///
@@ -247,7 +249,25 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
 /// ("id" = String, path, deprecated, description = "Pet database id"),
 /// ("id", path, deprecated, description = "Pet database id"),
 /// ```
-///  
+///
+/// # Security Requirement Attributes
+///
+/// * **name** Define the name for security requirement. This must match to name of existing
+///   [`SecuritySchema`][security_schema].
+/// * **scopes** Define the list of scopes needed. These must be scopes defined already in
+///   existing [`SecuritySchema`][security_schema].
+///
+/// **Security Requirement supported formats:**
+///
+/// ```text
+/// (),
+/// ("name" = []),
+/// ("name" = ["scope1", "scope2"]),
+/// ```
+///
+/// Leaving empty _`()`_ creates an empty [`SecurityRequirement`][security] this is useful when
+/// security requirement is optional for operation.
+///
 /// # Examples
 ///
 /// Example with all possible arguments.
@@ -273,6 +293,11 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
 ///    ],
 ///    params = [
 ///      ("x-csrf-token" = String, header, deprecated, description = "Current csrf token of user"),
+///    ],
+///    security = [
+///        (),
+///        ("my_auth" = ["read:items", "edit:items"]),
+///        ("token_jwt" = [])
 ///    ]
 /// )]
 /// fn post_pet(pet: Pet) -> Pet {
@@ -315,6 +340,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
 ///
 /// [path]: trait.Path.html
 /// [openapi]: derive.OpenApi.html
+/// [security]: openapi/security/struct.SecurityRequirement.html
+/// [security_schema]: openapi/security/struct.SecuritySchema.html
 /// [primitive]: https://doc.rust-lang.org/std/primitive/index.html
 /// [^json]: **json** feature need to be enabled for `json!(...)` type to work.
 pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
@@ -365,6 +392,8 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
 /// * **components**  List of [`Component`][component]s in OpenAPI schema.
 /// * **modifiers** List of items implemeting [`Modify`][modify] trait for runtime OpenApi modification.
 ///   See the [trait documentation][modify] for more details.
+/// * **security** List of [`SecurityRequirement`][security]s global to all operations.
+///   See more details in [`#[utoipa::path(...)]`][path] [attribute macro security options][path_security].
 ///
 /// OpenApi derive macro will also derive [`Info`][info] for OpenApi specification using Cargo
 /// environment variables.
@@ -406,7 +435,15 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
 /// }
 ///
 /// #[derive(OpenApi)]
-/// #[openapi(handlers = [get_pet, get_status], components = [Pet, Status])]
+/// #[openapi(
+///     handlers = [get_pet, get_status],
+///     components = [Pet, Status],
+///     security = [
+///         (),
+///         ("my_auth" = ["read:items", "edit:items"]),
+///         ("token_jwt" = [])
+///     ]
+/// )]
 /// struct ApiDoc;
 /// ```
 ///
@@ -415,6 +452,8 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
 /// [path]: attr.path.html
 /// [modify]: trait.Modify.html
 /// [info]: openapi/info/struct.Info.html
+/// [security]: openapi/security/struct.SecurityRequirement.html
+/// [path_security]: attr.path.html#security-requirement-attributes
 pub fn openapi(input: TokenStream) -> TokenStream {
     let DeriveInput { attrs, ident, .. } = syn::parse_macro_input!(input);
 
@@ -430,32 +469,54 @@ pub fn openapi(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Tokenizes slice or Vec of tokenizable items as slice reference (`&[...]`) correctly to OpenAPI JSON.
-struct ValueArray<V>(Vec<V>)
+/// Tokenizes slice or Vec of tokenizable items as array either with reference (`&[...]`)
+/// or without correctly to OpenAPI JSON.
+enum Array<T>
+where
+    T: Sized + ToTokens,
+{
+    Owned(Vec<T>),
+    Referenced(Vec<T>),
+}
+
+impl<T> Array<T>
 where
-    V: Sized + ToTokens;
+    T: ToTokens + Sized,
+{
+    fn into_referenced_array(self) -> Self {
+        match self {
+            Array::Owned(values) => Self::Referenced(values),
+            Array::Referenced(_) => self,
+        }
+    }
+}
 
-impl<V> FromIterator<V> for ValueArray<V>
+impl<V> FromIterator<V> for Array<V>
 where
     V: Sized + ToTokens,
 {
     fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
-        Self {
-            0: iter.into_iter().collect::<Vec<_>>(),
-        }
+        Self::Owned(iter.into_iter().collect())
     }
 }
 
-impl<T> ToTokens for ValueArray<T>
+impl<T> ToTokens for Array<T>
 where
     T: Sized + ToTokens,
 {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        tokens.append(Punct::new('&', proc_macro2::Spacing::Joint));
+        let (add_and, values) = match self {
+            Array::Owned(values) => (false, values),
+            Array::Referenced(values) => (true, values),
+        };
+
+        if add_and {
+            tokens.append(Punct::new('&', proc_macro2::Spacing::Joint));
+        }
 
         tokens.append(Group::new(
             proc_macro2::Delimiter::Bracket,
-            self.0
+            values
                 .iter()
                 .fold(Punctuated::new(), |mut punctuated, item| {
                     punctuated.push_value(item);
diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs
index 67fdf5c..a68f773 100644
--- a/utoipa-gen/src/openapi.rs
+++ b/utoipa-gen/src/openapi.rs
@@ -11,7 +11,10 @@ use syn::{
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote, quote_spanned, ToTokens};
 
-use crate::parse_utils;
+use crate::{
+    parse_utils,
+    security_requirement::{self, SecurityRequirementAttr},
+};
 
 mod info;
 
@@ -23,6 +26,7 @@ pub struct OpenApiAttr {
     handlers: Vec<ExprPath>,
     components: Vec<Component>,
     modifiers: Punctuated<Modifier, Comma>,
+    security: Option<Vec<SecurityRequirementAttr>>,
 }
 
 #[cfg_attr(feature = "debug", derive(Debug))]
@@ -92,11 +96,18 @@ impl Parse for OpenApiAttr {
                 "modifiers" => {
                     openapi.modifiers = parse_modifiers(input)?;
                 }
+                "security" => {
+                    openapi.security = Some(
+                        security_requirement::parse_security_requirements(input)?
+                            .into_iter()
+                            .collect::<Vec<_>>(),
+                    )
+                }
                 _ => {
                     return Err(Error::new(
                         ident.span(),
                         format!(
-                            "unexpected attribute: {}, expected: handlers, components, modifiers",
+                            "unexpected attribute: {}, expected: handlers, components, modifiers, security",
                             ident
                         ),
                     ));
@@ -224,12 +235,23 @@ impl ToTokens for OpenApi {
 
         let path_items = impl_paths(&attributes.handlers);
 
+        let securities = if let Some(ref securities) = self.0.security {
+            let securities_tokens =
+                security_requirement::security_requirements_to_tokens(securities);
+            Some(quote! {
+                .with_securities(#securities_tokens)
+            })
+        } else {
+            None
+        };
+
         tokens.extend(quote! {
             impl utoipa::OpenApi for #ident {
                 fn openapi() -> utoipa::openapi::OpenApi {
                     use utoipa::{Component, Path};
                     let mut openapi = utoipa::openapi::OpenApi::new(#info, #path_items)
-                        .with_components(#components);
+                        .with_components(#components)
+                        #securities;
 
                     let _mods: [&dyn utoipa::Modify; #modifiers_len] = [#modifiers];
                     _mods.iter().for_each(|modifier| modifier.modify(&mut openapi));
diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs
index 5a89e2d..e495838 100644
--- a/utoipa-gen/src/path.rs
+++ b/utoipa-gen/src/path.rs
@@ -14,6 +14,7 @@ use syn::{
 use crate::{
     component_type::ComponentType,
     ext::{Argument, ArgumentIn},
+    security_requirement::{self, SecurityRequirementAttr},
 };
 use crate::{parse_utils, Deprecated};
 
@@ -70,6 +71,7 @@ pub struct PathAttr {
     operation_id: Option<String>,
     tag: Option<String>,
     params: Option<Vec<Parameter>>,
+    security: Option<Vec<SecurityRequirementAttr>>,
 }
 
 impl PathAttr {
@@ -192,6 +194,13 @@ impl Parse for PathAttr {
                         "unparseable tag, expected literal string",
                     ));
                 }
+                "security" => {
+                    path_attr.security = Some(
+                        security_requirement::parse_security_requirements(input)?
+                            .into_iter()
+                            .collect::<Vec<_>>(),
+                    )
+                }
                 _ => {
                     // any other case it is expected to be path operation
                     if let Some(path_operation) =
@@ -199,7 +208,7 @@ impl Parse for PathAttr {
                     {
                         path_attr.path_operation = Some(path_operation)
                     } else {
-                        let erro_msg = format!("unexpected identifier: {}, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag", attribute);
+                        let erro_msg = format!("unexpected identifier: {}, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag, security", attribute);
                         return Err(syn::Error::new(ident.span(), erro_msg));
                     }
                 }
@@ -385,6 +394,7 @@ impl ToTokens for Path {
             parameters: self.path_attr.params.as_ref(),
             request_body: self.path_attr.request_body.as_ref(),
             responses: self.path_attr.responses.as_ref(),
+            security: self.path_attr.security.as_ref(),
         };
 
         tokens.extend(quote! {
@@ -419,6 +429,7 @@ struct Operation<'a> {
     parameters: Option<&'a Vec<Parameter>>,
     request_body: Option<&'a RequestBodyAttr>,
     responses: &'a Vec<Response>,
+    security: Option<&'a Vec<SecurityRequirementAttr>>,
 }
 
 impl ToTokens for Operation<'_> {
@@ -435,7 +446,13 @@ impl ToTokens for Operation<'_> {
         tokens.extend(quote! {
             .with_responses(#responses)
         });
-        //         // .with_security()
+        if let Some(security_requirements) = self.security {
+            let security =
+                security_requirement::security_requirements_to_tokens(security_requirements);
+            tokens.extend(quote! {
+                .with_securities(#security)
+            })
+        }
         let operation_id = self.operation_id;
         tokens.extend(quote! {
             .with_operation_id(
diff --git a/utoipa-gen/src/security_requirement.rs b/utoipa-gen/src/security_requirement.rs
new file mode 100644
index 0000000..1c28ac8
--- /dev/null
+++ b/utoipa-gen/src/security_requirement.rs
@@ -0,0 +1,80 @@
+use proc_macro2::{Group, TokenStream};
+use proc_macro_error::ResultExt;
+use quote::{quote, ToTokens};
+use syn::{
+    bracketed,
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    token::Comma,
+    LitStr, Token,
+};
+
+use crate::{parse_utils, Array};
+
+#[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub struct SecurityRequirementAttr {
+    name: Option<String>,
+    scopes: Option<Vec<String>>,
+}
+
+impl Parse for SecurityRequirementAttr {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        if input.is_empty() {
+            return Ok(Self {
+                ..Default::default()
+            });
+        }
+        let name = input.parse::<LitStr>()?.value();
+        input.parse::<Token![=]>()?;
+
+        let scopes_stream;
+        bracketed!(scopes_stream in input);
+        let scopes = Punctuated::<LitStr, Comma>::parse_terminated(&scopes_stream)?
+            .iter()
+            .map(LitStr::value)
+            .collect::<Vec<_>>();
+
+        Ok(Self {
+            name: Some(name),
+            scopes: Some(scopes),
+        })
+    }
+}
+
+pub fn parse_security_requirements(
+    stream: ParseStream,
+) -> syn::Result<Vec<SecurityRequirementAttr>> {
+    parse_utils::parse_next(stream, || {
+        let content;
+        bracketed!(content in stream);
+
+        let groups = Punctuated::<Group, Comma>::parse_terminated(&content)?;
+
+        Ok(groups
+            .into_iter()
+            .map(|parameter_group| {
+                syn::parse2::<SecurityRequirementAttr>(parameter_group.stream()).unwrap_or_abort()
+            })
+            .collect::<Vec<_>>())
+    })
+}
+
+pub fn security_requirements_to_tokens(
+    security_requirements: &[SecurityRequirementAttr],
+) -> TokenStream {
+    security_requirements.iter().map(| security | {
+        if let (Some(name), Some(scopes)) = (&security.name, &security.scopes) {
+            let scopes_array = scopes.iter().collect::<Array<&String>>();
+            let scopes_len = scopes.len();
+            
+            quote! {
+                utoipa::openapi::security::SecurityRequirement::new::<&str, [&str; #scopes_len], &str>(#name, #scopes_array)
+            }
+        } else {
+            quote!{
+                utoipa::openapi::security::SecurityRequirement::default()
+            }
+        }
+    }).collect::<Array<TokenStream>>().into_token_stream()
+}


Test patch:
diff --git a/tests/openapi_derive.rs b/tests/openapi_derive.rs
new file mode 100644
index 0000000..6073186
--- /dev/null
+++ b/tests/openapi_derive.rs
@@ -0,0 +1,25 @@
+#![cfg(feature = "json")]
+
+use utoipa::OpenApi;
+
+mod common;
+
+#[test]
+fn derive_openapi_with_security_requirement() {
+    #[derive(Default, OpenApi)]
+    #[openapi(security = [
+            (),
+            ("my_auth" = ["read:items", "edit:items"]),
+            ("token_jwt" = [])
+        ])]
+    struct ApiDoc;
+
+    let doc_value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
+
+    assert_value! {doc_value=>
+        "security.[0]" = "{}", "Optional security requirement"
+        "security.[1].my_auth.[0]" = r###""read:items""###, "api_oauth first scope"
+        "security.[1].my_auth.[1]" = r###""edit:items""###, "api_oauth second scope"
+        "security.[2].token_jwt" = "[]", "jwt_token auth scopes"
+    }
+}
diff --git a/tests/path_derive.rs b/tests/path_derive.rs
index bf4ad1f..50076ee 100644
--- a/tests/path_derive.rs
+++ b/tests/path_derive.rs
@@ -198,3 +198,35 @@ fn derive_path_with_extra_attributes_without_nested_module() {
         "parameters.[1].schema.type" = r#""string""#, "Parameter 1 schema type"
     }
 }
+
+#[test]
+fn derive_path_with_security_requirements() {
+    #[utoipa::path(
+        get,
+        path = "/items",
+        responses = [
+            (status = 200, description = "success response")
+        ],
+        security = [
+            (),
+            ("api_oauth" = ["read:items", "edit:items"]),
+            ("jwt_token" = [])
+        ]
+    )]
+    #[allow(unused)]
+    fn get_items() -> String {
+        "".to_string()
+    }
+    let operation = test_api_fn_doc! {
+        get_items,
+        operation: get,
+        path: "/items"
+    };
+
+    assert_value! {operation=>
+        "security.[0]" = "{}", "Optional security requirement"
+        "security.[1].api_oauth.[0]" = r###""read:items""###, "api_oauth first scope"
+        "security.[1].api_oauth.[1]" = r###""edit:items""###, "api_oauth second scope"
+        "security.[2].jwt_token" = "[]", "jwt_token auth scopes"
+    }
+}
diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs
index 36dbdec..465ff30 100644
--- a/tests/utoipa_gen_test.rs
+++ b/tests/utoipa_gen_test.rs
@@ -59,6 +59,11 @@ mod pet_api {
         params = [
             ("id" = u64, path, description = "Pet database id to get Pet for"),
         ]
+        security = [
+            (),
+            ("my_auth" = ["read:items", "edit:items"]),
+            ("token_jwt" = [])
+        ]
     )]
     #[allow(unused)]
     async fn get_pet_by_id(pet_id: u64) -> Pet {
@@ -71,7 +76,11 @@ mod pet_api {
 }
 
 #[derive(Default, OpenApi)]
-#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet], modifiers = [&Foo])]
+#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet], modifiers = [&Foo], security = [
+            (),
+            ("my_auth" = ["read:items", "edit:items"]),
+            ("token_jwt" = [])
+        ])]
 struct ApiDoc;
 
 #[test]