better_auth_api/plugins/
custom_session.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use better_auth_core::{
7 AuthContext, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
8 AuthSchema, FieldValue, FromFieldMap, HttpMethod, ResponseBody, session::SessionData,
9};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(Debug, Deserialize, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CustomSessionInput {
17 #[serde(flatten)]
18 pub data: SessionData,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub needs_refresh: Option<bool>,
21}
22
23impl CustomSessionInput {
24 fn from_field_value(value: &FieldValue) -> AuthResult<Self> {
25 let fields = value.as_object().ok_or_else(|| {
26 better_auth_core::AuthError::internal("Custom session input must be an object")
27 })?;
28 Ok(Self {
29 data: SessionData::from_field_values(fields.clone())?,
30 needs_refresh: fields
31 .get("needsRefresh")
32 .cloned()
33 .unwrap_or_default()
34 .decode()?,
35 })
36 }
37
38 fn from_response(body: &ResponseBody) -> AuthResult<Option<Self>> {
39 match body {
40 ResponseBody::Native(FieldValue::Null | FieldValue::Undefined) => Ok(None),
41 ResponseBody::Native(value) => Self::from_field_value(value).map(Some),
42 ResponseBody::Bytes(bytes) => Ok(serde_json::from_slice(bytes)?),
43 }
44 }
45
46 fn list_from_response(body: &ResponseBody) -> AuthResult<Option<Vec<Self>>> {
47 match body {
48 ResponseBody::Native(FieldValue::Null | FieldValue::Undefined) => Ok(None),
49 ResponseBody::Native(FieldValue::Array(values)) => values
50 .iter()
51 .map(Self::from_field_value)
52 .collect::<AuthResult<_>>()
53 .map(Some),
54 ResponseBody::Native(_) => Err(better_auth_core::AuthError::internal(
55 "Device sessions response must be an array",
56 )),
57 ResponseBody::Bytes(bytes) => Ok(serde_json::from_slice(bytes)?),
58 }
59 }
60}
61
62#[async_trait]
64pub trait CustomSessionCallback<S: AuthSchema>: Send + Sync + 'static {
65 async fn customize(
66 &self,
67 session: CustomSessionInput,
68 request: &AuthRequest,
69 context: &AuthContext<S>,
70 ) -> AuthResult<Value>;
71}
72
73pub struct CustomSessionPlugin<S: AuthSchema> {
75 callback: Arc<dyn CustomSessionCallback<S>>,
76 mutate_list_device_sessions: bool,
77}
78
79impl<S: AuthSchema> Clone for CustomSessionPlugin<S> {
80 fn clone(&self) -> Self {
81 Self {
82 callback: self.callback.clone(),
83 mutate_list_device_sessions: self.mutate_list_device_sessions,
84 }
85 }
86}
87
88impl<S: AuthSchema> CustomSessionPlugin<S> {
89 pub fn new(callback: impl CustomSessionCallback<S>) -> Self {
90 Self {
91 callback: Arc::new(callback),
92 mutate_list_device_sessions: false,
93 }
94 }
95
96 pub fn mutate_list_device_sessions(mut self, enabled: bool) -> Self {
98 self.mutate_list_device_sessions = enabled;
99 self
100 }
101
102 pub(crate) async fn get_session(
103 &self,
104 request: &AuthRequest,
105 context: &AuthContext<S>,
106 ) -> AuthResult<AuthResponse> {
107 let response = super::session_management::SessionManagementPlugin::new()
110 .handle_get_session(request, context)
111 .await;
112 let mut response = match response {
113 Ok(response) => response,
114 Err(_) => {
115 let _ = request.take_response_headers()?;
116 return Ok(AuthResponse::json(200, &Value::Null)?);
117 }
118 };
119 let data = CustomSessionInput::from_response(&response.body)?;
120 if let Some(data) = data {
121 let value = self.callback.customize(data, request, context).await?;
122 response.body = ResponseBody::Bytes(serde_json::to_vec(&value)?);
123 }
124 Ok(response)
125 }
126}
127
128#[async_trait]
129impl<S: AuthSchema> AuthPlugin<S> for CustomSessionPlugin<S> {
130 fn name(&self) -> &'static str {
131 "custom-session"
132 }
133
134 fn routes(&self) -> Vec<AuthRoute> {
135 vec![
136 AuthRoute::get("/get-session", "getSession")
137 .require_headers(true)
138 .query_validator(better_auth_core::query::session_query),
139 ]
140 }
141
142 async fn on_init(&self, context: &mut AuthInitContext<S>) -> AuthResult<()> {
143 context.extensions.insert(self.clone());
144 Ok(())
145 }
146
147 async fn on_request(
148 &self,
149 request: &AuthRequest,
150 context: &AuthContext<S>,
151 ) -> AuthResult<Option<AuthResponse>> {
152 match (request.method(), request.path()) {
153 (HttpMethod::Get, "/get-session") => self.get_session(request, context).await.map(Some),
154 (HttpMethod::Post, "/get-session") => Ok(Some(AuthResponse::new(404))),
155 _ => Ok(None),
156 }
157 }
158
159 async fn after_request(
160 &self,
161 request: &AuthRequest,
162 response: &mut AuthResponse,
163 context: &AuthContext<S>,
164 ) -> AuthResult<()> {
165 if !(self.mutate_list_device_sessions
166 && request.path() == "/multi-session/list-device-sessions")
167 {
168 return Ok(());
169 }
170 better_auth_core::observability::instrumentation::with_endpoint_hook(
171 &context.config,
172 request,
173 "after",
174 "plugin:custom-session",
175 async {
176 if !self.mutate_list_device_sessions
177 || request.path() != "/multi-session/list-device-sessions"
178 || response.status != 200
179 {
180 return Ok(());
181 }
182 let sessions = CustomSessionInput::list_from_response(&response.body)?;
183 if let Some(sessions) = sessions {
184 let context = Arc::new(context.clone());
185 let request = Arc::new(request.clone());
186 let hook_context = better_auth_core::hooks::current_request_hook_context();
187 let callbacks: Vec<_> = sessions
190 .into_iter()
191 .map(|session| {
192 let callback = self.callback.clone();
193 let context = context.clone();
194 let request = request.clone();
195 let hook_context = hook_context.clone();
196 tokio::spawn(async move {
197 let future = callback.customize(session, &request, &context);
198 match hook_context {
199 Some(hook_context) => {
200 better_auth_core::with_request_hook_context_value(
201 hook_context,
202 future,
203 )
204 .await
205 }
206 None => future.await,
207 }
208 })
209 })
210 .collect();
211 let values = futures_util::future::try_join_all(callbacks.into_iter().map(
212 |callback| async {
213 callback.await.map_err(|error| {
214 better_auth_core::AuthError::internal(format!(
215 "Custom session callback task failed: {error}"
216 ))
217 })?
218 },
219 ))
220 .await?;
221 response.body = ResponseBody::Bytes(serde_json::to_vec(&values)?);
222 for (name, value) in request.take_response_headers()? {
223 if name.eq_ignore_ascii_case("set-cookie") {
224 response.headers.append(name, value);
225 } else {
226 let _ = response.headers.insert(name, value);
227 }
228 }
229 }
230 Ok(())
231 },
232 )
233 .await
234 }
235}