Skip to main content

Users & Permissions plugin

🏗 Work in progress

The content of this page might not be fully up-to-date with Strapi 5 yet.

This plugin provides a full authentication process based on JSON Web Tokens (JWT) to protect your API. It also provides an access-control list (ACL) strategy that enables you to manage permissions between groups of users.

The user guide describes how to use the Users & Permissions plugin from the admin panel. The present page is more about the developer-related aspects of using the Users & Permissions plugin.

Concept

The Users & Permissions plugin adds an access layer to your application. The plugin uses JWTs to authenticate users. Your JWT contains your user ID, which is matched to the group your user is in and used to determine whether to allow access to the route.

Each time an API request is sent the server checks if an Authorization header is present and verifies if the user making the request has access to the resource.

Manage role permissions

Public role

This is the default role used when the server receives a request without an Authorization header. Any permissions (i.e. accessible endpoints) granted to this role will be accessible by anyone.

It is common practice to select find / findOne endpoints when you want your front-end application to access all the content without requiring user authentication and authorization.

Authenticated role

This is the default role that is given to every new user at creation if no role is provided. In this role you define routes that a user can access.

Permissions management

By clicking on the Role name, you can see all functions available in your application (with these functions related to the specific route displayed).

If you check a function name, it makes this route accessible by the current role you are editing. On the right sidebar you can see the URL related to this function.

Update the default role

When you create a user without a role, or if you use the /api/auth/local/register route, the authenticated role is given to the user.

To change the default role, go to the Advanced settings tab and update the Default role for authenticated users option.

Authentication

Login

Submit the user's identifier and password credentials for authentication. On successful authentication the response data will have the user's information along with an authentication token.

Local

The identifier param can be an email or username.

import axios from 'axios';

// Request API.
axios
.post('http://localhost:1337/api/auth/local', {
identifier: 'user@strapi.io',
password: 'strapiPassword',
})
.then(response => {
// Handle success.
console.log('Well done!');
console.log('User profile', response.data.user);
console.log('User token', response.data.jwt);
})
.catch(error => {
// Handle error.
console.log('An error occurred:', error.response);
});

Token usage

The jwt may then be used for making permission-restricted API requests. To make an API request as a user place the JWT into an Authorization header of the GET request.

Any request without a token will assume the public role permissions by default. Modify the permissions of each user's role in the admin dashboard.

Authentication failures return a 401 (unauthorized) error.

Usage

The token variable is the data.jwt received when logging in or registering.

import axios from 'axios';

const token = 'YOUR_TOKEN_HERE';

