Skip to main content

Authentication and Permissions

Strapi ships with a full authentication system out of the box via the Users & Permissions plugin. Understanding how to configure and extend it is critical for any production application.

Authentication flow


User registration and login

Register a new user

const response = await fetch('/api/auth/local/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'johndoe',
email: 'john@example.com',
password: 'SecureP@ss123',
}),
});

const { jwt, user } = await response.json();
// Store jwt for subsequent requests

Login

const response = await fetch('/api/auth/local', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identifier: 'john@example.com', // email or username
password: 'SecureP@ss123',
}),
});

const { jwt, user } = await response.json();

Using the JWT

const articles = await fetch('/api/articles', {
headers: {
Authorization: `Bearer ${jwt}`,
},
});

Role-based access control (RBAC)

Strapi comes with two default roles for API users:

RoleDefault behaviour
PublicUnauthenticated requests. No permissions by default.
AuthenticatedLogged-in users. Basic read permissions by default.

You can create additional roles (e.g., Editor, Moderator, Premium) in the admin panel under Settings > Users & Permissions > Roles.

Checking roles in code

// In a policy or controller
const user = ctx.state.user;

if (!user) {
return ctx.unauthorized('You must be logged in');
}

// user.role is populated automatically
if (user.role.type !== 'editor') {
return ctx.forbidden('Only editors can perform this action');
}

Role-based policy

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

if (!user) {
return false;
}

const allowedRoles = config.roles || [];

if (!allowedRoles.includes(user.role.type)) {
return false;
}

return true;
};
// Usage in route config
module.exports = createCoreRouter('api::article.article', {
config: {
create: {
policies: [
{
name: 'global::has-role',
config: { roles: ['editor', 'admin'] },
},
],
},
},
});

API tokens (machine-to-machine)

For server-to-server communication, use API tokens instead of user JWTs. Create them in Settings > API Tokens.

Token typeUse case
Read-onlyExternal frontend fetching published content
Full accessCI/CD pipelines, automated imports
CustomFine-grained per-content-type permissions
# Using an API token
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
http://localhost:1337/api/articles

Transfer tokens

Transfer tokens are a separate concept, used by the strapi transfer CLI command to move data between Strapi instances. Do not confuse them with API tokens.


Custom registration flow

Override the default registration to add fields, send welcome emails, or restrict sign-ups:

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

plugin.controllers.auth.register = async (ctx) => {
const { email } = ctx.request.body;

// Restrict registration to company domain
if (!email.endsWith('@mycompany.com')) {
return ctx.badRequest('Registration is limited to company email addresses');
}

// Call the original register
await originalRegister(ctx);

// After successful registration, send welcome email
const user = ctx.response.body.user;
if (user) {
await strapi.plugins['email'].services.email.send({
to: user.email,
subject: 'Welcome!',
html: `<h1>Welcome, ${user.username}!</h1><p>Your account has been created.</p>`,
});
}
};

return plugin;
};

OAuth / third-party providers

Strapi supports OAuth providers (Google, GitHub, Facebook, etc.) through the Users & Permissions plugin.

Configuration

// config/plugins.js
module.exports = ({ env }) => ({
'users-permissions': {
config: {
providers: {
google: {
enabled: true,
key: env('GOOGLE_CLIENT_ID'),
secret: env('GOOGLE_CLIENT_SECRET'),
callbackURL: '/api/connect/google/callback',
scope: ['email', 'profile'],
},
github: {
enabled: true,
key: env('GITHUB_CLIENT_ID'),
secret: env('GITHUB_CLIENT_SECRET'),
callbackURL: '/api/connect/github/callback',
scope: ['user:email'],
},
},
},
},
});

Frontend redirect flow

// 1. Redirect user to Strapi's provider endpoint
window.location.href = 'http://localhost:1337/api/connect/google';

// 2. After OAuth, Strapi redirects back with an access_token param
// 3. Exchange for a Strapi JWT:
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');

const response = await fetch(
`http://localhost:1337/api/auth/google/callback?access_token=${accessToken}`
);
const { jwt, user } = await response.json();

Securing the admin panel

The admin panel has its own separate auth system. Key security settings:

// config/admin.js
module.exports = ({ env }) => ({
auth: {
secret: env('ADMIN_JWT_SECRET'),
},
apiToken: {
salt: env('API_TOKEN_SALT'),
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT'),
},
},
// Rate limiting for admin login
rateLimit: {
enabled: true,
interval: { min: 5 },
max: 5,
},
});

Hardening permissions checklist

ActionWhy
Review the Public role permissionsBy default, nothing is exposed. Only enable what anonymous users need.
Use find and findOne only for publicNever expose create, update, delete to the Public role
Set strong JWT secretsUse long, random secrets via environment variables
Rotate API tokensTreat API tokens like passwords. Rotate regularly.
Enable rate limitingPrevent brute-force attacks on /api/auth/local
Validate email before loginEnable email confirmation in Users & Permissions settings
Restrict registrationIf your app doesn't need public sign-up, disable it
Use HTTPSNever send JWTs over unencrypted connections

Common pitfalls

PitfallProblemFix
Storing JWT in localStorageVulnerable to XSS attacksUse httpOnly cookies or secure session storage
Public role exposes createAnyone can create contentAudit Public role permissions
No email confirmationFake accounts flood the systemEnable email confirmation
Hardcoded JWT secretSame secret across environmentsUse env vars: env('JWT_SECRET')
Forgetting ctx.state.user checkController assumes user existsAlways guard with if (!ctx.state.user)

See also