Skip to main content

Customizing Users & Permissions plugin routes

Page summary:

The Users & Permissions plugin exposes /users and /auth routes that can be extended or overridden using the plugin extension system. This guide shows how to add custom policies, override controllers, and add new routes to the User collection.

The Users & Permissions plugin ships with built-in routes for authentication (/auth) and user management (/users). Because these routes belong to a plugin rather than a user-created content-type, they cannot be customized with createCoreRouter. Instead, you extend them through the plugin extension system using a strapi-server.js or strapi-server.ts file in the ./src/extensions/users-permissions/ folder.

Prerequisites
  • A Strapi 5 project with the Users & Permissions plugin installed (included by default).
  • Familiarity with routes and policies.

How Users & Permissions routes work

Unlike content-types you create (e.g., api::restaurant.restaurant), the Users & Permissions plugin registers its routes inside the plugin.routes['content-api'].routes array. This array contains all /users, /auth, and /roles route definitions.

Each route is an object with the following shape:

{
method: 'GET', // HTTP method
path: '/users', // URL path (relative to /api)
handler: 'user.find', // controller.action
config: {
prefix: '', // path prefix (empty means /api)
policies: [], // array of policies to run before the handler
middlewares: [], // array of middlewares
},
}

The available user controller actions are: find, findOne, create, update, destroy, me, and count.

The available auth controller actions are: callback (login), register, forgotPassword, resetPassword, changePassword, emailConfirmation, sendEmailConfirmation, connect, and refresh.

Extend routes with strapi-server

All customizations to the Users & Permissions plugin go in a single file:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
// Your customizations here

return plugin;
};

The function receives the full plugin object and must return it. You can modify plugin.routes, plugin.controllers, plugin.policies, and plugin.services before returning.

Add a custom policy to a user route

A common requirement is to restrict who can update or delete user accounts. For example, you might want to ensure that users can only update their own profile.

1. Create the policy file

Create a global policy that checks whether the authenticated user matches the target user:

./src/policies/is-own-user.js
module.exports = (policyContext, config, { strapi }) => {
const currentUser = policyContext.state.user;

if (!currentUser) {
return false;
}

const targetUserId = Number(policyContext.params.id);

if (currentUser.id !== targetUserId) {
return false;
}

return true;
};

2. Attach the policy to the user routes

In the plugin extension file, find the update and delete routes and add the policy:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
// Find the routes that need the policy
const routes = plugin.routes['content-api'].routes;

// Add the 'is-own-user' policy to the update route
const updateRoute = routes.find(
(route) => route.handler === 'user.update'
);

if (updateRoute) {
updateRoute.config = updateRoute.config || {};
updateRoute.config.policies = updateRoute.config.policies || [];
updateRoute.config.policies.push('global::is-own-user');
}

// Add the same policy to the delete route
const deleteRoute = routes.find(
(route) => route.handler === 'user.destroy'
);

if (deleteRoute) {
deleteRoute.config = deleteRoute.config || {};
deleteRoute.config.policies = deleteRoute.config.policies || [];
deleteRoute.config.policies.push('global::is-own-user');
}

return plugin;
};

With this configuration, PUT /api/users/:id and DELETE /api/users/:id will return a 403 Forbidden error if the authenticated user does not match the :id in the URL.

Tip

For a more informative error message, throw a PolicyError instead of returning false:

const { errors } = require('@strapi/utils');
const { PolicyError } = errors;

// Inside the policy:
throw new PolicyError('You can only modify your own account');

See the policies documentation for more details.

Override a controller action

You can replace or wrap an existing controller action. For instance, to add custom logic to the me endpoint:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
const originalMe = plugin.controllers.user.me;

plugin.controllers.user.me = async (ctx) => {
// Call the original controller
await originalMe(ctx);

// Add extra data to the response
if (ctx.body) {
ctx.body.timestamp = new Date().toISOString();
}
};

return plugin;
};
Caution

When wrapping a controller, always call the original function first to preserve the default behavior. Skipping the original function means you take over the full request handling, including sanitization and error handling.

Add a new route

You can add custom routes to the Users & Permissions plugin. For example, to add an endpoint that deactivates a user account:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
// Add a new controller action
plugin.controllers.user.deactivate = async (ctx) => {
const { id } = ctx.params;

const user = await strapi
.plugin('users-permissions')
.service('user')
.edit(id, { blocked: true });

ctx.body = { message: `User ${user.username} has been deactivated` };
};

// Register the route
plugin.routes['content-api'].routes.push({
method: 'POST',
path: '/users/:id/deactivate',
handler: 'user.deactivate',
config: {
prefix: '',
policies: ['global::is-own-user'],
},
});

return plugin;
};

After restarting Strapi, POST /api/users/:id/deactivate becomes available. Grant the corresponding permission in the admin panel under Users & Permissions plugin > Roles for the roles that should access this endpoint.

Override an auth route

Authentication routes can be customized in the same way. For example, to add a rate-limiting middleware to the login endpoint or to run custom logic after registration:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
const originalRegister = plugin.controllers.auth.register;

plugin.controllers.auth.register = async (ctx) => {
// Call the original register logic
await originalRegister(ctx);

// Custom post-registration logic
if (ctx.body && ctx.body.user) {
strapi.log.info(`New user registered: ${ctx.body.user.email}`);
}
};

return plugin;
};

Remove a route

You can disable a route by filtering it out of the routes array. For example, to disable the user count endpoint:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter(
(route) => route.handler !== 'user.count'
);

return plugin;
};

Combine multiple customizations

In practice, you often combine several customizations in the same file. The following example adds a policy to update and delete, wraps the me controller, and adds a new route:

./src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
const routes = plugin.routes['content-api'].routes;

// 1. Add 'is-own-user' policy to update and delete
for (const route of routes) {
if (route.handler === 'user.update' || route.handler === 'user.destroy') {
route.config = route.config || {};
route.config.policies = route.config.policies || [];
route.config.policies.push('global::is-own-user');
}
}

// 2. Wrap the 'me' controller to include the user's role
const originalMe = plugin.controllers.user.me;

plugin.controllers.user.me = async (ctx) => {
await originalMe(ctx);

if (ctx.state.user && ctx.body) {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });

ctx.body.role = user.role;
}
};

// 3. Add a custom route
plugin.controllers.user.profile = async (ctx) => {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });

ctx.body = {
username: user.username,
email: user.email,
role: user.role?.name,
createdAt: user.createdAt,
};
};

routes.push({
method: 'GET',
path: '/users/profile',
handler: 'user.profile',
config: { prefix: '' },
});

return plugin;
};

Validation

After making changes, restart Strapi and verify your customizations:

  1. Run yarn strapi routes:list to confirm your new or modified routes appear.
  2. Test protected routes without authentication to verify policies return 403 Forbidden.
  3. Test with an authenticated user to confirm the expected behavior.
  4. Check the Strapi server logs for errors during startup.

Troubleshooting

SymptomPossible cause
Route not found (404)The route was not pushed to plugin.routes['content-api'].routes, or the prefix property is missing.
Policy not appliedThe policy name is incorrect. Global policies require the global:: prefix (e.g., global::is-own-user).
Controller returns 500The controller action name does not match the handler value in the route definition.
Changes not reflectedStrapi was not restarted after modifying the extension file. Extensions are loaded at startup.
Permission denied (403)The new action is not enabled for the role. Enable it in Users & Permissions plugin > Roles.