Skip to main content

better_auth_api/plugins/admin/
mod.rs

1use std::collections::HashMap;
2
3use better_auth_core::entity::AuthUser;
4use better_auth_core::utils::cookie_utils::{
5    create_clear_cookie, create_session_cookie_with_max_age, create_session_like_cookie,
6    related_cookie_name,
7};
8use better_auth_core::utils::username::{UsernameValidationError, validate_username};
9use better_auth_core::wire::{SessionView, UserView};
10use better_auth_core::{
11    AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, ErrorCodeMessageResponse,
12};
13use validator::Validate;
14
15pub mod access;
16pub(super) mod handlers;
17pub(super) mod types;
18
19#[cfg(test)]
20mod tests;
21
22use crate::plugins::helpers::{delete_session_cookie_headers, get_cookie};
23use access::{has_permission, is_admin_role, is_admin_user_id};
24use handlers::*;
25use types::*;
26
27const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
28const MESSAGE_CREATE_USERS: &str = "You are not allowed to create users";
29const MESSAGE_LIST_USERS: &str = "You are not allowed to list users";
30const MESSAGE_LIST_USER_SESSIONS: &str = "You are not allowed to list users sessions";
31const MESSAGE_BAN_USERS: &str = "You are not allowed to ban users";
32const MESSAGE_IMPERSONATE_USERS: &str = "You are not allowed to impersonate users";
33const MESSAGE_REVOKE_USER_SESSIONS: &str = "You are not allowed to revoke users sessions";
34const MESSAGE_DELETE_USERS: &str = "You are not allowed to delete users";
35const MESSAGE_SET_USER_PASSWORD: &str = "You are not allowed to set users password";
36const MESSAGE_GET_USER: &str = "You are not allowed to get user";
37const MESSAGE_UPDATE_USERS: &str = "You are not allowed to update users";
38const MESSAGE_USERNAME_IS_ALREADY_TAKEN: &str = "Username is already taken. Please try another.";
39const MESSAGE_USERNAME_TOO_SHORT: &str = "Username is too short";
40const MESSAGE_USERNAME_TOO_LONG: &str = "Username is too long";
41const MESSAGE_INVALID_USERNAME: &str = "Username is invalid";
42
43fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
44    AuthResponse::json(
45        status,
46        &ErrorCodeMessageResponse {
47            code: code.to_string(),
48            message: message.to_string(),
49        },
50    )
51    .map_err(AuthError::from)
52}
53
54/// Admin plugin for user management operations.
55pub struct AdminPlugin {
56    config: AdminConfig,
57}
58
59/// Configuration for the admin plugin.
60#[derive(Debug, Clone, better_auth_core::PluginConfig)]
61#[plugin(name = "AdminPlugin")]
62pub struct AdminConfig {
63    /// Default role assigned to new users and role-less permission checks.
64    #[config(default = "user".to_string())]
65    pub default_role: String,
66    /// Roles treated as "admin" for target-admin checks such as impersonation.
67    #[config(default = vec!["admin".to_string()])]
68    pub admin_roles: Vec<String>,
69    /// Users that always bypass admin permission checks.
70    #[config(default = None)]
71    pub admin_user_ids: Option<Vec<String>>,
72    /// Custom role definitions. When provided, these replace the built-in
73    /// `admin` and `user` role permissions.
74    #[config(default = HashMap::new())]
75    pub roles: HashMap<String, access::RolePermissions>,
76    /// Default reason applied when banning a user without an explicit reason.
77    #[config(default = None)]
78    pub default_ban_reason: Option<String>,
79    /// Default ban duration in seconds when banning a user without an explicit duration.
80    #[config(default = None)]
81    pub default_ban_expires_in: Option<i64>,
82    /// Custom impersonation session duration in seconds.
83    #[config(default = None)]
84    pub impersonation_session_duration: Option<i64>,
85    /// Message surfaced to banned users.
86    #[config(default = "You have been banned from this application. Please contact support if you believe this is an error.".to_string())]
87    pub banned_user_message: String,
88    /// Whether other admin users may be impersonated.
89    #[config(default = false)]
90    pub allow_impersonating_admins: bool,
91}
92
93better_auth_core::impl_auth_plugin! {
94    AdminPlugin, "admin";
95    routes {
96        post "/admin/set-role" => handle_set_role, "admin_set_role";
97        get  "/admin/get-user" => handle_get_user, "admin_get_user";
98        post "/admin/create-user" => handle_create_user, "admin_create_user";
99        post "/admin/update-user" => handle_update_user, "admin_update_user";
100        get  "/admin/list-users" => handle_list_users, "admin_list_users";
101        post "/admin/list-user-sessions" => handle_list_user_sessions, "admin_list_user_sessions";
102        post "/admin/ban-user" => handle_ban_user, "admin_ban_user";
103        post "/admin/unban-user" => handle_unban_user, "admin_unban_user";
104        post "/admin/impersonate-user" => handle_impersonate_user, "admin_impersonate_user";
105        post "/admin/stop-impersonating" => handle_stop_impersonating, "admin_stop_impersonating";
106        post "/admin/revoke-user-session" => handle_revoke_user_session, "admin_revoke_user_session";
107        post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "admin_revoke_user_sessions";
108        post "/admin/remove-user" => handle_remove_user, "admin_remove_user";
109        post "/admin/set-user-password" => handle_set_user_password, "admin_set_user_password";
110        post "/admin/has-permission" => handle_has_permission, "admin_has_permission";
111    }
112    extra {
113        async fn on_init(
114            &self,
115            ctx: &mut better_auth_core::AuthInitContext<S>,
116        ) -> better_auth_core::AuthResult<()> {
117            ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
118            ctx.set_metadata(
119                "admin.default_role",
120                serde_json::Value::String(self.config.default_role.clone()),
121            );
122            ctx.set_metadata(
123                "admin.banned_user_message",
124                serde_json::Value::String(self.config.banned_user_message.clone()),
125            );
126            Ok(())
127        }
128    }
129}
130
131impl AdminPlugin {
132    async fn require_session(
133        &self,
134        req: &AuthRequest,
135        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
136    ) -> AuthResult<(UserView, SessionView)> {
137        let (user, session) = ctx.require_session(req).await?;
138        Ok((UserView::from(&user), SessionView::from(&session)))
139    }
140
141    fn authorize(
142        &self,
143        user: &UserView,
144        resource: &str,
145        action: &str,
146        message: &str,
147    ) -> AuthResult<()> {
148        let permissions = HashMap::from([(resource.to_string(), vec![action.to_string()])]);
149        if has_permission(
150            Some(user.id.as_str()),
151            user.role.as_deref(),
152            &self.config,
153            &permissions,
154        ) {
155            Ok(())
156        } else {
157            Err(AuthError::forbidden(message))
158        }
159    }
160
161    async fn handle_set_role(
162        &self,
163        req: &AuthRequest,
164        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
165    ) -> AuthResult<AuthResponse> {
166        let (user, _session) = self.require_session(req, ctx).await?;
167        self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
168        let body: SetRoleRequest = match better_auth_core::validate_request_body(req) {
169            Ok(v) => v,
170            Err(resp) => return Ok(resp),
171        };
172        let response = set_role_core(&body, &self.config, ctx).await?;
173        AuthResponse::json(200, &response).map_err(AuthError::from)
174    }
175
176    async fn handle_get_user(
177        &self,
178        req: &AuthRequest,
179        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
180    ) -> AuthResult<AuthResponse> {
181        let (user, _session) = self.require_session(req, ctx).await?;
182        self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
183        let query = GetUserQuery {
184            id: req.query.get("id").cloned().unwrap_or_default(),
185        };
186        query
187            .validate()
188            .map_err(|error| AuthError::validation(error.to_string()))?;
189        let response = get_user_core(&query, ctx).await?;
190        AuthResponse::json(200, &response).map_err(AuthError::from)
191    }
192
193    async fn handle_create_user(
194        &self,
195        req: &AuthRequest,
196        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
197    ) -> AuthResult<AuthResponse> {
198        let (user, _session) = self.require_session(req, ctx).await?;
199        self.authorize(&user, "user", "create", MESSAGE_CREATE_USERS)?;
200        let body: CreateUserRequest = match better_auth_core::validate_request_body(req) {
201            Ok(v) => v,
202            Err(resp) => return Ok(resp),
203        };
204        let response = create_user_core(&body, &self.config, ctx).await?;
205        AuthResponse::json(200, &response).map_err(AuthError::from)
206    }
207
208    async fn handle_update_user(
209        &self,
210        req: &AuthRequest,
211        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212    ) -> AuthResult<AuthResponse> {
213        let (user, _session) = self.require_session(req, ctx).await?;
214        self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
215        let mut body: AdminUpdateUserRequest = match better_auth_core::validate_request_body(req) {
216            Ok(v) => v,
217            Err(resp) => return Ok(resp),
218        };
219        let username = body
220            .data
221            .remove("username")
222            .and_then(|value| value.as_str().map(ToOwned::to_owned))
223            .map(|value| value.to_lowercase());
224        let display_username = body
225            .data
226            .remove("displayUsername")
227            .and_then(|value| value.as_str().map(ToOwned::to_owned));
228
229        if let Some(username) = username.as_deref() {
230            match validate_username(username) {
231                Ok(()) => {}
232                Err(UsernameValidationError::TooShort) => {
233                    return username_error_response(
234                        400,
235                        "USERNAME_TOO_SHORT",
236                        MESSAGE_USERNAME_TOO_SHORT,
237                    );
238                }
239                Err(UsernameValidationError::TooLong) => {
240                    return username_error_response(
241                        400,
242                        "USERNAME_IS_TOO_LONG",
243                        MESSAGE_USERNAME_TOO_LONG,
244                    );
245                }
246                Err(UsernameValidationError::Invalid) => {
247                    return username_error_response(
248                        400,
249                        "USERNAME_IS_INVALID",
250                        MESSAGE_INVALID_USERNAME,
251                    );
252                }
253            }
254
255            if let Some(existing_user) = ctx.database.get_user_by_username(username).await?
256                && AuthUser::id(&existing_user).as_ref() != body.user_id
257            {
258                return username_error_response(
259                    400,
260                    "USERNAME_IS_ALREADY_TAKEN",
261                    MESSAGE_USERNAME_IS_ALREADY_TAKEN,
262                );
263            }
264        }
265
266        if let Some(username) = username {
267            _ = body
268                .data
269                .insert("username".to_string(), serde_json::Value::String(username));
270        }
271        if let Some(display_username) = display_username {
272            _ = body.data.insert(
273                "displayUsername".to_string(),
274                serde_json::Value::String(display_username),
275            );
276        }
277
278        let response = update_user_core(&body, &user, &self.config, ctx).await?;
279        AuthResponse::json(200, &response).map_err(AuthError::from)
280    }
281
282    async fn handle_list_users(
283        &self,
284        req: &AuthRequest,
285        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
286    ) -> AuthResult<AuthResponse> {
287        let (user, _session) = self.require_session(req, ctx).await?;
288        self.authorize(&user, "user", "list", MESSAGE_LIST_USERS)?;
289        let query = ListUsersQueryParams {
290            limit: req.query.get("limit").and_then(|value| value.parse().ok()),
291            offset: req.query.get("offset").and_then(|value| value.parse().ok()),
292            search_field: req.query.get("searchField").cloned(),
293            search_value: req.query.get("searchValue").cloned(),
294            search_operator: req.query.get("searchOperator").cloned(),
295            sort_by: req.query.get("sortBy").cloned(),
296            sort_direction: req.query.get("sortDirection").cloned(),
297            filter_field: req.query.get("filterField").cloned(),
298            filter_value: req.query.get("filterValue").cloned(),
299            filter_operator: req.query.get("filterOperator").cloned(),
300        };
301        let response = list_users_core(&query, ctx).await?;
302        AuthResponse::json(200, &response).map_err(AuthError::from)
303    }
304
305    async fn handle_list_user_sessions(
306        &self,
307        req: &AuthRequest,
308        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
309    ) -> AuthResult<AuthResponse> {
310        let (user, _session) = self.require_session(req, ctx).await?;
311        self.authorize(&user, "session", "list", MESSAGE_LIST_USER_SESSIONS)?;
312        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
313            Ok(v) => v,
314            Err(resp) => return Ok(resp),
315        };
316        let response = list_user_sessions_core(&body, ctx).await?;
317        AuthResponse::json(200, &response).map_err(AuthError::from)
318    }
319
320    async fn handle_ban_user(
321        &self,
322        req: &AuthRequest,
323        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
324    ) -> AuthResult<AuthResponse> {
325        let (user, _session) = self.require_session(req, ctx).await?;
326        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
327        let body: BanUserRequest = match better_auth_core::validate_request_body(req) {
328            Ok(v) => v,
329            Err(resp) => return Ok(resp),
330        };
331        let response = ban_user_core(&body, user.id.as_str(), &self.config, ctx).await?;
332        AuthResponse::json(200, &response).map_err(AuthError::from)
333    }
334
335    async fn handle_unban_user(
336        &self,
337        req: &AuthRequest,
338        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
339    ) -> AuthResult<AuthResponse> {
340        let (user, _session) = self.require_session(req, ctx).await?;
341        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
342        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
343            Ok(v) => v,
344            Err(resp) => return Ok(resp),
345        };
346        let response = unban_user_core(&body, ctx).await?;
347        AuthResponse::json(200, &response).map_err(AuthError::from)
348    }
349
350    async fn handle_impersonate_user(
351        &self,
352        req: &AuthRequest,
353        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
354    ) -> AuthResult<AuthResponse> {
355        let (user, session) = self.require_session(req, ctx).await?;
356        self.authorize(&user, "user", "impersonate", MESSAGE_IMPERSONATE_USERS)?;
357        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
358            Ok(v) => v,
359            Err(resp) => return Ok(resp),
360        };
361        let (response, token) = impersonate_user_core(
362            &body,
363            user.id.as_str(),
364            req.headers
365                .get("x-forwarded-for")
366                .map(|value| value.as_str()),
367            req.headers.get("user-agent").map(|value| value.as_str()),
368            &self.config,
369            ctx,
370        )
371        .await?;
372        let dont_remember =
373            get_cookie(req, &related_cookie_name(&ctx.config, "dont_remember")).is_some();
374        let admin_cookie = create_admin_session_cookie_value(
375            &ctx.config.secret,
376            &AdminSessionCookiePayload {
377                session_token: session.token.clone(),
378                dont_remember,
379            },
380            ctx.config.session.expires_in,
381        )?;
382        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
383
384        let mut auth_response = AuthResponse::json(200, &response)?;
385        for cookie in delete_session_cookie_headers(&ctx.config) {
386            auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
387        }
388        auth_response = auth_response.with_appended_header(
389            "Set-Cookie",
390            create_session_like_cookie(
391                &admin_cookie_name,
392                &admin_cookie,
393                Some(ctx.config.session.expires_in.num_seconds()),
394                &ctx.config,
395            ),
396        );
397        auth_response = auth_response.with_appended_header(
398            "Set-Cookie",
399            create_session_cookie_with_max_age(Some(&token), None, &ctx.config),
400        );
401        auth_response = auth_response.with_appended_header(
402            "Set-Cookie",
403            create_session_like_cookie(
404                &related_cookie_name(&ctx.config, "dont_remember"),
405                "true",
406                None,
407                &ctx.config,
408            ),
409        );
410        Ok(auth_response)
411    }
412
413    async fn handle_stop_impersonating(
414        &self,
415        req: &AuthRequest,
416        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
417    ) -> AuthResult<AuthResponse> {
418        let session_manager = ctx.session_manager();
419        let token = session_manager
420            .extract_session_token(req)
421            .ok_or(AuthError::Unauthenticated)?;
422        let session = session_manager
423            .get_session(&token)
424            .await?
425            .ok_or(AuthError::Unauthenticated)?;
426        let session = SessionView::from(&session);
427        if session.impersonated_by.is_none() {
428            return Err(AuthError::bad_request("You are not impersonating anyone"));
429        }
430
431        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
432        let admin_cookie_value = get_cookie(req, &admin_cookie_name)
433            .ok_or_else(|| AuthError::internal("Failed to find admin session"))?;
434        let admin_cookie =
435            decode_admin_session_cookie_value(&ctx.config.secret, &admin_cookie_value)
436                .map_err(|_| AuthError::internal("Failed to find admin session"))?;
437
438        let (response, new_token) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
439
440        let mut auth_response = AuthResponse::json(200, &response)?;
441        auth_response = auth_response.with_appended_header(
442            "Set-Cookie",
443            create_session_cookie_with_max_age(
444                Some(&new_token),
445                if admin_cookie.dont_remember {
446                    None
447                } else {
448                    Some(ctx.config.session.expires_in.num_seconds())
449                },
450                &ctx.config,
451            ),
452        );
453        if admin_cookie.dont_remember {
454            auth_response = auth_response.with_appended_header(
455                "Set-Cookie",
456                create_session_like_cookie(
457                    &related_cookie_name(&ctx.config, "dont_remember"),
458                    "true",
459                    None,
460                    &ctx.config,
461                ),
462            );
463        }
464        auth_response = auth_response.with_appended_header(
465            "Set-Cookie",
466            create_clear_cookie(&admin_cookie_name, &ctx.config),
467        );
468        Ok(auth_response)
469    }
470
471    async fn handle_revoke_user_session(
472        &self,
473        req: &AuthRequest,
474        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
475    ) -> AuthResult<AuthResponse> {
476        let (user, _session) = self.require_session(req, ctx).await?;
477        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
478        let body: RevokeSessionRequest = match better_auth_core::validate_request_body(req) {
479            Ok(v) => v,
480            Err(resp) => return Ok(resp),
481        };
482        let response = revoke_user_session_core(&body, ctx).await?;
483        AuthResponse::json(200, &response).map_err(AuthError::from)
484    }
485
486    async fn handle_revoke_user_sessions(
487        &self,
488        req: &AuthRequest,
489        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
490    ) -> AuthResult<AuthResponse> {
491        let (user, _session) = self.require_session(req, ctx).await?;
492        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
493        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
494            Ok(v) => v,
495            Err(resp) => return Ok(resp),
496        };
497        let response = revoke_user_sessions_core(&body, ctx).await?;
498        AuthResponse::json(200, &response).map_err(AuthError::from)
499    }
500
501    async fn handle_remove_user(
502        &self,
503        req: &AuthRequest,
504        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
505    ) -> AuthResult<AuthResponse> {
506        let (user, _session) = self.require_session(req, ctx).await?;
507        self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
508        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
509            Ok(v) => v,
510            Err(resp) => return Ok(resp),
511        };
512        let response = remove_user_core(&body, user.id.as_str(), ctx).await?;
513        AuthResponse::json(200, &response).map_err(AuthError::from)
514    }
515
516    async fn handle_set_user_password(
517        &self,
518        req: &AuthRequest,
519        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
520    ) -> AuthResult<AuthResponse> {
521        let (user, _session) = self.require_session(req, ctx).await?;
522        self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
523        let body: SetUserPasswordRequest = match better_auth_core::validate_request_body(req) {
524            Ok(v) => v,
525            Err(resp) => return Ok(resp),
526        };
527        let response = set_user_password_core(&body, ctx).await?;
528        AuthResponse::json(200, &response).map_err(AuthError::from)
529    }
530
531    async fn handle_has_permission(
532        &self,
533        req: &AuthRequest,
534        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
535    ) -> AuthResult<AuthResponse> {
536        let (user, _session) = self.require_session(req, ctx).await?;
537        let body: HasPermissionRequest = match better_auth_core::validate_request_body(req) {
538            Ok(v) => v,
539            Err(resp) => return Ok(resp),
540        };
541        let response = has_permission_core(&body, &user, &self.config)?;
542        AuthResponse::json(200, &response).map_err(AuthError::from)
543    }
544}
545
546pub(super) fn target_is_admin(
547    user_id: Option<&str>,
548    role: Option<&str>,
549    config: &AdminConfig,
550) -> bool {
551    is_admin_user_id(user_id, config) || is_admin_role(role, config)
552}
553
554pub use access::RolePermissions;