Skip to main content

soma_auth/
sqlite_browser_sessions.rs

1//! Persistence for the interactive login-session tables: `browser_sessions`,
2//! `browser_login_states`, and `native_authorization_results`. Split out of
3//! `sqlite.rs` to stay under the PATTERNS.md module size hard limit.
4use rusqlite::{OptionalExtension, params};
5
6use crate::error::AuthError;
7use crate::types::{BrowserLoginStateRow, BrowserSessionRow, NativeAuthorizationResultRow};
8use crate::util::now_unix;
9
10use super::sqlite_rows::{
11    row_to_browser_login_state, row_to_browser_session, row_to_native_authorization_result,
12};
13use super::{SqliteStore, sqlite_error};
14
15impl SqliteStore {
16    pub async fn upsert_browser_session(
17        &self,
18        session: BrowserSessionRow,
19    ) -> Result<(), AuthError> {
20        self.with_conn(move |conn| {
21            conn.execute(
22                "INSERT INTO browser_sessions (
23                    session_id, subject, email, csrf_token, created_at, expires_at
24                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
25                 ON CONFLICT(session_id) DO UPDATE SET
26                    subject = excluded.subject,
27                    email = excluded.email,
28                    csrf_token = excluded.csrf_token,
29                    created_at = excluded.created_at,
30                    expires_at = excluded.expires_at",
31                params![
32                    session.session_id,
33                    session.subject,
34                    session.email,
35                    session.csrf_token,
36                    session.created_at,
37                    session.expires_at,
38                ],
39            )
40            .map_err(sqlite_error)?;
41            Ok(())
42        })
43        .await
44    }
45
46    pub async fn find_browser_session(
47        &self,
48        session_id: &str,
49    ) -> Result<Option<BrowserSessionRow>, AuthError> {
50        let session_id = session_id.to_string();
51        let now = now_unix();
52        self.with_conn(move |conn| {
53            conn.query_row(
54                "SELECT session_id, subject, email, csrf_token, created_at, expires_at
55                 FROM browser_sessions
56                 WHERE session_id = ?1
57                   AND expires_at > ?2",
58                params![session_id, now],
59                row_to_browser_session,
60            )
61            .optional()
62            .map_err(sqlite_error)
63        })
64        .await
65    }
66
67    pub async fn revoke_browser_session(&self, session_id: &str) -> Result<(), AuthError> {
68        let session_id = session_id.to_string();
69        self.with_conn(move |conn| {
70            conn.execute(
71                "DELETE FROM browser_sessions WHERE session_id = ?1",
72                params![session_id],
73            )
74            .map_err(sqlite_error)?;
75            Ok(())
76        })
77        .await
78    }
79
80    pub async fn insert_browser_login_state(
81        &self,
82        login: BrowserLoginStateRow,
83    ) -> Result<(), AuthError> {
84        self.with_conn(move |conn| {
85            conn.execute(
86                "INSERT INTO browser_login_states (
87                    state, return_to, provider_code_verifier, created_at, expires_at, provider
88                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
89                params![
90                    login.state,
91                    login.return_to,
92                    login.provider_code_verifier,
93                    login.created_at,
94                    login.expires_at,
95                    login.provider,
96                ],
97            )
98            .map_err(sqlite_error)?;
99            Ok(())
100        })
101        .await
102    }
103
104    pub async fn take_browser_login_state(
105        &self,
106        state: &str,
107    ) -> Result<Option<BrowserLoginStateRow>, AuthError> {
108        let state = state.to_string();
109        let now = now_unix();
110        self.with_conn(move |conn| {
111            conn.query_row(
112                "DELETE FROM browser_login_states
113                 WHERE state = ?1
114                   AND expires_at > ?2
115                 RETURNING state, return_to, provider_code_verifier, created_at, expires_at, provider",
116                params![state, now],
117                row_to_browser_login_state,
118            )
119            .optional()
120            .map_err(sqlite_error)
121        })
122        .await
123    }
124
125    /// Store a native-flow authorization code keyed by `state`, for the
126    /// polling desktop client to retrieve via `take_native_authorization_result`.
127    ///
128    /// Last-write-wins on a `state` collision (e.g. a client retrying
129    /// `/authorize` with the same `state` after a timeout): each row is
130    /// single-use (deleted on first successful poll), so overwriting with the
131    /// newest code is correct — silently dropping the newest code instead
132    /// (`DO NOTHING`) would leave the polling client hung until the row's TTL
133    /// expires, with no error surfaced anywhere.
134    pub async fn insert_native_authorization_result(
135        &self,
136        result: NativeAuthorizationResultRow,
137    ) -> Result<(), AuthError> {
138        self.with_conn(move |conn| {
139            conn.execute(
140                "INSERT INTO native_authorization_results (state, code, created_at, expires_at)
141                 VALUES (?1, ?2, ?3, ?4)
142                 ON CONFLICT(state) DO UPDATE SET
143                    code = excluded.code,
144                    created_at = excluded.created_at,
145                    expires_at = excluded.expires_at",
146                params![
147                    result.state,
148                    result.code,
149                    result.created_at,
150                    result.expires_at,
151                ],
152            )
153            .map_err(sqlite_error)?;
154            Ok(())
155        })
156        .await
157    }
158
159    /// One-shot read-and-delete of a pending native-flow authorization code.
160    pub async fn take_native_authorization_result(
161        &self,
162        state: &str,
163    ) -> Result<Option<NativeAuthorizationResultRow>, AuthError> {
164        let state = state.to_string();
165        let now = now_unix();
166        self.with_conn(move |conn| {
167            conn.query_row(
168                "DELETE FROM native_authorization_results
169                 WHERE state = ?1
170                   AND expires_at > ?2
171                 RETURNING state, code, created_at, expires_at",
172                params![state, now],
173                row_to_native_authorization_result,
174            )
175            .optional()
176            .map_err(sqlite_error)
177        })
178        .await
179    }
180}