use indexmap::IndexMap;
use utoipa::openapi::security::{
ApiKey as UtoipaApiKey, ApiKeyValue, AuthorizationCode, ClientCredentials, Flow, Http,
HttpAuthScheme, Implicit, OAuth2 as UtoipaOAuth2, OpenIdConnect as UtoipaOpenIdConnect,
Password, Scopes, SecurityScheme as UtoipaSecurityScheme,
};
#[derive(Debug, Clone, PartialEq)]
pub enum SecurityScheme {
Bearer {
format: Option<String>,
description: Option<String>,
},
Basic {
description: Option<String>,
},
ApiKey {
name: String,
location: ApiKeyLocation,
description: Option<String>,
},
OAuth2 {
flows: Box<OAuth2Flows>,
description: Option<String>,
},
OpenIdConnect {
open_id_connect_url: String,
description: Option<String>,
},
}
impl SecurityScheme {
pub fn bearer() -> Self {
Self::Bearer {
format: None,
description: None,
}
}
pub fn bearer_with_format(format: impl Into<String>) -> Self {
Self::Bearer {
format: Some(format.into()),
description: None,
}
}
pub fn basic() -> Self {
Self::Basic { description: None }
}
pub fn api_key(name: impl Into<String>, location: ApiKeyLocation) -> Self {
Self::ApiKey {
name: name.into(),
location,
description: None,
}
}
pub fn openid_connect(url: impl Into<String>) -> Self {
Self::OpenIdConnect {
open_id_connect_url: url.into(),
description: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
match &mut self {
SecurityScheme::Bearer {
description: desc, ..
} => *desc = Some(description.into()),
SecurityScheme::Basic { description: desc } => *desc = Some(description.into()),
SecurityScheme::ApiKey {
description: desc, ..
} => *desc = Some(description.into()),
SecurityScheme::OAuth2 {
description: desc, ..
} => *desc = Some(description.into()),
SecurityScheme::OpenIdConnect {
description: desc, ..
} => *desc = Some(description.into()),
}
self
}
pub(crate) fn to_utoipa(&self) -> UtoipaSecurityScheme {
match self {
SecurityScheme::Bearer {
format,
description,
} => {
let mut http = Http::new(HttpAuthScheme::Bearer);
if let Some(fmt) = format {
http.bearer_format = Some(fmt.clone());
}
if let Some(desc) = description {
http.description = Some(desc.clone());
}
UtoipaSecurityScheme::Http(http)
}
SecurityScheme::Basic { description } => {
let mut http = Http::new(HttpAuthScheme::Basic);
if let Some(desc) = description {
http.description = Some(desc.clone());
}
UtoipaSecurityScheme::Http(http)
}
SecurityScheme::ApiKey {
name,
location,
description,
} => {
let api_key_value = if let Some(desc) = description {
ApiKeyValue::with_description(name, desc)
} else {
ApiKeyValue::new(name)
};
let api_key = match location {
ApiKeyLocation::Header => UtoipaApiKey::Header(api_key_value),
ApiKeyLocation::Query => UtoipaApiKey::Query(api_key_value),
ApiKeyLocation::Cookie => UtoipaApiKey::Cookie(api_key_value),
};
UtoipaSecurityScheme::ApiKey(api_key)
}
SecurityScheme::OAuth2 { flows, description } => {
let mut oauth2 = flows.to_utoipa();
if let Some(desc) = description {
oauth2.description = Some(desc.clone());
}
UtoipaSecurityScheme::OAuth2(oauth2)
}
SecurityScheme::OpenIdConnect {
open_id_connect_url,
description,
} => {
let mut oidc = UtoipaOpenIdConnect::new(open_id_connect_url);
if let Some(desc) = description {
oidc.description = Some(desc.clone());
}
UtoipaSecurityScheme::OpenIdConnect(oidc)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApiKeyLocation {
Header,
Query,
Cookie,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OAuth2Flows {
pub authorization_code: Option<OAuth2Flow>,
pub client_credentials: Option<OAuth2Flow>,
pub implicit: Option<OAuth2ImplicitFlow>,
pub password: Option<OAuth2Flow>,
}
impl OAuth2Flows {
pub fn authorization_code(
authorization_url: impl Into<String>,
token_url: impl Into<String>,
scopes: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
Self {
authorization_code: Some(OAuth2Flow {
authorization_url: Some(authorization_url.into()),
token_url: token_url.into(),
refresh_url: None,
scopes: scopes
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}),
..Default::default()
}
}
pub fn client_credentials(
token_url: impl Into<String>,
scopes: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
Self {
client_credentials: Some(OAuth2Flow {
authorization_url: None,
token_url: token_url.into(),
refresh_url: None,
scopes: scopes
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}),
..Default::default()
}
}
fn to_utoipa(&self) -> UtoipaOAuth2 {
let mut flows: Vec<Flow> = Vec::new();
if let Some(flow) = &self.authorization_code {
let scopes = Scopes::from_iter(flow.scopes.clone());
let auth_code = if let Some(ref refresh) = flow.refresh_url {
AuthorizationCode::with_refresh_url(
flow.authorization_url.as_deref().unwrap_or_default(),
&flow.token_url,
scopes,
refresh,
)
} else {
AuthorizationCode::new(
flow.authorization_url.as_deref().unwrap_or_default(),
&flow.token_url,
scopes,
)
};
flows.push(Flow::AuthorizationCode(auth_code));
}
if let Some(flow) = &self.client_credentials {
let scopes = Scopes::from_iter(flow.scopes.clone());
let client_creds = if let Some(ref refresh) = flow.refresh_url {
ClientCredentials::with_refresh_url(&flow.token_url, scopes, refresh)
} else {
ClientCredentials::new(&flow.token_url, scopes)
};
flows.push(Flow::ClientCredentials(client_creds));
}
if let Some(flow) = &self.implicit {
let scopes = Scopes::from_iter(flow.scopes.clone());
let implicit = if let Some(ref refresh) = flow.refresh_url {
Implicit::with_refresh_url(&flow.authorization_url, scopes, refresh)
} else {
Implicit::new(&flow.authorization_url, scopes)
};
flows.push(Flow::Implicit(implicit));
}
if let Some(flow) = &self.password {
let scopes = Scopes::from_iter(flow.scopes.clone());
let password = if let Some(ref refresh) = flow.refresh_url {
Password::with_refresh_url(&flow.token_url, scopes, refresh)
} else {
Password::new(&flow.token_url, scopes)
};
flows.push(Flow::Password(password));
}
UtoipaOAuth2::new(flows)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OAuth2Flow {
pub authorization_url: Option<String>,
pub token_url: String,
pub refresh_url: Option<String>,
pub scopes: IndexMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OAuth2ImplicitFlow {
pub authorization_url: String,
pub refresh_url: Option<String>,
pub scopes: IndexMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityRequirement {
pub name: String,
pub scopes: Vec<String>,
}
impl SecurityRequirement {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
scopes: Vec::new(),
}
}
pub fn with_scopes(
name: impl Into<String>,
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
name: name.into(),
scopes: scopes.into_iter().map(Into::into).collect(),
}
}
pub(crate) fn to_utoipa(&self) -> utoipa::openapi::security::SecurityRequirement {
utoipa::openapi::security::SecurityRequirement::new(
&self.name,
self.scopes.iter().map(String::as_str),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bearer_scheme_creation() {
let scheme = SecurityScheme::bearer();
assert!(matches!(
scheme,
SecurityScheme::Bearer {
format: None,
description: None
}
));
}
#[test]
fn test_bearer_with_format() {
let scheme = SecurityScheme::bearer_with_format("JWT");
assert!(matches!(
scheme,
SecurityScheme::Bearer {
format: Some(ref f),
description: None
} if f == "JWT"
));
}
#[test]
fn test_basic_scheme_creation() {
let scheme = SecurityScheme::basic();
assert!(matches!(
scheme,
SecurityScheme::Basic { description: None }
));
}
#[test]
fn test_api_key_scheme_creation() {
let scheme = SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header);
assert!(matches!(
scheme,
SecurityScheme::ApiKey {
ref name,
location: ApiKeyLocation::Header,
description: None
} if name == "X-API-Key"
));
}
#[test]
fn test_with_description() {
let scheme = SecurityScheme::bearer().with_description("JWT Bearer token");
assert!(matches!(
scheme,
SecurityScheme::Bearer {
format: None,
description: Some(ref d)
} if d == "JWT Bearer token"
));
}
#[test]
fn test_security_requirement_new() {
let req = SecurityRequirement::new("bearerAuth");
assert_eq!(req.name, "bearerAuth");
assert!(req.scopes.is_empty());
}
#[test]
fn test_security_requirement_with_scopes() {
let req = SecurityRequirement::with_scopes("oauth2", ["read:users", "write:users"]);
assert_eq!(req.name, "oauth2");
assert_eq!(req.scopes, vec!["read:users", "write:users"]);
}
#[test]
fn test_bearer_to_utoipa() {
let scheme = SecurityScheme::bearer_with_format("JWT").with_description("JWT token");
let utoipa_scheme = scheme.to_utoipa();
assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::Http(_)));
}
#[test]
fn test_basic_to_utoipa() {
let scheme = SecurityScheme::basic();
let utoipa_scheme = scheme.to_utoipa();
assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::Http(_)));
}
#[test]
fn test_api_key_to_utoipa() {
let scheme = SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header);
let utoipa_scheme = scheme.to_utoipa();
assert!(matches!(utoipa_scheme, UtoipaSecurityScheme::ApiKey(_)));
}
#[test]
fn test_openid_connect_to_utoipa() {
let scheme = SecurityScheme::openid_connect("https://auth.example.com/.well-known/openid");
let utoipa_scheme = scheme.to_utoipa();
assert!(matches!(
utoipa_scheme,
UtoipaSecurityScheme::OpenIdConnect(_)
));
}
#[test]
fn test_oauth2_authorization_code_flows() {
let flows = OAuth2Flows::authorization_code(
"https://auth.example.com/authorize",
"https://auth.example.com/token",
[("read:users", "Read user data")],
);
assert!(flows.authorization_code.is_some());
assert!(flows.client_credentials.is_none());
}
#[test]
fn test_oauth2_client_credentials_flows() {
let flows = OAuth2Flows::client_credentials(
"https://auth.example.com/token",
[("api:access", "API access")],
);
assert!(flows.client_credentials.is_some());
assert!(flows.authorization_code.is_none());
}
#[test]
fn test_security_requirement_to_utoipa() {
let req = SecurityRequirement::with_scopes("oauth2", ["read:users"]);
let utoipa_req = req.to_utoipa();
assert!(format!("{utoipa_req:?}").contains("oauth2"));
}
}