use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Auth {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bearer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub basic: Option<BasicAuth>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<ApiKeyAuth>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oauth: Option<OAuthAuth>,
}
impl Auth {
pub(crate) fn validate_exclusivity(&self) -> Result<(), String> {
let mut set = Vec::new();
if self.bearer.is_some() {
set.push("bearer");
}
if self.basic.is_some() {
set.push("basic");
}
if self.api_key.is_some() {
set.push("api_key");
}
if self.oauth.is_some() {
set.push("oauth");
}
if set.len() != 1 {
return Err(format!(
"exactly one of `auth.bearer`, `auth.basic`, `auth.api_key` or `auth.oauth` must \
be set, but found: {}",
if set.is_empty() {
"neither".to_string()
} else {
set.join(", ")
}
));
}
Ok(())
}
pub(crate) fn collision_reason(
&self,
headers: &[(String, String)],
query: &[(String, String)],
) -> Option<String> {
if (self.bearer.is_some() || self.basic.is_some() || self.oauth.is_some())
&& headers
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case("Authorization"))
{
return Some(
"`auth` and an explicit `Authorization` header cannot both be set on the same \
request; remove one"
.to_string(),
);
}
if let Some(api_key) = &self.api_key {
match api_key.r#in {
ApiKeyLocation::Header => {
if headers
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(&api_key.name))
{
return Some(format!(
"`auth.api_key` and an explicit `headers.{}` cannot both be set on \
the same request; remove one",
api_key.name
));
}
}
ApiKeyLocation::Query => {
if query.iter().any(|(name, _)| name == &api_key.name) {
return Some(format!(
"`auth.api_key` and an explicit `query.{}` cannot both be set on the \
same request; remove one",
api_key.name
));
}
}
}
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct BasicAuth {
pub user: String,
pub pass: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ApiKeyAuth {
pub r#in: ApiKeyLocation,
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum ApiKeyLocation {
Header,
Query,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct OAuthAuth {
pub grant_type: OAuthGrantType,
pub token_url: String,
pub client_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redirect_uri: Option<String>,
}
impl OAuthAuth {
pub(crate) fn validate_grant_fields(&self) -> Result<(), String> {
match self.grant_type {
OAuthGrantType::Password if self.username.is_none() || self.password.is_none() => Err(
"`auth.oauth` with `grant_type: password` requires both `username` and \
`password` to be set"
.to_string(),
),
OAuthGrantType::ClientCredentials if self.client_secret.is_none() => Err(
"`auth.oauth` with `grant_type: client_credentials` requires `client_secret` to \
be set"
.to_string(),
),
OAuthGrantType::AuthorizationCode
if self.authorization_url.is_none() || self.redirect_uri.is_none() =>
{
Err(
"`auth.oauth` with `grant_type: authorization_code` requires both \
`authorization_url` and `redirect_uri` to be set"
.to_string(),
)
}
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum OAuthGrantType {
ClientCredentials,
Password,
AuthorizationCode,
}
impl OAuthGrantType {
pub(crate) fn as_str(self) -> &'static str {
match self {
OAuthGrantType::ClientCredentials => "client_credentials",
OAuthGrantType::Password => "password",
OAuthGrantType::AuthorizationCode => "authorization_code",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base(grant_type: OAuthGrantType) -> OAuthAuth {
OAuthAuth {
grant_type,
token_url: "https://auth.example.com/token".to_string(),
client_id: "client".to_string(),
client_secret: None,
scope: None,
username: None,
password: None,
authorization_url: None,
redirect_uri: None,
}
}
#[test]
fn client_credentials_requires_a_client_secret() {
let err = base(OAuthGrantType::ClientCredentials)
.validate_grant_fields()
.expect_err("no client_secret must be rejected");
assert!(err.contains("client_secret"), "got {err}");
let ok = OAuthAuth {
client_secret: Some("s".to_string()),
..base(OAuthGrantType::ClientCredentials)
};
assert!(ok.validate_grant_fields().is_ok());
}
#[test]
fn password_requires_username_and_password() {
let err = base(OAuthGrantType::Password)
.validate_grant_fields()
.expect_err("no username/password must be rejected");
assert!(
err.contains("username") && err.contains("password"),
"got {err}"
);
let ok = OAuthAuth {
username: Some("ada".to_string()),
password: Some("hunter2".to_string()),
..base(OAuthGrantType::Password)
};
assert!(ok.validate_grant_fields().is_ok());
}
#[test]
fn authorization_code_requires_authorization_url_and_redirect_uri_but_not_client_secret() {
let err = base(OAuthGrantType::AuthorizationCode)
.validate_grant_fields()
.expect_err("no authorization_url/redirect_uri must be rejected");
assert!(
err.contains("authorization_url") && err.contains("redirect_uri"),
"got {err}"
);
let ok = OAuthAuth {
authorization_url: Some("https://auth.example.com/authorize".to_string()),
redirect_uri: Some("http://127.0.0.1:8899/callback".to_string()),
..base(OAuthGrantType::AuthorizationCode)
};
assert!(
ok.validate_grant_fields().is_ok(),
"authorization_code must not require client_secret"
);
}
#[test]
fn authorization_code_missing_only_redirect_uri_is_still_rejected() {
let err = OAuthAuth {
authorization_url: Some("https://auth.example.com/authorize".to_string()),
..base(OAuthGrantType::AuthorizationCode)
}
.validate_grant_fields()
.expect_err("redirect_uri alone missing must still be rejected");
assert!(err.contains("redirect_uri"), "got {err}");
}
}