1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2use base64::Engine;
3use hmac::{Hmac, Mac};
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7use sha2::Sha256;
8
9use crate::crypto::jwt::{sign_jwt, verify_jwt};
10use crate::crypto::JweSecretSource;
11use crate::error::RustAuthError;
12use crate::options::CookieCacheStrategy;
13
14use super::chunked::ChunkedCookieStore;
15use super::types::{AuthCookies, Cookie, CookieOptions};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct CookieCachePayload<S, U> {
19 pub session: S,
20 pub user: U,
21 pub updated_at: i64,
22 pub version: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26struct CompactCookieEnvelope<S, U> {
27 session: CookieCachePayload<S, U>,
28 expires_at: i64,
29 signature: String,
30}
31
32fn encode_compact_cache<S, U>(
33 payload: &CookieCachePayload<S, U>,
34 secret: &str,
35 max_age: u64,
36) -> Result<String, RustAuthError>
37where
38 S: Serialize,
39 U: Serialize,
40{
41 let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + max_age as i64;
42 let signed = cache_signature_value(payload, expires_at)?;
43 let signature = hmac_base64url(
44 &serde_json::to_string(&signed).map_err(|error| {
45 RustAuthError::Cookie(format!(
46 "could not serialize cookie cache signature: {error}"
47 ))
48 })?,
49 secret,
50 )?;
51 let envelope = json!({
52 "session": payload,
53 "expires_at": expires_at,
54 "signature": signature,
55 });
56 let json = serde_json::to_vec(&envelope).map_err(|error| {
57 RustAuthError::Cookie(format!(
58 "could not serialize cookie cache envelope: {error}"
59 ))
60 })?;
61
62 Ok(URL_SAFE_NO_PAD.encode(json))
63}
64
65fn decode_compact_cache<S, U>(
66 value: &str,
67 secret: &str,
68) -> Result<Option<CookieCachePayload<S, U>>, RustAuthError>
69where
70 S: DeserializeOwned + Serialize,
71 U: DeserializeOwned + Serialize,
72{
73 let Ok(decoded) = URL_SAFE_NO_PAD.decode(value) else {
74 return Ok(None);
75 };
76 let envelope: CompactCookieEnvelope<S, U> = match serde_json::from_slice(&decoded) {
77 Ok(envelope) => envelope,
78 Err(_) => return Ok(None),
79 };
80 if envelope.expires_at < time::OffsetDateTime::now_utc().unix_timestamp() {
81 return Ok(None);
82 }
83
84 let signed = cache_signature_value(&envelope.session, envelope.expires_at)?;
85 let expected = hmac_base64url(
86 &serde_json::to_string(&signed).map_err(|error| {
87 RustAuthError::Cookie(format!(
88 "could not serialize cookie cache signature: {error}"
89 ))
90 })?,
91 secret,
92 )?;
93 if !crate::crypto::buffer::constant_time_equal(expected, envelope.signature) {
94 return Ok(None);
95 }
96
97 Ok(Some(envelope.session))
98}
99
100fn cache_signature_value<S, U>(
101 payload: &CookieCachePayload<S, U>,
102 expires_at: i64,
103) -> Result<Value, RustAuthError>
104where
105 S: Serialize,
106 U: Serialize,
107{
108 serde_json::to_value(json!({
109 "session": payload.session,
110 "user": payload.user,
111 "updated_at": payload.updated_at,
112 "version": payload.version,
113 "expires_at": expires_at,
114 }))
115 .map_err(|error| RustAuthError::Cookie(error.to_string()))
116}
117
118fn hmac_base64url(value: &str, secret: &str) -> Result<String, RustAuthError> {
119 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
120 .map_err(|error| RustAuthError::Cookie(error.to_string()))?;
121 mac.update(value.as_bytes());
122 Ok(URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()))
123}
124
125pub fn set_cookie_cache<S, U>(
126 auth_cookies: &AuthCookies,
127 secret: &(impl JweSecretSource + ?Sized),
128 payload: &CookieCachePayload<S, U>,
129 strategy: CookieCacheStrategy,
130 max_age: u64,
131) -> Result<Vec<Cookie>, RustAuthError>
132where
133 S: Serialize,
134 U: Serialize,
135{
136 let data = match strategy {
137 CookieCacheStrategy::Compact => {
138 encode_compact_cache(payload, &secret.current_jwe_secret()?, max_age)?
139 }
140 CookieCacheStrategy::Jwt => {
141 sign_jwt(payload, &secret.current_jwe_secret()?, max_age as i64)?
142 }
143 CookieCacheStrategy::Jwe => encode_jwe_cache(payload, secret, max_age)?,
144 };
145 let mut attributes = auth_cookies.session_data.attributes.clone();
146 attributes.max_age = Some(max_age);
147 let store = ChunkedCookieStore::new(auth_cookies.session_data.name.clone(), attributes, "");
148
149 Ok(store.chunk(&data))
150}
151
152pub fn get_cookie_cache<S, U>(
153 cookie_header: &str,
154 cookie_name: &str,
155 secret: &(impl JweSecretSource + ?Sized),
156 strategy: CookieCacheStrategy,
157 expected_version: Option<&str>,
158) -> Result<Option<CookieCachePayload<S, U>>, RustAuthError>
159where
160 S: DeserializeOwned + Serialize,
161 U: DeserializeOwned + Serialize,
162{
163 let store = ChunkedCookieStore::new(cookie_name, CookieOptions::default(), cookie_header);
164 let Some(data) = store.value() else {
165 return Ok(None);
166 };
167 let Some(payload) = (match strategy {
168 CookieCacheStrategy::Compact => decode_compact_cache(&data, &secret.current_jwe_secret()?)?,
169 CookieCacheStrategy::Jwt => verify_jwt(&data, &secret.current_jwe_secret()?)?,
170 CookieCacheStrategy::Jwe => decode_jwe_cache(&data, secret)?,
171 }) else {
172 return Ok(None);
173 };
174
175 if expected_version.is_some_and(|version| payload.version != version) {
176 return Ok(None);
177 }
178
179 Ok(Some(payload))
180}
181
182#[cfg(feature = "jose")]
183fn encode_jwe_cache<S, U>(
184 payload: &CookieCachePayload<S, U>,
185 secret: &(impl JweSecretSource + ?Sized),
186 max_age: u64,
187) -> Result<String, RustAuthError>
188where
189 S: Serialize,
190 U: Serialize,
191{
192 crate::crypto::symmetric_encode_jwt(payload, secret, max_age)
193}
194
195#[cfg(not(feature = "jose"))]
196fn encode_jwe_cache<S, U>(
197 _payload: &CookieCachePayload<S, U>,
198 _secret: &(impl JweSecretSource + ?Sized),
199 _max_age: u64,
200) -> Result<String, RustAuthError>
201where
202 S: Serialize,
203 U: Serialize,
204{
205 Err(RustAuthError::FeatureDisabled { feature: "jose" })
206}
207
208#[cfg(feature = "jose")]
209fn decode_jwe_cache<S, U>(
210 data: &str,
211 secret: &(impl JweSecretSource + ?Sized),
212) -> Result<Option<CookieCachePayload<S, U>>, RustAuthError>
213where
214 S: DeserializeOwned,
215 U: DeserializeOwned,
216{
217 crate::crypto::symmetric_decode_jwt(data, secret)
218}
219
220#[cfg(not(feature = "jose"))]
221fn decode_jwe_cache<S, U>(
222 _data: &str,
223 _secret: &(impl JweSecretSource + ?Sized),
224) -> Result<Option<CookieCachePayload<S, U>>, RustAuthError>
225where
226 S: DeserializeOwned,
227 U: DeserializeOwned,
228{
229 Err(RustAuthError::FeatureDisabled { feature: "jose" })
230}
231
232#[cfg(all(test, not(feature = "jose")))]
233mod tests {
234 use super::*;
235 use crate::cookies::get_cookies;
236 use crate::options::RustAuthOptions;
237
238 #[derive(Debug, Serialize)]
239 struct TestSession {
240 id: String,
241 }
242
243 #[derive(Debug, Serialize)]
244 struct TestUser {
245 id: String,
246 }
247
248 #[test]
249 fn jwe_cache_strategy_fails_closed_without_jose() -> Result<(), RustAuthError> {
250 let cookies = get_cookies(&RustAuthOptions::default())?;
251 let payload = CookieCachePayload {
252 session: TestSession {
253 id: "session_1".to_owned(),
254 },
255 user: TestUser {
256 id: "user_1".to_owned(),
257 },
258 updated_at: 0,
259 version: "1".to_owned(),
260 };
261
262 let result = set_cookie_cache(
263 &cookies,
264 "secret-a-at-least-32-chars-long!!",
265 &payload,
266 CookieCacheStrategy::Jwe,
267 300,
268 );
269
270 assert!(matches!(
271 result,
272 Err(RustAuthError::FeatureDisabled { feature: "jose" })
273 ));
274 Ok(())
275 }
276}