store the hashed accessKey in the database

This commit is contained in:
Dieter Stinglhamber 2021-08-27 16:23:19 +02:00
parent 5305f2e757
commit e9b897b66b
12 changed files with 157 additions and 39 deletions

View File

@ -44,6 +44,7 @@ module.exports = async () => {
const permissionService = getService('permission');
const userService = getService('user');
const roleService = getService('role');
const apiTokenService = getService('api-token');
await roleService.createRolesIfNoneExist();
await roleService.resetSuperAdminPermissions();
@ -55,4 +56,6 @@ module.exports = async () => {
await userService.displayWarningIfUsersDontHaveRole();
await syncAuthSettings();
apiTokenService.createSaltIfNotDefined();
};

View File

@ -0,0 +1,7 @@
'use strict';
const { env } = require('../../../utils/lib');
module.exports = {
salt: env('API_TOKEN_SALT'),
};

View File

@ -6,4 +6,5 @@ module.exports = {
forgotPassword: {
emailTemplate: forgotPasswordTemplate,
},
'api-token': require('./api-token'),
};

View File

@ -1,8 +1,5 @@
'use strict';
/**
* Lifecycle callbacks for the `ApiToken` model.
*/
module.exports = {
collectionName: 'strapi_api_tokens',
info: {

View File

@ -4,5 +4,5 @@ module.exports = {
permission: { schema: require('./Permission') },
user: { schema: require('./User') },
role: { schema: require('./Role') },
['api-token']: { schema: require('./api-token') },
'api-token': { schema: require('./api-token') },
};

View File

@ -11,7 +11,7 @@ describe('API Token Controller', () => {
type: 'read-only',
};
test('Fails if API Token already exist', async () => {
test('Fails if API Token already exists', async () => {
const exists = jest.fn(() => true);
const badRequest = jest.fn();
const ctx = createContext({ body }, { badRequest });
@ -19,7 +19,7 @@ describe('API Token Controller', () => {
global.strapi = {
admin: {
services: {
['api-token']: {
'api-token': {
exists,
},
},
@ -42,7 +42,7 @@ describe('API Token Controller', () => {
global.strapi = {
admin: {
services: {
['api-token']: {
'api-token': {
exists,
create,
},

View File

@ -14,12 +14,12 @@ module.exports = {
return ctx.badRequest('ValidationError', err);
}
if (await apiTokenService.exists({ name: attributes.name })) {
const alreadyExists = await apiTokenService.exists({ name: attributes.name });
if (alreadyExists) {
return ctx.badRequest('Name already taken');
}
const apiToken = await apiTokenService.create(attributes);
ctx.created({ data: apiToken });
},
};

View File

@ -23,6 +23,9 @@ describe('API Token', () => {
query() {
return { create };
},
config: {
get: jest.fn(() => ({})),
},
};
const attributes = {
@ -34,10 +37,10 @@ describe('API Token', () => {
const res = await apiTokenService.create(attributes);
expect(create).toHaveBeenCalledWith({
select: ['id', 'name', 'description', 'type', 'accessKey'],
select: ['id', 'name', 'description', 'type'],
data: {
...attributes,
accessKey: mockedApiToken.hexedString,
accessKey: apiTokenService.hash(mockedApiToken.hexedString),
},
});
expect(res).toEqual({
@ -46,4 +49,46 @@ describe('API Token', () => {
});
});
});
describe('createSaltIfNotDefined', () => {
test('It does nothing if the salt is alread defined', () => {
const mockedAppendFile = jest.fn();
const mockedConfigSet = jest.fn();
global.strapi = {
config: {
get: jest.fn(() => ({
server: {
admin: { 'api-token': { salt: 'api-token_tests-salt' } },
},
})),
set: mockedConfigSet,
},
fs: { appendFile: mockedAppendFile },
};
apiTokenService.createSaltIfNotDefined();
expect(mockedAppendFile).not.toHaveBeenCalled();
expect(mockedConfigSet).not.toHaveBeenCalled();
});
test('It creates a new salt, appendit to the .env file and sets it in the configuration', () => {
const mockedAppendFile = jest.fn();
const mockedConfigSet = jest.fn();
global.strapi = {
config: {
get: jest.fn(() => null),
set: mockedConfigSet,
},
fs: { appendFile: mockedAppendFile },
};
apiTokenService.createSaltIfNotDefined();
expect(mockedAppendFile).toHaveBeenCalled();
expect(mockedConfigSet).toHaveBeenCalled();
});
});
});

View File

@ -13,14 +13,28 @@ const crypto = require('crypto');
*/
/**
* @param {Object} attributes
* @param {string} attributes.name
* @param {string} [attributes.description]
* @param {Object} whereParams
* @param {string} whereParams.name
* @param {string} [whereParams.description]
*
* @returns {Promise<boolean>}
*/
const exists = async (attributes = {}) => {
return (await strapi.query('admin::api-token').count({ where: attributes })) > 0;
const exists = async (whereParams = {}) => {
const apiToken = await strapi.query('admin::api-token').findOne({ where: whereParams });
return !!apiToken;
};
/**
* @param {string} accessKey
*
* @returns {string}
*/
const hash = accessKey => {
return crypto
.createHash('sha512')
.update(`${strapi.config.get('server.admin.api-token.salt')}${accessKey}`)
.digest('hex');
};
/**
@ -34,16 +48,39 @@ const exists = async (attributes = {}) => {
const create = async attributes => {
const accessKey = crypto.randomBytes(128).toString('hex');
return strapi.query('admin::api-token').create({
select: ['id', 'name', 'description', 'type', 'accessKey'],
const apiToken = await strapi.query('admin::api-token').create({
select: ['id', 'name', 'description', 'type'],
data: {
...attributes,
accessKey,
accessKey: hash(accessKey),
},
});
return {
...apiToken,
accessKey,
};
};
/**
* @returns {void}
*/
const createSaltIfNotDefined = () => {
if (strapi.config.get('server.admin.api-token.salt')) {
return;
}
const salt = crypto.randomBytes(16).toString('hex');
if (!process.env.API_TOKEN_SALT) {
strapi.fs.appendFile('.env', `API_TOKEN_SALT=${salt}\n`);
strapi.config.set('server.admin.api-token.salt', salt);
}
};
module.exports = {
create,
exists,
createSaltIfNotDefined,
hash,
};

View File

@ -12,5 +12,5 @@ module.exports = {
condition: require('./condition'),
auth: require('./auth'),
action: require('./action'),
['api-token']: require('./api-token'),
'api-token': require('./api-token'),
};

View File

@ -8,8 +8,10 @@ const { createAuthRequest } = require('../../../../../test/helpers/request');
*
* N° Description
* -------------------------------------------
* 1. Creates an api token (wrong body)
* 2. Creates an api token (successfully)
* 1. Fails to creates an api token (missing parameters from the body)
* 2. Fails to creates an api token (invalid `type` in the body)
* 3. Creates an api token (successfully)
* 4. Creates an api token without a description (successfully)
*/
describe('Admin API Token CRUD (e2e)', () => {
@ -27,7 +29,7 @@ describe('Admin API Token CRUD (e2e)', () => {
await strapi.destroy();
});
test('1. Creates an api token (wrong body)', async () => {
test('1. Fails to creates an api token (missing parameters from the body)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
@ -50,7 +52,31 @@ describe('Admin API Token CRUD (e2e)', () => {
});
});
test('2. Creates an api token (successfully)', async () => {
test('2. Fails to creates an api token (invalid `type` in the body)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
type: 'invalid-type',
};
const res = await rq({
url: '/admin/api-tokens',
method: 'POST',
body,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: {
type: ['type must be one of the following values: read-only, full-access'],
},
});
});
test('3. Creates an api token (successfully)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
@ -64,15 +90,16 @@ describe('Admin API Token CRUD (e2e)', () => {
});
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
expect(res.body.data.id).toBe(1);
expect(res.body.data.accessKey).toBeDefined();
expect(res.body.data.name).toBe(body.name);
expect(res.body.data.description).toBe(body.description);
expect(res.body.data.type).toBe(body.type);
expect(res.body.data).toMatchObject({
accessKey: expect.any(String),
name: body.name,
description: body.description,
type: body.type,
id: expect.any(Number),
});
});
test('3. Creates an api token without a description (successfully)', async () => {
test('4. Creates an api token without a description (successfully)', async () => {
const body = {
name: 'api-token_tests-name-without-description',
type: 'read-only',
@ -85,11 +112,12 @@ describe('Admin API Token CRUD (e2e)', () => {
});
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
expect(res.body.data.id).toBe(2);
expect(res.body.data.accessKey).toBeDefined();
expect(res.body.data.name).toBe(body.name);
expect(res.body.data.description).toBe('');
expect(res.body.data.type).toBe(body.type);
expect(res.body.data).toMatchObject({
accessKey: expect.any(String),
name: body.name,
description: '',
type: body.type,
id: expect.any(Number),
});
});
});

View File

@ -11,11 +11,11 @@ type S = {
role: typeof role;
user: typeof user;
permission: typeof permission;
['content-type']: typeof contentType;
'content-type': typeof contentType;
token: typeof token;
auth: typeof auth;
metrics: typeof metrics;
['api-token']: typeof apiToken;
'api-token': typeof apiToken;
};
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;