1use better_auth_core::config::OAuthStateStrategy;
6use better_auth_core::entity::{AuthAccount, AuthUser};
7use better_auth_core::{AuthContext, AuthError, AuthRequest, AuthResult, CreateUser, UpdateUser};
8use chrono::Utc;
9
10pub fn expires_in_to_at(expires_in_secs: Option<i64>) -> AuthResult<Option<String>> {
15 match expires_in_secs {
16 Some(secs) => {
17 let duration = chrono::Duration::try_seconds(secs)
18 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
19 let dt = chrono::Utc::now()
20 .checked_add_signed(duration)
21 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
22 Ok(Some(dt.to_rfc3339()))
23 }
24 None => Ok(None),
25 }
26}
27
28pub async fn get_owned_api_key(
34 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
35 config: &crate::plugins::api_key::ApiKeyConfig,
36 key_id: &str,
37 user_id: &str,
38 action: &str,
39) -> AuthResult<better_auth_core::ApiKey> {
40 use crate::plugins::api_key::{ApiKeyReferences, config_id_matches};
41
42 let api_key = ctx
43 .database
44 .get_api_key_by_id(key_id)
45 .await?
46 .ok_or_else(|| AuthError::not_found("API Key not found"))?;
47
48 if !config_id_matches(&api_key.config_id, &config.config_id) {
50 return Err(AuthError::not_found("API Key not found"));
51 }
52
53 match config.references {
54 ApiKeyReferences::User => {
55 if api_key.reference_id != user_id {
56 return Err(AuthError::not_found("API Key not found"));
57 }
58 }
59 ApiKeyReferences::Organization => {
60 require_org_api_key_permission(ctx, user_id, &api_key.reference_id, action).await?;
61 }
62 }
63
64 Ok(api_key)
65}
66
67pub async fn require_org_api_key_permission(
70 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
71 user_id: &str,
72 organization_id: &str,
73 action: &str,
74) -> AuthResult<()> {
75 use crate::plugins::api_key::{ApiKeyErrorCode, api_key_error};
76 use crate::plugins::organization::rbac::{Action, Resource, has_permission_any};
77 use crate::plugins::organization::{
78 METADATA_CREATOR_ROLE, METADATA_ENABLED, METADATA_ROLES, RolePermissions,
79 };
80 use std::collections::HashMap;
81
82 if !ctx
85 .get_metadata(METADATA_ENABLED)
86 .and_then(|value| value.as_bool())
87 .unwrap_or(false)
88 {
89 return Err(api_key_error(ApiKeyErrorCode::OrganizationPluginRequired));
90 }
91
92 let Some(member) = ctx.database.get_member(organization_id, user_id).await? else {
93 return Err(api_key_error(ApiKeyErrorCode::UserNotMemberOfOrganization));
94 };
95
96 let creator_role = ctx
100 .get_metadata(METADATA_CREATOR_ROLE)
101 .and_then(|value| value.as_str().map(str::to_string))
102 .unwrap_or_else(|| "owner".to_string());
103 if member
104 .role
105 .split(',')
106 .map(str::trim)
107 .any(|role| role == creator_role)
108 {
109 return Ok(());
110 }
111
112 let custom_roles: HashMap<String, RolePermissions> = ctx
113 .get_metadata(METADATA_ROLES)
114 .and_then(|value| serde_json::from_value(value.clone()).ok())
115 .unwrap_or_default();
116
117 let allowed = Action::parse(action)
118 .map(|action| has_permission_any(&member.role, &Resource::ApiKey, &action, &custom_roles))
119 .unwrap_or(false);
120
121 if allowed {
122 Ok(())
123 } else {
124 Err(api_key_error(
125 ApiKeyErrorCode::InsufficientApiKeyPermissions,
126 ))
127 }
128}
129
130pub async fn get_credential_account<S: better_auth_core::AuthSchema>(
132 ctx: &AuthContext<S>,
133 user_id: impl AsRef<str>,
134) -> AuthResult<Option<S::Account>> {
135 Ok(ctx
136 .database
137 .get_user_accounts(user_id.as_ref())
138 .await?
139 .into_iter()
140 .find(|account| account.provider_id() == "credential"))
141}
142
143pub async fn get_credential_password_hash(
145 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
146 user: &impl AuthUser,
147) -> AuthResult<Option<String>> {
148 Ok(get_credential_account(ctx, user.id())
149 .await?
150 .and_then(|account| account.password().map(str::to_string)))
151}
152
153pub async fn user_has_password(
155 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
156 user: &impl AuthUser,
157) -> AuthResult<bool> {
158 Ok(get_credential_password_hash(ctx, user).await?.is_some())
159}
160
161pub fn apply_default_role(
164 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
165 create_user: &mut CreateUser,
166) {
167 if create_user.role.is_some() {
168 return;
169 }
170
171 if let Some(default_role) = ctx
172 .get_metadata("admin.default_role")
173 .and_then(|value| value.as_str())
174 {
175 create_user.role = Some(default_role.to_string());
176 }
177}
178
179pub struct IssuedSession<S: better_auth_core::AuthSchema> {
181 pub user: S::User,
182 pub session: S::Session,
183}
184
185pub enum SessionIssueError {
188 Auth(AuthError),
189 Banned { message: String },
190}
191
192impl SessionIssueError {
193 pub fn into_auth_error(self) -> AuthError {
194 match self {
195 Self::Auth(error) => error,
196 Self::Banned { message } => AuthError::banned_user(message),
197 }
198 }
199
200 pub fn banned_message(&self) -> Option<&str> {
201 match self {
202 Self::Banned { message } => Some(message.as_str()),
203 Self::Auth(_) => None,
204 }
205 }
206}
207
208impl From<AuthError> for SessionIssueError {
209 fn from(value: AuthError) -> Self {
210 Self::Auth(value)
211 }
212}
213
214pub fn admin_plugin_enabled(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> bool {
216 ctx.get_metadata("admin.enabled")
217 .and_then(|value| value.as_bool())
218 .unwrap_or(false)
219}
220
221pub fn admin_banned_user_message(
224 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
225) -> Option<String> {
226 ctx.get_metadata("admin.banned_user_message")
227 .and_then(|value| value.as_str())
228 .map(ToOwned::to_owned)
229}
230
231pub async fn issue_user_session<S: better_auth_core::AuthSchema>(
234 ctx: &AuthContext<S>,
235 user_id: &str,
236 ip_address: Option<String>,
237 user_agent: Option<String>,
238) -> Result<IssuedSession<S>, SessionIssueError> {
239 let mut user = ctx
240 .database
241 .get_user_by_id(user_id)
242 .await?
243 .ok_or(AuthError::UserNotFound)?;
244
245 if admin_plugin_enabled(ctx) && user.banned() {
246 if user
247 .ban_expires()
248 .is_some_and(|expires| expires <= Utc::now())
249 {
250 user = ctx
251 .database
252 .update_user(
253 user_id,
254 UpdateUser {
255 banned: Some(false),
256 ban_reason: None,
257 ban_expires: None,
258 ..Default::default()
259 },
260 )
261 .await?;
262 } else {
263 return Err(SessionIssueError::Banned {
264 message: admin_banned_user_message(ctx).unwrap_or_else(|| {
265 "You have been banned from this application. Please contact support if you believe this is an error.".to_string()
266 }),
267 });
268 }
269 }
270
271 let session = ctx
272 .session_manager()
273 .create_session(&user, ip_address, user_agent)
274 .await?;
275
276 Ok(IssuedSession { user, session })
277}
278
279pub fn get_cookie(req: &AuthRequest, name: &str) -> Option<String> {
281 let header = req.headers.get("cookie")?;
282 header
283 .split(';')
284 .filter_map(|cookie| {
285 let trimmed = cookie.trim();
286 let (cookie_name, cookie_value) = trimmed.split_once('=')?;
287 (cookie_name == name).then_some(cookie_value.to_string())
288 })
289 .next()
290}
291
292pub fn delete_session_cookie_headers(config: &better_auth_core::AuthConfig) -> Vec<String> {
294 let mut cookies = vec![
295 better_auth_core::utils::cookie_utils::create_clear_session_cookie(config),
296 better_auth_core::utils::cookie_utils::create_clear_cookie(
297 &better_auth_core::utils::cookie_utils::related_cookie_name(config, "session_data"),
298 config,
299 ),
300 better_auth_core::utils::cookie_utils::create_clear_cookie(
301 &better_auth_core::utils::cookie_utils::related_cookie_name(config, "dont_remember"),
302 config,
303 ),
304 ];
305
306 if config.account.store_account_cookie {
307 cookies.push(better_auth_core::utils::cookie_utils::create_clear_cookie(
308 &better_auth_core::utils::cookie_utils::related_cookie_name(config, "account_data"),
309 config,
310 ));
311 }
312
313 if matches!(
314 config.account.store_state_strategy,
315 OAuthStateStrategy::Cookie
316 ) {
317 cookies.push(better_auth_core::utils::cookie_utils::create_clear_cookie(
318 &better_auth_core::utils::cookie_utils::related_cookie_name(config, "oauth_state"),
319 config,
320 ));
321 }
322
323 cookies
324}