Skip to main content

better_auth_api/plugins/admin/
mod.rs

1use std::collections::HashMap;
2
3use better_auth_core::entity::{AuthSession, AuthUser};
4use better_auth_core::utils::cookie_utils::{
5    create_clear_cookie, create_session_like_cookie, related_cookie_name,
6};
7use better_auth_core::wire::{SessionView, UserView};
8use better_auth_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult};
9
10pub mod access;
11mod banned_message;
12mod native;
13mod permission_api;
14mod request;
15pub use banned_message::{BannedUserMessage, BannedUserMessageFuture};
16pub use native::AdminApi;
17pub use permission_api::{AdminPermissionApi, AdminPermissionError, AuthorizedSession};
18pub use types::{
19    CreateUserRequest as CreateAdminUser, RoleInput as AdminRole, UserResponse as AdminUserResponse,
20};
21pub(super) mod handlers;
22pub(super) mod types;
23
24#[cfg(test)]
25mod tests;
26
27use crate::plugins::helpers::{delete_session_cookies, get_cookie};
28use access::{has_permission, is_admin_role, is_admin_user_id};
29use handlers::*;
30use types::*;
31
32const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
33const MESSAGE_CREATE_USERS: &str = "You are not allowed to create users";
34const MESSAGE_LIST_USERS: &str = "You are not allowed to list users";
35const MESSAGE_LIST_USER_SESSIONS: &str = "You are not allowed to list users sessions";
36const MESSAGE_BAN_USERS: &str = "You are not allowed to ban users";
37const MESSAGE_IMPERSONATE_USERS: &str = "You are not allowed to impersonate users";
38const MESSAGE_REVOKE_USER_SESSIONS: &str = "You are not allowed to revoke users sessions";
39const MESSAGE_DELETE_USERS: &str = "You are not allowed to delete users";
40const MESSAGE_SET_USER_PASSWORD: &str = "You are not allowed to set users password";
41const MESSAGE_GET_USER: &str = "You are not allowed to get user";
42const MESSAGE_UPDATE_USERS: &str = "You are not allowed to update users";
43
44/// Admin plugin for user management operations.
45pub struct AdminPlugin {
46    config: AdminConfig,
47}
48
49/// Configuration for the admin plugin.
50#[derive(Debug, Clone, better_auth_core::PluginConfig)]
51#[plugin(name = "AdminPlugin")]
52pub struct AdminConfig {
53    /// Default role assigned to new users and role-less permission checks.
54    #[config(default = "user".to_string())]
55    pub default_role: String,
56    /// Roles treated as "admin" for target-admin checks such as impersonation.
57    #[config(default = None)]
58    pub admin_roles: Option<Vec<String>>,
59    /// Users that always bypass admin permission checks.
60    #[config(default = None)]
61    pub admin_user_ids: Option<Vec<String>>,
62    /// Custom role definitions. When provided, these replace the built-in
63    /// `admin` and `user` role permissions.
64    #[config(default = None)]
65    pub roles: Option<HashMap<String, access::RolePermissions>>,
66    /// Default reason applied when banning a user without an explicit reason.
67    #[config(default = None)]
68    pub default_ban_reason: Option<String>,
69    /// Default ban duration in fractional seconds when the request has no nonzero duration.
70    #[config(default = None)]
71    pub default_ban_expires_in: Option<f64>,
72    /// Custom impersonation session duration in fractional seconds; zero uses one hour.
73    #[config(default = None)]
74    pub impersonation_session_duration: Option<f64>,
75    /// Message surfaced to banned users.
76    #[config(default = BannedUserMessage::default(), skip)]
77    pub banned_user_message: BannedUserMessage,
78    /// Whether other admin users may be impersonated.
79    #[config(default = false)]
80    pub allow_impersonating_admins: bool,
81}
82
83better_auth_core::impl_auth_plugin! {
84    AdminPlugin, "admin";
85    routes {
86        post "/admin/set-role" => handle_set_role, "setUserRole", body = request::validate, require_headers = true;
87        get  "/admin/get-user" => handle_get_user, "getUser", query = crate::plugins::query_input::get_user;
88        post "/admin/create-user" => handle_create_user, "createUser", body = request::validate;
89        post "/admin/update-user" => handle_update_user, "adminUpdateUser", body = request::validate;
90        get  "/admin/list-users" => handle_list_users, "listUsers", query = crate::plugins::query_input::list_users;
91        post "/admin/list-user-sessions" => handle_list_user_sessions, "adminListUserSessions", body = request::validate;
92        post "/admin/ban-user" => handle_ban_user, "banUser", body = request::validate;
93        post "/admin/unban-user" => handle_unban_user, "unbanUser", body = request::validate;
94        post "/admin/impersonate-user" => handle_impersonate_user, "impersonateUser", body = request::validate;
95        post "/admin/stop-impersonating" => handle_stop_impersonating, "stopImpersonating", require_headers = true;
96        post "/admin/revoke-user-session" => handle_revoke_user_session, "revokeUserSession", body = request::validate;
97        post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "revokeUserSessions", body = request::validate;
98        post "/admin/remove-user" => handle_remove_user, "removeUser", body = request::validate;
99        post "/admin/set-user-password" => handle_set_user_password, "setUserPassword", body = request::validate;
100        post "/admin/has-permission" => handle_has_permission, "userHasPermission", body = request::validate;
101    }
102    extra {
103        async fn on_init(
104            &self,
105            ctx: &mut better_auth_core::AuthInitContext<S>,
106        ) -> better_auth_core::AuthResult<()> {
107            self.config.validate()?;
108            S::User::require_plugin_fields("admin", &["role", "banned", "ban_reason", "ban_expires"])?;
109            S::Session::require_plugin_fields("admin", &["impersonated_by"])?;
110            ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
111            ctx.set_metadata(
112                "admin.default_role",
113                serde_json::Value::String(self.config.default_role.clone()),
114            );
115            ctx.extensions.insert(self.config.banned_user_message.clone());
116            ctx.extensions.insert(self.config.clone());
117            Ok(())
118        }
119    }
120}
121
122impl AdminPlugin {
123    /// Set a fixed or asynchronous message for rejected session creation.
124    pub fn banned_user_message(mut self, message: impl Into<BannedUserMessage>) -> Self {
125        self.config.banned_user_message = message.into();
126        self
127    }
128
129    async fn require_session(
130        &self,
131        req: &AuthRequest,
132        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
133    ) -> AuthResult<(UserView, SessionView)> {
134        ctx.require_authoritative_session(req)
135            .await
136            .map_err(|error| match error {
137                AuthError::Unauthenticated => AuthResponse::new(401).into(),
138                error => error,
139            })
140    }
141
142    async fn optional_session(
143        &self,
144        req: &AuthRequest,
145        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
146    ) -> AuthResult<Option<(UserView, SessionView)>> {
147        let data = ctx
148            .session_manager()
149            .resolve(req, better_auth_core::session::SessionRead::Authoritative)
150            .await?
151            .data;
152        if data.is_none() && (req.endpoint_headers().is_some() || req.original_request().is_some())
153        {
154            return Err(AuthResponse::new(401).into());
155        }
156        Ok(data.map(|data| (data.user, data.session)))
157    }
158
159    fn authorize(
160        &self,
161        user: &UserView,
162        resource: &str,
163        action: &str,
164        message: &str,
165    ) -> AuthResult<()> {
166        let permissions = HashMap::from([(resource.to_string(), vec![action.to_string()])]);
167        if has_permission(
168            user.id.as_str(),
169            user.role.as_deref(),
170            &self.config,
171            &permissions,
172        ) {
173            Ok(())
174        } else {
175            Err(AuthError::forbidden(message))
176        }
177    }
178
179    async fn handle_set_role(
180        &self,
181        req: &AuthRequest,
182        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
183    ) -> AuthResult<AuthResponse> {
184        let (user, _session) = self.require_session(req, ctx).await?;
185        self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
186        let body: SetRoleRequest = request::read(req)?;
187        let response = set_role_core(&body, &self.config, ctx).await?;
188        AuthResponse::json(200, &response).map_err(AuthError::from)
189    }
190
191    async fn handle_get_user(
192        &self,
193        req: &AuthRequest,
194        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
195    ) -> AuthResult<AuthResponse> {
196        let (user, _session) = self.require_session(req, ctx).await?;
197        self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
198        let query = GetUserQuery {
199            id: req
200                .query_string("id")?
201                .map(str::to_owned)
202                .unwrap_or_default(),
203        };
204        let response = get_user_core(&query, ctx).await?;
205        AuthResponse::json(200, &response).map_err(AuthError::from)
206    }
207
208    async fn handle_create_user(
209        &self,
210        req: &AuthRequest,
211        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212    ) -> AuthResult<AuthResponse> {
213        let body: CreateUserRequest = request::read(req)?;
214        let session = self.optional_session(req, ctx).await?;
215        if let Some((user, _)) = &session {
216            self.authorize(user, "user", "create", MESSAGE_CREATE_USERS)?;
217        }
218        let response = create_user_core(&body, Some(req), session, &self.config, ctx).await?;
219        AuthResponse::json(200, &response).map_err(AuthError::from)
220    }
221
222    async fn handle_update_user(
223        &self,
224        req: &AuthRequest,
225        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
226    ) -> AuthResult<AuthResponse> {
227        let (user, _session) = self.require_session(req, ctx).await?;
228        self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
229        let body: AdminUpdateUserRequest = request::read(req)?;
230        let response = update_user_core(&body, &user, &self.config, ctx).await?;
231        AuthResponse::json(200, &response).map_err(AuthError::from)
232    }
233
234    async fn handle_list_users(
235        &self,
236        req: &AuthRequest,
237        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
238    ) -> AuthResult<AuthResponse> {
239        let (user, _session) = self.require_session(req, ctx).await?;
240        self.authorize(&user, "user", "list", MESSAGE_LIST_USERS)?;
241        let query: ListUsersQueryParams = crate::plugins::query_input::parse(&req.query)?;
242        // The upstream admin endpoint catches list/count/projection failures.
243        let response = list_users_core(&query, ctx)
244            .await
245            .unwrap_or_else(|_| ListUsersResponse {
246                users: Vec::new(),
247                total: 0,
248                limit: None,
249                offset: None,
250            });
251        AuthResponse::json(200, &response).map_err(AuthError::from)
252    }
253
254    async fn handle_list_user_sessions(
255        &self,
256        req: &AuthRequest,
257        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
258    ) -> AuthResult<AuthResponse> {
259        let (user, _session) = self.require_session(req, ctx).await?;
260        self.authorize(&user, "session", "list", MESSAGE_LIST_USER_SESSIONS)?;
261        let body: UserIdRequest = request::read(req)?;
262        let response = list_user_sessions_core(&body, ctx).await?;
263        AuthResponse::json(200, &response).map_err(AuthError::from)
264    }
265
266    async fn handle_ban_user(
267        &self,
268        req: &AuthRequest,
269        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
270    ) -> AuthResult<AuthResponse> {
271        let (user, _session) = self.require_session(req, ctx).await?;
272        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
273        let body: BanUserRequest = request::read(req)?;
274        let response = ban_user_core(&body, user.id.typed()?.as_str(), &self.config, ctx).await?;
275        AuthResponse::json(200, &response).map_err(AuthError::from)
276    }
277
278    async fn handle_unban_user(
279        &self,
280        req: &AuthRequest,
281        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
282    ) -> AuthResult<AuthResponse> {
283        let (user, _session) = self.require_session(req, ctx).await?;
284        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
285        let body: UserIdRequest = request::read(req)?;
286        let response = unban_user_core(&body, ctx).await?;
287        AuthResponse::json(200, &response).map_err(AuthError::from)
288    }
289
290    async fn handle_impersonate_user(
291        &self,
292        req: &AuthRequest,
293        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
294    ) -> AuthResult<AuthResponse> {
295        let (user, session) = self.require_session(req, ctx).await?;
296        self.authorize(&user, "user", "impersonate", MESSAGE_IMPERSONATE_USERS)?;
297        let body: UserIdRequest = request::read(req)?;
298        let (response, data) = impersonate_user_core(
299            &body,
300            &user,
301            ctx.config.advanced.ip_address.resolve(req).as_deref(),
302            req.headers.get("user-agent").map(|value| value.as_str()),
303            &self.config,
304            ctx,
305        )
306        .await?;
307        let dont_remember = ctx.session_manager().dont_remember(req);
308        let admin_cookie = create_admin_session_cookie_value(
309            ctx.config.signing_secret(),
310            &AdminSessionCookiePayload {
311                session_token: session.token.clone(),
312                dont_remember,
313            },
314            ctx.config.session.expires_in(),
315        )?;
316        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
317
318        delete_session_cookies(req, &ctx.config, false, None)?;
319        req.append_response_header(
320            "Set-Cookie",
321            create_session_like_cookie(
322                &admin_cookie_name,
323                &admin_cookie,
324                Some(ctx.config.session.expires_in().as_seconds_f64()),
325                &ctx.config,
326            )?,
327        )?;
328        ctx.session_manager()
329            .set_session_cookie(req, data, Some(true))
330            .await?;
331        let auth_response = AuthResponse::json(200, &response)?;
332        Ok(auth_response)
333    }
334
335    async fn handle_stop_impersonating(
336        &self,
337        req: &AuthRequest,
338        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
339    ) -> AuthResult<AuthResponse> {
340        let session_manager = ctx.session_manager();
341        let session = session_manager
342            .resolve(req, better_auth_core::session::SessionRead::Authoritative)
343            .await?
344            .data
345            .ok_or_else(|| AuthError::from(AuthResponse::new(401)))?
346            .session;
347        if session.impersonated_by.is_none() {
348            return Err(AuthError::bad_request("You are not impersonating anyone"));
349        }
350
351        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
352        let admin_cookie_value = get_cookie(req, &admin_cookie_name)
353            .ok_or_else(|| AuthError::internal("Failed to find admin session"))?;
354        let admin_cookie =
355            decode_admin_session_cookie_value(ctx.config.signing_secret(), &admin_cookie_value)
356                .map_err(|_| AuthError::internal("Failed to find admin session"))?;
357
358        let (response, data) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
359
360        ctx.session_manager()
361            .set_session_cookie(req, data, Some(admin_cookie.dont_remember))
362            .await?;
363        let mut auth_response = AuthResponse::json(200, &response)?;
364        better_auth_core::utils::cookie_utils::remove_set_cookie_entries(
365            req,
366            Some(&mut auth_response.headers),
367            &admin_cookie_name,
368        )?;
369        auth_response = auth_response.with_appended_header(
370            "Set-Cookie",
371            create_clear_cookie(&admin_cookie_name, &ctx.config)?,
372        );
373        Ok(auth_response)
374    }
375
376    async fn handle_revoke_user_session(
377        &self,
378        req: &AuthRequest,
379        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
380    ) -> AuthResult<AuthResponse> {
381        let (user, _session) = self.require_session(req, ctx).await?;
382        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
383        let body: RevokeSessionRequest = request::read(req)?;
384        let response = revoke_user_session_core(&body, ctx).await?;
385        AuthResponse::json(200, &response).map_err(AuthError::from)
386    }
387
388    async fn handle_revoke_user_sessions(
389        &self,
390        req: &AuthRequest,
391        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
392    ) -> AuthResult<AuthResponse> {
393        let (user, _session) = self.require_session(req, ctx).await?;
394        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
395        let body: UserIdRequest = request::read(req)?;
396        let response = revoke_user_sessions_core(&body, ctx).await?;
397        AuthResponse::json(200, &response).map_err(AuthError::from)
398    }
399
400    async fn handle_remove_user(
401        &self,
402        req: &AuthRequest,
403        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
404    ) -> AuthResult<AuthResponse> {
405        let (user, _session) = self.require_session(req, ctx).await?;
406        self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
407        let body: UserIdRequest = request::read(req)?;
408        let response = remove_user_core(&body, user.id.typed()?.as_str(), ctx).await?;
409        AuthResponse::json(200, &response).map_err(AuthError::from)
410    }
411
412    async fn handle_set_user_password(
413        &self,
414        req: &AuthRequest,
415        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
416    ) -> AuthResult<AuthResponse> {
417        let (user, _session) = self.require_session(req, ctx).await?;
418        self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
419        let body: SetUserPasswordRequest = request::read(req)?;
420        let response = set_user_password_core(&body, ctx).await?;
421        AuthResponse::json(200, &response).map_err(AuthError::from)
422    }
423
424    async fn handle_has_permission(
425        &self,
426        req: &AuthRequest,
427        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
428    ) -> AuthResult<AuthResponse> {
429        let body: HasPermissionRequest = request::read(req)?;
430        let requested = body.requested_permissions().ok_or_else(|| {
431            AuthError::bad_request("invalid permission check. no permission(s) were passed.")
432        })?;
433        let session = self.optional_session(req, ctx).await?;
434        let user_id = body.user_id.as_deref().filter(|value| !value.is_empty());
435        let role = body.role.as_deref().filter(|value| !value.is_empty());
436        if session.is_none() && user_id.is_none() && role.is_none() {
437            return Err(AuthError::bad_request("user id or role is required"));
438        }
439        let user = match session {
440            Some((user, _)) => Some(user),
441            None if role.is_some() => None,
442            None => Some(
443                ctx.database
444                    .get_user_by_id(
445                        user_id.ok_or_else(|| AuthError::bad_request("user not found"))?,
446                    )
447                    .await?
448                    .ok_or_else(|| AuthError::bad_request("user not found"))?,
449            ),
450        };
451        let response = match user {
452            Some(user) => has_permission_core(&body, &user, &self.config)?,
453            None => PermissionResponse {
454                error: None,
455                success: has_permission(user_id, role, &self.config, requested),
456            },
457        };
458        AuthResponse::json(200, &response).map_err(AuthError::from)
459    }
460}
461
462pub(super) fn target_is_admin(
463    user_id: Option<&str>,
464    role: Option<&str>,
465    config: &AdminConfig,
466) -> bool {
467    is_admin_user_id(user_id, config) || is_admin_role(role, config)
468}
469
470pub use access::RolePermissions;