Skip to main content

rustauth_core/auth/
session.rs

1//! Core session resolution and sign-out behavior.
2
3use 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}