use better_auth_core::{AuthContext, AuthResult, AuthSchema, SecretKey};
pub fn encrypt_token<'a>(plaintext: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
crate::plugins::symmetric::encrypt(secret, plaintext)
}
pub fn decrypt_token<'a>(encrypted: &str, secret: impl Into<SecretKey<'a>>) -> AuthResult<String> {
crate::plugins::symmetric::decrypt(secret, encrypted)
}
pub fn maybe_encrypt<'a>(
value: Option<String>,
encrypt: bool,
secret: impl Into<SecretKey<'a>>,
) -> AuthResult<Option<String>> {
match value {
Some(value) if encrypt && !value.is_empty() => encrypt_token(&value, secret).map(Some),
value => Ok(value),
}
}
pub fn maybe_decrypt<'a>(
value: Option<&str>,
encrypt: bool,
secret: impl Into<SecretKey<'a>>,
) -> AuthResult<Option<String>> {
match value {
Some(value)
if encrypt
&& (value.starts_with("$ba$")
|| (!value.is_empty()
&& value.len().is_multiple_of(2)
&& value.bytes().all(|byte| byte.is_ascii_hexdigit()))) =>
{
decrypt_token(value, secret).map(Some)
}
value => Ok(value.map(str::to_owned)),
}
}
pub struct EncryptedTokenSet {
pub access_token: Option<String>,
pub refresh_token: Option<String>,
pub id_token: Option<String>,
}
pub fn encrypt_token_set(
ctx: &AuthContext<impl AuthSchema>,
access_token: Option<String>,
refresh_token: Option<String>,
id_token: Option<String>,
) -> AuthResult<EncryptedTokenSet> {
let encrypt = ctx.config.account.encrypt_oauth_tokens();
let secret = ctx.config.encryption_secret();
Ok(EncryptedTokenSet {
access_token: maybe_encrypt(access_token, encrypt, secret)?,
refresh_token: maybe_encrypt(refresh_token, encrypt, secret)?,
id_token,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let secret = "a]vt!MFX8H-e!4igKa5)Tu.{ec:2$z%n";
let plaintext = "ya29.a0AfH6SMBx-some-access-token";
let encrypted = encrypt_token(plaintext, secret).unwrap();
assert_ne!(encrypted, plaintext);
let decrypted = decrypt_token(&encrypted, secret).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_maybe_encrypt_none() {
let result = maybe_encrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
assert!(result.is_none());
}
#[test]
fn test_maybe_encrypt_disabled() {
let token = "plain-token".to_string();
let result = maybe_encrypt(Some(token.clone()), false, "secret").unwrap();
assert_eq!(result, Some(token));
}
#[test]
fn test_maybe_decrypt_none() {
let result = maybe_decrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
assert!(result.is_none());
}
#[test]
fn test_maybe_decrypt_preserves_plaintext_when_encryption_is_enabled() {
let plaintext = "ya29.a0AfH6SMBx-some-access-token";
let result = maybe_decrypt(Some(plaintext), true, "some-secret");
assert_eq!(result.unwrap().as_deref(), Some(plaintext));
}
}