1use better_auth_core::entity::AuthUser;
6use better_auth_core::{AuthContext, AuthError, AuthRequest, AuthResult, CreateUser, UpdateUser};
7use chrono::Utc;
8
9mod user_input;
10pub(crate) use user_input::apply_user_create_fields;
11
12pub(crate) fn session_is_fresh(
13 session: &impl better_auth_core::AuthSession,
14 config: &better_auth_core::AuthConfig,
15) -> bool {
16 let fresh_age = config
17 .session
18 .fresh_age
19 .unwrap_or_else(|| chrono::Duration::hours(24));
20 fresh_age.is_zero()
21 || Utc::now().timestamp_millis() as f64 - session.created_at().milliseconds()
22 < fresh_age.num_milliseconds() as f64
23}
24
25pub fn expires_in_to_at(expires_in_secs: Option<i64>) -> AuthResult<Option<String>> {
30 match expires_in_secs {
31 Some(secs) => {
32 let duration = chrono::Duration::try_seconds(secs)
33 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
34 let dt = chrono::Utc::now()
35 .checked_add_signed(duration)
36 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
37 Ok(Some(
38 dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
39 ))
40 }
41 None => Ok(None),
42 }
43}
44
45pub async fn get_owned_api_key(
51 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
52 config: &crate::plugins::api_key::ApiKeyConfig,
53 key_id: &str,
54 user_id: &str,
55 action: &str,
56) -> AuthResult<better_auth_core::ApiKey> {
57 use crate::plugins::api_key::{ApiKeyReferences, config_id_matches};
58
59 let api_key = crate::plugins::api_key::storage::get_by_id(config, ctx, key_id)
60 .await?
61 .ok_or_else(|| AuthError::not_found("API Key not found"))?;
62
63 if !config_id_matches(&api_key.config_id, &config.config_id) {
65 return Err(AuthError::not_found("API Key not found"));
66 }
67
68 match config.references {
69 ApiKeyReferences::User => {
70 if api_key.reference_id != user_id {
71 return Err(AuthError::not_found("API Key not found"));
72 }
73 }
74 ApiKeyReferences::Organization => {
75 require_org_api_key_permission(ctx, user_id, &api_key.reference_id, action).await?;
76 }
77 }
78
79 Ok(api_key)
80}
81
82pub async fn require_org_api_key_permission(
85 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
86 user_id: &str,
87 organization_id: &str,
88 action: &str,
89) -> AuthResult<()> {
90 use crate::plugins::api_key::{ApiKeyErrorCode, api_key_error};
91 use crate::plugins::organization::rbac::check_permission;
92 use crate::plugins::organization::{
93 METADATA_AC, METADATA_CREATOR_ROLE, METADATA_DYNAMIC_ACCESS_CONTROL, METADATA_ENABLED,
94 METADATA_ROLES, OrganizationConfig, RolePermissions,
95 };
96 use std::collections::HashMap;
97
98 if !ctx
101 .get_metadata(METADATA_ENABLED)
102 .and_then(|value| value.as_bool())
103 .unwrap_or(false)
104 {
105 return Err(api_key_error(ApiKeyErrorCode::OrganizationPluginRequired));
106 }
107
108 let Some(member) = ctx.database.get_member(organization_id, user_id).await? else {
109 return Err(api_key_error(ApiKeyErrorCode::UserNotMemberOfOrganization));
110 };
111
112 let creator_role = ctx
116 .get_metadata(METADATA_CREATOR_ROLE)
117 .and_then(|value| value.as_str().map(str::to_string))
118 .unwrap_or_else(|| "owner".to_string());
119 if member
120 .role
121 .typed()?
122 .split(',')
123 .map(str::trim)
124 .any(|role| role == creator_role)
125 {
126 return Ok(());
127 }
128
129 let custom_roles: Option<HashMap<String, RolePermissions>> = ctx
130 .get_metadata(METADATA_ROLES)
131 .map(|value| serde_json::from_value(value.clone()))
132 .transpose()?
133 .flatten();
134
135 let config = OrganizationConfig {
136 roles: custom_roles,
137 dynamic_access_control: ctx
138 .get_metadata(METADATA_DYNAMIC_ACCESS_CONTROL)
139 .and_then(serde_json::Value::as_bool)
140 == Some(true),
141 ac: ctx
142 .get_metadata(METADATA_AC)
143 .map(|value| serde_json::from_value(value.clone()))
144 .transpose()?
145 .flatten(),
146 ..Default::default()
147 };
148 let allowed = check_permission(
149 member.role.typed()?,
150 organization_id,
151 "apikey",
152 &[action],
153 &config,
154 ctx,
155 )
156 .await?;
157
158 if allowed {
159 Ok(())
160 } else {
161 Err(api_key_error(
162 ApiKeyErrorCode::InsufficientApiKeyPermissions,
163 ))
164 }
165}
166
167pub async fn get_credential_account<S: better_auth_core::AuthSchema>(
169 ctx: &AuthContext<S>,
170 user_id: impl Into<better_auth_core::SchemaValue<String>>,
171) -> AuthResult<Option<better_auth_core::wire::AccountView>> {
172 let user_id = user_id.into();
173 let Some(user_id) = user_id.as_str() else {
174 return Ok(None);
175 };
176 ctx.database.get_credential_account(user_id).await
177}
178
179pub async fn get_credential_password_hash(
181 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
182 user: &impl AuthUser,
183) -> AuthResult<Option<String>> {
184 let Some(account) = get_credential_account(ctx, user.id().into_owned()).await? else {
185 return Ok(None);
186 };
187 if !account.password.is_truthy()? {
188 return Ok(None);
189 }
190 Ok(account.password.typed()?.clone())
191}
192
193pub async fn user_has_password(
195 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
196 user: &impl AuthUser,
197) -> AuthResult<bool> {
198 Ok(get_credential_password_hash(ctx, user).await?.is_some())
199}
200
201pub fn apply_default_role(
204 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
205 create_user: &mut CreateUser,
206) {
207 if create_user.role.is_some() {
208 return;
209 }
210
211 if let Some(default_role) = ctx
212 .get_metadata("admin.default_role")
213 .and_then(|value| value.as_str())
214 {
215 create_user.role = Some(default_role.to_string());
216 }
217}
218
219pub struct IssuedSession {
221 pub user: better_auth_core::wire::UserView,
222 pub session: better_auth_core::wire::SessionView,
223}
224
225pub enum SessionIssueError {
228 Auth(AuthError),
229 Banned { message: String },
230}
231
232impl SessionIssueError {
233 pub fn into_auth_error(self) -> AuthError {
234 match self {
235 Self::Auth(error) => error,
236 Self::Banned { message } => AuthError::banned_user(message),
237 }
238 }
239
240 pub fn banned_message(&self) -> Option<&str> {
241 match self {
242 Self::Banned { message } => Some(message.as_str()),
243 Self::Auth(_) => None,
244 }
245 }
246}
247
248impl From<AuthError> for SessionIssueError {
249 fn from(value: AuthError) -> Self {
250 Self::Auth(value)
251 }
252}
253
254pub fn admin_plugin_enabled(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> bool {
256 ctx.get_metadata("admin.enabled")
257 .and_then(|value| value.as_bool())
258 .unwrap_or(false)
259}
260
261pub async fn admin_banned_user_message(
264 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
265 user: &impl AuthUser,
266) -> AuthResult<String> {
267 let message = ctx
268 .extensions
269 .get::<super::admin::BannedUserMessage>()
270 .cloned()
271 .unwrap_or_default();
272 message.resolve(&ctx.internal_user_view(user).await?).await
273}
274
275pub async fn issue_user_session<S: better_auth_core::AuthSchema>(
278 ctx: &AuthContext<S>,
279 user_id: &str,
280 ip_address: Option<String>,
281 user_agent: Option<String>,
282) -> Result<IssuedSession, SessionIssueError> {
283 issue_user_session_with_lifetime(
284 ctx,
285 user_id,
286 ip_address,
287 user_agent,
288 ctx.config.session.expires_in(),
289 )
290 .await
291}
292
293pub(crate) async fn issue_user_session_with_lifetime<S: better_auth_core::AuthSchema>(
294 ctx: &AuthContext<S>,
295 user_id: &str,
296 ip_address: Option<String>,
297 user_agent: Option<String>,
298 expires_in: chrono::Duration,
299) -> Result<IssuedSession, SessionIssueError> {
300 let user = session_user(ctx, user_id, None).await?;
301
302 let session = ctx
303 .session_manager()
304 .create_session_with_lifetime(&user, ip_address, user_agent, expires_in)
305 .await?;
306
307 Ok(IssuedSession { user, session })
308}
309
310pub(crate) async fn issue_selected_user_session<S: better_auth_core::AuthSchema>(
312 ctx: &AuthContext<S>,
313 user: better_auth_core::FieldValue,
314 meta: &better_auth_core::RequestMeta,
315 expires_in: chrono::Duration,
316) -> Result<better_auth_core::session::NativeSessionData, SessionIssueError> {
317 let user_id = better_auth_core::SchemaValue::<String>::from_field(
318 user.as_object()
319 .and_then(|fields| fields.get("id"))
320 .cloned()
321 .unwrap_or_default(),
322 );
323 if admin_plugin_enabled(ctx)
324 && user_id.is_truthy()?
325 && let Some(stored_user) = ctx.database.get_user_by_id_field(&user_id).await?
326 {
327 apply_session_ban(ctx, &user_id, &stored_user, None).await?;
328 }
329 let session = ctx
330 .session_manager()
331 .create_session_for_id_with_lifetime(
332 user_id,
333 meta.ip_address.clone(),
334 meta.user_agent.clone(),
335 expires_in,
336 )
337 .await?;
338 Ok(better_auth_core::session::NativeSessionData { user, session })
339}
340
341pub(crate) async fn session_user<S: better_auth_core::AuthSchema>(
343 ctx: &AuthContext<S>,
344 user_id: &str,
345 transaction: Option<&dyn better_auth_core::store::AuthTransaction<S>>,
346) -> Result<better_auth_core::wire::UserView, SessionIssueError> {
347 let user = match transaction {
348 Some(tx) => tx.get_user_by_id(user_id).await?,
349 None => ctx.database.get_user_by_id(user_id).await?,
350 }
351 .ok_or(AuthError::UserNotFound)?;
352
353 apply_session_ban(ctx, &user_id.into(), &user, transaction).await?;
354 Ok(user)
355}
356
357async fn apply_session_ban<S: better_auth_core::AuthSchema>(
358 ctx: &AuthContext<S>,
359 user_id: &better_auth_core::SchemaValue<String>,
360 user: &better_auth_core::wire::UserView,
361 transaction: Option<&dyn better_auth_core::store::AuthTransaction<S>>,
362) -> Result<(), SessionIssueError> {
363 if admin_plugin_enabled(ctx) && user.banned() {
364 if user
365 .ban_expires()
366 .is_some_and(|expires| expires.milliseconds() < Utc::now().timestamp_millis() as f64)
367 {
368 let update = UpdateUser {
369 banned: Some(false),
370 ban_reason: Some(None),
371 ban_expires: Some(None),
372 ..Default::default()
373 };
374 let _ = match transaction {
376 Some(tx) => {
377 tx.update_user_by_id_value(&user_id.field_value(), update)
378 .await?
379 }
380 None => {
381 ctx.database
382 .update_user_by_id_value(&user_id.field_value(), update)
383 .await?
384 }
385 };
386 } else {
387 return Err(SessionIssueError::Banned {
388 message: admin_banned_user_message(ctx, user).await?,
389 });
390 }
391 }
392
393 Ok(())
394}
395
396pub fn get_cookie(req: &AuthRequest, name: &str) -> Option<String> {
398 let header = req.headers.get("cookie")?;
399 header
400 .split(';')
401 .filter_map(|cookie| {
402 let trimmed = cookie.trim();
403 let (cookie_name, cookie_value) = trimmed.split_once('=')?;
404 (cookie_name == name).then_some(cookie_value.to_string())
405 })
406 .next()
407}
408
409pub use better_auth_core::utils::cookie_utils::delete_session_cookies;
411
412pub(crate) fn parse_stored_scopes(scope: Option<&str>) -> Vec<String> {
414 scope
415 .unwrap_or_default()
416 .split(',')
417 .map(|scope| scope.trim_matches(oauth_scope_whitespace))
418 .filter(|scope| !scope.is_empty())
419 .map(str::to_owned)
420 .collect()
421}
422
423pub(crate) fn oauth_scope_whitespace(character: char) -> bool {
424 (character.is_whitespace() && character != '\u{85}') || character == '\u{feff}'
425}
426
427#[cfg(test)]
428#[path = "helpers/response_tests.rs"]
429mod response_tests;
430
431#[cfg(test)]
432#[path = "helpers/session_tests.rs"]
433mod session_tests;