2021-09-16 14:36:54 +02:00
|
|
|
'use strict';
|
|
|
|
|
2022-08-24 10:55:28 +02:00
|
|
|
const { castArray, isNil } = require('lodash/fp');
|
2021-10-27 18:54:58 +02:00
|
|
|
const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors;
|
2021-09-16 14:36:54 +02:00
|
|
|
const constants = require('../services/constants');
|
|
|
|
const { getService } = require('../utils');
|
|
|
|
|
2022-08-08 23:33:39 +02:00
|
|
|
const isReadScope = (scope) => scope.endsWith('find') || scope.endsWith('findOne');
|
2021-10-11 09:49:35 +02:00
|
|
|
|
2022-08-08 23:33:39 +02:00
|
|
|
const extractToken = (ctx) => {
|
2021-11-15 17:54:17 +01:00
|
|
|
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
|
|
|
|
const parts = ctx.request.header.authorization.split(/\s+/);
|
2021-09-16 14:36:54 +02:00
|
|
|
|
2021-11-15 17:54:17 +01:00
|
|
|
if (parts[0].toLowerCase() !== 'bearer' || parts.length !== 2) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return parts[1];
|
|
|
|
}
|
2021-09-16 14:36:54 +02:00
|
|
|
|
2021-11-15 17:54:17 +01:00
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
2022-08-22 09:25:55 +02:00
|
|
|
/**
|
|
|
|
* Authenticate the validity of the token
|
|
|
|
*
|
|
|
|
* @type {import('.').AuthenticateFunction} */
|
2022-08-08 23:33:39 +02:00
|
|
|
const authenticate = async (ctx) => {
|
2021-11-15 17:54:17 +01:00
|
|
|
const apiTokenService = getService('api-token');
|
|
|
|
const token = extractToken(ctx);
|
2021-09-16 14:36:54 +02:00
|
|
|
|
2021-11-15 17:54:17 +01:00
|
|
|
if (!token) {
|
2021-09-16 14:36:54 +02:00
|
|
|
return { authenticated: false };
|
|
|
|
}
|
|
|
|
|
|
|
|
const apiToken = await apiTokenService.getBy({
|
|
|
|
accessKey: apiTokenService.hash(token),
|
|
|
|
});
|
|
|
|
|
2022-08-22 09:25:55 +02:00
|
|
|
// token not found
|
2021-09-16 14:36:54 +02:00
|
|
|
if (!apiToken) {
|
|
|
|
return { authenticated: false };
|
|
|
|
}
|
|
|
|
|
2022-08-24 10:32:24 +02:00
|
|
|
// token has expired
|
2022-08-24 10:55:28 +02:00
|
|
|
if (!isNil(apiToken.expiresAt) && apiToken.expiresAt < Date.now()) {
|
2022-08-24 10:32:24 +02:00
|
|
|
throw new UnauthorizedError('Token expired');
|
|
|
|
}
|
|
|
|
|
2022-08-19 16:36:28 +02:00
|
|
|
// update lastUsedAt
|
2022-08-17 23:28:35 +02:00
|
|
|
await apiTokenService.update(apiToken.id, {
|
2022-08-19 16:36:28 +02:00
|
|
|
lastUsedAt: new Date(),
|
2022-08-17 23:28:35 +02:00
|
|
|
});
|
|
|
|
|
2022-08-05 12:01:36 +02:00
|
|
|
if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
|
|
|
const ability = await strapi.contentAPI.permissions.engine.generateAbility(
|
2022-08-18 12:20:45 +02:00
|
|
|
apiToken.permissions.map((action) => ({ action }))
|
2022-08-05 12:01:36 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
return { authenticated: true, ability, credentials: apiToken };
|
|
|
|
}
|
|
|
|
|
2021-09-16 14:36:54 +02:00
|
|
|
return { authenticated: true, credentials: apiToken };
|
|
|
|
};
|
|
|
|
|
2022-08-22 09:25:55 +02:00
|
|
|
/**
|
|
|
|
* Verify the token has the required abilities for the requested scope
|
|
|
|
*
|
|
|
|
* @type {import('.').VerifyFunction} */
|
2021-09-16 14:36:54 +02:00
|
|
|
const verify = (auth, config) => {
|
2022-08-05 12:01:36 +02:00
|
|
|
const { credentials: apiToken, ability } = auth;
|
2021-09-16 14:36:54 +02:00
|
|
|
|
|
|
|
if (!apiToken) {
|
2022-08-24 10:32:24 +02:00
|
|
|
throw new UnauthorizedError('Token not found');
|
2021-09-16 14:36:54 +02:00
|
|
|
}
|
|
|
|
|
2022-08-22 09:25:55 +02:00
|
|
|
// token has expired
|
2022-08-24 10:55:28 +02:00
|
|
|
if (!isNil(apiToken.expiresAt) && apiToken.expiresAt < Date.now()) {
|
2022-08-24 10:32:24 +02:00
|
|
|
throw new UnauthorizedError('Token expired');
|
2022-08-22 09:25:55 +02:00
|
|
|
}
|
|
|
|
|
2022-08-05 12:01:36 +02:00
|
|
|
// Full access
|
2021-09-17 14:07:39 +02:00
|
|
|
if (apiToken.type === constants.API_TOKEN_TYPE.FULL_ACCESS) {
|
2021-09-16 14:36:54 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-05 12:01:36 +02:00
|
|
|
// Read only
|
2022-08-18 12:20:45 +02:00
|
|
|
if (apiToken.type === constants.API_TOKEN_TYPE.READ_ONLY) {
|
2022-08-05 12:01:36 +02:00
|
|
|
/**
|
|
|
|
* If you don't have `full-access` you can only access `find` and `findOne`
|
|
|
|
* scopes. If the route has no scope, then you can't get access to it.
|
|
|
|
*/
|
|
|
|
const scopes = castArray(config.scope);
|
2021-10-11 09:49:35 +02:00
|
|
|
|
2022-08-05 12:01:36 +02:00
|
|
|
if (config.scope && scopes.every(isReadScope)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Custom
|
2022-08-11 12:18:53 +02:00
|
|
|
else if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
|
|
|
if (!ability) {
|
|
|
|
throw new ForbiddenError();
|
|
|
|
}
|
|
|
|
|
2022-08-05 12:01:36 +02:00
|
|
|
const scopes = castArray(config.scope);
|
|
|
|
|
2022-08-18 12:20:45 +02:00
|
|
|
const isAllowed = scopes.every((scope) => ability.can(scope));
|
2022-08-05 12:01:36 +02:00
|
|
|
|
|
|
|
if (isAllowed) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-16 14:36:54 +02:00
|
|
|
}
|
|
|
|
|
2021-10-27 18:54:58 +02:00
|
|
|
throw new ForbiddenError();
|
2021-09-16 14:36:54 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/** @type {import('.').AuthStrategy} */
|
|
|
|
module.exports = {
|
|
|
|
name: 'api-token',
|
|
|
|
authenticate,
|
|
|
|
verify,
|
|
|
|
};
|