mirror of
https://github.com/strapi/strapi.git
synced 2025-07-24 17:40:18 +00:00
store the hashed accessKey in the database
This commit is contained in:
parent
5305f2e757
commit
e9b897b66b
3
packages/core/admin/server/bootstrap.js
vendored
3
packages/core/admin/server/bootstrap.js
vendored
@ -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();
|
||||
};
|
||||
|
7
packages/core/admin/server/config/api-token.js
Normal file
7
packages/core/admin/server/config/api-token.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { env } = require('../../../utils/lib');
|
||||
|
||||
module.exports = {
|
||||
salt: env('API_TOKEN_SALT'),
|
||||
};
|
@ -6,4 +6,5 @@ module.exports = {
|
||||
forgotPassword: {
|
||||
emailTemplate: forgotPasswordTemplate,
|
||||
},
|
||||
'api-token': require('./api-token'),
|
||||
};
|
||||
|
@ -1,8 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Lifecycle callbacks for the `ApiToken` model.
|
||||
*/
|
||||
module.exports = {
|
||||
collectionName: 'strapi_api_tokens',
|
||||
info: {
|
||||
|
@ -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') },
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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 });
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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'),
|
||||
};
|
||||
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
4
packages/core/admin/server/utils/index.d.ts
vendored
4
packages/core/admin/server/utils/index.d.ts
vendored
@ -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]>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user