Skip to main content

soma_auth/
sqlite_assertions.rs

1use rusqlite::params;
2
3use crate::error::AuthError;
4
5use super::{SqliteStore, hash_token, sqlite_error};
6
7const MAX_ASSERTION_ISSUER_BYTES: usize = 2048;
8const MAX_ASSERTION_JTI_BYTES: usize = 256;
9const MAX_ASSERTION_LIFETIME_SECS: i64 = 300;
10const MAX_ASSERTION_CLOCK_SKEW_SECS: i64 = 60;
11
12impl SqliteStore {
13    /// Consume one JWT assertion identifier exactly once.
14    ///
15    /// The check and insert are atomic across processes sharing this database.
16    pub async fn consume_assertion_jti(
17        &self,
18        issuer: &str,
19        jti: &str,
20        issued_at: i64,
21        expires_at: i64,
22        now: i64,
23    ) -> Result<bool, AuthError> {
24        if issuer.is_empty() || issuer.len() > MAX_ASSERTION_ISSUER_BYTES {
25            return Ok(false);
26        }
27        if jti.is_empty() || jti.len() > MAX_ASSERTION_JTI_BYTES {
28            return Ok(false);
29        }
30        if issued_at > now + MAX_ASSERTION_CLOCK_SKEW_SECS
31            || expires_at <= now
32            || expires_at.saturating_sub(issued_at) > MAX_ASSERTION_LIFETIME_SECS
33        {
34            return Ok(false);
35        }
36
37        let issuer = issuer.to_owned();
38        let jti = jti.to_owned();
39        self.with_conn(move |conn| {
40            conn.execute_batch("BEGIN IMMEDIATE")
41                .map_err(sqlite_error)?;
42            let result = (|| {
43                conn.execute(
44                    "DELETE FROM assertion_jtis WHERE expires_at <= ?1",
45                    params![now],
46                )
47                .map_err(sqlite_error)?;
48                conn.execute(
49                    "INSERT OR IGNORE INTO assertion_jtis (issuer, jti, expires_at)
50                     VALUES (?1, ?2, ?3)",
51                    params![issuer, jti, expires_at],
52                )
53                .map(|inserted| inserted == 1)
54                .map_err(sqlite_error)
55            })();
56            match result {
57                Ok(consumed) => {
58                    conn.execute_batch("COMMIT").map_err(sqlite_error)?;
59                    Ok(consumed)
60                }
61                Err(error) => {
62                    let _ = conn.execute_batch("ROLLBACK");
63                    Err(error)
64                }
65            }
66        })
67        .await
68    }
69
70    /// Revoke one refresh token only when it belongs to the authenticated client.
71    pub async fn revoke_refresh_token(
72        &self,
73        refresh_token: &str,
74        client_id: &str,
75    ) -> Result<bool, AuthError> {
76        let token_hash = hash_token(refresh_token);
77        let client_id = client_id.to_owned();
78        self.with_conn(move |conn| {
79            conn.execute(
80                "DELETE FROM refresh_tokens
81                 WHERE refresh_token_hash = ?1 AND client_id = ?2",
82                params![token_hash, client_id],
83            )
84            .map(|deleted| deleted == 1)
85            .map_err(sqlite_error)
86        })
87        .await
88    }
89}
90
91#[cfg(test)]
92#[path = "sqlite_assertions_tests.rs"]
93mod tests;