Skip to main content

alibi_sqlx/store/
oauth_token_conversion.rs

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