Microservice to bring 2FA to self hosted PDSes
1use jacquard_common::IntoStatic;
2use jacquard_common::session::SessionStoreError;
3use jacquard_common::types::did::Did;
4use jacquard_oauth::authstore::ClientAuthStore;
5use jacquard_oauth::session::{AuthRequestData, ClientSessionData};
6use sqlx::SqlitePool;
7
8fn sqlx_to_session_err(e: sqlx::Error) -> SessionStoreError {
9 SessionStoreError::Other(Box::new(e))
10}
11
12#[derive(Clone)]
13pub struct SqlAuthStore {
14 pool: SqlitePool,
15}
16
17impl SqlAuthStore {
18 pub fn new(pool: SqlitePool) -> Self {
19 Self { pool }
20 }
21}
22
23impl ClientAuthStore for SqlAuthStore {
24 async fn get_session(
25 &self,
26 did: &Did<'_>,
27 session_id: &str,
28 ) -> Result<Option<ClientSessionData<'_>>, SessionStoreError> {
29 let key = format!("{}_{}", did, session_id);
30
31 let row: Option<(String,)> =
32 sqlx::query_as("SELECT data FROM oauth_client_sessions WHERE session_key = ?")
33 .bind(&key)
34 .fetch_optional(&self.pool)
35 .await
36 .map_err(sqlx_to_session_err)?;
37
38 match row {
39 Some((json_data,)) => {
40 let session: ClientSessionData<'_> = serde_json::from_str(&json_data)?;
41 Ok(Some(session.into_static()))
42 }
43 None => Ok(None),
44 }
45 }
46
47 async fn upsert_session(
48 &self,
49 session: ClientSessionData<'_>,
50 ) -> Result<(), SessionStoreError> {
51 let static_session = session.into_static();
52 let did = static_session.account_did.to_string();
53 let session_id = static_session.session_id.to_string();
54 let key = format!("{}_{}", did, session_id);
55 let json_data = serde_json::to_string(&static_session)?;
56 let now = chrono::Utc::now().to_rfc3339();
57
58 sqlx::query(
59 "INSERT INTO oauth_client_sessions (session_key, did, session_id, data, created_at, updated_at)
60 VALUES (?, ?, ?, ?, ?, ?)
61 ON CONFLICT(session_key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at",
62 )
63 .bind(&key)
64 .bind(&did)
65 .bind(&session_id)
66 .bind(&json_data)
67 .bind(&now)
68 .bind(&now)
69 .execute(&self.pool)
70 .await
71 .map_err(sqlx_to_session_err)?;
72
73 Ok(())
74 }
75
76 async fn delete_session(
77 &self,
78 did: &Did<'_>,
79 session_id: &str,
80 ) -> Result<(), SessionStoreError> {
81 let key = format!("{}_{}", did, session_id);
82
83 sqlx::query("DELETE FROM oauth_client_sessions WHERE session_key = ?")
84 .bind(&key)
85 .execute(&self.pool)
86 .await
87 .map_err(sqlx_to_session_err)?;
88
89 Ok(())
90 }
91
92 async fn get_auth_req_info(
93 &self,
94 state: &str,
95 ) -> Result<Option<AuthRequestData<'_>>, SessionStoreError> {
96 let row: Option<(String,)> =
97 sqlx::query_as("SELECT data FROM oauth_auth_requests WHERE state = ?")
98 .bind(state)
99 .fetch_optional(&self.pool)
100 .await
101 .map_err(sqlx_to_session_err)?;
102
103 match row {
104 Some((json_data,)) => {
105 let auth_req: AuthRequestData<'_> = serde_json::from_str(&json_data)?;
106 Ok(Some(auth_req.into_static()))
107 }
108 None => Ok(None),
109 }
110 }
111
112 async fn save_auth_req_info(
113 &self,
114 auth_req_info: &AuthRequestData<'_>,
115 ) -> Result<(), SessionStoreError> {
116 let static_info = auth_req_info.clone().into_static();
117 let state = static_info.state.to_string();
118 let json_data = serde_json::to_string(&static_info)?;
119 let now = chrono::Utc::now().to_rfc3339();
120
121 sqlx::query("INSERT INTO oauth_auth_requests (state, data, created_at) VALUES (?, ?, ?)")
122 .bind(&state)
123 .bind(&json_data)
124 .bind(&now)
125 .execute(&self.pool)
126 .await
127 .map_err(sqlx_to_session_err)?;
128
129 Ok(())
130 }
131
132 async fn delete_auth_req_info(&self, state: &str) -> Result<(), SessionStoreError> {
133 sqlx::query("DELETE FROM oauth_auth_requests WHERE state = ?")
134 .bind(state)
135 .execute(&self.pool)
136 .await
137 .map_err(sqlx_to_session_err)?;
138
139 Ok(())
140 }
141}
142
143/// Delete auth requests older than the given number of minutes.
144pub async fn cleanup_stale_auth_requests(
145 pool: &SqlitePool,
146 max_age_minutes: i64,
147) -> Result<u64, sqlx::Error> {
148 let cutoff = (chrono::Utc::now() - chrono::Duration::minutes(max_age_minutes)).to_rfc3339();
149 let result = sqlx::query("DELETE FROM oauth_auth_requests WHERE created_at < ?")
150 .bind(&cutoff)
151 .execute(pool)
152 .await?;
153 Ok(result.rows_affected())
154}