1use time::{Duration, OffsetDateTime};
4
5use serde::Serialize;
6
7use crate::context::AuthContext;
8use crate::cookies::{
9 delete_session_cookie, get_cookie_cache, get_session_cookie, parse_cookies, set_cookie_cache,
10 set_session_cookie, verify_cookie_value, Cookie, CookieCachePayload, CookieOptions,
11 SessionCookieOptions, SECURE_COOKIE_PREFIX,
12};
13use crate::db::{Session, User};
14use crate::error::RustAuthError;
15use crate::session::SessionStore;
16use crate::user::DbUserStore;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct GetSessionInput {
20 pub cookie_header: String,
21 pub disable_cookie_cache: bool,
22 pub disable_refresh: bool,
23 pub defer_refresh: bool,
24}
25
26impl GetSessionInput {
27 pub fn new(cookie_header: impl Into<String>) -> Self {
28 Self {
29 cookie_header: cookie_header.into(),
30 disable_cookie_cache: false,
31 disable_refresh: false,
32 defer_refresh: false,
33 }
34 }
35
36 #[must_use]
37 pub fn disable_cookie_cache(mut self) -> Self {
38 self.disable_cookie_cache = true;
39 self
40 }
41
42 #[must_use]
43 pub fn disable_refresh(mut self) -> Self {
44 self.disable_refresh = true;
45 self
46 }
47
48 #[must_use]
49 pub fn defer_refresh(mut self) -> Self {
50 self.defer_refresh = true;
51 self
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct GetSessionResult {
57 pub session: Option<Session>,
58 pub user: Option<User>,
59 pub cookies: Vec<Cookie>,
60 pub needs_refresh: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64pub struct SignOutResult {
65 pub success: bool,
66 #[serde(skip)]
67 pub cookies: Vec<Cookie>,
68}
69
70#[derive(Clone, Copy)]
71pub struct SessionAuth<'a> {
72 context: &'a AuthContext,
73}
74
75impl<'a> SessionAuth<'a> {
76 pub fn new(context: &'a AuthContext) -> Result<Self, RustAuthError> {
77 context.adapter_ref()?;
78 Ok(Self { context })
79 }
80
81 pub async fn get_session(
82 &self,
83 input: GetSessionInput,
84 ) -> Result<Option<GetSessionResult>, RustAuthError> {
85 let signed_token = match get_session_cookie(
86 &input.cookie_header,
87 cookie_prefix(self.context),
88 None,
89 secure_cookies(self.context),
90 ) {
91 Some(value) => value,
92 None => return Ok(None),
93 };
94 let Some(token) = verify_cookie_value(&signed_token, &self.context.secret)? else {
95 return Ok(Some(unauthenticated(delete_session_cookie(
96 &self.context.auth_cookies,
97 &input.cookie_header,
98 false,
99 ))));
100 };
101
102 let session_store = SessionStore::new(self.context)?;
103 if self.context.options.session.cookie_cache.enabled && !input.disable_cookie_cache {
104 if let Some(cached) = get_cookie_cache::<Session, User>(
105 &input.cookie_header,
106 &self.context.auth_cookies.session_data.name,
107 &self.context.secret,
108 self.context.options.session.cookie_cache.strategy,
109 self.context.options.session.cookie_cache.version.as_deref(),
110 )? {
111 if cached.session.token == token
112 && cached.session.expires_at > OffsetDateTime::now_utc()
113 {
114 if session_store.find_session(&token).await?.is_none() {
115 return Ok(Some(unauthenticated(delete_session_cookie(
116 &self.context.auth_cookies,
117 &input.cookie_header,
118 false,
119 ))));
120 }
121 return Ok(Some(authenticated(
122 cached.session,
123 cached.user,
124 Vec::new(),
125 false,
126 )));
127 }
128 }
129 }
130
131 let Some(mut session) = session_store.find_session(&token).await? else {
132 return Ok(Some(unauthenticated(delete_session_cookie(
133 &self.context.auth_cookies,
134 &input.cookie_header,
135 false,
136 ))));
137 };
138
139 let user_store = DbUserStore::from_context(self.context)?;
140 let Some(user) = user_store.find_user_by_id(&session.user_id).await? else {
141 return Ok(Some(unauthenticated(delete_session_cookie(
142 &self.context.auth_cookies,
143 &input.cookie_header,
144 false,
145 ))));
146 };
147
148 let dont_remember = signed_cookie(
149 &input.cookie_header,
150 &self.context.auth_cookies.dont_remember_token.name,
151 &self.context.secret,
152 )?
153 .is_some();
154 let needs_refresh = !dont_remember
155 && !input.disable_refresh
156 && !self.context.options.session.disable_session_refresh
157 && session_needs_refresh(&session, self.context);
158 let mut cookies = Vec::new();
159
160 if needs_refresh && !input.defer_refresh {
161 let refreshed_expires_at = OffsetDateTime::now_utc()
162 + Duration::seconds(self.context.session_config.expires_in.whole_seconds());
163 if let Some(updated_session) = session_store
164 .update_session_expiry(&session.token, refreshed_expires_at)
165 .await?
166 {
167 session = updated_session;
168 cookies.extend(set_session_cookie(
169 &self.context.auth_cookies,
170 &self.context.secret,
171 &session.token,
172 SessionCookieOptions {
173 dont_remember: false,
174 overrides: CookieOptions {
175 max_age: seconds_until(session.expires_at),
176 ..CookieOptions::default()
177 },
178 },
179 )?);
180 } else {
181 return Ok(Some(unauthenticated(delete_session_cookie(
182 &self.context.auth_cookies,
183 &input.cookie_header,
184 false,
185 ))));
186 }
187 }
188
189 if self.context.options.session.cookie_cache.enabled {
190 cookies.extend(self.cookie_cache_cookies(&session, &user)?);
191 }
192
193 Ok(Some(authenticated(session, user, cookies, needs_refresh)))
194 }
195
196 pub async fn sign_out(
197 &self,
198 cookie_header: impl AsRef<str>,
199 ) -> Result<SignOutResult, RustAuthError> {
200 let cookie_header = cookie_header.as_ref();
201 if let Some(signed_token) = get_session_cookie(
202 cookie_header,
203 cookie_prefix(self.context),
204 None,
205 secure_cookies(self.context),
206 ) {
207 if let Some(token) = verify_cookie_value(&signed_token, &self.context.secret)? {
208 SessionStore::new(self.context)?
209 .delete_session(&token)
210 .await?;
211 }
212 }
213
214 Ok(SignOutResult {
215 success: true,
216 cookies: delete_session_cookie(&self.context.auth_cookies, cookie_header, false),
217 })
218 }
219
220 fn cookie_cache_cookies(
221 &self,
222 session: &Session,
223 user: &User,
224 ) -> Result<Vec<Cookie>, RustAuthError> {
225 let payload = CookieCachePayload {
226 session: session.clone(),
227 user: user.clone(),
228 updated_at: OffsetDateTime::now_utc().unix_timestamp(),
229 version: self
230 .context
231 .options
232 .session
233 .cookie_cache
234 .version
235 .clone()
236 .unwrap_or_else(|| "1".to_owned()),
237 };
238 let max_age = self
239 .context
240 .options
241 .session
242 .cookie_cache
243 .max_age
244 .unwrap_or(time::Duration::minutes(5));
245 set_cookie_cache(
246 &self.context.auth_cookies,
247 &self.context.secret,
248 &payload,
249 self.context.options.session.cookie_cache.strategy,
250 max_age.whole_seconds() as u64,
251 )
252 }
253}
254
255fn cookie_prefix(context: &AuthContext) -> Option<&str> {
256 context.options.advanced.cookie_prefix.as_deref()
257}
258
259fn secure_cookies(context: &AuthContext) -> bool {
260 context
261 .auth_cookies
262 .session_token
263 .name
264 .starts_with(SECURE_COOKIE_PREFIX)
265}
266
267fn signed_cookie(
268 cookie_header: &str,
269 cookie_name: &str,
270 secret: &str,
271) -> Result<Option<String>, RustAuthError> {
272 let Some(value) = parse_cookies(cookie_header).get(cookie_name).cloned() else {
273 return Ok(None);
274 };
275 verify_cookie_value(&value, secret)
276}
277
278fn session_needs_refresh(session: &Session, context: &AuthContext) -> bool {
279 if context.options.session.cookie_cache.refresh_cache {
280 return false;
281 }
282 let due_at = session.expires_at
283 - Duration::seconds(context.session_config.expires_in.whole_seconds())
284 + context.session_config.update_age;
285 due_at <= OffsetDateTime::now_utc()
286}
287
288fn seconds_until(expires_at: OffsetDateTime) -> Option<u64> {
289 let seconds = (expires_at - OffsetDateTime::now_utc()).whole_seconds();
290 u64::try_from(seconds).ok()
291}
292
293fn authenticated(
294 session: Session,
295 user: User,
296 cookies: Vec<Cookie>,
297 needs_refresh: bool,
298) -> GetSessionResult {
299 GetSessionResult {
300 session: Some(session),
301 user: Some(user),
302 cookies,
303 needs_refresh,
304 }
305}
306
307fn unauthenticated(cookies: Vec<Cookie>) -> GetSessionResult {
308 GetSessionResult {
309 session: None,
310 user: None,
311 cookies,
312 needs_refresh: false,
313 }
314}