use base64::Engine;
use http::header::AUTHORIZATION;
use rskit_errors::{AppError, AppResult, ErrorCode};
use rskit_security::{BASIC_AUTH_SCHEME, BEARER_AUTH_SCHEME, SecretString};
use std::fmt;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum Auth {
Bearer(SecretString),
Basic {
username: String,
password: SecretString,
},
ApiKey {
name: String,
value: SecretString,
},
#[default]
None,
}
impl Auth {
pub fn bearer(token: impl Into<String>) -> Self {
Self::bearer_secret(SecretString::new(token))
}
pub fn bearer_secret(token: SecretString) -> Self {
Auth::Bearer(token)
}
pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
Self::basic_secret(username, SecretString::new(password))
}
pub fn basic_secret(username: impl Into<String>, password: SecretString) -> Self {
Auth::Basic {
username: username.into(),
password,
}
}
pub fn api_key(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::api_key_secret(name, SecretString::new(value))
}
pub fn api_key_secret(name: impl Into<String>, value: SecretString) -> Self {
Auth::ApiKey {
name: name.into(),
value,
}
}
pub fn header(&self) -> AppResult<Option<(String, String)>> {
match self {
Auth::Bearer(token) => Ok(Some((
AUTHORIZATION.as_str().to_string(),
format!("{BEARER_AUTH_SCHEME} {}", token.expose()),
))),
Auth::Basic { username, password } => {
let credentials = format!("{}:{}", username, password.expose());
let encoded = base64::engine::general_purpose::STANDARD.encode(&credentials);
Ok(Some((
AUTHORIZATION.as_str().to_string(),
format!("{BASIC_AUTH_SCHEME} {encoded}"),
)))
}
Auth::ApiKey { name, value } => {
if name.parse::<http::HeaderName>().is_err() {
return Err(AppError::new(
ErrorCode::InvalidInput,
format!("invalid API key header name '{name}'"),
));
}
if value.expose().parse::<http::HeaderValue>().is_err() {
return Err(AppError::new(
ErrorCode::InvalidInput,
format!("invalid API key header value for '{name}'"),
));
}
Ok(Some((name.clone(), value.expose().to_string())))
}
Auth::None => Ok(None),
}
}
}
impl fmt::Display for Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Auth::Bearer(_) => write!(f, "{BEARER_AUTH_SCHEME}"),
Auth::Basic { .. } => write!(f, "{BASIC_AUTH_SCHEME}"),
Auth::ApiKey { name, .. } => write!(f, "ApiKey({})", name),
Auth::None => write!(f, "None"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_key_rejects_invalid_header_value() {
let auth = Auth::api_key("x-api-key", "bad\nvalue");
assert!(auth.header().is_err());
}
#[test]
fn api_key_rejects_invalid_header_name() {
let auth = Auth::api_key("bad header", "secret");
let error = auth.header().expect_err("invalid header name");
assert_eq!(error.code(), ErrorCode::InvalidInput);
assert!(error.message().contains("invalid API key header name"));
}
#[test]
fn api_key_and_none_headers_are_explicit() {
assert_eq!(
Auth::api_key("x-api-key", "secret").header().unwrap(),
Some(("x-api-key".to_string(), "secret".to_string()))
);
assert_eq!(Auth::None.header().unwrap(), None);
assert_eq!(Auth::None.to_string(), "None");
}
#[test]
fn debug_redacts_secret_values() {
let cases = [
format!("{:?}", Auth::bearer("secret-token")),
format!("{:?}", Auth::basic("user", "secret-password")),
format!("{:?}", Auth::api_key("x-api-key", "secret-key")),
];
for formatted in cases {
assert!(formatted.contains("SecretString(***)"));
assert!(!formatted.contains("secret-token"));
assert!(!formatted.contains("secret-password"));
assert!(!formatted.contains("secret-key"));
}
}
#[test]
fn header_exposes_secret_only_for_request_application() {
let auth = Auth::bearer_secret(SecretString::new("secret-token"));
assert_eq!(
auth.header().unwrap(),
Some((
AUTHORIZATION.as_str().to_string(),
format!("{BEARER_AUTH_SCHEME} secret-token")
))
);
}
}