1use crate::config::AuthConfig;
4use crate::identity::AuthIdentity;
5use crate::strategy::AuthStrategy;
6use async_trait::async_trait;
7use doido_controller::session::{EncryptedCookieSessionStore, Session};
8use doido_core::Result;
9use doido_model::sea_orm::DatabaseConnection;
10use http::header;
11use http::request::Parts;
12
13pub const USER_ID_KEY: &str = "user_id";
15
16pub const SIGNED_IN_AT_KEY: &str = "signed_in_at";
18
19pub const SESSION_COOKIE: &str = "_doido_session";
21
22pub struct SessionStrategy {
24 store: EncryptedCookieSessionStore,
25}
26
27impl SessionStrategy {
28 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
29 Self {
30 store: EncryptedCookieSessionStore::new(secret),
31 }
32 }
33
34 pub fn default_dev() -> Self {
35 Self {
36 store: EncryptedCookieSessionStore::default(),
37 }
38 }
39
40 fn session_from_parts(&self, parts: &Parts) -> Option<Session> {
41 let header = parts.headers.get(header::COOKIE)?.to_str().ok()?;
42 let raw = header
43 .split(';')
44 .filter_map(|pair| pair.trim().split_once('='))
45 .find(|(k, _)| *k == SESSION_COOKIE)
46 .map(|(_, v)| v.to_string())?;
47 self.store.decode(&raw)
48 }
49}
50
51#[async_trait]
52impl AuthStrategy for SessionStrategy {
53 fn name(&self) -> &str {
54 "cookie"
55 }
56
57 async fn authenticate(
58 &self,
59 parts: &Parts,
60 _db: &DatabaseConnection,
61 ) -> Result<Option<AuthIdentity>> {
62 let session = match self.session_from_parts(parts) {
63 Some(s) => s,
64 None => return Ok(None),
65 };
66 let user_id = match session.data.get(USER_ID_KEY) {
67 Some(value) => value.clone(),
68 None => return Ok(None),
69 };
70 if user_id.is_null() {
71 return Ok(None);
72 }
73 if is_session_expired(&session) {
75 return Ok(None);
76 }
77 Ok(Some(AuthIdentity { user_id }))
78 }
79}
80
81pub fn sign_in_session(session: &mut Session, user_id: impl serde::Serialize) {
84 session.set(USER_ID_KEY, user_id);
85 session.set(SIGNED_IN_AT_KEY, chrono::Utc::now().timestamp());
86}
87
88pub fn is_session_expired(session: &Session) -> bool {
93 let state = match crate::state::try_global() {
94 Some(state) => state,
95 None => return false,
96 };
97 if !state
98 .config
99 .has_module(crate::config::AuthModule::Timeoutable)
100 {
101 return false;
102 }
103 match session.data.get(SIGNED_IN_AT_KEY).and_then(|v| v.as_i64()) {
104 Some(signed_in_at) => {
105 chrono::Utc::now().timestamp() - signed_in_at > state.config.timeout as i64
106 }
107 None => false,
108 }
109}
110
111pub fn sign_out_session(session: &mut Session) {
113 if session.data.is_object() {
114 if let serde_json::Value::Object(map) = &mut session.data {
115 map.remove(USER_ID_KEY);
116 }
117 }
118}
119
120pub fn encode_session_cookie(store: &EncryptedCookieSessionStore, session: &Session) -> String {
122 let value = store.encode(session);
123 format!("{SESSION_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax")
124}
125
126pub fn clear_session_cookie() -> String {
128 format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax")
129}
130
131pub fn from_config(_config: &AuthConfig) -> SessionStrategy {
133 SessionStrategy::new(doido_controller::secret::key_base())
134}