Using Strapi v4 API Tokens to Validate Custom Plugin Routes

Maddy Bishop Van Horn on background

Maddy Closs

Light up keyboard

Strapi v4 provides authentication via API Token in addition to validation using the User Roles & Permissions system. This works out of the box for routes created within the src/api directory, but does not work for routes created via custom plugins generated via strapi generate, and the documentation is lacking.

The right way (according to me)

Instead of using the route configuration generated with strapi generate plugin, refactor the routes array into an object with keys for content-api. You can refer to core plugin configuration, such as in node_modules/@strapi/plugin-email/server/routes.

Before (generated via strapi generate)

// my-plugin/server/routes/index.js

module.exports = [
 {
   method: 'GET',
   path: '/',
   handler: 'myController.index',
   config: {
     policies: [],
   },
 },
];

After

// my-plugin/server/routes/index.js

'use strict';

module.exports = {
 'content-api': require('./content-api'),
};

// my-plugin/server/routes/content-api.js

'use strict';

module.exports = {
 type: 'content-api',
 routes: [
   {
     method: 'GET',
     path: '/',
     handler: 'myController.index',
     config: {
       policies: [],
     },
   },
 ],
};

IMPORTANT: The route is now prefixed with `/api`. Calling the original route will result in a 404 and much frustration.

Calling GET /api/my-plugin with an appropriately scoped API token or JWT in the authorization header should work now. Finally!

This approach allows you to use the same authentication technique (API Token-based or User Roles & Permissions-based) for your custom plugin routes as you use for your content api routes.

Custom policy for validating token

I won’t be using the approach because I’d rather utilize the authentication pathways built into Strapi. But I’m including this in case it’s helpful for anyone.

Maybe you need more granular token validation than is offered by Strapi? Read on.

The Policy

// my-plugin/policies/has-full-access-token.js

'use strict';

module.exports = async (policyContext, config, {strapi}) => {
 const bearerToken = policyContext.request.header?.authorization?.substring('Bearer '.length);
 if (!bearerToken) {
   return false;
 }
 const apiTokenService = strapi.services['admin::api-token'];
 const accessKey = await apiTokenService.hash(bearerToken);
 const storedToken = await apiTokenService.getBy({accessKey: accessKey});
 if (!storedToken) {
   return false;
 }
 // Deny access if expired.
 if (storedToken.expiresAt && storedToken.expiresAt < new Date()) {
   return false;
 }
// Or add your own logic here.
 if (storedToken.type === 'full-access') {
   return true;
 }
 return false;
};

 

// my-plugin/policies/index.js

'use strict';

const hasToken = require('./has-full-access-token');
module.exports = {'has-full-access-token': hasToken};

To use this policy:

// my-plugin/routes/index.js

module.exports = [ {
	// Edit route config:
   config: {
     // Set auth to false to pass through to API token validation policy.
     auth: false,
     policies: ['plugin::my-plugin.has-full-access-token'],
   },
 },
];

Again, I would NOT recommend the custom policy approach because it isn’t particularly DRY. It’s better to use the built in authentication pathway by adjusting the routes configuration, even if it’s not documented particularly well by Strapi as of March 2023.