Skip to main content

alibi_sqlx/store/
oauth_token_conversion.rs

1use super::SqlxStore;
2use crate::pool::Engine;
3use crate::{
4    model::SqlxModel,
5    pool::Exec,
6    schema::{AuthSchema, SqlxAccountModel},
7    sql::Sql,
8};
9use alibi_core::{
10    AuthError, AuthResult,
11    oauth_token_conversion::{OAuthTokenConversionStore, OAuthTokenSnapshot, OAuthTokenValues},
12};
13use async_trait::async_trait;
14
15#[async_trait]
16impl<S> OAuthTokenConversionStore<S> for SqlxStore<S>
17where
18    S: AuthSchema + Send + Sync,
19    S::Account: SqlxAccountModel,
20{
21    async fn compare_and_swap_oauth_tokens(
22        &self,
23        observed: &OAuthTokenSnapshot,
24        replacement: &OAuthTokenValues,
25    ) -> AuthResult<bool> {
26        let columns = S::Account::oauth_token_columns().ok_or_else(|| {
27            AuthError::NotImplemented("Account token conversion columns are not configured".into())
28        })?;
29        let table = <S::Account as SqlxModel>::TABLE;
30        let mut sql = Sql::with(self.exec().engine(), "UPDATE ");
31        sql.ident(table);
32        sql.push(" SET ");
33        for (index, (column, value)) in columns
34            .iter()
35            .zip([
36                &replacement.access_token,
37                &replacement.refresh_token,
38                &replacement.id_token,
39            ])
40            .enumerate()
41        {
42            if index > 0 {
43                sql.push(", ");
44            }
45            sql.assign(column, value.clone());
46        }
47        sql.push(" WHERE ");
48        // Validate application ID types, then compare their canonical physical
49        // text too: an application NOCASE collation must not admit another owner.
50        _ = S::Account::parse_id(&observed.id)?;
51        _ = S::Account::parse_user_id(&observed.user_id)?;
52        for (index, (column, value)) in [
53            (S::Account::id_column(), &observed.id),
54            (S::Account::user_id_column(), &observed.user_id),
55            (S::Account::provider_id_column(), &observed.provider_id),
56            (S::Account::account_id_column(), &observed.account_id),
57        ]
58        .into_iter()
59        .enumerate()
60        {
61            if index > 0 {
62                sql.push(" AND ");
63            }
64            sql.push("CAST(");
65            sql.column(table, column);
66            sql.push(" AS TEXT)");
67            exact_collation(&mut sql);
68            sql.push(" = ");
69            sql.bind(value.clone());
70        }
71        for (column, value) in columns.iter().zip([
72            &observed.tokens.access_token,
73            &observed.tokens.refresh_token,
74            &observed.tokens.id_token,
75        ]) {
76            sql.push(" AND ");
77            // Override application collations: CAS compares the exact stored text.
78            sql.column(table, column);
79            exact_collation(&mut sql);
80            if let Some(value) = value {
81                sql.push(" = ");
82                sql.bind(value.clone());
83            } else {
84                sql.push(" IS NULL");
85            }
86        }
87        self.in_transaction(true, async move |tx| {
88            Ok(Exec::tx(tx).execute(sql).await? == 1)
89        })
90        .await
91    }
92}
93
94fn exact_collation(sql: &mut Sql) {
95    match sql.engine() {
96        Engine::Sqlite => sql.push(" COLLATE BINARY"),
97        Engine::Postgres => sql.push(" COLLATE \"C\""),
98    }
99}