use std::env;
use axum::body::Body;
use axum::extract::Request;
use axum::middleware::Next;
use axum::response::Response;
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, HeaderMap, StatusCode},
};
use base64::{engine::general_purpose, Engine as _};
use subtle::ConstantTimeEq;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BasicAuthError {
#[error("Missing Authorization header")]
MissingAuthHeader,
#[error("Invalid Authorization header format")]
InvalidHeaderFormat,
#[error("Invalid base64 encoding")]
InvalidBase64,
#[error("Invalid credentials format")]
InvalidCredentialsFormat,
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Missing server credentials configuration")]
MissingServerCredentials,
}
#[derive(Debug, Clone)]
pub struct BasicAuthCredentials {
pub username: String,
pub password: String,
}
impl BasicAuthCredentials {
pub fn from_headers(headers: &HeaderMap) -> Result<Self, BasicAuthError> {
let auth_header = headers
.get("Authorization")
.ok_or(BasicAuthError::MissingAuthHeader)?
.to_str()
.map_err(|_| BasicAuthError::InvalidHeaderFormat)?;
if !auth_header.starts_with("Basic ") {
return Err(BasicAuthError::InvalidHeaderFormat);
}
let base64_credentials = &auth_header[6..];
let decoded = general_purpose::STANDARD
.decode(base64_credentials)
.map_err(|_| BasicAuthError::InvalidBase64)?;
let credentials_str =
String::from_utf8(decoded).map_err(|_| BasicAuthError::InvalidBase64)?;
let parts: Vec<&str> = credentials_str.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(BasicAuthError::InvalidCredentialsFormat);
}
Ok(BasicAuthCredentials { username: parts[0].to_string(), password: parts[1].to_string() })
}
pub fn validate(&self) -> Result<(), BasicAuthError> {
let server_username = env::var("RRELAYER_AUTH_USERNAME")
.map_err(|_| BasicAuthError::MissingServerCredentials)?;
let server_password = env::var("RRELAYER_AUTH_PASSWORD")
.map_err(|_| BasicAuthError::MissingServerCredentials)?;
let username_match = self.username.as_bytes().ct_eq(server_username.as_bytes());
let password_match = self.password.as_bytes().ct_eq(server_password.as_bytes());
if username_match.into() && password_match.into() {
Ok(())
} else {
Err(BasicAuthError::InvalidCredentials)
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct Authenticated;
#[async_trait]
impl<S> FromRequestParts<S> for Authenticated
where
S: Send + Sync,
{
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let credentials = BasicAuthCredentials::from_headers(&parts.headers)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
credentials.validate().map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Authenticated)
}
}
pub fn validate_basic_auth(headers: &HeaderMap) -> Result<(), BasicAuthError> {
let credentials = BasicAuthCredentials::from_headers(headers)?;
credentials.validate()
}
pub async fn inject_basic_auth_status(
req: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
let mut req = req;
let basic_auth_valid = validate_basic_auth(req.headers()).is_ok();
if basic_auth_valid {
req.headers_mut().insert("x-rrelayer-basic-auth-valid", "true".parse().unwrap());
} else {
req.headers_mut().insert("x-rrelayer-basic-auth-valid", "false".parse().unwrap());
}
Ok(next.run(req).await)
}