alibi_api/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> {
30 encrypt_token_with_config(plaintext, &AuthConfig::new(secret))
31}
32
33pub fn decrypt_token(stored: &str, secret: &str) -> Result<String, AuthError> {
40 decrypt_token_with_config(stored, &AuthConfig::new(secret))
41}
42
43pub fn maybe_encrypt(
48 value: Option<String>,
49 encrypt: bool,
50 secret: &str,
51) -> Result<Option<String>, AuthError> {
52 maybe_encrypt_with_config(value, encrypt, &AuthConfig::new(secret))
53}
54
55pub fn maybe_decrypt(
60 value: Option<&str>,
61 encrypt: bool,
62 secret: &str,
63) -> Result<Option<String>, AuthError> {
64 maybe_decrypt_with_config(value, encrypt, &AuthConfig::new(secret))
65}
66
67pub fn encrypt_token_with_config(
75 plaintext: &str,
76 secret: &alibi_core::AuthConfig,
77) -> Result<String, AuthError> {
78 crate::plugins::token_crypto::encrypt_with_config(plaintext, secret)
79}
80
81pub fn decrypt_token_with_config(
87 stored: &str,
88 secret: &alibi_core::AuthConfig,
89) -> Result<String, AuthError> {
90 let likely_encrypted = stored.starts_with("$ba$")
91 || (!stored.is_empty()
92 && stored.len().is_multiple_of(2)
93 && stored.bytes().all(|byte| byte.is_ascii_hexdigit()));
94 if !likely_encrypted {
95 return Ok(stored.to_owned());
96 }
97 crate::plugins::token_crypto::decrypt_with_config(stored, secret)
98}
99
100pub fn maybe_encrypt_with_config(
106 value: Option<String>,
107 encrypt: bool,
108 secret: &alibi_core::AuthConfig,
109) -> Result<Option<String>, AuthError> {
110 match (value, encrypt) {
111 (Some(v), true) if !v.is_empty() => Ok(Some(encrypt_token_with_config(&v, secret)?)),
112 (v, _) => Ok(v),
113 }
114}
115
116pub fn maybe_decrypt_with_config(
123 value: Option<&str>,
124 encrypt: bool,
125 secret: &alibi_core::AuthConfig,
126) -> Result<Option<String>, AuthError> {
127 match (value, encrypt) {
128 (Some(v), true) => decrypt_token_with_config(v, secret).map(Some),
129 (Some(v), false) => Ok(Some(v.to_owned())),
130 (None, _) => Ok(None),
131 }
132}
133
134pub fn encrypt_token_set(
140 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
141 access_token: Option<String>,
142 refresh_token: Option<String>,
143 id_token: Option<String>,
144) -> Result<EncryptedTokenSet, AuthError> {
145 let encrypt = ctx.config.account.encrypt_oauth_tokens;
146 let secret = &ctx.config;
147 Ok(EncryptedTokenSet {
148 access_token: maybe_encrypt_with_config(access_token, encrypt, secret)?,
149 refresh_token: maybe_encrypt_with_config(refresh_token, encrypt, secret)?,
150 id_token,
152 })
153}
154
155pub(super) fn provider_token_nulls(
156 tokens: &super::providers::OAuthTokenSet,
157 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
158) -> [bool; 3] {
159 ["access_token", "refresh_token", "id_token"].map(|field| {
160 policy.is_some_and(|policy| {
161 policy.preserve_raw_profile_scalars
162 && (field != "id_token" || !policy.token_response_omits_id_token)
163 }) && tokens
164 .raw
165 .as_ref()
166 .and_then(|raw| raw.get(field))
167 .is_some_and(serde_json::Value::is_null)
168 })
169}
170
171pub(super) async fn encrypt_provider_token_set(
175 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
176 tokens: &super::providers::OAuthTokenSet,
177 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
178) -> Result<EncryptedTokenSet, AuthError> {
179 let Some(raw) = tokens
180 .raw
181 .as_ref()
182 .filter(|_| policy.is_some_and(|policy| policy.preserve_raw_profile_scalars))
183 else {
184 return encrypt_token_set(
185 ctx,
186 tokens.access_token.clone(),
187 tokens.refresh_token.clone(),
188 tokens.id_token.clone(),
189 );
190 };
191 for field in ["expires_in", "refresh_token_expires_in"] {
195 let always =
196 field == "expires_in" && policy.is_some_and(|policy| policy.token_expiry_always);
197 let invalid = match raw.get(field) {
198 None => always,
199 Some(value) => {
200 (always || super::providers::remaining_profile::truthy(value))
201 && super::providers::remaining_profile::grant_expiry(value, !always).is_none()
202 }
203 };
204 if invalid {
205 return Err(AuthError::internal("Invalid provider token expiry"));
206 }
207 }
208 let mut result = EncryptedTokenSet {
209 access_token: None,
210 refresh_token: None,
211 id_token: None,
212 };
213 for (field, target, encrypted) in [
214 (
215 "access_token",
216 &mut result.access_token,
217 ctx.config.account.encrypt_oauth_tokens,
218 ),
219 (
220 "refresh_token",
221 &mut result.refresh_token,
222 ctx.config.account.encrypt_oauth_tokens,
223 ),
224 ("id_token", &mut result.id_token, false),
225 ] {
226 if field == "id_token" && policy.is_some_and(|policy| policy.token_response_omits_id_token)
227 {
228 continue;
229 }
230 let Some(value) = raw.get(field) else {
231 continue;
232 };
233 if encrypted && super::providers::remaining_profile::truthy(value) {
234 let value = value.as_str().ok_or_else(|| {
235 AuthError::internal("Provider token encryption requires a string")
236 })?;
237 *target = Some(encrypt_token_with_config(value, &ctx.config)?);
238 } else {
239 *target = ctx.database.provider_token_text(value).await?;
240 }
241 }
242 Ok(result)
243}
244
245#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
252 fn test_encrypt_decrypt_roundtrip() {
253 let secret = "a]vt!MFX8H-e!4igKa5)Tu.{ec:2$z%n";
254 let plaintext = "ya29.a0AfH6SMBx-some-access-token";
255
256 let encrypted = encrypt_token(plaintext, secret).unwrap();
257 assert_ne!(encrypted, plaintext);
258
259 let decrypted = decrypt_token(&encrypted, secret).unwrap();
260 assert_eq!(decrypted, plaintext);
261 }
262
263 #[test]
265 fn test_maybe_encrypt_none() {
266 let result = maybe_encrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
267 assert!(result.is_none());
268 }
269
270 #[test]
272 fn test_maybe_encrypt_disabled() {
273 let token = "plain-token".to_owned();
274 let result = maybe_encrypt(Some(token.clone()), false, "secret").unwrap();
275 assert_eq!(result, Some(token));
276 }
277
278 #[test]
280 fn test_maybe_decrypt_none() {
281 let result = maybe_decrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
282 assert!(result.is_none());
283 }
284}
285