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(
67 stored: &str,
68 secret: &alibi_core::AuthConfig,
69) -> Result<String, AuthError> {
70 crate::token_crypto::decrypt_with_config(stored, secret)
71}
72
73pub fn maybe_encrypt_with_config(
76 value: Option<String>,
77 encrypt: bool,
78 secret: &alibi_core::AuthConfig,
79) -> Result<Option<String>, AuthError> {
80 match (value, encrypt) {
81 (Some(v), true) if !v.is_empty() => Ok(Some(encrypt_token_with_config(&v, secret)?)),
82 (v, _) => Ok(v),
83 }
84}
85
86pub fn maybe_decrypt_with_config(
89 value: Option<&str>,
90 encrypt: bool,
91 secret: &alibi_core::AuthConfig,
92) -> Result<Option<String>, AuthError> {
93 match (value, encrypt) {
94 (Some(v), true) => decrypt_token_with_config(v, secret).map(Some),
95 (Some(v), false) => Ok(Some(v.to_owned())),
96 (None, _) => Ok(None),
97 }
98}
99
100pub fn encrypt_token_set(
103 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
104 access_token: Option<String>,
105 refresh_token: Option<String>,
106 id_token: Option<String>,
107) -> Result<EncryptedTokenSet, AuthError> {
108 let encrypt = ctx.config.account.encrypt_oauth_tokens;
109 let secret = &ctx.config;
110 Ok(EncryptedTokenSet {
111 access_token: maybe_encrypt_with_config(access_token, encrypt, secret)?,
112 refresh_token: maybe_encrypt_with_config(refresh_token, encrypt, secret)?,
113 id_token,
115 })
116}
117
118pub(super) fn provider_token_nulls(
119 tokens: &super::providers::OAuthTokenSet,
120 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
121) -> [bool; 3] {
122 ["access_token", "refresh_token", "id_token"].map(|field| {
123 policy.is_some_and(|policy| {
124 policy.preserve_raw_profile_scalars
125 && (field != "id_token" || !policy.token_response_omits_id_token)
126 }) && tokens
127 .raw
128 .as_ref()
129 .and_then(|raw| raw.get(field))
130 .is_some_and(serde_json::Value::is_null)
131 })
132}
133
134pub(super) async fn encrypt_provider_token_set(
138 ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
139 tokens: &super::providers::OAuthTokenSet,
140 policy: Option<&super::providers::OAuthAuthorizationPolicy>,
141) -> Result<EncryptedTokenSet, AuthError> {
142 let Some(raw) = tokens
143 .raw
144 .as_ref()
145 .filter(|_| policy.is_some_and(|policy| policy.preserve_raw_profile_scalars))
146 else {
147 return encrypt_token_set(
148 ctx,
149 tokens.access_token.clone(),
150 tokens.refresh_token.clone(),
151 tokens.id_token.clone(),
152 );
153 };
154 for field in ["expires_in", "refresh_token_expires_in"] {
158 let always =
159 field == "expires_in" && policy.is_some_and(|policy| policy.token_expiry_always);
160 let invalid = match raw.get(field) {
161 None => always,
162 Some(value) => {
163 (always || super::providers::remaining_profile::truthy(value))
164 && super::providers::remaining_profile::grant_expiry(value, !always).is_none()
165 }
166 };
167 if invalid {
168 return Err(AuthError::internal("Invalid provider token expiry"));
169 }
170 }
171 let mut result = EncryptedTokenSet {
172 access_token: None,
173 refresh_token: None,
174 id_token: None,
175 };
176 for (field, target, encrypted) in [
177 (
178 "access_token",
179 &mut result.access_token,
180 ctx.config.account.encrypt_oauth_tokens,
181 ),
182 (
183 "refresh_token",
184 &mut result.refresh_token,
185 ctx.config.account.encrypt_oauth_tokens,
186 ),
187 ("id_token", &mut result.id_token, false),
188 ] {
189 if field == "id_token" && policy.is_some_and(|policy| policy.token_response_omits_id_token)
190 {
191 continue;
192 }
193 let Some(value) = raw.get(field) else {
194 continue;
195 };
196 if encrypted && super::providers::remaining_profile::truthy(value) {
197 let value = value.as_str().ok_or_else(|| {
198 AuthError::internal("Provider token encryption requires a string")
199 })?;
200 *target = Some(encrypt_token_with_config(value, &ctx.config)?);
201 } else {
202 *target = ctx.database.provider_token_text(value).await?;
203 }
204 }
205 Ok(result)
206}
207
208#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
215 fn test_encrypt_decrypt_roundtrip() {
216 let secret = "a]vt!MFX8H-e!4igKa5)Tu.{ec:2$z%n";
217 let plaintext = "ya29.a0AfH6SMBx-some-access-token";
218
219 let encrypted = encrypt_token(plaintext, secret).unwrap();
220 assert_ne!(encrypted, plaintext);
221
222 let decrypted = decrypt_token(&encrypted, secret).unwrap();
223 assert_eq!(decrypted, plaintext);
224 assert!(decrypt_token("unversioned-old-plaintext", secret).is_err());
225 }
226
227 #[test]
229 fn test_maybe_encrypt_none() {
230 let result = maybe_encrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
231 assert!(result.is_none());
232 }
233
234 #[test]
236 fn test_maybe_encrypt_disabled() {
237 let token = "plain-token".to_owned();
238 let result = maybe_encrypt(Some(token.clone()), false, "secret").unwrap();
239 assert_eq!(result, Some(token));
240 }
241
242 #[test]
244 fn test_maybe_decrypt_none() {
245 let result = maybe_decrypt(None, true, "secret-key-that-is-32-chars-long").unwrap();
246 assert!(result.is_none());
247 }
248}
249