better_auth_api/plugins/oauth/
encryption.rs1use better_auth_core::{AuthContext, AuthResult, AuthSchema, SecretKey};
4
5pub fn encrypt_token<'a>(plaintext: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
7 crate::plugins::symmetric::encrypt(secret, plaintext)
8}
9
10pub fn decrypt_token<'a>(encrypted: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
12 crate::plugins::symmetric::decrypt(secret, encrypted)
13}
14
15pub 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
27pub 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
47pub 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 #[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 #[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 #[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 #[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 #[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}