use std::{
any::TypeId,
future::{ready, Future},
ops::{Deref, DerefMut},
};
use http_kit::{HttpError, Request, Response};
use skyzen_core::{
middleware::{Middleware, Next},
Error, Extractor, Requirement,
};
#[cfg(feature = "auth")]
pub use crate::extract::auth::BearerToken;
#[cfg(all(feature = "jwt", not(target_arch = "wasm32")))]
pub use crate::auth::jwt::{JwtAuthenticator, JwtConfig, JwtError};
#[cfg(feature = "auth")]
pub use crate::auth::guard::{Admin, AuthorizationError, HasRoles, RoleExtractor};
#[derive(Debug, Clone)]
pub struct AuthUser<U>(pub U);
impl<U> Deref for AuthUser<U> {
type Target = U;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<U> DerefMut for AuthUser<U> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
http_kit::http_error!(
pub NotAuthenticated,
http_kit::StatusCode::UNAUTHORIZED,
"Not authenticated."
);
impl<U: Send + Sync + Clone + 'static> Extractor for AuthUser<U> {
type Error = NotAuthenticated;
fn extract(request: &mut Request) -> impl Future<Output = Result<Self, Self::Error>> + Send {
ready(
request
.extensions()
.get::<Self>()
.cloned()
.ok_or_else(NotAuthenticated::new),
)
}
fn requirements() -> Vec<Requirement> {
vec![Requirement::of::<Self>(
"`.with(AuthMiddleware::new(authenticator))`",
)]
}
}
pub trait Authenticator {
type User;
type Error;
fn authenticate(
&self,
req: &Request,
) -> impl Future<Output = Result<Self::User, Self::Error>> + Send;
}
#[derive(Clone, Debug)]
pub struct AuthMiddleware<A: Authenticator> {
authenticator: A,
}
impl<A: Authenticator> AuthMiddleware<A> {
pub const fn new(authenticator: A) -> Self {
Self { authenticator }
}
}
impl<A> Middleware for AuthMiddleware<A>
where
A: Authenticator + Send + Sync + 'static,
A::User: Send + Sync + Clone + 'static,
A::Error: HttpError,
{
async fn handle(&self, request: &mut Request, next: Next<'_>) -> Result<Response, Error> {
let user = self.authenticator.authenticate(request).await?;
request.extensions_mut().insert(AuthUser(user));
next.run(request).await
}
fn provisions(&self) -> Vec<TypeId> {
vec![TypeId::of::<AuthUser<A::User>>()]
}
}
#[cfg(test)]
mod tests {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use core::future::{ready, Future};
use http_kit::{http_error, HttpError};
use super::{AuthMiddleware, AuthUser, Authenticator};
use crate::{
routing::{CreateRouteNode, Route},
Body, Request, Result, StatusCode,
};
#[derive(Clone, Debug, PartialEq, Eq)]
struct TestUser {
name: &'static str,
}
http_error!(
pub TestAuthError,
StatusCode::UNAUTHORIZED,
"authentication failed"
);
#[derive(Clone, Debug)]
enum AuthOutcome {
Allow(TestUser),
Deny,
}
#[derive(Clone, Debug)]
struct TestAuthenticator {
outcome: AuthOutcome,
}
impl TestAuthenticator {
const fn allow(user: TestUser) -> Self {
Self {
outcome: AuthOutcome::Allow(user),
}
}
const fn deny() -> Self {
Self {
outcome: AuthOutcome::Deny,
}
}
}
impl Authenticator for TestAuthenticator {
type User = TestUser;
type Error = TestAuthError;
fn authenticate(
&self,
_req: &Request,
) -> impl Future<Output = std::result::Result<Self::User, Self::Error>> + Send {
ready(match &self.outcome {
AuthOutcome::Allow(user) => Ok(user.clone()),
AuthOutcome::Deny => Err(TestAuthError::new()),
})
}
}
#[derive(Clone, Debug)]
struct Calls(Arc<AtomicUsize>);
fn get(path: &str) -> Request {
let mut request = Request::new(Body::empty());
*request.uri_mut() = path.parse().expect("valid path");
request
}
fn router(authenticator: TestAuthenticator, calls: &Calls) -> crate::routing::Router {
let calls = calls.clone();
Route::new(("/me".at(move |AuthUser(user): AuthUser<TestUser>| {
let calls = calls.clone();
async move {
calls.0.fetch_add(1, Ordering::SeqCst);
Result::Ok(user.name)
}
}),))
.with(AuthMiddleware::new(authenticator))
.build()
}
#[tokio::test]
async fn successful_authentication_injects_the_user_for_the_endpoint() {
let calls = Calls(Arc::new(AtomicUsize::new(0)));
let router = router(TestAuthenticator::allow(TestUser { name: "lexo" }), &calls);
let response = router.go(get("/me")).await.unwrap();
assert_eq!(calls.0.load(Ordering::SeqCst), 1);
let body = response.into_body().into_string().await.unwrap();
assert_eq!(body, "lexo");
}
#[tokio::test]
async fn failed_authentication_short_circuits_endpoint_execution() {
let calls = Calls(Arc::new(AtomicUsize::new(0)));
let router = router(TestAuthenticator::deny(), &calls);
let error = router.go(get("/me")).await.unwrap_err();
assert_eq!(calls.0.load(Ordering::SeqCst), 0);
assert_eq!(error.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn an_unauthenticated_route_is_rejected_at_build_time() {
let error = Route::new((
"/me".at(|AuthUser(user): AuthUser<TestUser>| async move { Result::Ok(user.name) }),
))
.try_build()
.unwrap_err();
assert!(
error.to_string().contains("AuthMiddleware"),
"the error should name the fix: {error}"
);
}
#[test]
fn application_state_and_the_authenticated_user_no_longer_share_a_slot() {
use crate::utils::State;
Route::new(("/me".at(
|AuthUser(user): AuthUser<TestUser>, State(config): State<TestUser>| async move {
Result::Ok(format!("{}/{}", user.name, config.name))
},
),))
.with(AuthMiddleware::new(TestAuthenticator::allow(TestUser {
name: "lexo",
})))
.with(State(TestUser { name: "config" }))
.try_build()
.expect("distinct slots are both provided");
}
}