1use std::{
2 future::Future,
3 pin::Pin,
4 sync::Arc,
5 task::{Context, Poll},
6};
7
8use axum::{
9 body::Body,
10 http::{header, Request, Response, StatusCode},
11};
12use base64::{engine::general_purpose::STANDARD, Engine};
13use tower::{Layer, Service};
14
15use rok_auth::Claims;
16
17pub type VerifyFn = Arc<
24 dyn Fn(String, String) -> Pin<Box<dyn Future<Output = Option<Claims>> + Send>> + Send + Sync,
25>;
26
27#[derive(Clone)]
57pub struct BasicAuthLayer {
58 verifier: VerifyFn,
59 realm: String,
60}
61
62impl BasicAuthLayer {
63 pub fn new<F, Fut>(realm: impl Into<String>, verifier: F) -> Self
68 where
69 F: Fn(String, String) -> Fut + Send + Sync + 'static,
70 Fut: Future<Output = Option<Claims>> + Send + 'static,
71 {
72 Self {
73 realm: realm.into(),
74 verifier: Arc::new(move |u, p| Box::pin(verifier(u, p))),
75 }
76 }
77}
78
79impl<Svc> Layer<Svc> for BasicAuthLayer {
80 type Service = BasicAuthService<Svc>;
81
82 fn layer(&self, inner: Svc) -> Self::Service {
83 BasicAuthService {
84 inner,
85 verifier: Arc::clone(&self.verifier),
86 realm: self.realm.clone(),
87 }
88 }
89}
90
91#[derive(Clone)]
94pub struct BasicAuthService<Svc> {
95 inner: Svc,
96 verifier: VerifyFn,
97 realm: String,
98}
99
100type BoxFut<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
101
102impl<Svc, ReqBody> Service<Request<ReqBody>> for BasicAuthService<Svc>
103where
104 Svc: Service<Request<ReqBody>, Response = Response<Body>> + Clone + Send + 'static,
105 Svc::Error: Send,
106 Svc::Future: Send,
107 ReqBody: Send + 'static,
108{
109 type Response = Response<Body>;
110 type Error = Svc::Error;
111 type Future = BoxFut<Self::Response, Self::Error>;
112
113 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
114 self.inner.poll_ready(cx)
115 }
116
117 fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
118 let verifier = Arc::clone(&self.verifier);
119 let realm = self.realm.clone();
120 let clone = self.inner.clone();
121 let mut inner = std::mem::replace(&mut self.inner, clone);
122
123 Box::pin(async move {
124 let Some((username, password)) = parse_basic_header(req.headers()) else {
126 return Ok(deny(&realm));
127 };
128
129 let Some(claims) = verifier(username, password).await else {
131 return Ok(deny(&realm));
132 };
133
134 req.extensions_mut().insert(claims);
136
137 inner.call(req).await
139 })
140 }
141}
142
143pub fn parse_basic_header(headers: &axum::http::HeaderMap) -> Option<(String, String)> {
147 let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
148 let encoded = value.strip_prefix("Basic ")?;
149 let decoded = STANDARD.decode(encoded.trim()).ok()?;
150 let s = String::from_utf8(decoded).ok()?;
151 let mut parts = s.splitn(2, ':');
152 let username = parts.next()?.to_string();
153 let password = parts.next()?.to_string();
154 Some((username, password))
155}
156
157fn deny(realm: &str) -> Response<Body> {
158 Response::builder()
159 .status(StatusCode::UNAUTHORIZED)
160 .header(header::WWW_AUTHENTICATE, format!("Basic realm=\"{realm}\""))
161 .header(header::CONTENT_TYPE, "application/json")
162 .body(Body::from(
163 r#"{"error":"Unauthorized","message":"Basic authentication required"}"#,
164 ))
165 .unwrap()
166}