Skip to main content

better_auth_api/plugins/oauth/
encryption.rs

1//! Upstream-compatible OAuth token encryption and plaintext transition behavior.
2
3use better_auth_core::{AuthContext, AuthResult, AuthSchema, SecretKey};
4
5/// Encrypt a token with Better Auth's shared record codec.
6pub fn encrypt_token<'a>(plaintext: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
7    crate::plugins::symmetric::encrypt(secret, plaintext)
8}
9
10/// Decrypt an encrypted token with its configured key.
11pub fn decrypt_token<'a>(encrypted: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
12    crate::plugins::symmetric::decrypt(secret, encrypted)
13}
14
15/// Preserve absent and empty tokens; encrypt nonempty values when enabled.
16pub fn maybe_encrypt<'a>(
17    value: Option<String>,
18    encrypt: bool,
19    secret: impl Into<SecretKey<'a>>,
20) -> AuthResult<Option<String>> {
21    match value {
22        Some(value) if encrypt && !value.is_empty() => encrypt_token(&value, secret).map(Some),
23        value => Ok(value),
24    }
25}
26
27/// Preserve existing plaintext records while encryption is enabled.
28pub fn maybe_decrypt<'a>(
29    value: Option<&str>,
30    encrypt: bool,
31    secret: impl Into<SecretKey<'a>>,
32) -> AuthResult<Option<String>> {
33    match value {
34        Some(value)
35            if encrypt
36                && (value.starts_with("$ba$")
37                    || (!value.is_empty()
38                        && value.len().is_multiple_of(2)
39                        && value.bytes().all(|byte| byte.is_ascii_hexdigit()))) =>
40        {
41            decrypt_token(value, secret).map(Some)
42        }
43        value => Ok(value.map(str::to_owned)),
44    }
45}
46
47/// A set of OAuth tokens after conditional encryption.
48pub struct EncryptedTokenSet {
49    pub access_token: Option<String>,
50    pub refresh_token: Option<String>,
51    pub id_token: Option<String>,
52}
53
54pub fn encrypt_token_set(
55    ctx: &AuthContext<impl AuthSchema>,
56    access_token: Option<String>,
57    refresh_token: Option<String>,
58    id_token: Option<String>,
59) -> AuthResult<EncryptedTokenSet> {
60    let encrypt = ctx.config.account.encrypt_oauth_tokens();
61    let secret = ctx.config.encryption_secret();
62    Ok(EncryptedTokenSet {
63        access_token: maybe_encrypt(access_token, encrypt, secret)?,
64        refresh_token: maybe_encrypt(refresh_token, encrypt, secret)?,
65        id_token,
66    })
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    // Upstream reference: packages/better-auth/src/api/routes/account.test.ts :: describe("account") and packages/better-auth/src/oauth2/utils.ts; adapted to the Rust OAuth token encryption helpers.
74    #[test]
75    fn test_encrypt_decrypt_roundtrip() {
76        let secret = "a]vt!MFX8H-e!4igKa5)Tu.{ec:2$z%n";
77        let plaintext = "ya29.a0AfH6SMBx-some-access-token";
78
79        let encrypted = encrypt_token(plaintext, secret).unwrap();
80        assert_ne!(encrypted, plaintext);
81
82        let decrypted = decrypt_token(&encrypted, secret).unwrap();
83        assert_eq!(decrypted, plaintext);
84    }
85
86    // Upstream reference: packages/better-auth/src/api/routes/account.test.ts :: describe("account") and packages/better-auth/src/oauth2/utils.ts; adapted to the Rust OAuth token encryption helpers.
87    #[test]
88    fn test_maybe_encrypt_none() {
89        let result = maybe_encrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
90        assert!(result.is_none());
91    }
92
93    // Upstream reference: packages/better-auth/src/api/routes/account.test.ts :: describe("account") and packages/better-auth/src/oauth2/utils.ts; adapted to the Rust OAuth token encryption helpers.
94    #[test]
95    fn test_maybe_encrypt_disabled() {
96        let token = "plain-token".to_string();
97        let result = maybe_encrypt(Some(token.clone()), false, "secret").unwrap();
98        assert_eq!(result, Some(token));
99    }
100
101    // Upstream reference: packages/better-auth/src/api/routes/account.test.ts :: describe("account") and packages/better-auth/src/oauth2/utils.ts; adapted to the Rust OAuth token encryption helpers.
102    #[test]
103    fn test_maybe_decrypt_none() {
104        let result = maybe_decrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
105        assert!(result.is_none());
106    }
107
108    // Upstream reference: packages/better-auth/src/api/routes/account.test.ts :: describe("account") and packages/better-auth/src/oauth2/utils.ts; adapted to the Rust OAuth token encryption helpers.
109    #[test]
110    fn test_maybe_decrypt_preserves_plaintext_when_encryption_is_enabled() {
111        let plaintext = "ya29.a0AfH6SMBx-some-access-token";
112        let result = maybe_decrypt(Some(plaintext), true, "some-secret");
113        assert_eq!(result.unwrap().as_deref(), Some(plaintext));
114    }
115}