Skip to main content

doido_auth/
rememberable.rs

1//! `rememberable` module — a persistent "remember me" cookie that signs the user
2//! back in across browser sessions (the Devise `rememberable` analogue). Stamps
3//! `remember_created_at` and issues a signed, `Max-Age`-bearing cookie; a
4//! [`RememberStrategy`] resolves it on later requests when no session is present.
5
6use crate::config::AuthModule;
7use crate::error::AuthError;
8use crate::identity::AuthIdentity;
9use crate::strategy::AuthStrategy;
10use async_trait::async_trait;
11use doido_controller::CookieJar;
12use doido_core::Result;
13use doido_model::sea_orm::{ConnectionTrait, DatabaseConnection, DbBackend, Statement, Value};
14use http::header;
15use http::request::Parts;
16
17/// Signed cookie holding the remembered user id.
18pub const REMEMBER_COOKIE: &str = "_doido_remember";
19
20fn enabled() -> bool {
21    matches!(crate::state::try_global(), Some(state) if state.config.has_module(AuthModule::Rememberable))
22}
23
24/// The signed cookie value for a remembered user id (its JSON encoding, so it
25/// round-trips back to `AuthUser::Id`).
26pub fn cookie_value(user_id: &impl serde::Serialize) -> String {
27    serde_json::to_string(user_id).unwrap_or_default()
28}
29
30/// Stamp `remember_created_at` for `email`. No-op when the module is disabled.
31pub async fn record_remember(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
32    if !enabled() {
33        return Ok(());
34    }
35    db.execute_raw(Statement::from_sql_and_values(
36        DbBackend::Sqlite,
37        "UPDATE users SET remember_created_at = ? WHERE email = ?",
38        [
39            Value::from(chrono::Utc::now().to_rfc3339()),
40            Value::from(email.to_string()),
41        ],
42    ))
43    .await
44    .map_err(|e| AuthError::Internal(e.to_string()))?;
45    Ok(())
46}
47
48/// Clear `remember_created_at` for `email` (on sign-out). No-op when disabled.
49pub async fn forget(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
50    if !enabled() {
51        return Ok(());
52    }
53    db.execute_raw(Statement::from_sql_and_values(
54        DbBackend::Sqlite,
55        "UPDATE users SET remember_created_at = NULL WHERE email = ?",
56        [Value::from(email.to_string())],
57    ))
58    .await
59    .map_err(|e| AuthError::Internal(e.to_string()))?;
60    Ok(())
61}
62
63/// Auth strategy that resolves an identity from the signed remember cookie. Added
64/// to the strategy chain automatically when the `rememberable` module is enabled,
65/// so it only runs after the session/JWT strategies decline.
66pub struct RememberStrategy;
67
68#[async_trait]
69impl AuthStrategy for RememberStrategy {
70    fn name(&self) -> &str {
71        "remember"
72    }
73
74    async fn authenticate(
75        &self,
76        parts: &Parts,
77        _db: &DatabaseConnection,
78    ) -> Result<Option<AuthIdentity>> {
79        let header = parts
80            .headers
81            .get(header::COOKIE)
82            .and_then(|v| v.to_str().ok());
83        let jar = CookieJar::from_header(header, doido_controller::secret::key_base());
84        match jar.get_signed(REMEMBER_COOKIE) {
85            Some(raw) => {
86                let user_id = serde_json::from_str::<serde_json::Value>(&raw)
87                    .unwrap_or(serde_json::Value::String(raw));
88                Ok(Some(AuthIdentity { user_id }))
89            }
90            None => Ok(None),
91        }
92    }
93}