use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use subtle::ConstantTimeEq;
use crate::config::{get_basic_auth, QrushBasicAuthConfig};
pub fn check_basic_auth_header(auth_header: Option<&str>) -> bool {
check_credentials(auth_header, get_basic_auth())
}
pub(crate) fn check_credentials(
auth_header: Option<&str>,
config: Option<&QrushBasicAuthConfig>,
) -> bool {
let Some(config) = config else {
return true;
};
if let Some(auth_str) = auth_header {
if let Some(encoded) = auth_str.strip_prefix("Basic ") {
if let Ok(decoded) = STANDARD.decode(encoded) {
if let Ok(credentials) = std::str::from_utf8(&decoded) {
let mut parts = credentials.splitn(2, ':');
let user = parts.next().unwrap_or_default();
let pass = parts.next().unwrap_or_default();
let user_ok = user.as_bytes().ct_eq(config.username.as_bytes());
let pass_ok = pass.as_bytes().ct_eq(config.password.as_bytes());
if (user_ok & pass_ok).into() {
return true;
}
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::QrushBasicAuthConfig;
fn cfg() -> QrushBasicAuthConfig {
QrushBasicAuthConfig {
username: "user".into(),
password: "pass".into(),
}
}
fn header(user_pass: &str) -> String {
format!("Basic {}", STANDARD.encode(user_pass))
}
#[test]
fn allows_when_unconfigured() {
assert!(check_credentials(None, None));
assert!(check_credentials(Some(&header("anyone:anything")), None));
}
#[test]
fn accepts_correct_credentials() {
assert!(check_credentials(Some(&header("user:pass")), Some(&cfg())));
}
#[test]
fn rejects_wrong_credentials() {
assert!(!check_credentials(Some(&header("user:wrong")), Some(&cfg())));
assert!(!check_credentials(Some(&header("admin:pass")), Some(&cfg())));
}
#[test]
fn rejects_missing_or_malformed_header() {
assert!(!check_credentials(None, Some(&cfg())));
assert!(!check_credentials(Some("Bearer abc"), Some(&cfg())));
assert!(!check_credentials(Some("Basic %%%not-base64"), Some(&cfg())));
}
}
#[cfg(feature = "dashboard-actix")]
mod actix_impl {
use super::check_basic_auth_header;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpRequest, HttpResponse,
};
use futures_util::future::{ready, Either, Ready};
use std::future::Ready as StdReady;
pub struct BasicAuthMiddleware;
impl<S> Transform<S, ServiceRequest> for BasicAuthMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type InitError = ();
type Transform = BasicAuthMiddlewareService<S>;
type Future = StdReady<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
std::future::ready(Ok(BasicAuthMiddlewareService { service }))
}
}
pub struct BasicAuthMiddlewareService<S> {
service: S,
}
impl<S> Service<ServiceRequest> for BasicAuthMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Either<Ready<Result<Self::Response, Self::Error>>, S::Future>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
if check_basic_auth(req.request()) {
Either::Right(self.service.call(req))
} else {
let response = req.into_response(unauthorized_response());
Either::Left(ready(Ok(response)))
}
}
}
pub fn check_basic_auth(req: &HttpRequest) -> bool {
let header = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok());
check_basic_auth_header(header)
}
pub fn unauthorized_response() -> HttpResponse {
HttpResponse::Unauthorized()
.append_header(("WWW-Authenticate", r#"Basic realm="QRush""#))
.finish()
}
}
#[cfg(feature = "dashboard-actix")]
pub use actix_impl::{
check_basic_auth, unauthorized_response, BasicAuthMiddleware, BasicAuthMiddlewareService,
};
#[cfg(feature = "dashboard-axum")]
pub async fn axum_basic_auth(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
let auth = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok());
if check_basic_auth_header(auth) {
next.run(req).await
} else {
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, r#"Basic realm="QRush""#)],
"Unauthorized",
)
.into_response()
}
}