From c30425775d303db534e15b97e1b199db241abd8d Mon Sep 17 00:00:00 2001 From: Sebastijan K <58827427+sebastijankuzner@users.noreply.github.com> Date: Fri, 8 May 2020 12:09:26 +0200 Subject: [PATCH] feat(core-manager): implement basic authentication (#3687) --- .../plugins/rpc-response-handler.test.ts | 211 ++++++++++++++++++ .../{ => server}/server-boot-dispose.test.ts | 11 +- .../core-manager/{ => server}/server.test.ts | 138 ++++++++++-- packages/core-manager/package.json | 2 + packages/core-manager/src/actions/argon2id.ts | 46 ++++ packages/core-manager/src/contracts/action.ts | 2 +- .../src/contracts/authentication.ts | 7 + packages/core-manager/src/contracts/index.ts | 1 + packages/core-manager/src/defaults.ts | 12 +- packages/core-manager/src/ioc/identifiers.ts | 2 + .../src/{ => server}/plugins/index.ts | 0 .../{ => server}/plugins/plugin-factory.ts | 46 +++- .../plugins/rpc-response-handler.ts | 31 +-- .../core-manager/src/{ => server}/server.ts | 4 +- .../src/server/validators/argon2id.ts | 35 +++ .../src/server/validators/index.ts | 1 + packages/core-manager/src/service-provider.ts | 7 +- yarn.lock | 47 +++- 18 files changed, 556 insertions(+), 47 deletions(-) create mode 100644 __tests__/unit/core-manager/server/plugins/rpc-response-handler.test.ts rename __tests__/unit/core-manager/{ => server}/server-boot-dispose.test.ts (91%) rename __tests__/unit/core-manager/{ => server}/server.test.ts (59%) create mode 100644 packages/core-manager/src/actions/argon2id.ts create mode 100644 packages/core-manager/src/contracts/authentication.ts rename packages/core-manager/src/{ => server}/plugins/index.ts (100%) rename packages/core-manager/src/{ => server}/plugins/plugin-factory.ts (63%) rename packages/core-manager/src/{ => server}/plugins/rpc-response-handler.ts (52%) rename packages/core-manager/src/{ => server}/server.ts (96%) create mode 100644 packages/core-manager/src/server/validators/argon2id.ts create mode 100644 packages/core-manager/src/server/validators/index.ts diff --git a/__tests__/unit/core-manager/server/plugins/rpc-response-handler.test.ts b/__tests__/unit/core-manager/server/plugins/rpc-response-handler.test.ts new file mode 100644 index 0000000000..cd055d76ee --- /dev/null +++ b/__tests__/unit/core-manager/server/plugins/rpc-response-handler.test.ts @@ -0,0 +1,211 @@ +import "jest-extended"; + +import { Server as HapiServer } from "@hapi/hapi"; +import * as Boom from "@hapi/boom"; +import * as rpc from "@hapist/json-rpc"; + +import { Container } from "@packages/core-kernel"; +import { Validation } from "@packages/crypto"; +import { Sandbox } from "@packages/core-test-framework"; +import { Identifiers } from "@packages/core-manager/src/ioc"; +import { Server } from "@packages/core-manager/src/server/server"; +import { ActionReader } from "@packages/core-manager/src/action-reader"; +import { Actions, Plugins } from "@packages/core-manager/src/contracts"; +import { rpcResponseHandler } from "@packages/core-manager/src/server/plugins/rpc-response-handler"; +import { Assets } from "../../__fixtures__"; + +let sandbox: Sandbox; +let server: Server; + +let mockOnRequest = jest.fn(); + +// @ts-ignore +let dummyPlugin = { + name: "dummy", + version: "0.0.1", + register: (server: HapiServer) => { + server.ext({ + type: "onRequest", + method: mockOnRequest + }) + } +} + +let mockPluginFactory: Plugins.PluginFactory = { + preparePlugins() { + return [ + { + plugin: rpcResponseHandler + }, + { + plugin: rpc, + options: { + methods: [ + new Assets.DummyAction() + ], + processor: { + schema: { + properties: { + id: { + type: ["number", "string"], + }, + jsonrpc: { + pattern: "2.0", + type: "string", + }, + method: { + type: "string", + }, + params: { + type: "object", + }, + }, + required: ["jsonrpc", "method", "id"], + type: "object", + }, + validate(data: object, schema: object) { + try { + const { error } = Validation.validator.validate(schema, data); + return { value: data, error: error ? error : null }; + } catch (error) { + return { value: null, error: error.stack }; + } + }, + }, + } + }, + { + plugin: dummyPlugin + }, + ] + } +} + +let logger = { + info: jest.fn(), + notice: jest.fn(), + error: jest.fn(), +} + +beforeEach(() => { + + let dummyAction = new Assets.DummyAction(); + + let actionReader: Partial = { + discoverActions(): Actions.Action[] { + return [dummyAction]; + } + } + + sandbox = new Sandbox(); + + sandbox.app.bind(Identifiers.HTTP).to(Server).inSingletonScope(); + sandbox.app.bind(Identifiers.ActionReader).toConstantValue(actionReader); + sandbox.app.bind(Identifiers.PluginFactory).toConstantValue(mockPluginFactory); + + sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger); + + server = sandbox.app.get(Identifiers.HTTP); +}); + +afterEach(async () => { + await server.dispose(); + jest.clearAllMocks(); +}) + +describe("RPC Response Handler", () => { + let injectOptions; + + beforeEach(() => { + injectOptions = { + method: "POST", + url: "/", + payload: { + jsonrpc: "2.0", + id: "1", + method: "dummy", + params: { id: 123 }, + }, + headers: { + "content-type": "application/vnd.api+json", + }, + }; + }) + + it("should return result", async () => { + await server.initialize("serverName", {}) + await server.boot() + + mockOnRequest.mockImplementation((request, h) => { + return h.continue; + }) + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ body: { id: '1', jsonrpc: '2.0', result: {} }, statusCode: 200 }) + }); + + it("should return status -32001 if 401 response code", async () => { + await server.initialize("serverName", {}) + await server.boot() + + mockOnRequest.mockImplementation((request, h) => { + return Boom.unauthorized(); + }) + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32001, message: 'These credentials do not match our records' }, + id: null + }, + statusCode: 200 + }) + }); + + it("should return status -32003 if 403 response code", async () => { + await server.initialize("serverName", {}) + await server.boot() + + mockOnRequest.mockImplementation((request, h) => { + return Boom.forbidden(); + }) + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32003, message: 'Forbidden' }, + id: null + }, + statusCode: 200 + }) + }); + + it("should return status -32603 if unhandled response code", async () => { + await server.initialize("serverName", {}) + await server.boot() + + mockOnRequest.mockImplementation((request, h) => { + return Boom.notImplemented(); + }) + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32603, message: 'Internal server error' }, + id: null + }, + statusCode: 200 + }) + }); +}) diff --git a/__tests__/unit/core-manager/server-boot-dispose.test.ts b/__tests__/unit/core-manager/server/server-boot-dispose.test.ts similarity index 91% rename from __tests__/unit/core-manager/server-boot-dispose.test.ts rename to __tests__/unit/core-manager/server/server-boot-dispose.test.ts index 8f024968ce..efda4ac88e 100644 --- a/__tests__/unit/core-manager/server-boot-dispose.test.ts +++ b/__tests__/unit/core-manager/server/server-boot-dispose.test.ts @@ -1,14 +1,11 @@ import "jest-extended"; -// @ts-ignore -import { Server as HapiServer } from "@hapi/hapi"; - import { Container } from "@packages/core-kernel"; import { Sandbox } from "@packages/core-test-framework"; import { Identifiers } from "@packages/core-manager/src/ioc"; -import { Server } from "@packages/core-manager/src/server"; +import { Server } from "@packages/core-manager/src/server/server"; import { ActionReader } from "@packages/core-manager/src/action-reader"; -import { PluginFactory } from "@packages/core-manager/src/plugins/plugin-factory"; +import { PluginFactory } from "@packages/core-manager/src/server/plugins/plugin-factory"; import { defaults } from "@packages/core-manager/src/defaults"; let sandbox: Sandbox; @@ -41,11 +38,15 @@ jest.mock("@hapi/hapi", () => { }) beforeEach(() => { + pluginsConfiguration.basicAuthentication.enabled = false + sandbox = new Sandbox(); sandbox.app.bind(Identifiers.HTTP).to(Server).inSingletonScope(); sandbox.app.bind(Identifiers.ActionReader).to(ActionReader).inSingletonScope(); sandbox.app.bind(Identifiers.PluginFactory).to(PluginFactory).inSingletonScope(); + sandbox.app.bind(Identifiers.BasicCredentialsValidator).toConstantValue({}); + sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger); sandbox.app.bind(Container.Identifiers.PluginConfiguration).toConstantValue({ get: jest.fn().mockReturnValue(pluginsConfiguration) diff --git a/__tests__/unit/core-manager/server.test.ts b/__tests__/unit/core-manager/server/server.test.ts similarity index 59% rename from __tests__/unit/core-manager/server.test.ts rename to __tests__/unit/core-manager/server/server.test.ts index 809320d157..f3e472551b 100644 --- a/__tests__/unit/core-manager/server.test.ts +++ b/__tests__/unit/core-manager/server/server.test.ts @@ -1,15 +1,18 @@ import "jest-extended"; +// import * as Boom from "@hapi/boom" + import { Validation } from "@packages/crypto"; import { Container } from "@packages/core-kernel"; import { Sandbox } from "@packages/core-test-framework"; import { Identifiers } from "@packages/core-manager/src/ioc"; import { Actions } from "@packages/core-manager/src/contracts"; -import { Server } from "@packages/core-manager/src/server"; +import { Server } from "@packages/core-manager/src/server/server"; import { ActionReader } from "@packages/core-manager/src/action-reader"; -import { PluginFactory } from "@packages/core-manager/src/plugins/plugin-factory"; +import { PluginFactory } from "@packages/core-manager/src/server/plugins/plugin-factory"; +import { Argon2id } from "@packages/core-manager/src/server/validators"; import { defaults } from "@packages/core-manager/src/defaults"; -import { Assets } from "./__fixtures__"; +import { Assets } from "../__fixtures__"; let sandbox: Sandbox; let server: Server; @@ -21,9 +24,11 @@ let logger = { error: jest.fn(), } -let pluginsConfiguration = defaults.plugins +let pluginsConfiguration; beforeEach(() => { + pluginsConfiguration = {...defaults.plugins}; + let dummyAction = new Assets.DummyAction(); spyOnMethod = jest.spyOn(dummyAction, "method") @@ -33,12 +38,16 @@ beforeEach(() => { } } + pluginsConfiguration.basicAuthentication.enabled = false + sandbox = new Sandbox(); sandbox.app.bind(Identifiers.HTTP).to(Server).inSingletonScope(); sandbox.app.bind(Identifiers.ActionReader).toConstantValue(actionReader); - sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger); sandbox.app.bind(Identifiers.PluginFactory).to(PluginFactory).inSingletonScope(); + sandbox.app.bind(Identifiers.BasicCredentialsValidator).to(Argon2id).inSingletonScope(); + + sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger); sandbox.app.bind(Container.Identifiers.PluginConfiguration).toConstantValue({ get: jest.fn().mockReturnValue(pluginsConfiguration) }); @@ -48,16 +57,14 @@ beforeEach(() => { server = sandbox.app.get(Identifiers.HTTP); }); -afterEach(() => { - jest.clearAllMocks() +afterEach(async () => { + await server.dispose(); + jest.clearAllMocks(); + jest.restoreAllMocks(); }) describe("Server", () => { - afterEach(async () => { - await server.dispose(); - }) - - describe("inject", () => { + describe("RPC test with dummy class", () => { it("should be ok", async () => { await server.initialize("serverName", {}) await server.boot() @@ -180,7 +187,7 @@ describe("Server", () => { }, }; - Validation.validator.validate = jest.fn().mockImplementation(() => { + jest.spyOn(Validation.validator, "validate").mockImplementation(() => { throw new Error(); }) @@ -190,7 +197,9 @@ describe("Server", () => { expect(parsedResponse.statusCode).toBe(200) expect(parsedResponse.body.error.code).toBe(-32600) }); + }) + describe("Whitelist", () => { it("should return RCP error if whitelisted", async () => { pluginsConfiguration.whitelist = [] @@ -217,11 +226,112 @@ describe("Server", () => { expect(parsedResponse).toEqual({ body: { jsonrpc: '2.0', - error: { code: -32001, message: 'Unauthorized' }, + error: { code: -32001, message: 'These credentials do not match our records' }, + id: null + }, + statusCode: 200 + }) + }); + }) + + describe("Basic Authentication", () => { + let injectOptions; + + beforeEach(() => { + pluginsConfiguration.basicAuthentication.enabled = true; + pluginsConfiguration.basicAuthentication.users = [ + { + username: "username", + password: "$argon2id$v=19$m=4096,t=3,p=1$NiGA5Cy5vFWTxhBaZMG/3Q$TwEFlzTuIB0fDy+qozEas+GzEiBcLRkm5F+/ClVRCDY" + } + ]; + + injectOptions = { + method: "POST", + url: "/", + payload: { + jsonrpc: "2.0", + id: "1", + method: "dummy", + params: { id: 123 }, + }, + headers: { + "content-type": "application/vnd.api+json", + }, + }; + }) + + it("should return RCP error if no username and password in header", async () => { + await server.initialize("serverName", {}) + await server.boot() + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32001, message: 'These credentials do not match our records' }, + id: null + }, + statusCode: 200 + }) + }); + + it("should return RCP error if password is invalid", async () => { + await server.initialize("serverName", {}) + await server.boot() + + // Data in header: { username: "username", password: "invalid" } + injectOptions.headers.Authorization = "Basic dXNlcm5hbWU6aW52YWxpZA=="; + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32001, message: 'These credentials do not match our records' }, + id: null + }, + statusCode: 200 + }) + }); + + it("should return RCP error user not found", async () => { + pluginsConfiguration.basicAuthentication.users = []; + + await server.initialize("serverName", {}) + await server.boot() + + // Data in header: { username: "username", password: "password" } + injectOptions.headers.Authorization = "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ + body: { + jsonrpc: '2.0', + error: { code: -32001, message: 'These credentials do not match our records' }, id: null }, statusCode: 200 }) }); + + it("should be ok with valid username and password", async () => { + await server.initialize("serverName", {}) + await server.boot() + + // Data in header: { username: "username", password: "password" } + injectOptions.headers.Authorization = "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; + + const response = await server.inject(injectOptions); + const parsedResponse: Record = { body: response.result, statusCode: response.statusCode }; + + expect(parsedResponse).toEqual({ body: { id: '1', jsonrpc: '2.0', result: {} }, statusCode: 200 }) + }); }) }); + diff --git a/packages/core-manager/package.json b/packages/core-manager/package.json index 7a27122c51..1cb5bbe8e9 100644 --- a/packages/core-manager/package.json +++ b/packages/core-manager/package.json @@ -21,7 +21,9 @@ "pretest": "bash ../../scripts/pre-test.sh" }, "dependencies": { + "argon2": "^0.26.2", "@arkecosystem/core-kernel": "^3.0.0-next.0", + "@hapi/basic": "^6.0.0", "@hapi/boom": "^9.0.0", "@hapi/hapi": "^19.0.0", "@hapist/json-rpc": "^0.2.0", diff --git a/packages/core-manager/src/actions/argon2id.ts b/packages/core-manager/src/actions/argon2id.ts new file mode 100644 index 0000000000..15b4a4ce18 --- /dev/null +++ b/packages/core-manager/src/actions/argon2id.ts @@ -0,0 +1,46 @@ +import argon2 from "argon2" + +import { Container } from "@arkecosystem/core-kernel"; +import { Actions } from "../contracts" + +@Container.injectable() +export class Action implements Actions.Action { + public name = "argon2id"; + + public schema = { + type: "object", + properties: { + hash: { + type: "string" + }, + password: { + type: "string" + } + }, + required: ["hash", "password"], + } + + public async method(params: { hash: string, password: string }): Promise { + + // @ts-ignore + let options = { + // salt: Buffer.from(params.hash), + secret: Buffer.from("secret"), + type: argon2.argon2id + } + + let hashedPassword = await argon2.hash(params.password, options) + + return { + hash: params.hash, + password: params.password, + hashedPassword: hashedPassword.toString(), + isVerified: await argon2.verify(hashedPassword, params.password, options), + isNotVerified: await argon2.verify(hashedPassword, params.password, { + secret: Buffer.from("secret"), + type: argon2.argon2d + }), + isNotVerified2: await argon2.verify(hashedPassword, params.password + "a", options), + } + } +} diff --git a/packages/core-manager/src/contracts/action.ts b/packages/core-manager/src/contracts/action.ts index 5c968b2697..3bb9450dbd 100644 --- a/packages/core-manager/src/contracts/action.ts +++ b/packages/core-manager/src/contracts/action.ts @@ -1,4 +1,4 @@ -export type Method = (params: object) => Promise; +export type Method = (params: any) => Promise; export interface Action { name: string, diff --git a/packages/core-manager/src/contracts/authentication.ts b/packages/core-manager/src/contracts/authentication.ts new file mode 100644 index 0000000000..156f43d566 --- /dev/null +++ b/packages/core-manager/src/contracts/authentication.ts @@ -0,0 +1,7 @@ +export interface BasicCredentialsValidator { + validate(username: string, password: string): Promise +} + +export interface TokenValidator { + validate(token: string): Promise +} diff --git a/packages/core-manager/src/contracts/index.ts b/packages/core-manager/src/contracts/index.ts index 39e648618a..253c879e00 100644 --- a/packages/core-manager/src/contracts/index.ts +++ b/packages/core-manager/src/contracts/index.ts @@ -1,2 +1,3 @@ +export * as Authentication from "./authentication" export * as Actions from "./action" export * as Plugins from "./plugins" diff --git a/packages/core-manager/src/defaults.ts b/packages/core-manager/src/defaults.ts index 006260911a..b27665fe31 100644 --- a/packages/core-manager/src/defaults.ts +++ b/packages/core-manager/src/defaults.ts @@ -17,6 +17,16 @@ export const defaults = { }, }, plugins: { - whitelist: ["*"] + whitelist: ["*"], + basicAuthentication: { + enabled: true, + secret: "secret", + users: [ + { + username: "username", + password: "$argon2id$v=19$m=4096,t=3,p=1$NiGA5Cy5vFWTxhBaZMG/3Q$TwEFlzTuIB0fDy+qozEas+GzEiBcLRkm5F+/ClVRCDY" + } + ] + } } }; diff --git a/packages/core-manager/src/ioc/identifiers.ts b/packages/core-manager/src/ioc/identifiers.ts index 26b874dcaf..92e7156305 100644 --- a/packages/core-manager/src/ioc/identifiers.ts +++ b/packages/core-manager/src/ioc/identifiers.ts @@ -3,4 +3,6 @@ export const Identifiers = { HTTPS: Symbol.for("API"), ActionReader: Symbol.for("Discover"), PluginFactory: Symbol.for("Factory"), + BasicCredentialsValidator: Symbol.for("Validator"), + TokenValidator: Symbol.for("Validator"), }; diff --git a/packages/core-manager/src/plugins/index.ts b/packages/core-manager/src/server/plugins/index.ts similarity index 100% rename from packages/core-manager/src/plugins/index.ts rename to packages/core-manager/src/server/plugins/index.ts diff --git a/packages/core-manager/src/plugins/plugin-factory.ts b/packages/core-manager/src/server/plugins/plugin-factory.ts similarity index 63% rename from packages/core-manager/src/plugins/plugin-factory.ts rename to packages/core-manager/src/server/plugins/plugin-factory.ts index fcb91e8eb2..4038fad022 100644 --- a/packages/core-manager/src/plugins/plugin-factory.ts +++ b/packages/core-manager/src/server/plugins/plugin-factory.ts @@ -1,12 +1,14 @@ import * as rpc from "@hapist/json-rpc"; import * as whitelist from "@hapist/whitelist"; +import { Server as HapiServer } from "@hapi/hapi"; +import * as hapiBasic from "@hapi/basic"; import { Container, Providers } from "@arkecosystem/core-kernel"; import { Validation } from "@arkecosystem/crypto"; -import { Identifiers } from "../ioc"; -import { Plugins } from "../contracts"; -import { ActionReader } from "../action-reader"; +import { Identifiers } from "../../ioc"; +import { Authentication, Plugins } from "../../contracts"; +import { ActionReader } from "../../action-reader"; import { rpcResponseHandler } from "./rpc-response-handler"; @Container.injectable() @@ -18,10 +20,13 @@ export class PluginFactory implements Plugins.PluginFactory { @Container.inject(Identifiers.ActionReader) private readonly actionReader!: ActionReader; + @Container.inject(Identifiers.BasicCredentialsValidator) + private readonly basicCredentialsValidator!: Authentication.BasicCredentialsValidator; + public preparePlugins(): Array { let pluginConfig = this.configuration.get("plugins") - return [ + let plugins = [ { plugin: rpcResponseHandler }, @@ -30,8 +35,6 @@ export class PluginFactory implements Plugins.PluginFactory { options: { // @ts-ignore whitelist: pluginConfig.whitelist - // whitelist: ["*"], - // whitelist: [], }, }, { @@ -70,5 +73,36 @@ export class PluginFactory implements Plugins.PluginFactory { }, } ] + + // @ts-ignore + if (pluginConfig.basicAuthentication.enabled) { + plugins.push(this.prepareBasicAuthentication()) + } + + return plugins; + } + + private prepareBasicAuthentication (): Plugins.RegisterPluginObject { + return { + plugin: { + name: "basicAuthentication", + version: "0.1.0", + register: async (server: HapiServer) => { + await server.register(hapiBasic); + + server.auth.strategy('simple', 'basic', { validate: async (...params) => { + // @ts-ignore + return this.validateBasicCredentials(...params) + } }); + server.auth.default('simple'); + } + } + } + } + + private async validateBasicCredentials(request, username, password, h) { + let isValid = await this.basicCredentialsValidator.validate(username, password); + + return { isValid: isValid, credentials: { name: username } }; } } diff --git a/packages/core-manager/src/plugins/rpc-response-handler.ts b/packages/core-manager/src/server/plugins/rpc-response-handler.ts similarity index 52% rename from packages/core-manager/src/plugins/rpc-response-handler.ts rename to packages/core-manager/src/server/plugins/rpc-response-handler.ts index d8cee79475..86a7931e7f 100644 --- a/packages/core-manager/src/plugins/rpc-response-handler.ts +++ b/packages/core-manager/src/server/plugins/rpc-response-handler.ts @@ -1,16 +1,22 @@ import { Server as HapiServer } from "@hapi/hapi"; -const getRpcResponseCode = (httpResponseCode: number) => { - return -32001 +const getRpcError = (httpResponseCode: number) => { + if (httpResponseCode === 401) { + return { + code: -32001, + message: "These credentials do not match our records" + } + } if (httpResponseCode === 403) { + return { + code: -32003, + message: "Forbidden" // TODO: Maybe another message + } + } - // TODO: Implement after auth plugin - // if (httpResponseCode === 401) { - // return -32001 - // } if (httpResponseCode === 403) { - // return -32001 - // } - // - // throw new Error("Unsupported status code") + return { + code: -32603, + message: "Internal server error" + } } export const rpcResponseHandler = { @@ -27,10 +33,7 @@ export const rpcResponseHandler = { return h.response({ jsonrpc: "2.0", - error: { - code: getRpcResponseCode(response.output.statusCode), - message: response.output.payload.message, - }, + error: getRpcError(response.output.statusCode), id: null }); } diff --git a/packages/core-manager/src/server.ts b/packages/core-manager/src/server/server.ts similarity index 96% rename from packages/core-manager/src/server.ts rename to packages/core-manager/src/server/server.ts index e47405101a..7a6a511db0 100644 --- a/packages/core-manager/src/server.ts +++ b/packages/core-manager/src/server/server.ts @@ -3,8 +3,8 @@ import { readFileSync } from "fs"; import { Container, Contracts, Types } from "@arkecosystem/core-kernel"; -import { Identifiers } from "./ioc"; -import { Plugins } from "./contracts"; +import { Identifiers } from "../ioc"; +import { Plugins } from "../contracts"; @Container.injectable() export class Server { diff --git a/packages/core-manager/src/server/validators/argon2id.ts b/packages/core-manager/src/server/validators/argon2id.ts new file mode 100644 index 0000000000..f6fa3252a4 --- /dev/null +++ b/packages/core-manager/src/server/validators/argon2id.ts @@ -0,0 +1,35 @@ +import { Container, Providers, Utils } from "@arkecosystem/core-kernel"; +import { Authentication } from "../../contracts"; +import argon2 from "argon2"; + +@Container.injectable() +export class Argon2id implements Authentication.BasicCredentialsValidator { + @Container.inject(Container.Identifiers.PluginConfiguration) + @Container.tagged("plugin", "@arkecosystem/core-manager") + private readonly configuration!: Providers.PluginConfiguration; + + public async validate(username: string, password: string): Promise { + try { + const pluginsConfiguration = this.configuration.get("plugins") as any; + + // @ts-ignore + const secret = pluginsConfiguration.basicAuthentication.secret as string; + Utils.assert.defined(secret); + + const users = pluginsConfiguration.basicAuthentication.users as {username: string, password: string}[]; + Utils.assert.array(users); + + let user = users.find(x => x.username === username); + Utils.assert.defined(user); + + let options = { + secret: Buffer.from(secret), + type: argon2.argon2id + } + + return await argon2.verify(user!.password, password, options); + } catch { } + + return false; + } +} diff --git a/packages/core-manager/src/server/validators/index.ts b/packages/core-manager/src/server/validators/index.ts new file mode 100644 index 0000000000..c080a06316 --- /dev/null +++ b/packages/core-manager/src/server/validators/index.ts @@ -0,0 +1 @@ +export * from "./argon2id" diff --git a/packages/core-manager/src/service-provider.ts b/packages/core-manager/src/service-provider.ts index 574a0e1054..7fd77087e2 100644 --- a/packages/core-manager/src/service-provider.ts +++ b/packages/core-manager/src/service-provider.ts @@ -1,14 +1,15 @@ import { Providers} from "@arkecosystem/core-kernel"; import { Identifiers } from "./ioc"; -import { Server } from "./server"; +import { Server } from "./server/server"; import { ActionReader } from "./action-reader"; -import { PluginFactory } from "./plugins"; +import { PluginFactory } from "./server/plugins"; +import { Argon2id } from "./server/validators"; export class ServiceProvider extends Providers.ServiceProvider { public async register(): Promise { this.app.bind(Identifiers.ActionReader).to(ActionReader).inSingletonScope(); - this.app.bind(Identifiers.PluginFactory).to(PluginFactory).inSingletonScope(); + this.app.bind(Identifiers.BasicCredentialsValidator).to(Argon2id).inSingletonScope(); if (this.config().get("server.http.enabled")) { await this.buildServer("http", Identifiers.HTTP); diff --git a/yarn.lock b/yarn.lock index f8df9ffb69..307b7fa9d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1289,6 +1289,14 @@ dependencies: "@hapi/hoek" "9.x.x" +"@hapi/basic@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/basic/-/basic-6.0.0.tgz#d8372a8eea9583f7077c60b8d55eb5f3a447d4ef" + integrity sha512-nWWSXNCq3WptnP3To2c8kfQiRFDUnd9FQOcMS0B85y1x/m12c0hhp+VdmK60BMe44k6WIog1n6g8f9gZOagqBg== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/boom@7.x.x", "@hapi/boom@^7.4.2": version "7.4.11" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.11.tgz#37af8417eb9416aef3367aa60fa04a1a9f1fc262" @@ -3040,6 +3048,13 @@ "@otplib/plugin-crypto" "^12.0.1" "@otplib/plugin-thirty-two" "^12.0.1" +"@phc/format@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@phc/format/-/format-0.5.0.tgz#a99d27a83d78b3100a191412adda04315e2e3aba" + integrity sha512-JWtZ5P1bfXU0bAtTzCpOLYHDXuxSVdtL/oqz4+xa97h8w9E5IlVN333wugXVFv8vZ1hbXObKQf1ptXmFFcMByg== + dependencies: + safe-buffer "^5.1.2" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -3939,6 +3954,15 @@ argle@~1.1.1: lodash.isfunction "^3.0.8" lodash.isnumber "^3.0.3" +argon2@^0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.26.2.tgz#16c62637f79f8511d666c9a824dd991aa3a7731c" + integrity sha512-Tk9I/r3KIHCIHU5x2UawKsPi+g7MByAYnUZghXztQDXRp/997P31wa4qvdvokTaFBpsu6jOZACd+2qkBGGssRA== + dependencies: + "@phc/format" "^0.5.0" + node-addon-api "^2.0.0" + node-pre-gyp "^0.14.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -10075,6 +10099,11 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-addon-api@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" + integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA== + node-alias@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/node-alias/-/node-alias-1.0.4.tgz#1f1b916b56b9ea241c0135f97ced6940f556f292" @@ -10164,6 +10193,22 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" +node-pre-gyp@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" + integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4.4.2" + node-releases@^1.1.52: version "1.1.52" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" @@ -13097,7 +13142,7 @@ tar@4.4.10: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: +tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.2, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==