use super::dispatch::DispatchSupervisor;
use super::shared::{
CurrentSession, OptionalSession, auth_request, check_content_length, max_body_bytes,
payload_too_large, request_headers,
};
use crate::Alibi;
use alibi_core::endpoint::EndpointOptions;
use alibi_core::{
AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema, Headers, core_paths,
};
use alibi_plugins::{GetSessionQuery, GetSessionResponse, SessionManagementPlugin};
use axum::{
Router,
extract::{FromRef, FromRequestParts, OptionalFromRequestParts, OriginalUri, Request, State},
http::{HeaderMap, HeaderName, HeaderValue, header, request::Parts},
response::{IntoResponse, Response},
routing::any,
};
use std::{
collections::HashSet,
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tower::{Layer, Service};
pub trait AxumIntegration {
type Schema: AuthSchema;
fn axum_router(self) -> Router<Arc<Alibi<Self::Schema>>>;
fn axum_router_with_state<S>(self) -> Router<S>
where
Self: Sized,
Arc<Alibi<Self::Schema>>: FromRef<S>,
S: Clone + Send + Sync + 'static;
}
impl<T: AuthSchema> AxumIntegration for Arc<Alibi<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 = DispatchSupervisor::new(render);
let mut paths = 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(handler::<T>(supervisor.clone())));
}
router.fallback(handler::<T>(supervisor))
}
}
impl<S, T> FromRequestParts<S> for CurrentSession<T>
where
T: AuthSchema,
Arc<Alibi<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::<Alibi<T>>::from_ref(state);
Self::resolve(&auth, parts.uri.path(), &parts.headers)
.await
.map_err(IntoResponse::into_response)
}
}
impl<S, T> FromRequestParts<S> for OptionalSession<T>
where
T: AuthSchema,
Arc<Alibi<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(),
))
}
}
pub struct SessionLayer<S: AuthSchema> {
auth: Arc<Alibi<S>>,
query: Option<GetSessionQuery>,
}
impl<S: AuthSchema> SessionLayer<S> {
pub const fn new(auth: Arc<Alibi<S>>) -> Self {
Self { auth, query: None }
}
#[must_use]
pub const fn query(mut self, query: GetSessionQuery) -> Self {
self.query = Some(query);
self
}
async fn get_session(
&self,
headers: &HeaderMap,
) -> AuthResult<(Option<GetSessionResponse>, Headers)> {
let mut endpoint = SessionManagementPlugin::get_session_endpoint();
if let Some(query) = &self.query {
endpoint = endpoint.with_query(query)?;
}
let options = EndpointOptions {
headers: Some(request_headers(headers)),
..EndpointOptions::default()
};
let output = Box::pin(self.auth.dispatch_endpoint(endpoint, options))
.await
.map_err(|error| error.error)?;
Ok((output.decode()?, output.headers().clone()))
}
}
impl<S: AuthSchema> Clone for SessionLayer<S> {
fn clone(&self) -> Self {
Self {
auth: Arc::clone(&self.auth),
query: self.query.clone(),
}
}
}
impl<S: AuthSchema, I> Layer<I> for SessionLayer<S> {
type Service = SessionService<S, I>;
fn layer(&self, inner: I) -> Self::Service {
SessionService {
layer: self.clone(),
inner,
}
}
}
pub struct SessionService<S: AuthSchema, I> {
layer: SessionLayer<S>,
inner: I,
}
impl<S: AuthSchema, I: Clone> Clone for SessionService<S, I> {
fn clone(&self) -> Self {
Self {
layer: self.layer.clone(),
inner: self.inner.clone(),
}
}
}
impl<S, I> Service<Request> for SessionService<S, I>
where
S: AuthSchema,
I: Service<Request, Response = Response> + Clone + Send + 'static,
I::Future: Send,
{
type Response = Response;
type Error = I::Error;
type Future = Pin<Box<dyn Future<Output = Result<Response, I::Error>> + Send>>;
fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(context)
}
fn call(&mut self, mut request: Request) -> Self::Future {
let ready = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, ready);
let layer = self.layer.clone();
Box::pin(async move {
if request.extensions().get::<Resolution>().is_some() {
return inner.call(request).await;
}
let (session, headers) =
layer
.get_session(request.headers())
.await
.unwrap_or_else(|error| {
tracing::warn!(%error, "Failed to get session");
(None, Headers::new())
});
_ = request.extensions_mut().insert(Resolution(session));
let mut response = inner.call(request).await?;
merge_session_headers(response.headers_mut(), headers);
Ok(response)
})
}
}
fn merge_session_headers(response: &mut HeaderMap, session: Headers) {
let handler_cookies: Vec<_> = response
.get_all(header::SET_COOKIE)
.iter()
.cloned()
.collect();
_ = response.remove(header::SET_COOKIE);
for (name, value) in session {
let (Ok(name), Ok(value)) = (HeaderName::try_from(name), HeaderValue::try_from(value))
else {
continue;
};
if name == header::SET_COOKIE {
_ = response.append(name, value);
} else if name != header::CACHE_CONTROL
&& name != header::PRAGMA
&& !response.contains_key(&name)
{
_ = response.insert(name, value);
}
}
for cookie in handler_cookies {
_ = response.append(header::SET_COOKIE, cookie);
}
}
#[derive(Clone)]
struct Resolution(Option<GetSessionResponse>);
#[derive(Debug, Clone)]
pub struct ResolvedSession(pub GetSessionResponse);
impl ResolvedSession {
fn from_parts(parts: &Parts) -> AuthResult<Option<Self>> {
parts
.extensions
.get::<Resolution>()
.map(|Resolution(session)| session.clone().map(Self))
.ok_or_else(|| AuthError::internal("SessionLayer is not installed"))
}
}
impl<T: Send + Sync> FromRequestParts<T> for ResolvedSession {
type Rejection = AuthError;
fn from_request_parts(
parts: &mut Parts,
_state: &T,
) -> impl Future<Output = AuthResult<Self>> + Send {
std::future::ready(Self::from_parts(parts).and_then(|session| {
session.ok_or(AuthError::Upstream {
status: 401,
code: "UNAUTHORIZED",
message: "Unauthorized",
})
}))
}
}
impl<T: Send + Sync> OptionalFromRequestParts<T> for ResolvedSession {
type Rejection = AuthError;
fn from_request_parts(
parts: &mut Parts,
_state: &T,
) -> impl Future<Output = AuthResult<Option<Self>>> + Send {
std::future::ready(Self::from_parts(parts))
}
}
type HandlerFuture = Pin<Box<dyn Future<Output = Response> + Send>>;
fn handler<T: AuthSchema>(
supervisor: DispatchSupervisor<Response>,
) -> impl Fn(State<Arc<Alibi<T>>>, Request) -> HandlerFuture + Clone {
move |State(auth): State<Arc<Alibi<T>>>, request: Request| {
let supervisor = supervisor.clone();
Box::pin(async move {
match convert_request(&auth, request).await {
Ok(request) => supervisor.dispatch(auth, request).await,
Err(error) => error.into_response(),
}
})
}
}
async fn convert_request<T: AuthSchema>(
auth: &Alibi<T>,
request: Request,
) -> AuthResult<AuthRequest> {
let max_bytes = max_body_bytes(auth);
let (parts, body) = request.into_parts();
check_content_length(&parts.headers, max_bytes)?;
let body = axum::body::to_bytes(body, max_bytes)
.await
.map_err(|error| {
if is_length_limit_error(&error) {
return payload_too_large(max_bytes);
}
tracing::warn!(%error, "Failed to read request body");
AuthError::bad_request("Failed to read request body")
})?;
let original_uri = parts
.extensions
.get::<OriginalUri>()
.map_or(&parts.uri, |original| &original.0);
auth_request(
&parts.method,
&parts.uri,
original_uri,
&parts.headers,
body.to_vec(),
&auth.config().base_url,
)
}
fn is_length_limit_error(error: &axum::Error) -> bool {
std::iter::successors(std::error::Error::source(error), |source| source.source())
.any(<dyn std::error::Error>::is::<http_body_util::LengthLimitError>)
}
fn render(result: Result<AuthResponse, AuthError>) -> Response {
result
.unwrap_or_else(AuthError::to_auth_response)
.into_response()
}