From 516e09cce885f122fcc75fdfe3d516efc58ca281 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Wed, 23 Feb 2022 00:41:12 +0200 Subject: [PATCH 1/3] Wip add security schema --- src/openapi/schema.rs | 14 +- src/openapi/security.rs | 343 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+), 1 deletion(-) diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs index 4d7b0bf8..bde511e5 100644 --- a/src/openapi/schema.rs +++ b/src/openapi/schema.rs @@ -4,13 +4,15 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "serde_json")] use serde_json::Value; -use super::Deprecated; +use super::{security::SecuritySchema, Deprecated}; #[non_exhaustive] #[derive(Serialize, Deserialize, Default, Clone)] #[serde(rename_all = "camelCase")] pub struct Schema { schemas: HashMap, + + security_schemas: HashMap, } impl Schema { @@ -30,6 +32,16 @@ impl Schema { self } + + pub fn with_security_schemas>( + mut self, + name: I, + security_schema: SecuritySchema, + ) -> Self { + self.security_schemas.insert(name.into(), security_schema); + + self + } } #[non_exhaustive] diff --git a/src/openapi/security.rs b/src/openapi/security.rs index 3006ae68..eac37407 100644 --- a/src/openapi/security.rs +++ b/src/openapi/security.rs @@ -14,3 +14,346 @@ impl Security { Default::default() } } + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[serde(rename_all = "camelCase")] +pub struct SecuritySchema { + #[serde(rename = "type")] + schema_type: String, + + #[serde(skip_serializing_if = "String::is_empty")] + description: String, + + #[serde(flatten)] + security_type: SecurityType, +} + +impl SecuritySchema { + pub fn new(security_type: SecurityType) -> Self { + Self { + schema_type: String::from(SecuritySchema::resolve_type(&security_type)), + description: String::new(), + security_type, + } + } + + fn resolve_type(security_type: &SecurityType) -> &str { + match security_type { + SecurityType::OAuth2 { .. } => "oauth2", + SecurityType::ApiKey { .. } => "apiKey", + SecurityType::Http { .. } => "http", + SecurityType::OpenIdConnect { .. } => "openIdConnect", + } + } + + pub fn with_description>(mut self, description: S) -> Self { + self.description = description.into(); + + self + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum SecurityType { + #[serde(rename_all = "camelCase")] + OAuth2 { + #[serde(flatten)] + flows: Flows, + }, + #[serde(rename_all = "camelCase")] + ApiKey { + name: String, + #[serde(rename = "in")] + api_key_in: ApiKeyIn, + }, + #[serde(rename_all = "camelCase")] + Http { + schema: HttpAutheticationType, + #[serde(skip_serializing_if = "Option::is_none")] + bearer_format: Option, + }, + #[serde(rename_all = "camelCase")] + OpenIdConnect { open_id_connect_url: String }, +} + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Flows { + flows: HashMap, +} + +impl Flows { + pub fn new>(flows: I) -> Self { + Self { + flows: HashMap::from_iter( + flows + .into_iter() + .map(|auth| (String::from(auth.get_type_as_str()), auth)), + ), + } + } +} +/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1) which are maintained in +/// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[serde(rename_all = "lowercase")] +pub enum HttpAutheticationType { + Basic, + Bearer, + Digest, + Hoba, + Mutual, + Negotiate, + OAuth, + #[serde(rename = "scram-sha-1")] + ScramSha1, + #[serde(rename = "scram-sha-256")] + ScramSha256, + Vapid, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum ApiKeyIn { + Header, + Query, + Cookie, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum Flow { + Implicit(OAuth2Flow), + Password(OAuth2Flow), + ClientCredentials(OAuth2Flow), + AuthorizationCode(OAuth2Flow), +} + +impl Flow { + fn get_type_as_str(&self) -> &str { + match self { + Self::Implicit(_) => "implicit", + Self::Password(_) => "password", + Self::ClientCredentials(_) => "clientCredentials", + Self::AuthorizationCode(_) => "authorizationCode", + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct OAuth2Flow { + #[serde(skip_serializing_if = "Option::is_none")] + authorization_url: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + token_url: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + refresh_url: Option, + + scopes: HashMap, +} + +impl OAuth2Flow { + pub fn new, T: Into, R: Into>( + authorization_url: Option, + token_url: Option, + refresh_url: Option, + scopes: HashMap, + ) -> Self { + Self { + authorization_url: authorization_url.map(Into::into), + token_url: token_url.map(Into::into), + refresh_url: refresh_url.map(Into::into), + scopes, + } + } +} + +#[cfg(test)] +#[cfg(feature = "json")] +mod tests { + use super::*; + + macro_rules! test_fn { + ($name:ident: $schema:expr; $expected:literal) => { + #[test] + fn $name() { + let s = $schema; + let actual = serde_json::to_string_pretty(&s).unwrap(); + assert_eq!( + actual, + $expected, + "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}", + stringify!($name), + actual, + $expected + ); + + println!("{}", &actual); + } + }; + } + + test_fn! { + security_schema_correct_http_bearer_json: + SecuritySchema::new(SecurityType::Http { + bearer_format: Some("JWT".to_string()), + schema: HttpAutheticationType::Bearer, + }); + r###"{ + "type": "http", + "schema": "bearer", + "bearerFormat": "JWT" +}"### + } + + test_fn! { + security_schema_correct_basic_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Basic, bearer_format: None}); + r###"{ + "type": "http", + "schema": "basic" +}"### + } + + test_fn! { + security_schema_correct_digest_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Digest, bearer_format: None}); + r###"{ + "type": "http", + "schema": "digets" +}"### + } + + test_fn! { + security_schema_correct_hoba_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Hoba, bearer_format: None}); + r###"{ + "type": "http", + "schema": "hoba" +}"### + } + + test_fn! { + security_schema_correct_mutual_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Mutual, bearer_format: None}); + r###"{ + "type": "http", + "schema": "mutual" +}"### + } + + test_fn! { + security_schema_correct_negotiate_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Negotiate, bearer_format: None}); + r###"{ + "type": "http", + "schema": "negotiate" +}"### + } + + test_fn! { + security_schema_correct_oauth_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::OAuth, bearer_format: None}); + r###"{ + "type": "http", + "schema": "oauth" +}"### + } + + test_fn! { + security_schema_correct_scram_sha1_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::ScramSha1, bearer_format: None}); + r###"{ + "type": "http", + "schema": "scram-sha-1" +}"### + } + + test_fn! { + security_schema_correct_scram_sha256_auth: + SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::ScramSha256, bearer_format: None}); + r###"{ + "type": "http", + "schema": "scram-sha-256" +}"### + } + + test_fn! { + security_schema_correct_api_key_cookie_auth: + SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Cookie , name: String::from("api_key")}); + r###"{ + "type": "apiKey", + "name": "api_key", + "in": "cookie" +}"### + } + + test_fn! { + security_schema_correct_api_key_header_auth: + SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Header , name: String::from("api_key")}); + r###"{ + "type": "apiKey", + "name": "api_key", + "in": "header" +}"### + } + + test_fn! { + security_schema_correct_api_key_query_auth: + SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Query , name: String::from("api_key")}); + r###"{ + "type": "apiKey", + "name": "api_key", + "in": "query" +}"### + } + + test_fn! { + security_schema_correct_open_id_connect_auth: + SecuritySchema::new(SecurityType::OpenIdConnect{open_id_connect_url: String::from("http://localhost/openid")}); + r###"{ + "type": "openIdConnect", + "openIdConnectUrl": "http://localhost/openid" +}"### + } + + test_fn! { + security_schema_correct_oauth2_implicit: + SecuritySchema::new(SecurityType::OAuth2 { + flows: Flows::new([Flow::Implicit( + OAuth2Flow::new( + Some("http://localhost/auth/dialog"), + None::<&str>, + None::<&str>, + HashMap::from([ + ("edit:items".to_string(), "edit my items".to_string()), + ("read:items".to_string(), "read my items".to_string() + )]), + ), + )]) + }); + r###"{ + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://localhost/auth/dialog", + "scopes": { + "edit:items": "edit my items", + "read:items": "read my items" + } + } + } +}"### + } +} From 5e2ff9f575b98fbdc52a806a6a479f2927b8b6ec Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Fri, 25 Feb 2022 00:57:05 +0200 Subject: [PATCH 2/3] Wip security schema & tests --- src/openapi/security.rs | 183 +++++++++++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 51 deletions(-) diff --git a/src/openapi/security.rs b/src/openapi/security.rs index eac37407..104c3fe9 100644 --- a/src/openapi/security.rs +++ b/src/openapi/security.rs @@ -1,5 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, hash::Hash}; +use actix_web::client::Client; use serde::{Deserialize, Serialize}; #[non_exhaustive] @@ -80,23 +81,6 @@ pub enum SecurityType { OpenIdConnect { open_id_connect_url: String }, } -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Flows { - flows: HashMap, -} - -impl Flows { - pub fn new>(flows: I) -> Self { - Self { - flows: HashMap::from_iter( - flows - .into_iter() - .map(|auth| (String::from(auth.get_type_as_str()), auth)), - ), - } - } -} /// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1) which are maintained in /// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml #[derive(Serialize, Deserialize, Clone)] @@ -126,14 +110,32 @@ pub enum ApiKeyIn { Cookie, } +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Flows { + flows: HashMap, +} + +impl Flows { + pub fn new>(flows: I) -> Self { + Self { + flows: HashMap::from_iter( + flows + .into_iter() + .map(|auth| (String::from(auth.get_type_as_str()), auth)), + ), + } + } +} + #[derive(Serialize, Deserialize, Clone)] #[serde(untagged)] #[cfg_attr(feature = "debug", derive(Debug))] pub enum Flow { - Implicit(OAuth2Flow), - Password(OAuth2Flow), - ClientCredentials(OAuth2Flow), - AuthorizationCode(OAuth2Flow), + Implicit(Implicit), + Password(Password), + ClientCredentials(ClientCredentials), + AuthorizationCode(AuthorizationCode), } impl Flow { @@ -150,12 +152,37 @@ impl Flow { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct OAuth2Flow { - #[serde(skip_serializing_if = "Option::is_none")] - authorization_url: Option, +pub struct Implicit { + authorization_url: String, #[serde(skip_serializing_if = "Option::is_none")] - token_url: Option, + refresh_url: Option, + + scopes: HashMap, +} + +impl Implicit { + pub fn new>(authorization_url: S, scopes: HashMap) -> Self { + Self { + authorization_url: authorization_url.into(), + refresh_url: None, + scopes, + } + } + + pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { + self.refresh_url = Some(refresh_url.into()); + + self + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct AuthorizationCode { + authorization_url: String, + token_url: String, #[serde(skip_serializing_if = "Option::is_none")] refresh_url: Option, @@ -163,20 +190,75 @@ pub struct OAuth2Flow { scopes: HashMap, } -impl OAuth2Flow { +impl AuthorizationCode { pub fn new, T: Into, R: Into>( - authorization_url: Option, - token_url: Option, - refresh_url: Option, + authorization_url: A, + token_url: T, scopes: HashMap, ) -> Self { Self { - authorization_url: authorization_url.map(Into::into), - token_url: token_url.map(Into::into), - refresh_url: refresh_url.map(Into::into), + authorization_url: authorization_url.into(), + token_url: token_url.into(), + refresh_url: None, + scopes, + } + } + + pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { + self.refresh_url = Some(refresh_url.into()); + + self + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Password { + token_url: String, + refresh_url: Option, + scopes: HashMap, +} + +impl Password { + pub fn new>(token_url: S, scopes: HashMap) -> Self { + Self { + token_url: token_url.into(), + refresh_url: None, scopes, } } + + pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { + self.refresh_url = Some(refresh_url.into()); + + self + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct ClientCredentials { + token_url: String, + refresh_url: Option, + scopes: HashMap, +} + +impl ClientCredentials { + pub fn new>(token_url: S, scopes: HashMap) -> Self { + Self { + token_url: token_url.into(), + refresh_url: None, + scopes, + } + } + + pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { + self.refresh_url = Some(refresh_url.into()); + + self + } } #[cfg(test)] @@ -188,18 +270,19 @@ mod tests { ($name:ident: $schema:expr; $expected:literal) => { #[test] fn $name() { - let s = $schema; - let actual = serde_json::to_string_pretty(&s).unwrap(); + let value = serde_json::to_value($schema).unwrap(); + let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap(); + assert_eq!( - actual, - $expected, + value, + expected_value, "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}", stringify!($name), - actual, - $expected + value, + expected_value ); - println!("{}", &actual); + println!("{}", &serde_json::to_string_pretty(&$schema).unwrap()); } }; } @@ -231,7 +314,7 @@ mod tests { SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Digest, bearer_format: None}); r###"{ "type": "http", - "schema": "digets" + "schema": "digest" }"### } @@ -332,16 +415,14 @@ mod tests { security_schema_correct_oauth2_implicit: SecuritySchema::new(SecurityType::OAuth2 { flows: Flows::new([Flow::Implicit( - OAuth2Flow::new( - Some("http://localhost/auth/dialog"), - None::<&str>, - None::<&str>, - HashMap::from([ - ("edit:items".to_string(), "edit my items".to_string()), - ("read:items".to_string(), "read my items".to_string() - )]), - ), - )]) + Implicit::new( + "http://localhost/auth/dialog", + HashMap::from([ + ("edit:items".to_string(), "edit my items".to_string()), + ("read:items".to_string(), "read my items".to_string() + )]), + ), + )]) }); r###"{ "type": "oauth2", From 5e6367200dd885aa36dcb1e08e135b5df56d479d Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Sun, 27 Feb 2022 02:04:30 +0200 Subject: [PATCH 3/3] Wip security schema * Wip security schema types & documentation & tests --- src/openapi/mod.rs | 4 +- src/openapi/path.rs | 11 +- src/openapi/schema.rs | 9 +- src/openapi/security.rs | 619 ++++++++++++++++++++++++++++++++++------ 4 files changed, 543 insertions(+), 100 deletions(-) diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index d0d7f6fa..5855b5b4 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -15,7 +15,7 @@ pub use self::{ Array, Component, ComponentFormat, ComponentType, Object, OneOf, Property, Ref, Schema, ToArray, }, - security::Security, + security::SecurityRequirement, server::Server, tag::Tag, }; @@ -53,7 +53,7 @@ pub struct OpenApi { pub components: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub security: Option>, + pub security: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option>, diff --git a/src/openapi/path.rs b/src/openapi/path.rs index 11d7e774..2fb33588 100644 --- a/src/openapi/path.rs +++ b/src/openapi/path.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::{ request_body::RequestBody, response::{Response, Responses}, - Component, Deprecated, ExternalDocs, Required, Security, Server, + Component, Deprecated, ExternalDocs, Required, SecurityRequirement, Server, }; #[non_exhaustive] @@ -184,7 +184,7 @@ pub struct Operation { pub deprecated: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub security: Option>, + pub security: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub servers: Option>, @@ -272,13 +272,16 @@ impl Operation { self } - pub fn with_securities>(mut self, securities: I) -> Self { + pub fn with_securities>( + mut self, + securities: I, + ) -> Self { self.security = Some(securities.into_iter().collect()); self } - pub fn with_security(mut self, security: Security) -> Self { + pub fn with_security(mut self, security: SecurityRequirement) -> Self { self.security.as_mut().unwrap().push(security); self diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs index bde511e5..4232d584 100644 --- a/src/openapi/schema.rs +++ b/src/openapi/schema.rs @@ -33,12 +33,13 @@ impl Schema { self } - pub fn with_security_schemas>( + pub fn with_security_schemas, S: Into>( mut self, - name: I, - security_schema: SecuritySchema, + name: N, + security_schema: S, ) -> Self { - self.security_schemas.insert(name.into(), security_schema); + self.security_schemas + .insert(name.into(), security_schema.into()); self } diff --git a/src/openapi/security.rs b/src/openapi/security.rs index 104c3fe9..1b71c26a 100644 --- a/src/openapi/security.rs +++ b/src/openapi/security.rs @@ -1,92 +1,198 @@ -use std::{collections::HashMap, hash::Hash}; +//! OpenAPI schema's security components implementations. +//! +//! Refer to [`SecuritySchema`] for usage and more details. +use std::collections::HashMap; -use actix_web::client::Client; use serde::{Deserialize, Serialize}; #[non_exhaustive] #[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct Security { +pub struct SecurityRequirement { #[serde(flatten)] - pub value: HashMap>, + value: HashMap>, } -impl Security { +impl SecurityRequirement { pub fn new() -> Self { Default::default() } } +/// Defines OpenAPI security schema that the path operations can use. +/// +/// See more details at . +/// +/// # Examples +/// +/// Create implicit oauth2 flow security schema for path operations. +/// ```rust +/// # use utoipa::openapi::security::{SecuritySchema, Oauth2, Implicit, Flow}; +/// # use std::collections::HashMap; +/// SecuritySchema::Oauth2( +/// Oauth2::new([Flow::Implicit( +/// Implicit::new( +/// "http://localhost/auth/dialog", +/// HashMap::from([ +/// ("edit:items".to_string(), "edit my items".to_string()), +/// ("read:items".to_string(), "read my items".to_string() +/// )]), +/// ), +/// )]).with_description("my oauth2 flow") +/// ); +/// ``` +/// +/// Create JWT header authetication. +/// ```rust +/// # use utoipa::openapi::security::{SecuritySchema, HttpAuthenticationType, Http}; +/// SecuritySchema::Http( +/// Http::new(HttpAuthenticationType::Bearer).with_bearer_format("JWT") +/// ); +/// ``` +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum SecuritySchema { + /// Oauth flow authentication. + Oauth2(Oauth2), + /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*. + ApiKey(ApiKey), + /// Http authentication such as *`bearer`* or *`basic`*. + Http(Http), + /// Open id connect url to discover OAuth2 configuraiton values. + OpenIdConnect(OpenIdConnect), + /// Authentication is done via client side cerfiticate. + /// + /// OpenApi 3.1 type + #[serde(rename = "mutualTLS")] + MutualTls { + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, +} + +/// Api key authentication [`SecuritySchema`]. #[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(feature = "debug", derive(Debug))] -#[serde(rename_all = "camelCase")] -pub struct SecuritySchema { - #[serde(rename = "type")] - schema_type: String, +pub struct ApiKey { + name: String, - #[serde(skip_serializing_if = "String::is_empty")] - description: String, + #[serde(rename = "in")] + api_key_in: ApiKeyIn, - #[serde(flatten)] - security_type: SecurityType, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, } -impl SecuritySchema { - pub fn new(security_type: SecurityType) -> Self { +impl ApiKey { + /// Constructs new api key authentication schema. + /// + /// Accepts two arguments: one which is the name of parameter and second which defines where + /// the parameter is defined in. + /// + /// # Examples + /// + /// Create new api key security schema with parameter name `api_key` which must be present with value in + /// header. + /// ```rust + /// # use utoipa::openapi::security::{ApiKey, ApiKeyIn}; + /// let api_key = ApiKey::new("api_key", ApiKeyIn::Header); + /// ``` + pub fn new>(name: S, api_key_in: ApiKeyIn) -> Self { Self { - schema_type: String::from(SecuritySchema::resolve_type(&security_type)), - description: String::new(), - security_type, - } - } - - fn resolve_type(security_type: &SecurityType) -> &str { - match security_type { - SecurityType::OAuth2 { .. } => "oauth2", - SecurityType::ApiKey { .. } => "apiKey", - SecurityType::Http { .. } => "http", - SecurityType::OpenIdConnect { .. } => "openIdConnect", + name: name.into(), + api_key_in, + description: None, } } + /// Optional description supporting markdown syntax. pub fn with_description>(mut self, description: S) -> Self { - self.description = description.into(); + self.description = Some(description.into()); self } } +/// Define the location where api key must be provided. #[derive(Serialize, Deserialize, Clone)] -#[serde(untagged)] +#[serde(rename_all = "lowercase")] #[cfg_attr(feature = "debug", derive(Debug))] -pub enum SecurityType { - #[serde(rename_all = "camelCase")] - OAuth2 { - #[serde(flatten)] - flows: Flows, - }, - #[serde(rename_all = "camelCase")] - ApiKey { - name: String, - #[serde(rename = "in")] - api_key_in: ApiKeyIn, - }, - #[serde(rename_all = "camelCase")] - Http { - schema: HttpAutheticationType, - #[serde(skip_serializing_if = "Option::is_none")] - bearer_format: Option, - }, - #[serde(rename_all = "camelCase")] - OpenIdConnect { open_id_connect_url: String }, +pub enum ApiKeyIn { + Header, + Query, + Cookie, } -/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1) which are maintained in -/// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml +/// Http authentication [`SecuritySchema`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Http { + scheme: HttpAuthenticationType, + + #[serde(skip_serializing_if = "Option::is_none")] + bearer_format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, +} + +impl Http { + /// Create new http authentication security schema. + /// + /// Accepts one argument which defines the scheme of the http authentication. + /// + /// # Examples + /// + /// Create http securith schema with basic authentication. + /// ```rust + /// # use utoipa::openapi::security::{SecuritySchema, Http, HttpAuthenticationType}; + /// SecuritySchema::Http(Http::new(HttpAuthenticationType::Basic)); + /// ``` + pub fn new(scheme: HttpAuthenticationType) -> Self { + Self { + scheme, + bearer_format: None, + description: None, + } + } + + /// Add informative bearer format for http security schema. + /// + /// This is no-op in any other [`HttpAuthenticationType`] than [`HttpAuthenticationType::Bearer`]. + /// + /// # Examples + /// + /// Add JTW bearer format for security schema. + /// ```rust + /// # use utoipa::openapi::security::{Http, HttpAuthenticationType}; + /// Http::new(HttpAuthenticationType::Bearer).with_bearer_format("JWT"); + /// ``` + pub fn with_bearer_format>(mut self, bearer_format: S) -> Self { + if self.scheme == HttpAuthenticationType::Bearer { + self.bearer_format = Some(bearer_format.into()); + } + + self + } + + /// Optional description supporting markdown syntax. + pub fn with_description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + + self + } +} + +/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1). +/// +/// Types are maintainted at . +#[derive(Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "debug", derive(Debug))] #[serde(rename_all = "lowercase")] -pub enum HttpAutheticationType { +pub enum HttpAuthenticationType { Basic, Bearer, Digest, @@ -101,40 +207,119 @@ pub enum HttpAutheticationType { Vapid, } +/// Open id connect [`SecuritySchema`] +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] -pub enum ApiKeyIn { - Header, - Query, - Cookie, +pub struct OpenIdConnect { + open_id_connect_url: String, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, } +impl OpenIdConnect { + /// Construct a new open id connect security schema. + /// + /// # Examples + /// + /// ```rust + /// # use utoipa::openapi::security::OpenIdConnect; + /// OpenIdConnect::new("http://localhost/openid"); + /// ``` + pub fn new>(open_id_connect_url: S) -> Self { + Self { + open_id_connect_url: open_id_connect_url.into(), + description: None, + } + } + + /// Optional description supporting markdown syntax. + pub fn with_description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + + self + } +} + +/// OAuth2 [`Flow`] configuration for [`SecuritySchema`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct Flows { +pub struct Oauth2 { flows: HashMap, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, } -impl Flows { +impl Oauth2 { + /// Construct a new Oauth2 security schema configuration object. + /// + /// Oauth flow accepts slice of [`Flow`] configuration objects and can be optionally provided with description. + /// + /// # Examples + /// + /// Create new OAuth2 flow with multiple authentication flows. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::{Oauth2, Flow, Password, AuthorizationCode}; + /// Oauth2::new([Flow::Password( + /// Password::new( + /// "http://localhost/oauth/token", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )]), + /// ).with_refresh_url("http://localhost/refresh/token")), + /// Flow::AuthorizationCode( + /// AuthorizationCode::new( + /// "http://localhost/authorization/token", + /// "http://localhost/token/url", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )])), + /// ), + /// ]).with_description("my oauth2 flow"); + /// ``` pub fn new>(flows: I) -> Self { Self { flows: HashMap::from_iter( flows .into_iter() - .map(|auth| (String::from(auth.get_type_as_str()), auth)), + .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)), ), + description: None, } } + + /// Optional description supporting markdown syntax. + pub fn with_description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + + self + } } +/// [`Oauth2`] flow configuration object. +/// +/// +/// See more details at . #[derive(Serialize, Deserialize, Clone)] #[serde(untagged)] #[cfg_attr(feature = "debug", derive(Debug))] pub enum Flow { + /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details. + /// + /// Soon to be deprecated by . Implicit(Implicit), + /// Define password [`Flow`] type. See [`Password::new`] for usage details. Password(Password), + /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details. ClientCredentials(ClientCredentials), + /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details. AuthorizationCode(AuthorizationCode), } @@ -149,6 +334,8 @@ impl Flow { } } +/// Implicit [`Flow`] configuration for [`Oauth2`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] @@ -162,6 +349,35 @@ pub struct Implicit { } impl Implicit { + /// Construct a new implicit oauth2 flow. + /// + /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can + /// also be an empty map. + /// + /// # Examples + /// + /// Create new implicit flow with scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::Implicit; + /// Implicit::new( + /// "http://localhost/auth/dialog", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )]), + /// ); + /// ``` + /// + /// Create new implicit flow without any scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::Implicit; + /// Implicit::new( + /// "http://localhost/auth/dialog", + /// HashMap::new(), + /// ); + /// ``` pub fn new>(authorization_url: S, scopes: HashMap) -> Self { Self { authorization_url: authorization_url.into(), @@ -170,6 +386,7 @@ impl Implicit { } } + /// Add refresh url for getting refresh tokens. pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { self.refresh_url = Some(refresh_url.into()); @@ -177,6 +394,8 @@ impl Implicit { } } +/// Authorization code [`Flow`] configuration for [`Oauth2`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] @@ -191,7 +410,38 @@ pub struct AuthorizationCode { } impl AuthorizationCode { - pub fn new, T: Into, R: Into>( + /// Construct a new authorization code oauth flow. + /// + /// Accpets three arguments: one which is authorization url, two a token url and + /// three a map of scopes for oauth flow. + /// + /// # Examples + /// + /// Create new authorization code flow with scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::AuthorizationCode; + /// AuthorizationCode::new( + /// "http://localhost/auth/dialog", + /// "http://localhost/token", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )]), + /// ); + /// ``` + /// + /// Create new authorization code flow without any scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::AuthorizationCode; + /// AuthorizationCode::new( + /// "http://localhost/auth/dialog", + /// "http://localhost/token", + /// HashMap::new(), + /// ); + /// ``` + pub fn new, T: Into>( authorization_url: A, token_url: T, scopes: HashMap, @@ -204,6 +454,7 @@ impl AuthorizationCode { } } + /// Add refresh url for getting refresh tokens. pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { self.refresh_url = Some(refresh_url.into()); @@ -211,6 +462,8 @@ impl AuthorizationCode { } } +/// Password [`Flow`] configuration for [`Oauth2`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] @@ -221,6 +474,35 @@ pub struct Password { } impl Password { + /// Construct a new password oauth flow. + /// + /// Accpets two arguments: one which is a token url and + /// two a map of scopes for oauth flow. + /// + /// # Examples + /// + /// Create new password flow with scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::Password; + /// Password::new( + /// "http://localhost/token", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )]), + /// ); + /// ``` + /// + /// Create new password flow without any scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::Password; + /// Password::new( + /// "http://localhost/token", + /// HashMap::new(), + /// ); + /// ``` pub fn new>(token_url: S, scopes: HashMap) -> Self { Self { token_url: token_url.into(), @@ -229,6 +511,7 @@ impl Password { } } + /// Add refresh url for getting refresh tokens. pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { self.refresh_url = Some(refresh_url.into()); @@ -236,6 +519,8 @@ impl Password { } } +/// Client credentials [`Flow`] configuration for [`Oauth2`]. +#[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "debug", derive(Debug))] @@ -246,6 +531,35 @@ pub struct ClientCredentials { } impl ClientCredentials { + /// Construct a new client crendentials oauth flow. + /// + /// Accpets two arguments: one which is a token url and + /// two a map of scopes for oauth flow. + /// + /// # Examples + /// + /// Create new client credentials flow with scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::ClientCredentials; + /// ClientCredentials::new( + /// "http://localhost/token", + /// HashMap::from([ + /// ("edit:items".to_string(), "edit my items".to_string()), + /// ("read:items".to_string(), "read my items".to_string() + /// )]), + /// ); + /// ``` + /// + /// Create new client credentials flow without any scopes. + /// ```rust + /// # use std::collections::HashMap; + /// # use utoipa::openapi::security::ClientCredentials; + /// ClientCredentials::new( + /// "http://localhost/token", + /// HashMap::new(), + /// ); + /// ``` pub fn new>(token_url: S, scopes: HashMap) -> Self { Self { token_url: token_url.into(), @@ -254,6 +568,7 @@ impl ClientCredentials { } } + /// Add refresh url for getting refresh tokens. pub fn with_refresh_url>(mut self, refresh_url: S) -> Self { self.refresh_url = Some(refresh_url.into()); @@ -289,92 +604,91 @@ mod tests { test_fn! { security_schema_correct_http_bearer_json: - SecuritySchema::new(SecurityType::Http { - bearer_format: Some("JWT".to_string()), - schema: HttpAutheticationType::Bearer, - }); + SecuritySchema::Http( + Http::new(HttpAuthenticationType::Bearer).with_bearer_format("JWT") + ); r###"{ "type": "http", - "schema": "bearer", + "scheme": "bearer", "bearerFormat": "JWT" }"### } test_fn! { security_schema_correct_basic_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Basic, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::Basic)); r###"{ "type": "http", - "schema": "basic" + "scheme": "basic" }"### } test_fn! { security_schema_correct_digest_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Digest, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::Digest)); r###"{ "type": "http", - "schema": "digest" + "scheme": "digest" }"### } test_fn! { security_schema_correct_hoba_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Hoba, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::Hoba)); r###"{ "type": "http", - "schema": "hoba" + "scheme": "hoba" }"### } test_fn! { security_schema_correct_mutual_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Mutual, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::Mutual)); r###"{ "type": "http", - "schema": "mutual" + "scheme": "mutual" }"### } test_fn! { security_schema_correct_negotiate_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::Negotiate, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::Negotiate)); r###"{ "type": "http", - "schema": "negotiate" + "scheme": "negotiate" }"### } test_fn! { security_schema_correct_oauth_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::OAuth, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::OAuth)); r###"{ "type": "http", - "schema": "oauth" + "scheme": "oauth" }"### } test_fn! { security_schema_correct_scram_sha1_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::ScramSha1, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::ScramSha1)); r###"{ "type": "http", - "schema": "scram-sha-1" + "scheme": "scram-sha-1" }"### } test_fn! { security_schema_correct_scram_sha256_auth: - SecuritySchema::new(SecurityType::Http{schema: HttpAutheticationType::ScramSha256, bearer_format: None}); + SecuritySchema::Http(Http::new(HttpAuthenticationType::ScramSha256)); r###"{ "type": "http", - "schema": "scram-sha-256" + "scheme": "scram-sha-256" }"### } test_fn! { security_schema_correct_api_key_cookie_auth: - SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Cookie , name: String::from("api_key")}); + SecuritySchema::ApiKey(ApiKey::new(String::from("api_key"), ApiKeyIn::Cookie)); r###"{ "type": "apiKey", "name": "api_key", @@ -384,7 +698,7 @@ mod tests { test_fn! { security_schema_correct_api_key_header_auth: - SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Header , name: String::from("api_key")}); + SecuritySchema::ApiKey(ApiKey::new("api_key", ApiKeyIn::Header)); r###"{ "type": "apiKey", "name": "api_key", @@ -394,7 +708,7 @@ mod tests { test_fn! { security_schema_correct_api_key_query_auth: - SecuritySchema::new(SecurityType::ApiKey{api_key_in: ApiKeyIn::Query , name: String::from("api_key")}); + SecuritySchema::ApiKey(ApiKey::new(String::from("api_key"), ApiKeyIn::Query)); r###"{ "type": "apiKey", "name": "api_key", @@ -404,7 +718,7 @@ mod tests { test_fn! { security_schema_correct_open_id_connect_auth: - SecuritySchema::new(SecurityType::OpenIdConnect{open_id_connect_url: String::from("http://localhost/openid")}); + SecuritySchema::OpenIdConnect(OpenIdConnect::new("http://localhost/openid")); r###"{ "type": "openIdConnect", "openIdConnectUrl": "http://localhost/openid" @@ -413,8 +727,8 @@ mod tests { test_fn! { security_schema_correct_oauth2_implicit: - SecuritySchema::new(SecurityType::OAuth2 { - flows: Flows::new([Flow::Implicit( + SecuritySchema::Oauth2( + Oauth2::new([Flow::Implicit( Implicit::new( "http://localhost/auth/dialog", HashMap::from([ @@ -422,8 +736,8 @@ mod tests { ("read:items".to_string(), "read my items".to_string() )]), ), - )]) - }); + )]).with_description("my oauth2 flow") + ); r###"{ "type": "oauth2", "flows": { @@ -434,7 +748,132 @@ mod tests { "read:items": "read my items" } } + }, + "description": "my oauth2 flow" +}"### + } + + test_fn! { + security_schema_correct_oauth2_password: + SecuritySchema::Oauth2( + Oauth2::new([Flow::Password( + Password::new( + "http://localhost/oauth/token", + HashMap::from([ + ("edit:items".to_string(), "edit my items".to_string()), + ("read:items".to_string(), "read my items".to_string() + )]), + ).with_refresh_url("http://localhost/refresh/token"), + )]).with_description("my oauth2 flow") + ); + r###"{ + "type": "oauth2", + "flows": { + "password": { + "tokenUrl": "http://localhost/oauth/token", + "refreshUrl": "http://localhost/refresh/token", + "scopes": { + "edit:items": "edit my items", + "read:items": "read my items" + } + } + }, + "description": "my oauth2 flow" +}"### + } + + test_fn! { + security_schema_correct_oauth2_client_credentials: + SecuritySchema::Oauth2( + Oauth2::new([Flow::ClientCredentials( + ClientCredentials::new( + "http://localhost/oauth/token", + HashMap::from([ + ("edit:items".to_string(), "edit my items".to_string()), + ("read:items".to_string(), "read my items".to_string() + )]), + ).with_refresh_url("http://localhost/refresh/token"), + )]).with_description("my oauth2 flow") + ); + r###"{ + "type": "oauth2", + "flows": { + "clientCredentials": { + "tokenUrl": "http://localhost/oauth/token", + "refreshUrl": "http://localhost/refresh/token", + "scopes": { + "edit:items": "edit my items", + "read:items": "read my items" + } + } + }, + "description": "my oauth2 flow" +}"### + } + + test_fn! { + security_schema_correct_oauth2_authorization_code: + SecuritySchema::Oauth2( + Oauth2::new([Flow::AuthorizationCode( + AuthorizationCode::new( + "http://localhost/authorization/token", + "http://localhost/token/url", + HashMap::from([ + ("edit:items".to_string(), "edit my items".to_string()), + ("read:items".to_string(), "read my items".to_string() + )]), + ).with_refresh_url("http://localhost/refresh/token"), + )]).with_description("my oauth2 flow") + ); + r###"{ + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "http://localhost/authorization/token", + "tokenUrl": "http://localhost/token/url", + "refreshUrl": "http://localhost/refresh/token", + "scopes": { + "edit:items": "edit my items", + "read:items": "read my items" + } + } + }, + "description": "my oauth2 flow" +}"### + } + + test_fn! { + security_schema_correct_oauth2_authorization_code_no_scopes: + SecuritySchema::Oauth2( + Oauth2::new([Flow::AuthorizationCode( + AuthorizationCode::new( + "http://localhost/authorization/token", + "http://localhost/token/url", + HashMap::new(), + ).with_refresh_url("http://localhost/refresh/token"), + )]) + ); + r###"{ + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "http://localhost/authorization/token", + "tokenUrl": "http://localhost/token/url", + "refreshUrl": "http://localhost/refresh/token", + "scopes": {} + } } +}"### + } + + test_fn! { + security_schema_correct_mutual_tls: + SecuritySchema::MutualTls { + description: Some(String::from("authorizaion is performed with client side certificate")) + }; + r###"{ + "type": "mutualTLS", + "description": "authorizaion is performed with client side certificate" }"### } }