// Request API.
axios
.get('http://localhost:1337/posts', {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(response => {
// Handle success.
console.log('Data: ', response.data);
})
.catch(error => {
// Handle error.
console.log('An error occurred:', error.response);
});

JWT configuration

You can configure the JWT generation by using the plugins configuration file.

Strapi uses jsonwebtoken to generate the JWT.

Available options:

  • jwtSecret: random string used to create new JWTs, typically set using the JWT_SECRET environment variable.
  • jwt.expiresIn: expressed in seconds or a string describing a time span.
    Eg: 60, "45m", "10h", "2 days", "7d", "2y". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (minutes, hours, days, years, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms").
./config/plugins.js

module.exports = ({ env }) => ({
// ...
'users-permissions': {
config: {
jwt: {
expiresIn: '7d',
},
},
},
// ...
});
⚠️ Warning

Setting JWT expiry for more than 30 days is not recommended due to security concerns.

Registration

Configuration

If you have added any additional fields to your user model that need to be accepted on registration, they need to be added to the list of allowed fields in the register configuration option, otherwise they will not be accepted.

For example, if you have added a field called "nickname" that you wish to accept from the API on user registration:

./config/plugins.js
module.exports = ({ env }) => ({
// ...
"users-permissions": {
config: {
register: {
allowedFields: ["nickname"],
},
},
},
// ...
});

Usage

Creates a new user in the database with a default role as 'registered'.

import axios from 'axios';

// Request API.
// Add your own code here to customize or restrict how the public can register new users.
axios
.post('http://localhost:1337/api/auth/local/register', {
username: 'Strapi user',
email: 'user@strapi.io',
password: 'strapiPassword',
})
.then(response => {
// Handle success.
console.log('Well done!');
console.log('User profile', response.data.user);
console.log('User token', response.data.jwt);
})
.catch(error => {
// Handle error.
console.log('An error occurred:', error.response);
});

Providers

Grant and Purest allow you to use OAuth and OAuth2 providers to enable authentication in your application.

For a better understanding, review the following description of the login flow. The example uses github as the provider but it works the same for other providers.

Understanding the login flow

Let's say that:

  • Strapi's backend is located at: strapi.website.com, and
  • Your app frontend is located at: website.com
  1. The user goes on your frontend app (https://website.com) and clicks on your button connect with Github.
  2. The frontend redirects the tab to the backend URL: https://strapi.website.com/api/connect/github.
  3. The backend redirects the tab to the GitHub login page where the user logs in.
  4. Once done, Github redirects the tab to the backend URL:https://strapi.website.com/api/connect/github/callback?code=abcdef.
  5. The backend uses the given code to get an access_token from Github that can be used for a period of time to make authorized requests to Github to get the user info.
  6. Then, the backend redirects the tab to the url of your choice with the param access_token (example: http://website.com/connect/github/redirect?access_token=eyfvg).
  7. The frontend (http://website.com/connect/github/redirect) calls the backend with https://strapi.website.com/api/auth/github/callback?access_token=eyfvg that returns the Strapi user profile with its jwt.
    (Under the hood, the backend asks Github for the user's profile and a match is done on Github user's email address and Strapi user's email address).
  8. The frontend now possesses the user's jwt, which means the user is connected and the frontend can make authenticated requests to the backend!

An example of a frontend app that handles this flow can be found here: react login example app.

Setting up the server url

Before setting up a provider you must specify the absolute url of your backend in server.js.

example - config/server.js

config/server.js

module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('', 'http://localhost:1337'),
});
💡 Tip

Later you will give this url to your provider.
For development, some providers accept the use of localhost urls but many don't. In this case we recommend to use ngrok (ngrok http 1337) that will make a proxy tunnel from a url it created to your localhost url (ex: url: env('', 'https://5299e8514242.ngrok.io'),).

Setting up the provider - examples

Instead of a generic explanation we decided to show an example for each provider. You can also create your own custom provider.

In the following examples, the frontend app will be the react login example app.
It (the frontend app) will be running on http://localhost:3000.
Strapi (the backend) will be running on http://localhost:1337.

Using ngrok

Github doesn't accept localhost urls.
Use ngrok to serve the backend app.

ngrok http 1337

Don't forget to update the server url in the backend config file config/server.js and the server url in your frontend app (environment variable REACT_APP_BACKEND_URL if you use react login example app) with the generated ngrok url.

Github configuration

  • Visit the OAuth Apps list page https://github.com/settings/developers
  • Click on New OAuth App button
  • Fill the information (replace with your own ngrok url):
    • Application name: Strapi GitHub auth
    • Homepage URL: https://65e60559.ngrok.io
    • Application description: Strapi provider auth description
    • Authorization callback URL: https://65e60559.ngrok.io/api/connect/github/callback

Strapi configuration

  • Visit the User Permissions provider settings page
    http://localhost:1337/admin/settings/users-permissions/providers
  • Click on the GitHub provider
  • Fill the information (replace with your own client ID and secret):
    • Enable: ON
    • Client ID: 53de5258f8472c140917
    • Client Secret: fb9d0fe1d345d9ac7f83d7a1e646b37c554dae8b
    • The redirect URL to your front-end app: http://localhost:3000/connect/github/redirect

Your configuration is done. Launch the backend and the react login example app, go to http://localhost:3000 and try to connect to the provider your configured.

Creating a custom provider

You can also use the register lifecycle function to create your own custom provider in the src/index.js|ts file of your Strapi application. Use the following code example adjusted to your needs:

/src/index.js
module.exports = {
register({ strapi }) {
strapi
.plugin("users-permissions")
.service("providers-registry")
.add("example-provider-name", {
icon: "",
enabled: true,
grantConfig: {
key: "",
secret: "",
callback: `${strapi.config.server.url}/auth/test-provider/callback`,
scope: ["email"],
authorize_url: "https://awesome.com/authorize",
access_url: "https://awesome.com/token",
oauth: 2,
},
async authCallback({ accessToken, providers, purest }) {
// use whatever you want here to get the user info
return {
username: "test",
email: "test",
};
},
});
},
};

For additional information on parameters passed to grantConfig, please refer to the grant documentation. For additional information about purest please refer to purest documentation.

Setup the frontend

Once you have configured strapi and the provider, in your frontend app you have to :

  • Create a button that links to GET STRAPI_BACKEND_URL/api/connect/${provider} (ex: https://strapi.mywebsite/api/connect/github).
  • Create a frontend route like FRONTEND_URL/connect/${provider}/redirect that have to handle the access_token param and that have to request STRAPI_BACKEND_URL/api/auth/${provider}/callback with the access_token parameter.
    The JSON request response will be { "jwt": "...", "user": {...} }.

Now you can make authenticated requests. More info here: token usage.

Troubleshooting
  • Error 429: It's most likely because your login flow fell into a loop. To make new requests to the backend, you need to wait a few minutes or restart the backend.
  • Grant: missing session or misconfigured provider: It may be due to many things.
    • The redirect url can't be built: Make sure you have set the backend url in config/server.js: Setting up the server url
    • A session/cookie/cache problem: You can try again in a private tab.
    • The incorrect use of a domain with ngrok: Check your urls and make sure that you use the ngrok url instead of http://localhost:1337. Don't forget to check the backend url set in the example app at src/config.js.
  • You can't access your admin panel: It's most likely because you built it with the backend url set with a ngrok url and you stopped/restarted ngrok. You need to replace the backend url with the new ngrok url and run yarn build or npm run build again.

Reset password

Can only be used for users registered using the email provider.

The assumed general flow:

  1. The user goes to your forgotten password page.
  2. The user enters their email address.
  3. Your forgotten password page sends a request to the backend to send an email with the reset password link to the user.
  4. The user receives the email and clicks on the special link.
  5. The link redirects the user to your reset password page.
  6. The user enters their new password.
  7. The reset password page sends a request to the backend with the new password.
  8. If the request contains the code contained in the link at step 3, the password is updated.
  9. The user can log in with the new password.

The following section details steps 3 and 7.

This action sends an email to a user with the link to your reset password page. The link will be enriched with the url param code that is needed for the reset password at step 7.

First, you must specify the following:

  • In the admin panel: Settings > USERS & PERMISSIONS PLUGIN > Advanced Settings > Reset Password page, the url to your reset password page.
  • In the admin panel: Settings > USERS & PERMISSIONS PLUGIN > Email Template page, the Shipper email.

Then, your forgotten password page has to make the following request to your backend:

import axios from 'axios';

// Request API.
axios
.post('http://localhost:1337/api/auth/forgot-password', {
email: 'user@strapi.io', // user's email
})
.then(response => {
console.log('Your user received an email');
})
.catch(error => {
console.log('An error occurred:', error.response);
});

Reset Password: send the new password

This action will update the user password. This also works with the GraphQL Plugin, with the resetPassword mutation.

Your reset password page has to make the following request to your backend:

import axios from 'axios';

// Request API.
axios
.post('http://localhost:1337/api/auth/reset-password', {
code: 'privateCode', // code contained in the reset link of step 3.
password: 'userNewPassword',
passwordConfirmation: 'userNewPassword',
})
.then(response => {
console.log("Your user's password has been reset.");
})
.catch(error => {
console.log('An error occurred:', error.response);
});

Email validation

✏️ Note

In production, make sure the url config property is set. Otherwise the validation link will redirect to localhost. More info on the config here.

After registering, if you have set Enable email confirmation to ON, the user will receive a confirmation link by email. The user has to click on it to validate their registration.

Example of the confirmation link: https://yourwebsite.com/api/auth/email-confirmation?confirmation=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTk0OTgxMTE3LCJleHAiOjE1OTc1NzMxMTd9.0WeB-mvuguMyr4eY8CypTZDkunR--vZYzZH6h6sChFg

If needed you can re-send the confirmation email by making the following request:

import axios from 'axios';

// Request API.
axios
.post(`http://localhost:1337/api/auth/send-email-confirmation`, {
email: 'user@strapi.io', // user's email
})
.then(response => {
console.log('Your user received an email');
})
.catch(error => {
console.error('An error occurred:', error.response);
});

User object in Strapi context

The user object is available to successfully authenticated requests.

The authenticated user object is a property of ctx.state.

create: async ctx => {
const { id } = ctx.state.user;

const depositObj = {
...ctx.request.body,
depositor: id,
};

const data = await strapi.services.deposit.add(depositObj);

// Send 201 `created`
ctx.created(data);
};

Templating emails

By default this plugin comes with two templates: reset password and email address confirmation. The templates use Lodash's template() method to populate the variables.

You can update these templates under Plugins > Roles & Permissions > Email Templates tab in the admin panel.

Reset Password

  • USER (object)
    • username
    • email
  • TOKEN corresponds to the token generated to be able to reset the password.
  • URL is the link where the user will be redirected after clicking on it in the email.
  • SERVER_URL is the absolute server url (configured in server configuration).

Email address confirmation

  • USER (object)
    • username
    • email
  • CODE corresponds to the CODE generated to be able confirm the user email.
  • URL is the Strapi backend URL that confirms the code (by default /auth/email-confirmation).
  • SERVER_URL is the absolute server url (configured in server configuration).

Security configuration

JWTs can be verified and trusted because the information is digitally signed. To sign a token a secret is required. By default Strapi generates and stores it in ./extensions/users-permissions/config/jwt.js.

This is useful during development but for security reasons it is recommended to set a custom token via an environment variable JWT_SECRET when deploying to production.

By default you can set a JWT_SECRET environment variable and it will be used as secret. If you want to use another variable you can update the configuration file.

./extensions/users-permissions/config/jwt.js

module.exports = {
jwtSecret: process.env.SOME_ENV_VAR,
};
💡 Tip

You can learn more about configuration here.

Creating a custom callback validator

By default, Strapi SSO only redirects to the redirect URL that is exactly equal to the url in the configuration:

Users & Permissions configurationUsers & Permissions configuration

If you need to configure a custom handler to accept other URLs, you can create a callback validate function in your plugins.js for the users-permissions plugin.

/config/plugins.js|ts
  // ... other plugins configuration ...
// Users & Permissions configuration
'users-permissions': {
enabled: true,
config: {
callback: {
validate: (cbUrl, options) => {
// cbUrl is where Strapi is being asked to redirect the auth info
// that was received from the provider to

// in this case, we will only validate that the
// if using a base url, you should always include the trailing slash
// although in real-world usage you should also include the full paths
if (cbUrl.startsWith('https://myproxy.mysite.com/') ||
cbUrl.startsWith('https://mysite.com/')) {
return;
}

// Note that you MUST throw an error to fail validation
// return values are not checked
throw new Error('Invalid callback url');
},
},
},
},