alibi_plugins/oauth/encryption/
mod.rs1use alibi_core::{AuthConfig, AuthError};
8
9pub struct EncryptedTokenSet {
11 pub access_token: Option<String>,
12 pub refresh_token: Option<String>,
13 pub id_token: Option<String>,
14}
15
16impl std::fmt::Debug for EncryptedTokenSet {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.debug_struct("EncryptedTokenSet").finish_non_exhaustive()
19 }
20}
21
22pub fn encrypt_token(plaintext: &str, secret: &str) -> Result<String, AuthError> {
27 encrypt_token_with_config(plaintext, &AuthConfig::new(secret))
28}
29
30pub fn decrypt_token(stored: &str, secret: &str) -> Result<String, AuthError> {
34 decrypt_token_with_config(stored, &AuthConfig::new(secret))
35}
36
37pub fn maybe_encrypt(
39 value: Option<String>,
40 encrypt: bool,
41 secret: &str,
42) -> Result<Option<String>, AuthError> {
43 maybe_encrypt_with_config(value, encrypt, &AuthConfig::new(secret))
44}
45
46pub fn maybe_decrypt(
48 value: Option<&str>,
49 encrypt: bool,
50 secret: &str,
51) -> Result<Option<String>, AuthError> {
52 maybe_decrypt_with_config(value, encrypt, &AuthConfig::new(secret))
53}
54
55pub fn encrypt_token_with_config(
59 plaintext: &str,
60 secret: &alibi_core::AuthConfig,
61) -> Result<String, AuthError> {
62 crate::token_crypto::encrypt_with_config(plaintext, secret)
63}
64
65pub fn decrypt_token_with_config(
68 stored: &str,
69 secret: &alibi_core::AuthConfig,
70) -> Result<String, AuthError> {
71 let likely_encrypted = stored.starts_with("$ba$")
72 || (!stored.is_empty()
73 && stored.len().is_multiple_of(2)
74 && stored.bytes().all(|byte| byte.is_ascii_hexdigit()));
75 if !likely_encrypted {
76 return Ok(stored.to_owned());
77 }
78 crate::token_crypto::decrypt_with_config(stored, secret)
79}
80
81pub fn maybe_encrypt_with_config(
84 value: Option<String>,
85 encrypt: bool,
86 secret: &alibi_core::AuthConfig,
87) -> Result<Option<String>, AuthError> {
88 match (value, encrypt) {
89 (Some(v), true) if !v.is_empty() => Ok(Some(encrypt_token_with_config(&v, secret)?)),
90 (v, _) => Ok(v),
91 }
92}
93
94pub fn maybe_decrypt_with_config(
97 value: Option<&str>,
98 encrypt: bool,
99 secret: &alibi_core::AuthConfig,
100) -> Result<Option<String>, AuthError> {
101 match (value, encrypt) {
102 (Some(v), true) => decrypt_token_with_config(v, secret).map(Some),
103 (Some(v), false) => Ok(Some(v.to_owned())),
104 (None, _) => Ok(None),
105 }
106}
107
108pub fn encrypt_token_set(
111 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
112 access_token: Option<String>,
113 refresh_token: Option<String>,
114 id_token: Option<String>,
115) -> Result<EncryptedTokenSet, AuthError> {
116 let encrypt = ctx.config.account.encrypt_oauth_tokens;
117 let secret = &ctx.config;
118 Ok(EncryptedTokenSet {
119 access_token: maybe_encrypt_with_config(access_token, encrypt, secret)?,
120 refresh_token: maybe_encrypt_with_config(refresh_token, encrypt, secret)?,
121 id_token,
123 })
124}
125
126pub(super) fn provider_token_nulls(
127 tokens: &super::providers::OAuthTokenSet,
128 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
129) -> [bool; 3] {
130 ["access_token", "refresh_token", "id_token"].map(|field| {
131 policy.is_some_and(|policy| {
132 policy.preserve_raw_profile_scalars
133 && (field != "id_token" || !policy.token_response_omits_id_token)
134 }) && tokens
135 .raw
136 .as_ref()
137 .and_then(|raw| raw.get(field))
138 .is_some_and(serde_json::Value::is_null)
139 })
140}
141
142pub(super) async fn encrypt_provider_token_set(
146 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
147 tokens: &super::providers::OAuthTokenSet,
148 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
149) -> Result<EncryptedTokenSet, AuthError> {
150 let Some(raw) = tokens
151 .raw
152 .as_ref()
153 .filter(|_| policy.is_some_and(|policy| policy.preserve_raw_profile_scalars))
154 else {
155 return encrypt_token_set(
156 ctx,
157 tokens.access_token.clone(),
158 tokens.refresh_token.clone(),
159 tokens.id_token.clone(),
160 );
161 };
162 for field in ["expires_in", "refresh_token_expires_in"] {
166 let always =
167 field == "expires_in" && policy.is_some_and(|policy| policy.token_expiry_always);
168 let invalid = match raw.get(field) {
169 None => always,
170 Some(value) => {
171 (always || super::providers::remaining_profile::truthy(value))
172 && super::providers::remaining_profile::grant_expiry(value, !always).is_none()
173 }
174 };
175 if invalid {
176 return Err(AuthError::internal("Invalid provider token expiry"));
177 }
178 }
179 let mut result = EncryptedTokenSet {
180 access_token: None,
181 refresh_token: None,
182 id_token: None,
183 };
184 for (field, target, encrypted) in [
185 (
186 "access_token",
187 &mut result.access_token,
188 ctx.config.account.encrypt_oauth_tokens,
189 ),
190 (
191 "refresh_token",
192 &mut result.refresh_token,
193 ctx.config.account.encrypt_oauth_tokens,
194 ),
195 ("id_token", &mut result.id_token, false),
196 ] {
197 if field == "id_token" && policy.is_some_and(|policy| policy.token_response_omits_id_token)
198 {
199 continue;
200 }
201 let Some(value) = raw.get(field) else {
202 continue;
203 };
204 if encrypted && super::providers::remaining_profile::truthy(value) {
205 let value = value.as_str().ok_or_else(|| {
206 AuthError::internal("Provider token encryption requires a string")
207 })?;
208 *target = Some(encrypt_token_with_config(value, &ctx.config)?);
209 } else {
210 *target = ctx.database.provider_token_text(value).await?;
211 }
212 }
213 Ok(result)
214}
215
216#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
223 fn test_encrypt_decrypt_roundtrip() {
224 let secret = "a]vt!MFX8H-e!4igKa5)Tu.{ec:2$z%n";
225 let plaintext = "ya29.a0AfH6SMBx-some-access-token";
226
227 let encrypted = encrypt_token(plaintext, secret).unwrap();
228 assert_ne!(encrypted, plaintext);
229
230 let decrypted = decrypt_token(&encrypted, secret).unwrap();
231 assert_eq!(decrypted, plaintext);
232 }
233
234 #[test]
236 fn test_maybe_encrypt_none() {
237 let result = maybe_encrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
238 assert!(result.is_none());
239 }
240
241 #[test]
243 fn test_maybe_encrypt_disabled() {
244 let token = "plain-token".to_owned();
245 let result = maybe_encrypt(Some(token.clone()), false, "secret").unwrap();
246 assert_eq!(result, Some(token));
247 }
248
249 #[test]
251 fn test_maybe_decrypt_none() {
252 let result = maybe_decrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
253 assert!(result.is_none());
254 }
255}
256