2023-04-19 12:54:10 +02:00

144 lines
3.5 KiB
JavaScript

'use strict';
const { castArray, isNil, isEmpty } = require('lodash/fp');
const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors;
const constants = require('../services/constants');
const { getService } = require('../utils');
const isReadScope = (scope) => scope.endsWith('find') || scope.endsWith('findOne');
const extractToken = (ctx) => {
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
const parts = ctx.request.header.authorization.split(/\s+/);
if (parts[0].toLowerCase() !== 'bearer' || parts.length !== 2) {
return null;
}
return parts[1];
}
return null;
};
/**
* Authenticate the validity of the token
*
* @type {import('.').AuthenticateFunction}
*/
const authenticate = async (ctx) => {
const apiTokenService = getService('api-token');
const token = extractToken(ctx);
if (!token) {
return { authenticated: false };
}
const apiToken = await apiTokenService.getBy({
accessKey: apiTokenService.hash(token),
});
// token not found
if (!apiToken) {
return { authenticated: false };
}
const currentDate = new Date();
if (!isNil(apiToken.expiresAt)) {
const expirationDate = new Date(apiToken.expiresAt);
// token has expired
if (expirationDate < currentDate) {
return { authenticated: false, error: new UnauthorizedError('Token expired') };
}
}
// update lastUsedAt
await strapi.query('admin::api-token').update({
where: { id: apiToken.id },
data: { lastUsedAt: currentDate },
});
if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
const ability = await strapi.contentAPI.permissions.engine.generateAbility(
apiToken.permissions.map((action) => ({ action }))
);
return { authenticated: true, ability, credentials: apiToken };
}
return { authenticated: true, credentials: apiToken };
};
/**
* Verify the token has the required abilities for the requested scope
*
* @type {import('.').VerifyFunction}
*/
const verify = (auth, config) => {
const { credentials: apiToken, ability } = auth;
strapi.telemetry.send('didCompleteRequest', {
eventProperties: {
authenticationMetod: auth?.strategy?.name || 'api-token',
isAuthenticated: !isEmpty(apiToken),
},
});
if (!apiToken) {
throw new UnauthorizedError('Token not found');
}
const currentDate = new Date();
if (!isNil(apiToken.expiresAt)) {
const expirationDate = new Date(apiToken.expiresAt);
// token has expired
if (expirationDate < currentDate) {
throw new UnauthorizedError('Token expired');
}
}
// Full access
if (apiToken.type === constants.API_TOKEN_TYPE.FULL_ACCESS) {
return;
}
// Read only
if (apiToken.type === constants.API_TOKEN_TYPE.READ_ONLY) {
/**
* 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);
if (config.scope && scopes.every(isReadScope)) {
return;
}
}
// Custom
else if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
if (!ability) {
throw new ForbiddenError();
}
const scopes = castArray(config.scope);
const isAllowed = scopes.every((scope) => ability.can(scope));
if (isAllowed) {
return;
}
}
throw new ForbiddenError();
};
/** @type {import('.').AuthStrategy} */
module.exports = {
name: 'api-token',
authenticate,
verify,
};