Skip to main content

rustauth_core/api/
middleware.rs

1use http::header;
2use http::StatusCode;
3use time::OffsetDateTime;
4
5use crate::api::response_helpers::json_response;
6use crate::api::{ApiErrorResponse, EndpointMiddleware, PathParams};
7use crate::auth::session::{GetSessionInput, SessionAuth};
8use crate::context::request_state::current_session;
9use crate::db::{DbValue, FindOne, Where};
10use crate::error::RustAuthError;
11use crate::error_codes;
12
13/// Require the current request's session to be within `SessionOptions::fresh_age`.
14pub fn fresh_session_middleware() -> EndpointMiddleware {
15    EndpointMiddleware::new(|context, _request| {
16        Box::pin(async move {
17            let Some(current) = current_session()? else {
18                return Ok(None);
19            };
20            if context.session_config.fresh_age.is_zero() {
21                return Ok(None);
22            }
23            let age = OffsetDateTime::now_utc() - current.session.created_at;
24            if age < context.session_config.fresh_age {
25                return Ok(None);
26            }
27            json_response(
28                StatusCode::BAD_REQUEST,
29                &ApiErrorResponse {
30                    code: error_codes::SESSION_EXPIRED.to_owned(),
31                    message: "Session expired".to_owned(),
32                    original_message: None,
33                },
34                Vec::new(),
35            )
36            .map(Some)
37        })
38    })
39}
40
41/// Require the resource identified by a path param to belong to the current user.
42pub fn require_resource_ownership(
43    model: impl Into<String>,
44    resource_id_param: impl Into<String>,
45    owner_field: impl Into<String>,
46) -> EndpointMiddleware {
47    let model = model.into();
48    let resource_id_param = resource_id_param.into();
49    let owner_field = owner_field.into();
50    EndpointMiddleware::new(move |context, request| {
51        let model = model.clone();
52        let resource_id_param = resource_id_param.clone();
53        let owner_field = owner_field.clone();
54        Box::pin(async move {
55            let resource_id = request
56                .extensions()
57                .get::<PathParams>()
58                .and_then(|params| params.get(&resource_id_param))
59                .ok_or(RustAuthError::MissingPathParam {
60                    name: resource_id_param,
61                })?;
62            let Some(adapter) = context.adapter() else {
63                return Err(RustAuthError::InvalidConfig(
64                    "resource ownership middleware requires an adapter".to_owned(),
65                ));
66            };
67            let cookie_header = request
68                .headers()
69                .get(header::COOKIE)
70                .and_then(|value| value.to_str().ok())
71                .unwrap_or_default()
72                .to_owned();
73            let Some(result) = SessionAuth::new(context)?
74                .get_session(GetSessionInput::new(cookie_header))
75                .await?
76            else {
77                return unauthorized_response().map(Some);
78            };
79            let Some(user) = result.user else {
80                return unauthorized_response().map(Some);
81            };
82            let record = adapter
83                .find_one(
84                    FindOne::new(&model)
85                        .where_clause(Where::new("id", DbValue::String(resource_id.to_owned()))),
86                )
87                .await?;
88            let owns_resource = record.and_then(|record| record.get(&owner_field).cloned())
89                == Some(DbValue::String(user.id));
90            if owns_resource {
91                return Ok(None);
92            }
93            forbidden_response().map(Some)
94        })
95    })
96}
97
98fn unauthorized_response() -> Result<crate::api::ApiResponse, RustAuthError> {
99    json_response(
100        StatusCode::UNAUTHORIZED,
101        &ApiErrorResponse {
102            code: "UNAUTHORIZED".to_owned(),
103            message: "Authentication required".to_owned(),
104            original_message: None,
105        },
106        Vec::new(),
107    )
108}
109
110fn forbidden_response() -> Result<crate::api::ApiResponse, RustAuthError> {
111    json_response(
112        StatusCode::FORBIDDEN,
113        &ApiErrorResponse {
114            code: "FORBIDDEN".to_owned(),
115            message: "Forbidden".to_owned(),
116            original_message: None,
117        },
118        Vec::new(),
119    )
120}
121
122pub(crate) fn ensure_fresh_session(
123    context: &crate::context::AuthContext,
124    session: &crate::db::Session,
125) -> Result<(), RustAuthError> {
126    if context.session_config.fresh_age.is_zero() {
127        return Ok(());
128    }
129    let age = OffsetDateTime::now_utc() - session.created_at;
130    if age >= context.session_config.fresh_age {
131        return Err(RustAuthError::Api(error_codes::SESSION_EXPIRED.to_owned()));
132    }
133    Ok(())
134}