Skip to main content

alibi_plugins/oauth_provider/
client_secret.rs

1//! Application-owned client-secret hashing shared by registration, rotation and authentication.
2use alibi_core::{AuthContext, AuthResult, AuthSchema};
3use async_trait::async_trait;
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use sha2::{Digest as _, Sha256};
6use subtle::ConstantTimeEq;
7
8/// Custom client-secret hash storage, matching `storeClientSecret.hash` and optional `verify`.
9#[async_trait]
10pub trait OAuthClientSecretHasher: Send + Sync + std::fmt::Debug {
11    async fn hash(&self, secret: &str) -> AuthResult<String>;
12    /// Override verification for application-owned formats or password hashing.
13    /// The default hashes the presented secret and compares it in constant time.
14    async fn verify(&self, secret: &str, stored: &str) -> AuthResult<bool> {
15        let hash = self.hash(secret).await?;
16        Ok(bool::from(hash.as_bytes().ct_eq(stored.as_bytes())))
17    }
18}
19
20/// Custom reversible client-secret storage, matching `storeClientSecret.encrypt` and `decrypt`.
21#[async_trait]
22pub trait OAuthClientSecretCipher: Send + Sync + std::fmt::Debug {
23    async fn encrypt(&self, secret: &str) -> AuthResult<String>;
24    async fn decrypt(&self, stored: &str) -> AuthResult<String>;
25}
26
27pub(super) async fn store<S: AuthSchema>(
28    ctx: &AuthContext<S>,
29    config: &super::OAuthProviderConfig,
30    secret: &str,
31) -> AuthResult<String> {
32    if let Some(hasher) = &config.client_secret_hasher {
33        return hasher.hash(secret).await;
34    }
35    if let Some(cipher) = &config.client_secret_cipher {
36        return cipher.encrypt(secret).await;
37    }
38    if config.disable_jwt_plugin {
39        crate::token_crypto::encrypt_with_config(secret, &ctx.config)
40    } else {
41        Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(secret)))
42    }
43}
44
45pub(super) async fn verify<S: AuthSchema>(
46    ctx: &AuthContext<S>,
47    config: &super::OAuthProviderConfig,
48    secret: &str,
49    stored: &str,
50) -> AuthResult<bool> {
51    if secret.is_empty() {
52        return Ok(false);
53    }
54    if let Some(hasher) = &config.client_secret_hasher {
55        return hasher.verify(secret, stored).await;
56    }
57    if let Some(cipher) = &config.client_secret_cipher {
58        let plaintext = cipher.decrypt(stored).await?;
59        return Ok(bool::from(plaintext.as_bytes().ct_eq(secret.as_bytes())));
60    }
61    if config.disable_jwt_plugin {
62        Ok(
63            crate::token_crypto::decrypt_with_config(stored, &ctx.config)
64                .is_ok_and(|plaintext| bool::from(plaintext.as_bytes().ct_eq(secret.as_bytes()))),
65        )
66    } else {
67        let hash = URL_SAFE_NO_PAD.encode(Sha256::digest(secret));
68        Ok(bool::from(hash.as_bytes().ct_eq(stored.as_bytes())))
69    }
70}
71
72pub(super) async fn decrypt<S: AuthSchema>(
73    ctx: &AuthContext<S>,
74    config: &super::OAuthProviderConfig,
75    stored: &str,
76) -> AuthResult<String> {
77    if let Some(cipher) = &config.client_secret_cipher {
78        return cipher.decrypt(stored).await;
79    }
80    crate::token_crypto::decrypt_with_config(stored, &ctx.config)
81}