rustauth_core/api/
middleware.rs1use 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
13pub 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
41pub 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}