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}