use crate::error::Result;
use crate::handler::BoxFuture;
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use crate::security::constant_time_eq;
use async_trait::async_trait;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Principal(pub String);
impl Request {
pub fn principal(&self) -> Option<&str> {
self.extensions.get::<Principal>().map(|p| p.0.as_str())
}
}
fn unauthorized(challenge: &str, message: &str) -> Response {
Response::from_error(crate::error::Error::unauthorized(message))
.with_header("www-authenticate", challenge)
.with_header("cache-control", "no-store")
}
fn exempt(paths: &[String], req: &Request) -> bool {
paths.iter().any(|p| p == req.uri.path())
}
#[derive(Clone)]
pub struct ApiKeyAuth {
header: String,
keys: Vec<(String, String)>,
bearer: bool,
except: Vec<String>,
}
impl Default for ApiKeyAuth {
fn default() -> Self {
Self::new()
}
}
impl ApiKeyAuth {
pub fn new() -> Self {
Self {
header: "x-api-key".into(),
keys: Vec::new(),
bearer: false,
except: Vec::new(),
}
}
pub fn header(mut self, name: &str) -> Self {
self.header = name.to_ascii_lowercase();
self
}
pub fn allow_bearer(mut self) -> Self {
self.bearer = true;
self
}
pub fn key(mut self, name: &str, secret: &str) -> Self {
self.keys.push((name.to_string(), secret.to_string()));
self
}
pub fn except_paths<I, S>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.except = paths.into_iter().map(Into::into).collect();
self
}
fn lookup(&self, presented: &str) -> Option<&str> {
let mut found = None;
for (name, secret) in &self.keys {
if constant_time_eq(secret.as_bytes(), presented.as_bytes()) && found.is_none() {
found = Some(name.as_str());
}
}
found
}
}
#[async_trait]
impl Middleware for ApiKeyAuth {
async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
if exempt(&self.except, &req) {
return next.run(req).await;
}
let presented = req
.headers
.get(self.header.as_str())
.and_then(|v| v.to_str().ok())
.map(str::to_string)
.or_else(|| {
self.bearer
.then(|| {
req.headers
.get(http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::to_string)
})
.flatten()
});
let challenge = if self.bearer { "Bearer" } else { "ApiKey" };
let Some(presented) = presented else {
return Ok(unauthorized(challenge, "API key required"));
};
match self.lookup(&presented) {
Some(name) => {
req.extensions.insert(Principal(name.to_string()));
next.run(req).await
}
None => Ok(unauthorized(challenge, "Invalid API key")),
}
}
}
type Verifier = dyn Fn(String, String) -> BoxFuture<'static, bool> + Send + Sync;
#[derive(Clone)]
pub struct BasicAuth {
realm: String,
users: Vec<(String, String)>,
verifier: Option<Arc<Verifier>>,
except: Vec<String>,
}
impl BasicAuth {
pub fn new(realm: &str) -> Self {
Self {
realm: realm.replace(['"', '\\', '\r', '\n'], ""),
users: Vec::new(),
verifier: None,
except: Vec::new(),
}
}
pub fn user(mut self, name: &str, password: &str) -> Self {
self.users.push((name.to_string(), password.to_string()));
self
}
pub fn verify_with<F, Fut>(mut self, f: F) -> Self
where
F: Fn(String, String) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = bool> + Send + 'static,
{
self.verifier = Some(Arc::new(move |user, pass| Box::pin(f(user, pass))));
self
}
pub fn except_paths<I, S>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.except = paths.into_iter().map(Into::into).collect();
self
}
fn challenge(&self) -> String {
format!("Basic realm=\"{}\", charset=\"UTF-8\"", self.realm)
}
fn check_builtin(&self, user: &str, password: &str) -> bool {
let mut ok = false;
for (name, secret) in &self.users {
let name_ok = constant_time_eq(name.as_bytes(), user.as_bytes());
let pass_ok = constant_time_eq(secret.as_bytes(), password.as_bytes());
ok |= name_ok & pass_ok;
}
ok
}
}
fn base64_decode(input: &str) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(input.len() * 3 / 4);
let (mut buf, mut bits) = (0u32, 0u32);
for c in input.trim_end_matches('=').bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' | b'-' => 62,
b'/' | b'_' => 63,
_ => return None,
};
buf = (buf << 6) | u32::from(v);
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
Some(out)
}
#[async_trait]
impl Middleware for BasicAuth {
async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
if exempt(&self.except, &req) {
return next.run(req).await;
}
let credentials = req
.headers
.get(http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| {
let (scheme, rest) = v.split_once(' ')?;
scheme.eq_ignore_ascii_case("basic").then_some(rest.trim())
})
.and_then(base64_decode)
.and_then(|raw| String::from_utf8(raw).ok())
.and_then(|text| {
let (user, pass) = text.split_once(':')?;
Some((user.to_string(), pass.to_string()))
});
let Some((user, pass)) = credentials else {
return Ok(unauthorized(&self.challenge(), "Credentials required"));
};
let ok = match &self.verifier {
Some(verify) => verify(user.clone(), pass).await,
None => self.check_builtin(&user, &pass),
};
if !ok {
return Ok(unauthorized(&self.challenge(), "Invalid credentials"));
}
req.extensions.insert(Principal(user));
next.run(req).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_decoding() {
assert_eq!(base64_decode("dXNlcjpwYXNz").unwrap(), b"user:pass");
assert_eq!(base64_decode("dXNlcjpwYXNzMQ==").unwrap(), b"user:pass1");
assert_eq!(base64_decode("dXNlcjpwYXNzMQ").unwrap(), b"user:pass1");
assert_eq!(base64_decode("_-8").unwrap(), vec![0xff, 0xef]);
assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
assert!(base64_decode("not base64!").is_none());
}
#[test]
fn realm_cannot_break_the_header() {
let auth = BasicAuth::new("a\"b\r\nX-Evil: 1");
assert_eq!(
auth.challenge(),
"Basic realm=\"abX-Evil: 1\", charset=\"UTF-8\""
);
}
#[test]
fn api_key_lookup_returns_the_owner() {
let a = ApiKeyAuth::new().key("a", "secret-a").key("b", "secret-b");
assert_eq!(a.lookup("secret-b"), Some("b"));
assert_eq!(a.lookup("secret-"), None);
assert_eq!(a.lookup(""), None);
}
}