use super::token_crypto;
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit},
};
pub use alibi_core::oauth_token_conversion::{
OAuthTokenConversionStore, OAuthTokenSnapshot, OAuthTokenValues,
};
use alibi_core::{AuthConfig, AuthError, AuthResult, AuthSchema};
use base64::{Engine, engine::general_purpose::STANDARD};
use hkdf::Hkdf;
use sha2::Sha256;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TokenEncoding {
Absent,
Plain,
LegacyNative,
Source,
}
#[derive(Clone)]
pub struct TrustedOAuthTokenManifest {
pub observed: OAuthTokenSnapshot,
pub access_encoding: TokenEncoding,
pub refresh_encoding: TokenEncoding,
pub id_encoding: TokenEncoding,
}
pub struct OAuthTokenConversion {
observed: OAuthTokenSnapshot,
replacement: OAuthTokenValues,
}
impl OAuthTokenConversion {
pub fn prepare(
manifest: TrustedOAuthTokenManifest,
original_secret: &str,
destination: &AuthConfig,
) -> AuthResult<Self> {
if !destination.account.encrypt_oauth_tokens {
return Err(AuthError::bad_request(
"Destination OAuth token encryption must be enabled",
));
}
if manifest.id_encoding == TokenEncoding::Source {
return Err(AuthError::bad_request(
"Source ID tokens must be classified as plain",
));
}
let observed = manifest.observed;
let access = decode(
observed.tokens.access_token.as_deref(),
manifest.access_encoding,
original_secret,
destination,
)?;
let refresh = decode(
observed.tokens.refresh_token.as_deref(),
manifest.refresh_encoding,
original_secret,
destination,
)?;
let id_token = decode(
observed.tokens.id_token.as_deref(),
manifest.id_encoding,
original_secret,
destination,
)?;
let access_token = encode(
access,
observed.tokens.access_token.as_deref(),
manifest.access_encoding,
destination,
)?;
let refresh_token = encode(
refresh,
observed.tokens.refresh_token.as_deref(),
manifest.refresh_encoding,
destination,
)?;
Ok(Self {
observed,
replacement: OAuthTokenValues {
access_token,
refresh_token,
id_token,
},
})
}
pub async fn apply<S: AuthSchema>(
&self,
store: &impl OAuthTokenConversionStore<S>,
) -> AuthResult<bool> {
store
.compare_and_swap_oauth_tokens(&self.observed, &self.replacement)
.await
}
}
fn decode(
value: Option<&str>,
encoding: TokenEncoding,
secret: &str,
config: &AuthConfig,
) -> AuthResult<Option<String>> {
match (value, encoding) {
(None, TokenEncoding::Absent) => Ok(None),
(Some(value), TokenEncoding::Plain) => Ok(Some(value.to_owned())),
(Some(value), TokenEncoding::LegacyNative) => decrypt_legacy(value, secret).map(Some),
(Some(value), TokenEncoding::Source) => {
token_crypto::decrypt_with_config(value, config).map(Some)
}
_ => Err(AuthError::bad_request(
"Token classification does not match presence",
)),
}
}
fn encode(
plain: Option<String>,
observed: Option<&str>,
encoding: TokenEncoding,
config: &AuthConfig,
) -> AuthResult<Option<String>> {
if encoding == TokenEncoding::Source {
return Ok(observed.map(str::to_owned));
}
plain
.map(|plain| token_crypto::encrypt_with_config(&plain, config))
.transpose()
}
fn decrypt_legacy(value: &str, secret: &str) -> AuthResult<String> {
let invalid = || AuthError::Encryption("Legacy OAuth token authentication failed".into());
let mut key = [0; 32];
Hkdf::<Sha256>::new(None, secret.as_bytes())
.expand(b"better-auth-oauth-token-encryption", &mut key)
.map_err(|_| invalid())?;
let bytes = STANDARD.decode(value).map_err(|_| invalid())?;
let (nonce, ciphertext) = bytes.split_at_checked(12).ok_or_else(invalid)?;
let plain = Aes256Gcm::new_from_slice(&key)
.map_err(|_| invalid())?
.decrypt(
&Nonce::try_from(nonce)
.map_err(|_error| AuthError::Encryption("Invalid nonce".into()))?,
ciphertext,
)
.map_err(|_| invalid())?;
String::from_utf8(plain).map_err(|_| invalid())
}