Skip to main content

larust_auth/
middleware.rs

1//! Route middleware (Laravel's `auth`/`guest` middleware aliases).
2
3use axum::extract::Request;
4use axum::middleware::Next;
5use axum::response::{IntoResponse, Redirect, Response};
6use larust_http::session::Session;
7
8use crate::guard;
9use crate::Authenticatable;
10
11/// Redirects a guest to `"login"` (falling back to a hardcoded `/login`,
12/// with a warning, if that route name isn't registered - the same degrade
13/// pattern `larust_support::redirect()->route()` already uses) rather than
14/// running the handler. Pair with routes that require a logged-in user
15/// (Laravel's `auth` middleware); use the [`crate::Auth`] extractor instead
16/// on routes that should fail with a 401 rather than redirect.
17///
18/// Checks the default (`"web"`) guard; for a named second guard, use
19/// [`require_auth_for`] instead.
20pub async fn require_auth(session: Session, request: Request, next: Next) -> Response {
21    match guard::check(&session).await {
22        Ok(true) => next.run(request).await,
23        Ok(false) => Redirect::to(&login_path()).into_response(),
24        // Fail closed: a session-store error is treated the same as "not
25        // authenticated" rather than letting the request through.
26        Err(error) => {
27            tracing::warn!(%error, "require_auth: failed to read session; denying access");
28            Redirect::to(&login_path()).into_response()
29        }
30    }
31}
32
33/// [`require_auth`]'s named-guard counterpart (Laravel's
34/// `Auth::guard('admin')` protecting a route group) - redirects unless
35/// `U::GUARD` specifically is logged in, regardless of whether some other
36/// guard is. Used as `require_auth_for::<Admin>`, e.g.
37/// `r.middleware(axum::middleware::from_fn(require_auth_for::<Admin>))`.
38///
39/// Redirects to a route named `"{guard}.login"` (e.g. `"admin.login"` for
40/// `Admin::GUARD == "admin"`) rather than the plain `"login"` [`require_auth`]
41/// uses - a named second guard almost always has its own, separate login
42/// page. Falls back to plain `"login"`, then to a hardcoded `/login`, with
43/// a warning at each step, if the more specific name isn't registered -
44/// the same degrade pattern [`require_auth`] already uses.
45pub async fn require_auth_for<U: Authenticatable>(
46    session: Session,
47    request: Request,
48    next: Next,
49) -> Response {
50    match guard::check_for::<U>(&session).await {
51        Ok(true) => next.run(request).await,
52        Ok(false) => Redirect::to(&guard_login_path(U::GUARD)).into_response(),
53        Err(error) => {
54            tracing::warn!(%error, "require_auth_for: failed to read session; denying access");
55            Redirect::to(&guard_login_path(U::GUARD)).into_response()
56        }
57    }
58}
59
60/// The inverse of [`require_auth`]: bounces an already-logged-in user away
61/// from guest-only routes (Laravel's `guest` middleware - typically wrapped
62/// around `/login`/`/register`) to `"/"`, rather than running the handler.
63///
64/// Checks the default (`"web"`) guard; for a named second guard, use
65/// [`redirect_authenticated_for`] instead.
66pub async fn redirect_authenticated(session: Session, request: Request, next: Next) -> Response {
67    match guard::check(&session).await {
68        Ok(true) => Redirect::to("/").into_response(),
69        Ok(false) => next.run(request).await,
70        // If we can't tell whether the session is authenticated, don't
71        // assume it is: err toward letting the guest-only page render
72        // rather than silently bouncing to "/".
73        Err(error) => {
74            tracing::warn!(%error, "redirect_authenticated: failed to read session; allowing access");
75            next.run(request).await
76        }
77    }
78}
79
80/// [`redirect_authenticated`]'s named-guard counterpart - bounces a user
81/// already logged in as `U::GUARD` away from that guard's own guest-only
82/// routes (e.g. `/admin/login`), regardless of whether some other guard is
83/// also logged in.
84pub async fn redirect_authenticated_for<U: Authenticatable>(
85    session: Session,
86    request: Request,
87    next: Next,
88) -> Response {
89    match guard::check_for::<U>(&session).await {
90        Ok(true) => Redirect::to("/").into_response(),
91        Ok(false) => next.run(request).await,
92        Err(error) => {
93            tracing::warn!(%error, "redirect_authenticated_for: failed to read session; allowing access");
94            next.run(request).await
95        }
96    }
97}
98
99fn login_path() -> String {
100    larust_http::resolve_route_name("login").unwrap_or_else(|| {
101        tracing::warn!(
102            "require_auth: no route named `login` is registered; falling back to /login"
103        );
104        "/login".to_string()
105    })
106}
107
108/// [`login_path`]'s named-guard counterpart: tries `"{guard}.login"` first
109/// (e.g. `"admin.login"`), then falls back to plain `"login"`, then to a
110/// hardcoded `/login` - each fallback step logs a warning naming which
111/// route it expected.
112fn guard_login_path(guard: &str) -> String {
113    let guard_specific_name = format!("{guard}.login");
114    larust_http::resolve_route_name(&guard_specific_name).unwrap_or_else(|| {
115        tracing::warn!(
116            "require_auth_for::<{guard}>: no route named `{guard_specific_name}` is \
117             registered; falling back to the default `login` route"
118        );
119        login_path()
120    })
121}