better_auth_core/session/
native.rs1use super::SessionData;
2use crate::{
3 AuthError, AuthResult, FieldMap, FieldValue, StructuredCloneContext,
4 store::JoinValue,
5 user_fields::UserConfig,
6 wire::{SessionView, UserView},
7};
8
9#[derive(Debug, Clone)]
11pub struct NativeSessionData {
12 pub session: SessionView,
14 pub user: FieldValue,
16}
17
18impl NativeSessionData {
19 pub fn user_field(&self, name: &str) -> &FieldValue {
21 self.user
22 .as_object()
23 .and_then(|fields| fields.get(name))
24 .unwrap_or(&FieldValue::Undefined)
25 }
26
27 pub fn public_user(&self, config: &UserConfig) -> AuthResult<FieldValue> {
30 let value = StructuredCloneContext::new().clone_value(&self.user);
31 let mut fields = match value {
32 FieldValue::Object(fields) => (*fields).clone(),
33 FieldValue::Array(values) => values
34 .iter()
35 .enumerate()
36 .map(|(index, value)| (index.to_string(), value.clone()))
37 .collect(),
38 _ => {
39 return Err(AuthError::internal(
40 "Session User must be an object or an array",
41 ));
42 }
43 };
44 fields.retain(|name, _| {
45 config
46 .fields()
47 .get(name)
48 .is_none_or(|field| field.returned())
49 });
50 Ok(fields.into())
51 }
52}
53
54impl From<SessionData> for NativeSessionData {
55 fn from(data: SessionData) -> Self {
56 Self {
57 session: data.session,
58 user: FieldMap::from(data.user).into(),
59 }
60 }
61}
62
63impl From<SessionData> for SessionData<JoinValue<UserView>> {
64 fn from(data: SessionData) -> Self {
65 Self {
66 session: data.session,
67 user: JoinValue::One(Some(data.user)),
68 }
69 }
70}
71
72impl SessionData<JoinValue<UserView>> {
73 pub fn into_typed(self) -> AuthResult<Option<SessionData>> {
75 match self.user {
76 JoinValue::One(user) => Ok(user.map(|user| SessionData {
77 session: self.session,
78 user,
79 })),
80 JoinValue::Many(_) => Err(super::relationship_array_error()),
81 }
82 }
83}
84
85impl From<SessionData<JoinValue<UserView>>> for NativeSessionData {
86 fn from(data: SessionData<JoinValue<UserView>>) -> Self {
87 Self {
88 session: data.session,
89 user: match data.user {
90 JoinValue::One(Some(user)) => FieldMap::from(user).into(),
91 JoinValue::One(None) => FieldValue::Null,
92 JoinValue::Many(users) => users
93 .into_iter()
94 .map(|user| FieldValue::from(FieldMap::from(user)))
95 .collect::<Vec<_>>()
96 .into(),
97 },
98 }
99 }
100}
101
102impl From<NativeSessionData> for FieldMap {
103 fn from(data: NativeSessionData) -> Self {
104 Self::from([
105 ("session".into(), Self::from(data.session).into()),
106 ("user".into(), data.user),
107 ])
108 }
109}
110
111impl serde::Serialize for NativeSessionData {
112 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
113 crate::field_value::serde::map::serialize(&FieldMap::from(self.clone()), serializer)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::{
121 AuthConfig, AuthRequest, CreateSession, CreateUser, FieldDate, FromFieldMap, HttpMethod,
122 config::{CookieCacheConfig, CookieCacheVersion},
123 session::{SessionManager, SessionRead, cookie_cache},
124 store::{EphemeralStore, StatelessSchema},
125 user_fields::{UserFieldConfig, UserFieldReference},
126 };
127 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
128 use std::sync::{
129 Arc,
130 atomic::{AtomicUsize, Ordering},
131 };
132
133 fn session() -> AuthResult<SessionView> {
134 let now = FieldDate::from_milliseconds(1_000.0);
135 SessionView::from_field_values(FieldMap::from([
136 ("id".into(), "session".into()),
137 ("token".into(), "session-token".into()),
138 ("expiresAt".into(), now.clone().into()),
139 ("createdAt".into(), now.clone().into()),
140 ("updatedAt".into(), now.into()),
141 ]))
142 }
143
144 #[tokio::test]
145 async fn typed_relationship_array_rejection_precedes_refresh_and_cookie_callbacks()
146 -> AuthResult<()> {
147 let calls = Arc::new(AtomicUsize::new(0));
148 let recorded = calls.clone();
149 let mut config =
150 AuthConfig::new("typed-session-relationship-secret-at-least-32-characters");
151 let _ = config.user.fields_mut().insert(
152 "image".into(),
153 UserFieldConfig {
154 references: Some(UserFieldReference {
155 model: "session".into(),
156 field: "id".into(),
157 }),
158 ..Default::default()
159 },
160 );
161 config.session.cookie_cache = Some(CookieCacheConfig {
162 enabled: Some(true),
163 version: CookieCacheVersion::dynamic(move |_| {
164 let recorded = recorded.clone();
165 async move {
166 let _ = recorded.fetch_add(1, Ordering::SeqCst);
167 Ok("selected-relation".into())
168 }
169 }),
170 ..Default::default()
171 });
172 let config = Arc::new(config);
173 let manager = SessionManager::<StatelessSchema>::new(
174 config.clone(),
175 Arc::new(EphemeralStore::new(config.clone())),
176 );
177 let session = manager
178 .database
179 .create_session(CreateSession {
180 user_id: "canonical-owner".into(),
181 expires_at: (chrono::Utc::now() + chrono::Duration::hours(1)).into(),
182 additional_fields: Default::default(),
183 ip_address: None,
184 user_agent: None,
185 impersonated_by: None,
186 active_organization_id: None,
187 })
188 .await?;
189 let mut user = CreateUser::new()
190 .with_email("selected-owner@example.test")
191 .with_name("Selected owner");
192 user.image = Some(session.id.typed()?.clone()).into();
193 let user = manager.database.create_user(user).await?;
194 assert!(manager.needs_refresh(&session));
195 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
196 let _ = request.headers.insert(
197 "cookie".into(),
198 format!(
199 "{}={}",
200 config.auth_cookie("session_token", Default::default()).name,
201 crate::utils::cookie_utils::sign_cookie_value(
202 &session.token,
203 config.signing_secret()
204 ),
205 ),
206 );
207 assert!(matches!(
208 manager.resolve(&request, SessionRead::Authoritative).await,
209 Err(AuthError::Internal(message)) if message == "A User relationship array cannot authenticate a typed User"
210 ));
211 assert_eq!(calls.load(Ordering::SeqCst), 0);
212 assert_eq!(
213 request
214 .take_response_headers()?
215 .get_all("set-cookie")
216 .count(),
217 0
218 );
219 let stored = manager
220 .database
221 .get_session(&session.token)
222 .await?
223 .ok_or_else(|| AuthError::internal("Selected Session must remain stored"))?;
224 assert_eq!(stored.expires_at, session.expires_at);
225 assert!(request.new_session()?.is_none());
226
227 let mut native_request = AuthRequest::new(HttpMethod::Get, "/get-session");
228 native_request.headers = request.headers.clone();
229 native_request.query = Some(serde_json::json!({"disableRefresh": true}));
230 let native = manager
231 .resolve_native(&native_request, SessionRead::Authoritative)
232 .await?
233 .data
234 .ok_or_else(|| {
235 AuthError::internal("Native resolution must preserve the selected relationship")
236 })?;
237 let selected = native
238 .user
239 .as_array()
240 .ok_or_else(|| AuthError::internal("Native User relationship must remain an array"))?;
241 assert_eq!(selected.len(), 1);
242 assert_eq!(
243 selected
244 .first()
245 .and_then(FieldValue::as_object)
246 .and_then(|fields| fields.get("id")),
247 Some(&user.id.field_value())
248 );
249 assert_eq!(calls.load(Ordering::SeqCst), 0);
250 Ok(())
251 }
252
253 #[test]
254 fn native_user_output_keeps_array_members_and_clone_identity() -> AuthResult<()> {
255 let date = FieldDate::from_milliseconds(123.0);
256 let user: FieldValue = FieldMap::from([
257 ("id".into(), "user-b".into()),
258 ("secretNote".into(), "private".into()),
259 ("ownUndefined".into(), FieldValue::Undefined),
260 ("when".into(), date.clone().into()),
261 ("sameWhen".into(), date.clone().into()),
262 ])
263 .into();
264 let mut config = UserConfig::default();
265 let _ = config.fields_mut().insert(
266 "secretNote".into(),
267 UserFieldConfig {
268 returned: Some(false),
269 ..Default::default()
270 },
271 );
272 let mut data = NativeSessionData {
273 session: session()?,
274 user: user.clone(),
275 };
276 let public = data.public_user(&config)?;
277 let public = public
278 .as_object()
279 .ok_or_else(|| AuthError::internal("Expected User object"))?;
280 assert!(!public.contains_key("secretNote"));
281 assert_eq!(public.get("ownUndefined"), Some(&FieldValue::Undefined));
282 data.user = vec![user.clone()].into();
283 assert!(data.user_field("id").is_undefined());
284 let public = data.public_user(&config)?;
285 assert_eq!(
286 public,
287 FieldValue::from(FieldMap::from([("0".into(), user)]))
288 );
289 let child = public
290 .as_object()
291 .and_then(|fields| fields.get("0"))
292 .and_then(FieldValue::as_object)
293 .ok_or_else(|| AuthError::internal("Expected numeric-key User output"))?;
294 let date_field = |name: &str| {
295 child
296 .get(name)
297 .and_then(FieldValue::as_date)
298 .ok_or_else(|| AuthError::internal(format!("Expected Date field: {name}")))
299 };
300 assert!(date_field("when")?.same_object(date_field("sameWhen")?));
301 assert!(!date_field("when")?.same_object(&date));
302 assert!(matches!(data.user, FieldValue::Array(_)));
303 Ok(())
304 }
305
306 #[tokio::test]
307 async fn native_cookie_issuance_keeps_array_for_callbacks_and_snapshot() -> AuthResult<()> {
308 let user: FieldValue = vec![FieldValue::from(FieldMap::from([
309 ("id".into(), "user-b".into()),
310 ("secretNote".into(), "child-value".into()),
311 ]))]
312 .into();
313 let calls = Arc::new(AtomicUsize::new(0));
314 let recorded = calls.clone();
315 let expected = user.clone();
316 let mut config = AuthConfig::new("native-session-cookie-secret-at-least-32-characters");
317 config.session.cookie_cache = Some(CookieCacheConfig {
318 enabled: Some(true),
319 version: CookieCacheVersion::dynamic(move |data| {
320 let expected = expected.clone();
321 let recorded = recorded.clone();
322 async move {
323 assert!(data.user.strict_equals(&expected));
324 assert!(data.session.user_id.is_undefined());
325 let _ = recorded.fetch_add(1, Ordering::SeqCst);
326 Ok("native".into())
327 }
328 }),
329 ..Default::default()
330 });
331 let config = Arc::new(config);
332 let manager = SessionManager::<StatelessSchema>::new(
333 config.clone(),
334 Arc::new(EphemeralStore::new(config.clone())),
335 );
336 let session = manager
337 .create_session_for_id(Default::default(), None, None)
338 .await?;
339 assert!(session.user_id.is_undefined());
340 let token = session.token.clone();
341 let data = NativeSessionData {
342 session,
343 user: user.clone(),
344 };
345 let request = AuthRequest::new(HttpMethod::Post, "/sign-in/social");
346 manager
347 .set_native_session_cookie(&request, data.clone(), None)
348 .await?;
349 assert_eq!(calls.load(Ordering::SeqCst), 1);
350 let snapshot = request
351 .new_session()?
352 .ok_or_else(|| AuthError::internal("Missing issued snapshot"))?;
353 assert!(snapshot.user.strict_equals(&user));
354 assert!(snapshot.session.user_id.is_undefined());
355 let headers = request.take_response_headers()?;
356 let cookies = headers
357 .get_all("set-cookie")
358 .map(|value| {
359 cookie::Cookie::parse(value.as_str())
360 .map_err(|error| AuthError::internal(format!("Parsing test cookie: {error}")))
361 })
362 .collect::<AuthResult<Vec<_>>>()?;
363 let mut browser = AuthRequest::new(HttpMethod::Get, "/get-session");
364 let _ = browser.headers.insert(
365 "cookie".into(),
366 cookies
367 .iter()
368 .map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
369 .collect::<Vec<_>>()
370 .join("; "),
371 );
372 assert_eq!(manager.extract_session_token(&browser), Some(token));
373 let cache = config
374 .session
375 .cookie_cache
376 .as_ref()
377 .ok_or_else(|| AuthError::internal("Missing cache config"))?;
378 let encoded = cookie_cache::read(&browser, "better-auth.session_data")
379 .ok_or_else(|| AuthError::internal("Missing issued cache cookie"))?;
380 let bytes = URL_SAFE_NO_PAD
381 .decode(&encoded)
382 .map_err(|error| AuthError::internal(format!("Decoding test cache: {error}")))?;
383 let payload: serde_json::Value = serde_json::from_slice(&bytes)?;
384 assert_eq!(
385 payload
386 .get("session")
387 .and_then(|data| data.get("user"))
388 .cloned(),
389 data.public_user(&config.user)?.json()?
390 );
391 assert!(cookie_cache::decode(&encoded, &config, cache).is_none());
392 Ok(())
393 }
394}