#[cfg(feature = "axum")]
use crate::BetterAuth;
#[cfg(feature = "axum")]
#[cfg(feature = "axum")]
use alibi_core::middleware::BodyLimitConfig;
use alibi_core::{AuthError, AuthRequest, AuthResponse, AuthSchema, HttpMethod, core_paths};
#[cfg(feature = "axum")]
use axum::{
Router,
extract::{FromRef, FromRequestParts, Request, State},
http::StatusCode,
http::request::Parts,
response::{IntoResponse, Response},
routing::any,
};
#[cfg(feature = "axum")]
use std::sync::Arc;
#[cfg(feature = "axum")]
type AxumAuthHandlerFuture = std::pin::Pin<Box<dyn Future<Output = Response> + Send>>;
#[cfg(feature = "axum")]
pub trait AxumIntegration {
type Schema: AuthSchema;
fn axum_router(self) -> Router<Arc<BetterAuth<Self::Schema>>>;
fn axum_router_with_state<S>(self) -> Router<S>
where
Self: Sized,
Arc<BetterAuth<Self::Schema>>: FromRef<S>,
S: Clone + Send + Sync + 'static;
}
#[cfg(feature = "axum")]
impl<T: AuthSchema> AxumIntegration for Arc<BetterAuth<T>> {
type Schema = T;
fn axum_router(self) -> Router<Self> {
self.axum_router_with_state::<Self>()
}
fn axum_router_with_state<S>(self) -> Router<S>
where
Self: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let supervisor = AxumDispatchSupervisor::new(render_dispatch);
let mut paths = std::collections::HashSet::from([
core_paths::OK.to_owned(),
core_paths::ERROR.to_owned(),
core_paths::UPDATE_USER.to_owned(),
]);
for plugin in self.plugins() {
paths.extend(plugin.routes().into_iter().map(|route| route.path));
}
let mut router = Router::new();
for path in paths {
router = router.route(&path, any(create_plugin_handler::<T>(supervisor.clone())));
}
router.fallback(create_plugin_handler::<T>(supervisor))
}
}
#[cfg(feature = "axum")]
#[derive(Debug, Clone)]
pub struct CurrentSession<T: AuthSchema> {
pub user: T::User,
pub session: T::Session,
}
#[cfg(feature = "axum")]
#[derive(Debug, Clone)]
pub struct OptionalSession<T: AuthSchema>(pub Option<CurrentSession<T>>);
#[cfg(feature = "axum")]
impl<S, T> FromRequestParts<S> for CurrentSession<T>
where
T: AuthSchema,
Arc<BetterAuth<T>>: FromRef<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let auth = Arc::<BetterAuth<T>>::from_ref(state);
let mut request = AuthRequest::new(HttpMethod::Get, parts.uri.path());
for (name, value) in &parts.headers {
if let Ok(value) = value.to_str() {
drop(request.headers.insert(name.to_string(), value.to_owned()));
}
}
let token = auth
.session_manager()
.extract_session_token(&request)
.ok_or_else(|| AuthError::Unauthenticated.into_response())?;
let session = auth
.session_manager()
.get_session(&token)
.await
.map_err(IntoResponse::into_response)?
.ok_or_else(|| AuthError::SessionNotFound.into_response())?;
let user = auth
.context()
.session_user(&session)
.await
.map_err(IntoResponse::into_response)?
.ok_or_else(|| AuthError::UserNotFound.into_response())?;
Ok(Self { user, session })
}
}
#[cfg(feature = "axum")]
impl<S, T> FromRequestParts<S> for OptionalSession<T>
where
T: AuthSchema,
Arc<BetterAuth<T>>: FromRef<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Ok(Self(
CurrentSession::<T>::from_request_parts(parts, state)
.await
.ok(),
))
}
}
#[cfg(feature = "axum")]
fn create_plugin_handler<T: AuthSchema>(
supervisor: AxumDispatchSupervisor,
) -> impl Fn(State<Arc<BetterAuth<T>>>, Request) -> AxumAuthHandlerFuture + Clone {
move |State(auth): State<Arc<BetterAuth<T>>>, req: Request| {
let supervisor = supervisor.clone();
Box::pin(async move {
match convert_axum_request(
req,
max_body_bytes(auth.body_limit()),
&auth.config().base_url,
)
.await
{
Ok(auth_req) => supervisor.dispatch(auth, auth_req).await,
Err(err) => err.into_response(),
}
})
}
}
#[cfg(feature = "axum")]
const fn max_body_bytes(config: &BodyLimitConfig) -> usize {
if config.enabled {
config.max_bytes
} else {
usize::MAX
}
}
#[cfg(feature = "axum")]
fn is_body_length_limit_error(err: &axum::Error) -> bool {
use std::error::Error;
let mut source = err.source();
while let Some(e) = source {
if e.is::<http_body_util::LengthLimitError>() {
return true;
}
source = e.source();
}
false
}
#[cfg(feature = "axum")]
async fn convert_axum_request(
req: Request,
max_body_bytes: usize,
base_url: &str,
) -> Result<AuthRequest, AuthError> {
use std::collections::HashMap;
let (parts, body) = req.into_parts();
let method = match parts.method {
axum::http::Method::GET => HttpMethod::Get,
axum::http::Method::POST => HttpMethod::Post,
axum::http::Method::PUT => HttpMethod::Put,
axum::http::Method::DELETE => HttpMethod::Delete,
axum::http::Method::PATCH => HttpMethod::Patch,
axum::http::Method::OPTIONS => HttpMethod::Options,
axum::http::Method::HEAD => HttpMethod::Head,
_ => {
return Err(AuthError::InvalidRequest(
"Unsupported HTTP method".to_owned(),
));
}
};
let mut headers: HashMap<String, String> = HashMap::new();
for (name, value) in &parts.headers {
if let Ok(value_str) = value.to_str() {
let _ = headers
.entry(name.to_string())
.and_modify(|value| {
value.push_str(if name == "cookie" { "; " } else { ", " });
value.push_str(value_str);
})
.or_insert_with(|| value_str.to_owned());
}
}
let deployment = url::Url::parse(base_url).ok();
let scheme = parts
.uri
.scheme_str()
.unwrap_or_else(|| deployment.as_ref().map_or("http", url::Url::scheme));
let authority = parts
.uri
.authority()
.map(|value| value.as_str())
.or_else(|| headers.get("host").map(String::as_str));
let original_uri = parts
.extensions
.get::<axum::extract::OriginalUri>()
.map_or(&parts.uri, |original| &original.0);
let request_url = authority.and_then(|authority| {
url::Url::parse(&format!(
"{scheme}://{authority}{}",
original_uri
.path_and_query()
.map_or("/", |value| value.as_str())
))
.ok()
});
let path = parts.uri.path().to_owned();
let query_pairs = parts
.uri
.query()
.map(|query| {
url::form_urlencoded::parse(query.as_bytes())
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let Some(len) = parts
.headers
.get(axum::http::header::CONTENT_LENGTH)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<usize>().ok())
&& len > max_body_bytes
{
return Err(AuthError::payload_too_large(format!(
"Request body exceeds the {max_body_bytes}-byte limit"
)));
}
let body_bytes = match axum::body::to_bytes(body, max_body_bytes).await {
Ok(bytes) => {
if bytes.is_empty() {
None
} else {
Some(bytes.to_vec())
}
}
Err(err) => {
if is_body_length_limit_error(&err) {
return Err(AuthError::payload_too_large(format!(
"Request body exceeds the {max_body_bytes}-byte limit"
)));
}
tracing::warn!(error = %err, "Failed to read request body");
return Err(AuthError::bad_request("Failed to read request body"));
}
};
let mut request = AuthRequest::from_parts(method, path, headers, body_bytes, HashMap::new());
request.set_query_pairs(query_pairs);
if let Some(url) = request_url {
request = request.with_url(url);
}
Ok(request)
}
#[cfg(feature = "axum")]
fn convert_auth_response(auth_response: AuthResponse) -> Response {
let mut response = Response::builder().status(
StatusCode::from_u16(auth_response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
);
for (name, value) in auth_response.headers {
if let (Ok(header_name), Ok(header_value)) = (
axum::http::HeaderName::from_bytes(name.as_bytes()),
axum::http::HeaderValue::from_str(&value),
) {
response = response.header(header_name, header_value);
}
}
response
.body(axum::body::Body::from(auth_response.body))
.unwrap_or_else(|_error| {
let (mut parts, ()) = Response::new(()).into_parts();
parts.status = StatusCode::INTERNAL_SERVER_ERROR;
Response::from_parts(parts, axum::body::Body::from("Internal server error"))
})
}
#[cfg(feature = "axum")]
type AxumDispatchSupervisor = crate::integrations::dispatch::DispatchSupervisor<Response>;
#[cfg(feature = "axum")]
fn render_dispatch(result: Result<AuthResponse, AuthError>) -> Response {
match result {
Ok(response) => convert_auth_response(response),
Err(error) => error.into_response(),
}
}