alibi_plugins/
oauth_token_conversion.rs1use super::token_crypto;
8use aes_gcm::{
9 Aes256Gcm, Nonce,
10 aead::{Aead, KeyInit},
11};
12pub use alibi_core::oauth_token_conversion::{
13 OAuthTokenConversionStore, OAuthTokenSnapshot, OAuthTokenValues,
14};
15use alibi_core::{AuthConfig, AuthError, AuthResult, AuthSchema};
16use base64::{Engine, engine::general_purpose::STANDARD};
17use hkdf::Hkdf;
18use sha2::Sha256;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum TokenEncoding {
23 Absent,
25 Plain,
27 LegacyNative,
29 Source,
32}
33
34#[derive(Clone)]
37pub struct TrustedOAuthTokenManifest {
38 pub observed: OAuthTokenSnapshot,
39 pub access_encoding: TokenEncoding,
40 pub refresh_encoding: TokenEncoding,
41 pub id_encoding: TokenEncoding,
42}
43
44pub struct OAuthTokenConversion {
47 observed: OAuthTokenSnapshot,
48 replacement: OAuthTokenValues,
49}
50
51impl OAuthTokenConversion {
52 pub fn prepare(
60 manifest: TrustedOAuthTokenManifest,
61 original_secret: &str,
62 destination: &AuthConfig,
63 ) -> AuthResult<Self> {
64 if !destination.account.encrypt_oauth_tokens {
65 return Err(AuthError::bad_request(
66 "Destination OAuth token encryption must be enabled",
67 ));
68 }
69 if manifest.id_encoding == TokenEncoding::Source {
70 return Err(AuthError::bad_request(
71 "Source ID tokens must be classified as plain",
72 ));
73 }
74 let observed = manifest.observed;
75 let access = decode(
77 observed.tokens.access_token.as_deref(),
78 manifest.access_encoding,
79 original_secret,
80 destination,
81 )?;
82 let refresh = decode(
83 observed.tokens.refresh_token.as_deref(),
84 manifest.refresh_encoding,
85 original_secret,
86 destination,
87 )?;
88 let id_token = decode(
89 observed.tokens.id_token.as_deref(),
90 manifest.id_encoding,
91 original_secret,
92 destination,
93 )?;
94 let access_token = encode(
95 access,
96 observed.tokens.access_token.as_deref(),
97 manifest.access_encoding,
98 destination,
99 )?;
100 let refresh_token = encode(
101 refresh,
102 observed.tokens.refresh_token.as_deref(),
103 manifest.refresh_encoding,
104 destination,
105 )?;
106 Ok(Self {
107 observed,
108 replacement: OAuthTokenValues {
109 access_token,
110 refresh_token,
111 id_token,
112 },
113 })
114 }
115
116 pub async fn apply<S: AuthSchema>(
123 &self,
124 store: &impl OAuthTokenConversionStore<S>,
125 ) -> AuthResult<bool> {
126 store
127 .compare_and_swap_oauth_tokens(&self.observed, &self.replacement)
128 .await
129 }
130}
131
132fn decode(
133 value: Option<&str>,
134 encoding: TokenEncoding,
135 secret: &str,
136 config: &AuthConfig,
137) -> AuthResult<Option<String>> {
138 match (value, encoding) {
139 (None, TokenEncoding::Absent) => Ok(None),
140 (Some(value), TokenEncoding::Plain) => Ok(Some(value.to_owned())),
141 (Some(value), TokenEncoding::LegacyNative) => decrypt_legacy(value, secret).map(Some),
142 (Some(value), TokenEncoding::Source) => {
143 token_crypto::decrypt_with_config(value, config).map(Some)
144 }
145 _ => Err(AuthError::bad_request(
146 "Token classification does not match presence",
147 )),
148 }
149}
150
151fn encode(
152 plain: Option<String>,
153 observed: Option<&str>,
154 encoding: TokenEncoding,
155 config: &AuthConfig,
156) -> AuthResult<Option<String>> {
157 if encoding == TokenEncoding::Source {
158 return Ok(observed.map(str::to_owned));
159 }
160 plain
161 .map(|plain| token_crypto::encrypt_with_config(&plain, config))
162 .transpose()
163}
164
165fn decrypt_legacy(value: &str, secret: &str) -> AuthResult<String> {
166 let invalid = || AuthError::Encryption("Legacy OAuth token authentication failed".into());
167 let mut key = [0; 32];
168 Hkdf::<Sha256>::new(None, secret.as_bytes())
169 .expand(b"better-auth-oauth-token-encryption", &mut key)
170 .map_err(|_| invalid())?;
171 let bytes = STANDARD.decode(value).map_err(|_| invalid())?;
172 let (nonce, ciphertext) = bytes.split_at_checked(12).ok_or_else(invalid)?;
173 let plain = Aes256Gcm::new_from_slice(&key)
174 .map_err(|_| invalid())?
175 .decrypt(
176 &Nonce::try_from(nonce)
177 .map_err(|_error| AuthError::Encryption("Invalid nonce".into()))?,
178 ciphertext,
179 )
180 .map_err(|_| invalid())?;
181 String::from_utf8(plain).map_err(|_| invalid())
182}