From 24dfb8e7444f44626842b525255bd31581537df2 Mon Sep 17 00:00:00 2001 From: Benoit Devos Date: Thu, 31 Mar 2022 18:12:54 +0200 Subject: [PATCH] feat: create/verify JWT token with node key/peer id pair. logion-network/logion-internal#418 --- .env.sample | 7 +- .github/workflows/test.yml | 2 +- Dockerfile | 8 +- docker/base/Dockerfile | 2 +- package.json | 6 +- src/logion/container/app.container.ts | 2 + .../controllers/locrequest.controller.ts | 34 +-- .../protectionrequest.controller.ts | 10 +- .../vaulttransferrequest.controller.ts | 12 +- src/logion/services/authentication.service.ts | 123 ++++++--- .../services/nodeauthorization.service.ts | 25 ++ test/helpers/testapp.ts | 9 +- .../controllers/health.controller.spec.ts | 6 +- test/unit/jose.spec.ts | 84 ++++++ test/unit/jsonwentoken.spec.ts | 57 ---- .../services/authentication.service.spec.ts | 131 +++++---- .../nodeauthorization.service.spec.ts | 52 ++++ yarn.lock | 260 +++++++++++------- 18 files changed, 544 insertions(+), 286 deletions(-) create mode 100644 src/logion/services/nodeauthorization.service.ts create mode 100644 test/unit/jose.spec.ts delete mode 100644 test/unit/jsonwentoken.spec.ts create mode 100644 test/unit/services/nodeauthorization.service.spec.ts diff --git a/.env.sample b/.env.sample index ad6eaa05..2f4d6d00 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,9 @@ WS_PROVIDER_URL=wss://example.org:9944 NODE_TLS_REJECT_UNAUTHORIZED=0 -# the base64-encoded version of "change-me-please": -JWT_SECRET=Y2hhbmdlLW1lLXBsZWFzZQ== -JWT_ISSUER=example.org +# Replace this value with the node key of the related validator node +JWT_SECRET=1c482e5368b84abe08e1a27d0670d303351989b3aa281cb1abfc2f48e4530b57 +# Replace this value with the base58-encoded peer id of the related validator node +JWT_ISSUER=12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1 JWT_TTL_SEC=3600 # Alice OWNER=5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7668a0c5..ecb09ee7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/Dockerfile b/Dockerfile index 6a1ecc61..549e82ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Build backend FROM logionnetwork/logion-backend-calc:v1 AS calc -FROM node:14 AS build-backend +FROM node:16 AS build-backend WORKDIR /tmp/logion-backend COPY . . COPY --from=calc /tmp/calc/pkg calc/pkg @@ -8,7 +8,7 @@ RUN yarn install RUN yarn build # Backend image -FROM logionnetwork/logion-backend-base:v1 +FROM logionnetwork/logion-backend-base:v2 COPY --from=build-backend /tmp/logion-backend/dist dist COPY --from=build-backend /tmp/logion-backend/node_modules node_modules @@ -18,8 +18,8 @@ COPY --from=build-backend /tmp/logion-backend/calc/pkg calc/pkg ENV NODE_ENV=production ENV WS_PROVIDER_URL=ws://localhost:9944 ENV NODE_TLS_REJECT_UNAUTHORIZED=0 -ENV JWT_SECRET=Y2hhbmdlLW1lLXBsZWFzZQ== -ENV JWT_ISSUER=localhost +ENV JWT_SECRET=1c482e5368b84abe08e1a27d0670d303351989b3aa281cb1abfc2f48e4530b57 +ENV JWT_ISSUER=12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1 ENV JWT_TTL_SEC=3600 ENV PORT=8080 ENV TYPEORM_CONNECTION=postgres diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index be34117e..b2d045a0 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -1,6 +1,6 @@ # Import pre-built calc FROM logionnetwork/logion-backend-calc:v1 AS calc -FROM node:14 +FROM node:16 RUN apt-get update && apt-get -y --no-install-recommends install postgresql-client ; rm -rf /var/lib/apt/lists/* WORKDIR /usr/share/logion-backend diff --git a/package.json b/package.json index 589447a0..b8e035ad 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,14 @@ "express-fileupload": "^1.2.1", "express-oas-generator": "^1.0.41", "inversify": "^6.0.1", - "jsonwebtoken": "^8.5.1", + "jose": "^4.6.0", "logion-api": "^0.1.1", "mongoose": "^6.1.6", "mongoose-to-swagger": "^1.4.0", "node-fetch": "3.1.1", "nodemailer": "^6.7.2", "openapi-typescript": "5.1.0", + "peer-id": "^0.16.0", "pg": "^8.7.1", "pug": "^3.0.2", "source-map-support": "^0.5.21", @@ -57,7 +58,6 @@ "@types/express": "^4.17.13", "@types/express-fileupload": "^1.2.2", "@types/jasmine": "^3.10.3", - "@types/jsonwebtoken": "^8.5.8", "@types/node": "^17.0.9", "@types/nodemailer": "^6.4.4", "@types/pug": "^2.0.6", @@ -76,7 +76,7 @@ "typescript": "^4.5.4" }, "engines": { - "node": ">=14" + "node": ">=16" }, "workspaces": [ "calc/pkg" diff --git a/src/logion/container/app.container.ts b/src/logion/container/app.container.ts index bdaaa580..b4f670c5 100644 --- a/src/logion/container/app.container.ts +++ b/src/logion/container/app.container.ts @@ -37,6 +37,7 @@ import { MailService } from "../services/mail.service"; import { DirectoryService } from "../services/directory.service"; import { VaultTransferRequestController } from '../controllers/vaulttransferrequest.controller'; import { VaultTransferRequestFactory, VaultTransferRequestRepository } from '../model/vaulttransferrequest.model'; +import { NodeAuthorizationService } from "../services/nodeauthorization.service"; let container = new Container({ defaultScope: "Singleton" }); container.bind(AuthenticationService).toSelf(); @@ -72,6 +73,7 @@ container.bind(MailService).toSelf() container.bind(DirectoryService).toSelf() container.bind(VaultTransferRequestRepository).toSelf(); container.bind(VaultTransferRequestFactory).toSelf(); +container.bind(NodeAuthorizationService).toSelf(); // Controllers are stateful so they must not be injected with singleton scope container.bind(ApplicationErrorController).toSelf().inTransientScope(); diff --git a/src/logion/controllers/locrequest.controller.ts b/src/logion/controllers/locrequest.controller.ts index 8feb28e4..c5856354 100644 --- a/src/logion/controllers/locrequest.controller.ts +++ b/src/logion/controllers/locrequest.controller.ts @@ -109,7 +109,7 @@ export class LocRequestController extends ApiController { @HttpPost('') @Async() async createLocRequest(createLocRequestView: CreateLocRequestView): Promise { - const authenticatedUser = this.authenticationService.authenticatedUser(this.request); + const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); authenticatedUser.require(user => user.is(createLocRequestView.requesterAddress) || user.isNodeOwner()); const description: LocRequestDescription = { requesterAddress: createLocRequestView.requesterAddress, @@ -238,7 +238,7 @@ export class LocRequestController extends ApiController { @HttpPut('') @Async() async fetchRequests(specificationView: FetchLocRequestsSpecificationView): Promise { - this.authenticationService.authenticatedUserIsOneOf(this.request, specificationView.requesterAddress, specificationView.ownerAddress) + await this.authenticationService.authenticatedUserIsOneOf(this.request, specificationView.requesterAddress, specificationView.ownerAddress) const specification: FetchLocRequestsSpecification = { expectedRequesterAddress: specificationView.requesterAddress, expectedOwnerAddress: specificationView.ownerAddress, @@ -287,7 +287,7 @@ export class LocRequestController extends ApiController { async getLocRequest(_body: any, requestId: string): Promise { try { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - this.authenticationService.authenticatedUserIsOneOf(this.request, + await this.authenticationService.authenticatedUserIsOneOf(this.request, request.requesterAddress, request.ownerAddress); const userIdentity = await this.findUserIdentity(request); return this.toView(request, userIdentity); @@ -366,7 +366,7 @@ export class LocRequestController extends ApiController { @Async() async rejectLocRequest(rejectLocRequestView: RejectLocRequestView, requestId: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.reject(rejectLocRequestView.rejectReason!, moment()); await this.locRequestRepository.save(request) @@ -386,7 +386,7 @@ export class LocRequestController extends ApiController { @Async() async acceptLocRequest(_ignoredBody: any, requestId: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.accept(moment()); await this.locRequestRepository.save(request) @@ -406,7 +406,7 @@ export class LocRequestController extends ApiController { @Async() async addFile(addFileView: AddFileView, requestId: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - const submitter = this.authenticationService.authenticatedUser(this.request) + const submitter = (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner() || user.is(request.requesterAddress), "Only LOC owner or requester can submit a file") .address @@ -457,7 +457,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async downloadFile(_body: any, requestId: string, hash: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner() || user.is(request.requesterAddress), "Only LOC owner or requester can download a file") const file = request.getFile(hash); @@ -486,7 +486,7 @@ export class LocRequestController extends ApiController { @Async() async deleteFile(_body: any, requestId: string, hash: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - const userCheck = this.authenticationService.authenticatedUser(this.request); + const userCheck = await this.authenticationService.authenticatedUser(this.request); userCheck.require(user => user.isNodeOwner() || user.is(request.requesterAddress), "Only LOC owner or requester can delete a file") const file = request.removeFile(userCheck.address, hash); @@ -511,7 +511,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async confirmFile(_body: any, requestId: string, hash: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.confirmFile(hash); @@ -533,7 +533,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async closeLoc(_body: any, requestId: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.preClose(); @@ -559,7 +559,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async voidLoc(body: VoidLocView, requestId: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.preVoid(body.reason!); @@ -581,7 +581,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async addLink(addLinkView: AddLinkView, requestId: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); const targetRequest = requireDefined(await this.locRequestRepository.findById(addLinkView.target!)); request.addLink({ @@ -607,7 +607,7 @@ export class LocRequestController extends ApiController { @Async() async deleteLink(_body: any, requestId: string, target: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - const userCheck = this.authenticationService.authenticatedUser(this.request); + const userCheck = await this.authenticationService.authenticatedUser(this.request); await userCheck.requireNodeOwner(); request.removeLink(userCheck.address, target); @@ -630,7 +630,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async confirmLink(_body: any, requestId: string, target: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); request.confirmLink(target); @@ -652,7 +652,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async addMetadata(addMetadataView: AddMetadataView, requestId: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - const submitter = this.authenticationService.authenticatedUser(this.request) + const submitter = (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner() || user.is(request.requesterAddress), "Only LOC owner or requester can submit metadata") .address const name = requireLength(addMetadataView, "name", 3, 255) @@ -677,7 +677,7 @@ export class LocRequestController extends ApiController { @Async() async deleteMetadata(_body: any, requestId: string, name: string): Promise { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - const userCheck = this.authenticationService.authenticatedUser(this.request); + const userCheck = await this.authenticationService.authenticatedUser(this.request); userCheck.require(user => user.isNodeOwner() || user.is(request.requesterAddress), "Only LOC owner or requester can delete metadata") const decodedName = decodeURIComponent(name); @@ -702,7 +702,7 @@ export class LocRequestController extends ApiController { @SendsResponse() async confirmMetadata(_body: any, requestId: string, name: string) { const request = requireDefined(await this.locRequestRepository.findById(requestId)); - await this.authenticationService.authenticatedUser(this.request) + await (await this.authenticationService.authenticatedUser(this.request)) .requireNodeOwner(); const decodedName = decodeURIComponent(name); diff --git a/src/logion/controllers/protectionrequest.controller.ts b/src/logion/controllers/protectionrequest.controller.ts index 8046146c..45f22f27 100644 --- a/src/logion/controllers/protectionrequest.controller.ts +++ b/src/logion/controllers/protectionrequest.controller.ts @@ -77,7 +77,7 @@ export class ProtectionRequestController extends ApiController { @Async() @HttpPost('') async createProtectionRequest(body: CreateProtectionRequestView): Promise { - this.authenticationService.authenticatedUserIs(this.request, body.requesterAddress); + await this.authenticationService.authenticatedUserIs(this.request, body.requesterAddress); const request = this.protectionRequestFactory.newProtectionRequest({ id: uuid(), description: { @@ -122,7 +122,7 @@ export class ProtectionRequestController extends ApiController { @Async() @HttpPut('') async fetchProtectionRequests(body: FetchProtectionRequestsSpecificationView): Promise { - this.authenticationService.authenticatedUserIsOneOf(this.request, + await this.authenticationService.authenticatedUserIsOneOf(this.request, body.requesterAddress, this.authenticationService.nodeOwner); const specification = new FetchProtectionRequestsSpecification({ @@ -182,7 +182,7 @@ export class ProtectionRequestController extends ApiController { @Async() @HttpPost('/:id/reject') async rejectProtectionRequest(body: RejectProtectionRequestView, id: string): Promise { - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner()); const request = requireDefined(await this.protectionRequestRepository.findById(id)); request.reject(body.rejectReason!, moment()); @@ -207,7 +207,7 @@ export class ProtectionRequestController extends ApiController { @Async() @HttpPost('/:id/accept') async acceptProtectionRequest(body: AcceptProtectionRequestView, id: string): Promise { - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner()); if(body.locId === undefined || body.locId === null) { throw badRequest("Missing LOC ID"); @@ -253,7 +253,7 @@ export class ProtectionRequestController extends ApiController { authorizeUsers.push(protectionRequests[0].addressToRecover); } authorizeUsers.push(protectionRequests[0].requesterAddress!); - this.authenticationService.authenticatedUserIsOneOf(this.request, ...authorizeUsers) + await this.authenticationService.authenticatedUserIsOneOf(this.request, ...authorizeUsers) return { recoveryAccount: this.adapt(recovery), accountToRecover: this.adapt(protectionRequests[0]), diff --git a/src/logion/controllers/vaulttransferrequest.controller.ts b/src/logion/controllers/vaulttransferrequest.controller.ts index 63f1dc6e..c9c4db53 100644 --- a/src/logion/controllers/vaulttransferrequest.controller.ts +++ b/src/logion/controllers/vaulttransferrequest.controller.ts @@ -100,7 +100,7 @@ export class VaultTransferRequestController extends ApiController { } private async userAuthorizedAndProtected(origin: string): Promise { - const user = this.authenticationService.authenticatedUser(this.request); + const user = await this.authenticationService.authenticatedUser(this.request); const protectionRequestDescription = await this.getProtectionRequestDescription(user.address); user.require(user => user.address === origin || @@ -155,7 +155,7 @@ export class VaultTransferRequestController extends ApiController { @Async() @HttpPut('') async fetchVaultTransferRequests(body: FetchVaultTransferRequestsSpecificationView): Promise { - this.authenticationService.authenticatedUserIsOneOf(this.request, + await this.authenticationService.authenticatedUserIsOneOf(this.request, body.requesterAddress, this.authenticationService.nodeOwner); const specification = new FetchVaultTransferRequestsSpecification({ @@ -210,7 +210,7 @@ export class VaultTransferRequestController extends ApiController { @Async() @HttpPost('/:id/reject') async rejectVaultTransferRequest(body: RejectVaultTransferRequestView, id: string): Promise { - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner()); const request = requireDefined(await this.vaultTransferRequestRepository.findById(id)); @@ -240,7 +240,7 @@ export class VaultTransferRequestController extends ApiController { @Async() @HttpPost('/:id/accept') async acceptVaultTransferRequest(_body: any, id: string): Promise { - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.isNodeOwner()); const request = requireDefined(await this.vaultTransferRequestRepository.findById(id)); const protectionRequestDescription = await this.getProtectionRequestDescription(request.requesterAddress!); @@ -258,7 +258,7 @@ export class VaultTransferRequestController extends ApiController { @HttpPost('/:id/cancel') async cancelVaultTransferRequest(_body: any, id: string): Promise { const request = requireDefined(await this.vaultTransferRequestRepository.findById(id)); - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.address === request.getDescription().requesterAddress); const protectionRequestDescription = await this.getProtectionRequestDescription(request.requesterAddress!); @@ -276,7 +276,7 @@ export class VaultTransferRequestController extends ApiController { @HttpPost('/:id/resubmit') async resubmitVaultTransferRequest(_body: any, id: string): Promise { const request = requireDefined(await this.vaultTransferRequestRepository.findById(id)); - this.authenticationService.authenticatedUser(this.request) + (await this.authenticationService.authenticatedUser(this.request)) .require(user => user.address === request.getDescription().requesterAddress); const protectionRequestDescription = await this.getProtectionRequestDescription(request.requesterAddress!); diff --git a/src/logion/services/authentication.service.ts b/src/logion/services/authentication.service.ts index 99ca4244..eef8c93c 100644 --- a/src/logion/services/authentication.service.ts +++ b/src/logion/services/authentication.service.ts @@ -1,11 +1,15 @@ -import jwt, { Algorithm, Jwt, VerifyErrors } from "jsonwebtoken"; import { injectable } from "inversify"; import { Request } from "express"; import { UnauthorizedException } from "dinoloop/modules/builtin/exceptions/exceptions"; import moment, { Moment } from "moment"; import { AuthorityService } from "./authority.service"; +import PeerId from "peer-id"; +import { KeyObject, createPublicKey, createPrivateKey } from "crypto"; +import { base64url, SignJWT, decodeJwt, jwtVerify, JWTPayload } from "jose"; +import { JWTVerifyResult } from "jose/dist/types/types"; +import { NodeAuthorizationService } from "./nodeauthorization.service"; -const ALGORITHM: Algorithm = "HS384"; +const ALGORITHM = "EdDSA"; export interface AuthenticatedUser { address: string, @@ -77,21 +81,21 @@ async function isLegalOfficer(authorityService: AuthorityService, address: strin @injectable() export class AuthenticationService { - authenticatedUser(request: Request): LogionUserCheck { - const user = this.extractLogionUser(request); + async authenticatedUser(request: Request): Promise { + const user = await this.extractLogionUser(request); return new LogionUserCheck(user, this.nodeOwner, address => isLegalOfficer(this.authorityService, address)); } - authenticatedUserIs(request: Request, address: string | undefined | null): LogionUserCheck { - const user = this.authenticatedUser(request); + async authenticatedUserIs(request: Request, address: string | undefined | null): Promise { + const user = await this.authenticatedUser(request); if (user.is(address)) { return user; } throw unauthorized("User has not access to this resource"); } - authenticatedUserIsOneOf(request: Request, ...addresses: (string | undefined | null)[]): LogionUserCheck { - const user = this.authenticatedUser(request); + async authenticatedUserIsOneOf(request: Request, ...addresses: (string | undefined | null)[]): Promise { + const user = await this.authenticatedUser(request); if (user.isOneOf(addresses)) { return user; } @@ -99,27 +103,40 @@ export class AuthenticationService { } async refreshToken(jwtToken: string): Promise { - const authenticatedUser = this.verifyToken(jwtToken) + const authenticatedUser = await this.verifyToken(jwtToken) return await this.createToken(authenticatedUser.address, moment()) } - private extractLogionUser(request: Request): AuthenticatedUser { + private async extractLogionUser(request: Request): Promise { const jwtToken = this.extractBearerToken(request); - return this.verifyToken(jwtToken) + return await this.verifyToken(jwtToken) } - private verifyToken(jwtToken: string): AuthenticatedUser { - jwt.verify(jwtToken, this.secret, { issuer: this.issuer }, err => { - if (err) { - throw this._unauthorized(err) - } - }); - const token = jwt.decode(jwtToken, { complete: true }) as Jwt; - if(typeof token.payload !== 'string') { - return { address: token.payload.sub! } - } else { - throw unauthorized("Unable to decode payload"); + private async verifyToken(jwtToken: string): Promise { + + let payload:JWTPayload; + try { + payload = decodeJwt(jwtToken) + } catch (error) { + throw unauthorized("" + error) + } + const issuer = payload.iss; + if (!issuer || ! await this.nodeAuthorizationService.isWellKnownNode(issuer)) { + throw unauthorized("Invalid issuer"); + } + + const publicKey = this.createPublicKey(issuer) + let result: JWTVerifyResult; + try { + result = await jwtVerify(jwtToken, publicKey, { algorithms: [ ALGORITHM ] }); + } catch (error) { + throw unauthorized("" + error) + } + const address = result.payload.sub; + if (!address) { + throw unauthorized("Unable to find issuer in payload"); } + return { address } } private extractBearerToken(request: Request): string { @@ -130,19 +147,20 @@ export class AuthenticationService { return header.split(' ')[1].trim(); } - private readonly secret: Buffer; + private readonly privateKey: KeyObject private readonly issuer: string; private readonly ttl: number; readonly nodeOwner: string; constructor( public authorityService: AuthorityService, + public nodeAuthorizationService: NodeAuthorizationService, ) { if (process.env.JWT_SECRET === undefined) { - throw Error("No JWT secret set, please set var JWT_SECRET"); + throw Error("No JWT secret set, please set var JWT_SECRET equal to NODE_SECRET_KEY"); } if (process.env.JWT_ISSUER === undefined) { - throw Error("No JWT issuer set, please set var JWT_ISSUER"); + throw Error("No JWT issuer set, please set var JWT_ISSUER equal to NODE_PEER_ID (base58 encoding)"); } if (process.env.JWT_TTL_SEC === undefined) { throw Error("No JWT Time-to-live set, please set var JWT_TTL_SEC"); @@ -150,30 +168,55 @@ export class AuthenticationService { if (process.env.OWNER === undefined) { throw Error("No node owner set, please set var OWNER"); } - const bas64EncodedSecret = process.env.JWT_SECRET as string; - this.secret = Buffer.from(bas64EncodedSecret, 'base64') this.issuer = process.env.JWT_ISSUER; + this.privateKey = this.createPrivateKey(this.publicKeyBytes(this.issuer), process.env.JWT_SECRET) this.ttl = parseInt(process.env.JWT_TTL_SEC); this.nodeOwner = process.env.OWNER; } + private publicKeyBytes(issuer: string): Uint8Array { + const peerId = PeerId.createFromB58String(issuer); + return peerId.pubKey.bytes.slice(4, 36) + } + + private createPrivateKey(publicKeyBytes: Uint8Array, nodeKey: string): KeyObject { + const privateKeyBytes = Buffer.from(nodeKey, "hex"); + + return createPrivateKey({ + key: { + kty: "OKP", + crv: "Ed25519", + x: base64url.encode(publicKeyBytes), + d: base64url.encode(privateKeyBytes), + }, + format: "jwk" + }) + } + + private createPublicKey(issuer: string): KeyObject { + + return createPublicKey({ + key: { + kty: "OKP", + crv: "Ed25519", + x: base64url.encode(this.publicKeyBytes(issuer)) + }, + format: "jwk" + }); + } + async createToken(address: string, issuedAt: Moment, expiresIn?: number): Promise { const now = Math.floor(issuedAt.unix()); const expiredOn = now + (expiresIn !== undefined ? expiresIn : this.ttl); - const payload = { - iat: now, - exp: expiredOn - }; - const encodedToken = jwt.sign(payload, this.secret, { - algorithm: ALGORITHM, - issuer: this.issuer, - subject: address - }); - return { value: encodedToken, expiredOn: moment.unix(expiredOn) }; - } + const encodedToken = await new SignJWT({}) + .setProtectedHeader({ alg: ALGORITHM }) + .setIssuedAt(now) + .setExpirationTime(expiredOn) + .setIssuer(this.issuer) + .setSubject(address) + .sign(this.privateKey) - private _unauthorized(error: VerifyErrors): UnauthorizedException<{ error: string }> { - return new UnauthorizedException({ error: error.name + ": " + error.message }); + return { value: encodedToken, expiredOn: moment.unix(expiredOn) }; } ensureAuthorizationBearer(request: Request, expectedToken: string | undefined) { diff --git a/src/logion/services/nodeauthorization.service.ts b/src/logion/services/nodeauthorization.service.ts new file mode 100644 index 00000000..68ebe822 --- /dev/null +++ b/src/logion/services/nodeauthorization.service.ts @@ -0,0 +1,25 @@ +import { PolkadotService } from "./polkadot.service"; +import { injectable } from "inversify"; +import { PeerId } from "logion-api/dist/interfaces"; +import { createFromB58String } from "peer-id"; + +@injectable() +export class NodeAuthorizationService { + + constructor( + private polkadotService: PolkadotService + ) { + } + + async isWellKnownNode(base58PeerId: string): Promise { + const hexPeerId = createFromB58String(base58PeerId).toHexString(); + const api = await this.polkadotService.readyApi(); + const wellKnowNodes: Set = await api.query.nodeAuthorization.wellKnownNodes(); + for (let wellKnowNode of wellKnowNodes) { + if (wellKnowNode.toHex() === `0x${ hexPeerId }`) { + return true + } + } + return false; + } +} diff --git a/test/helpers/testapp.ts b/test/helpers/testapp.ts index e4dcb3e5..3511832d 100644 --- a/test/helpers/testapp.ts +++ b/test/helpers/testapp.ts @@ -77,11 +77,11 @@ function mockAuthenticationSuccess(isNodeOwner: boolean, conditionFulfilled: boo const authenticationService = new Mock(); authenticationService.setup(instance => instance.authenticatedUserIs) - .returns(() => authenticatedUser.object()); + .returns(() => Promise.resolve(authenticatedUser.object())); authenticationService.setup(instance => instance.authenticatedUserIsOneOf) - .returns(() => authenticatedUser.object()); + .returns(() => Promise.resolve(authenticatedUser.object())); authenticationService.setup(instance => instance.authenticatedUser) - .returns(() => authenticatedUser.object()); + .returns(() => Promise.resolve(authenticatedUser.object())); authenticationService.setup(instance => instance.nodeOwner) .returns(ALICE); return authenticationService.object(); @@ -104,7 +104,8 @@ function mockAuthenticationFailure(): AuthenticationService { throw new UnauthorizedException(); }); - authenticationService.setup(instance => instance.authenticatedUser).returns(() => authenticatedUser.object()); + authenticationService.setup(instance => instance.authenticatedUser) + .returns(() => Promise.resolve(authenticatedUser.object())); authenticationService.setup(instance => instance.nodeOwner).returns(ALICE); return authenticationService.object(); } diff --git a/test/unit/controllers/health.controller.spec.ts b/test/unit/controllers/health.controller.spec.ts index 9067f6d1..211d6895 100644 --- a/test/unit/controllers/health.controller.spec.ts +++ b/test/unit/controllers/health.controller.spec.ts @@ -6,6 +6,7 @@ import { HealthController } from '../../../src/logion/controllers/health.control import { AuthenticationService } from '../../../src/logion/services/authentication.service'; import { SyncPointRepository } from '../../../src/logion/model/syncpoint.model'; import { AuthorityService } from "../../../src/logion/services/authority.service"; +import { NodeAuthorizationService } from "../../../src/logion/services/nodeauthorization.service"; describe('HealthController', () => { @@ -65,8 +66,9 @@ const UNEXPECTED_TOKEN = "wrong-health-check-token"; function bindMocks(container: Container, up: boolean): void { const authorityService = new Mock(); - - container.rebind(AuthenticationService).toConstantValue(new AuthenticationService(authorityService.object())); + const nodeAuthorizationService = new Mock() + container.rebind(AuthenticationService) + .toConstantValue(new AuthenticationService(authorityService.object(), nodeAuthorizationService.object())); const syncPointRepository = new Mock(); if(up) { diff --git a/test/unit/jose.spec.ts b/test/unit/jose.spec.ts new file mode 100644 index 00000000..e8224d4c --- /dev/null +++ b/test/unit/jose.spec.ts @@ -0,0 +1,84 @@ +import { ALICE } from "../helpers/addresses"; +import PeerId from "peer-id"; +import { createPublicKey, KeyObject, createPrivateKey } from "crypto"; +import { base64url, SignJWT, jwtVerify, decodeJwt } from "jose"; + +const NODE_1_PEER_ID = PeerId.createFromB58String("12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1"); +const NODE_1_KEY = "1c482e5368b84abe08e1a27d0670d303351989b3aa281cb1abfc2f48e4530b57"; + +const ANOTHER_NODE_PEER_ID = PeerId.createFromB58String("12D3KooWE6cKjo2QTQqFNJFG7ymfndXrY3vZa7kaMgGHPDKmdP9F"); + +const TOKEN = "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2MjM2NzQwOTksImV4cCI6MTgyMzY3NDA5OSwiaXNzIjoiMTJEM0tvb1dEQ3VHVTdXWTNWYVdqQlMxRTQ0eDRFbm1UZ0szSFJ4V0ZxWUczZHFYRGZQMSIsInN1YiI6IjVHcnd2YUVGNXpYYjI2Rno5cmNRcERXUzU3Q3RFUkhwTmVoWENQY05vSEdLdXRRWSJ9.GggMsAlDO2GoRFm8IBxuHKVtZ7Ms1pipCTzzoaDbGxXGhm4niFX_kEetMVdXo69oG0vI7XWfWHs7Z-x-nOjUCQ"; +const ALGORITHM = "EdDSA"; + +describe('jwt ', () => { + + function publicKey(peerId: PeerId): KeyObject { + + const publicKeyBytes = peerId.pubKey.bytes.slice(4, 36) + + return createPublicKey({ + key: { + kty: "OKP", + crv: "Ed25519", + x: base64url.encode(publicKeyBytes) + }, + format: "jwk" + }); + + } + + function privateKey(peerId: PeerId, nodeKey: string): KeyObject { + const publicKeyBytes = peerId.pubKey.bytes.slice(4, 36) + const privateKeyBytes = Buffer.from(nodeKey, "hex"); + + return createPrivateKey({ + key: { + kty: "OKP", + crv: "Ed25519", + x: base64url.encode(publicKeyBytes), + d: base64url.encode(privateKeyBytes), + }, + format: "jwk" + }) + } + + it("signs successfully", async () => { + const token = await new SignJWT({}) + .setProtectedHeader({alg: ALGORITHM}) + .setIssuedAt(1623674099) + .setExpirationTime(1823674099) + .setIssuer(NODE_1_PEER_ID.toB58String()) + .setSubject(ALICE) + .sign(privateKey(NODE_1_PEER_ID, NODE_1_KEY)) + expect(token).toBe(TOKEN); + }) + + it("decodes successfully", () => { + const payload = decodeJwt(TOKEN) + console.log(payload); + expect(payload!.iss).toBe(NODE_1_PEER_ID.toB58String()); + expect(payload!.sub).toBe(ALICE); + }) + + it("verifies the signature", async () => { + await jwtVerify(TOKEN, publicKey(NODE_1_PEER_ID), { + issuer: NODE_1_PEER_ID.toB58String(), + subject: ALICE, + algorithms: [ ALGORITHM ] + }) + }) + + it ("prevents fake token", async () => { + try { + await jwtVerify(TOKEN, publicKey(ANOTHER_NODE_PEER_ID), { + issuer: NODE_1_PEER_ID.toB58String(), + subject: ALICE, + algorithms: [ ALGORITHM ] + }) + fail("Token verified with an invalid key") + } catch (error) { + expect("" + error).toEqual("JWSSignatureVerificationFailed: signature verification failed") + } + }) +}) diff --git a/test/unit/jsonwentoken.spec.ts b/test/unit/jsonwentoken.spec.ts deleted file mode 100644 index b66d2803..00000000 --- a/test/unit/jsonwentoken.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import jwt, { JwtPayload } from 'jsonwebtoken'; -import { ALICE } from "../helpers/addresses"; - -const SECRET = "secret-key-that-no-one-could-possibly-know"; -const TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjM2NzQwOTksImV4cCI6MTgyMzY3NDA5OSwibGVnYWxPZmZpY2VyIjp0cnVlLCJpc3MiOiJ3d3cuZXhhbXBsZS5vcmciLCJzdWIiOiI1R3J3dmFFRjV6WGIyNkZ6OXJjUXBEV1M1N0N0RVJIcE5laFhDUGNOb0hHS3V0UVkifQ.yoaRk0oSixyYIztFDS5QCRop-0xAP_xw4UY30uTwVtM"; -const ISSUER = "www.example.org"; - -// HEADER -// { -// "alg": "HS256", -// "typ": "JWT" -// } -// PAYLOAD -// { -// "iat": 1623674099, -// "exp": 1823674099, -// "legalOfficer": true, -// "iss": "www.example.org", -// "sub": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" -// } -describe('jwt tests', () => { - - it("signs successfully", () => { - const payload = { - iat: 1623674099, - exp: 1823674099, - legalOfficer: true - }; - const encoded = jwt.sign(payload, Buffer.from(SECRET, 'base64'), { - algorithm: "HS256", - issuer: ISSUER, - subject: ALICE, - }) - expect(encoded).toBe(TOKEN); - }) - - it("decodes successfully", () => { - const payload = jwt.decode(TOKEN) as JwtPayload; - console.log(payload); - expect(payload!.iss).toBe(ISSUER); - expect(payload!.sub).toBe(ALICE); - expect(payload!.legalOfficer).toBe(true); - }) - - it("verifies the signature", () => { - jwt.verify(TOKEN, Buffer.from(SECRET, 'base64'), { - issuer: ISSUER, - subject: ALICE - }) - }) - - it ("prevents fake token", () => { - jwt.verify(TOKEN, Buffer.from("wrong-secret", 'base64'), (error, decoded) => { - expect(error?.message).toBe("invalid signature"); - }); - }) -}) diff --git a/test/unit/services/authentication.service.spec.ts b/test/unit/services/authentication.service.spec.ts index 7d7ed356..d1272d40 100644 --- a/test/unit/services/authentication.service.spec.ts +++ b/test/unit/services/authentication.service.spec.ts @@ -1,19 +1,20 @@ -import { AuthenticationService } from "../../../src/logion/services/authentication.service"; +import { AuthenticationService, LogionUserCheck } from "../../../src/logion/services/authentication.service"; import { Mock, It } from "moq.ts"; import { Request } from "express"; import moment from "moment"; import { UnauthorizedException } from "dinoloop/modules/builtin/exceptions/exceptions"; import { ALICE } from "../../helpers/addresses"; import { AuthorityService } from "../../../src/logion/services/authority.service"; +import { NodeAuthorizationService } from "../../../src/logion/services/nodeauthorization.service"; -const USER_TOKEN = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzEyMTc2MTEsImV4cCI6NDc4NDgxNzYxMSwiaXNzIjoiZXhhbXBsZS5vcmciLCJzdWIiOiI1SDRNdkFzb2JmWjZiQkNEeWo1ZHNyV1lMckE4SHJSemFxYTlwNjFVWHR4TWhTQ1kifQ.7X-8bX3l5MMDMBSjgjrZYEwXs07rL0xp5cvqn1ecQN7x1PH40eE4Cf1sDghKa8ER" -const USER_TOKEN_WRONG_SIGNATURE = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzEyMTc2MTEsImxlZ2FsT2ZmaWNlciI6ZmFsc2UsImV4cCI6NDc4NDgxNzYxMSwiaXNzIjoiZXhhbXBsZS5vcmciLCJzdWIiOiI1SDRNdkFzb2JmWjZiQkNEeWo1ZHNyV1lMckE4SHJSemFxYTlwNjFVWHR4TWhTQ1kifQ.GTLJB_uMjsdcuWzM3CWL92n1UNI0WYXFUDW7QQ1Vi6k3TQIEvG_WwMuuZ2d9cexY" -const ALICE_TOKEN = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzEyMTc2MTEsImV4cCI6NDc4NDgxNzYxMSwiaXNzIjoiZXhhbXBsZS5vcmciLCJzdWIiOiI1R3J3dmFFRjV6WGIyNkZ6OXJjUXBEV1M1N0N0RVJIcE5laFhDUGNOb0hHS3V0UVkifQ.WBsbj7ypc2ZO1IOGdgjLmzblWl9oOquReVODKmQyT_XjkqPL3epd-PxHZd6LJAxP" +const USER_TOKEN = "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2MzEyMTc2MTEsImV4cCI6NDc4NDgxNzYxMSwiaXNzIjoiMTJEM0tvb1dEQ3VHVTdXWTNWYVdqQlMxRTQ0eDRFbm1UZ0szSFJ4V0ZxWUczZHFYRGZQMSIsInN1YiI6IjVINE12QXNvYmZaNmJCQ0R5ajVkc3JXWUxyQThIclJ6YXFhOXA2MVVYdHhNaFNDWSJ9.pBYUyYxq2I_HZiYyeJ-rc8ANxVgckLyd2Y1Snu685mDK4fSwanb6EHsMAP47iCtzSxhaB5bDu7zDmY-XMAyuAw" +const USER_TOKEN_WRONG_SIGNATURE = "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2MzEyMTc2MTEsImV4cCI6NDc4NDgxNzYxMSwiaXNzIjoiMTJEM0tvb1dEQ3VHVTdXWTNWYVdqQlMxRTQ0eDRFbm1UZ0szSFJ4V0ZxWUczZHFYRGZQMSIsInN1YiI6IjVINE12QXNvYmZaNmJCQ0R5ajVkc3JXWUxyQThIclJ6YXFhOXA2MVVYdHhNaFNDWSJ9.GTLJB_uMjsdcuWzM3CWL92n1UNI0WYXFUDW7QQ1Vi6k3TQIEvG_WwMuuZ2d9cexY" +const ALICE_TOKEN = "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2MjM2NzQwOTksImV4cCI6MTgyMzY3NDA5OSwiaXNzIjoiMTJEM0tvb1dEQ3VHVTdXWTNWYVdqQlMxRTQ0eDRFbm1UZ0szSFJ4V0ZxWUczZHFYRGZQMSIsInN1YiI6IjVHcnd2YUVGNXpYYjI2Rno5cmNRcERXUzU3Q3RFUkhwTmVoWENQY05vSEdLdXRRWSJ9.GggMsAlDO2GoRFm8IBxuHKVtZ7Ms1pipCTzzoaDbGxXGhm4niFX_kEetMVdXo69oG0vI7XWfWHs7Z-x-nOjUCQ" const USER_ADDRESS = "5H4MvAsobfZ6bBCDyj5dsrWYLrA8HrRzaqa9p61UXtxMhSCY" const TTL = 100 * 365 * 24 * 3600; // 100 years -process.env.JWT_SECRET = "Y2hhbmdlLW1lLXBsZWFzZQ=="; -process.env.JWT_ISSUER = "example.org"; +process.env.JWT_SECRET = "1c482e5368b84abe08e1a27d0670d303351989b3aa281cb1abfc2f48e4530b57"; +process.env.JWT_ISSUER = "12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1"; process.env.JWT_TTL_SEC = "3600"; process.env.OWNER = ALICE; @@ -21,7 +22,11 @@ describe('AuthenticationService createToken()', () => { it('generates a token for user', async () => { givenAuthorityService(); - const authenticationService = new AuthenticationService(authorityService.object()); + givenNodeAuthorizationService(); + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const actual = await authenticationService.createToken(USER_ADDRESS, moment.unix(1631217611), TTL); expect(actual.value).toBe(USER_TOKEN) expect(actual.expiredOn.unix).toBe(moment.unix(1631217611 + TTL).unix) @@ -36,66 +41,82 @@ function givenAuthorityService(isLegalOfficer?: boolean) { let authorityService: Mock; +function givenNodeAuthorizationService(isWellKnownNode: boolean = true) { + nodeAuthorizationService = new Mock() + nodeAuthorizationService.setup(instance => instance.isWellKnownNode(It.IsAny())) + .returns(Promise.resolve(isWellKnownNode)) +} + +let nodeAuthorizationService: Mock; + describe('AuthenticationService authenticatedUserIs()', () => { - it('authenticates user based on token', () => { + it('authenticates user based on token', async () => { givenAuthorityService(); - const authenticationService = new AuthenticationService(authorityService.object()); + givenNodeAuthorizationService(); + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const request = new Mock(); request.setup(instance => instance.header("Authorization")).returns("Bearer " + USER_TOKEN) - const logionUser = authenticationService.authenticatedUserIs(request.object(), USER_ADDRESS); + const logionUser = await authenticationService.authenticatedUserIs(request.object(), USER_ADDRESS); expect(logionUser.address).toBe(USER_ADDRESS) }) it('authenticates node owner based on token', async () => { givenAuthorityService(true); - const authenticationService = new AuthenticationService(authorityService.object()); + givenNodeAuthorizationService(); + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const request = new Mock(); request.setup(instance => instance.header("Authorization")).returns("Bearer " + ALICE_TOKEN) - const logionUser = authenticationService.authenticatedUserIs(request.object(), ALICE); + const logionUser = await authenticationService.authenticatedUserIs(request.object(), ALICE); await logionUser.requireNodeOwner(); }) - it('does not authenticate user different from token', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { - authenticationService.authenticatedUserIs(request, "SOME-OTHER-USER"); - }, "Bearer " + USER_TOKEN, "User has not access to this resource") + it('does not authenticate user different from token', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => + authenticationService.authenticatedUserIs(request, "SOME-OTHER-USER") + , "Bearer " + USER_TOKEN, "User has not access to this resource") }) - it('does not authenticate null user', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { + it('does not authenticate null user', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => authenticationService.authenticatedUserIs(request, null) - }, "Bearer " + USER_TOKEN, "User has not access to this resource") + , "Bearer " + USER_TOKEN, "User has not access to this resource") }) - it('does not authenticate undefined user', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { + it('does not authenticate undefined user', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => authenticationService.authenticatedUserIs(request, undefined) - }, "Bearer " + USER_TOKEN, "User has not access to this resource") + , "Bearer " + USER_TOKEN, "User has not access to this resource") }) - it('does not authenticate invalid header', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { + it('does not authenticate invalid header', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => authenticationService.authenticatedUserIs(request, "SOME-OTHER-USER") - }, "fake-auth-header", "Invalid Authorization header") + , "fake-auth-header", "Invalid Authorization header") }) - it('does not authenticate fake token', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { + it('does not authenticate fake token', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => authenticationService.authenticatedUserIs(request, USER_ADDRESS) - }, "Bearer FAKE", "JsonWebTokenError: jwt malformed") + , "Bearer FAKE", "JWTInvalid: Invalid JWT") }) - it('does not authenticate token with wrong signature', () => { - givenAuthorityService(); - testForFailure((authenticationService: AuthenticationService, request: Request) => { + it('does not authenticate token with wrong signature', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => authenticationService.authenticatedUserIs(request, USER_ADDRESS) - }, "Bearer " + USER_TOKEN_WRONG_SIGNATURE, "JsonWebTokenError: invalid signature") + , "Bearer " + USER_TOKEN_WRONG_SIGNATURE, "JWSSignatureVerificationFailed: signature verification failed") + }) + + it('does not authenticate token for unknown node', async () => { + await testForFailure((authenticationService: AuthenticationService, request: Request) => + authenticationService.authenticatedUserIs(request, USER_ADDRESS) + , "Bearer " + USER_TOKEN, "Invalid issuer", false) }) }) @@ -103,7 +124,11 @@ describe('AuthenticationService authenticatedUserIsOneOf()', () => { it('authenticates user based on token', () => { givenAuthorityService(); - const authenticationService = new AuthenticationService(authorityService.object()); + givenNodeAuthorizationService(); + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const request = new Mock(); request.setup(instance => instance.header("Authorization")).returns("Bearer " + USER_TOKEN) authenticationService.authenticatedUserIsOneOf(request.object(), "FAKE ADDRESS", USER_ADDRESS); @@ -111,24 +136,34 @@ describe('AuthenticationService authenticatedUserIsOneOf()', () => { it('authenticates legal officer based on token', async () => { givenAuthorityService(true); - const authenticationService = new AuthenticationService(authorityService.object()); + givenNodeAuthorizationService(); + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const request = new Mock(); request.setup(instance => instance.header("Authorization")).returns("Bearer " + ALICE_TOKEN) - const logionUser = authenticationService.authenticatedUserIsOneOf(request.object(), "FAKE ADDRESS", ALICE); + const logionUser = await authenticationService.authenticatedUserIsOneOf(request.object(), "FAKE ADDRESS", ALICE); await logionUser.requireNodeOwner(); }) }) -function testForFailure(fnToCheck: (authenticationService: AuthenticationService, request: Request) => void, authHeader: string, expectedError: string) { - const authenticationService = new AuthenticationService(authorityService.object()); +async function testForFailure(fnToCheck: (authenticationService: AuthenticationService, request: Request) => Promise, authHeader: string, expectedError: string, isWellKnownNode: boolean = true) { + givenAuthorityService() + givenNodeAuthorizationService(isWellKnownNode) + const authenticationService = new AuthenticationService( + authorityService.object(), + nodeAuthorizationService.object() + ); const request = new Mock(); request.setup(instance => instance.header("Authorization")).returns(authHeader) - let authentication = function () { - fnToCheck(authenticationService, request.object()); + try { + await fnToCheck(authenticationService, request.object()) + fail("Call should not succeed while testing for failure.") + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException) + const unauthorized = error as UnauthorizedException<{ error: string }>; + expect(unauthorized.content.error).toEqual(expectedError) } - expect(authentication).toThrowMatching((exception: UnauthorizedException<{ error: string }>) => { - expect(exception.content.error).toBe(expectedError) - return exception.content.error === expectedError - }) } diff --git a/test/unit/services/nodeauthorization.service.spec.ts b/test/unit/services/nodeauthorization.service.spec.ts new file mode 100644 index 00000000..b2b26dcc --- /dev/null +++ b/test/unit/services/nodeauthorization.service.spec.ts @@ -0,0 +1,52 @@ +import { NodeAuthorizationService } from "../../../src/logion/services/nodeauthorization.service"; +import { PolkadotService } from "../../../src/logion/services/polkadot.service"; +import { Mock } from "moq.ts"; +import { ApiPromise } from "@polkadot/api"; +import { PeerId } from "logion-api/dist/interfaces"; +import { createFromB58String } from "peer-id"; + +const WELL_KNOWN_NODES: string[] = ["12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2", "12D3KooWQYV9dGMFoRzNStwpXztXaBUjtPqi6aU76ZgUriHhKust"] + +describe("nodeAuthorizationService ", () => { + + const nodeAuthorizationService = new NodeAuthorizationService(mockPolkadotService()) + + it(" does detect a well known node", async () => { + const peerId = WELL_KNOWN_NODES[0] + expect(await nodeAuthorizationService.isWellKnownNode(peerId)).toBeTrue() + }) + + it(" does not detect a random peer id as well known node", async () => { + const peerId = "12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1" + expect(await nodeAuthorizationService.isWellKnownNode(peerId)).toBeFalse() + }) +}) + +function mockPolkadotService(): PolkadotService { + const api = mockApi(); + const polkadotService = new Mock(); + polkadotService.setup(instance => instance.readyApi()) + .returns(Promise.resolve(api)); + return polkadotService.object(); +} + +function peerId(base58PeerId: string): PeerId { + const hexPeerId = createFromB58String(base58PeerId).toHexString(); + const peerId = new Mock() + peerId.setup(instance => instance.toHex()) + .returns(`0x${hexPeerId}`) + return peerId.object(); +} + +function mockApi(): ApiPromise { + + const apiMock: unknown = { + query: { + nodeAuthorization: { + wellKnownNodes: () => new Set(WELL_KNOWN_NODES.map(peerId)) + } + } + }; + return apiMock as ApiPromise; +} + diff --git a/yarn.lock b/yarn.lock index 56f743db..6776deb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -278,6 +278,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@noble/ed25519@^1.5.1": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.6.0.tgz#b55f7c9e532b478bf1d7c4f609e1f3a37850b583" + integrity sha512-UKju89WV37IUALIMfKhKW3psO8AqmrE/GvH6QbPKjzolQ98zM7WmGUeY+xdIgSf5tqPFf75ZCYMgym6E9Jsw3Q== + "@noble/hashes@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-0.5.7.tgz#8605d84b34daf43d15c344fae54f0a1d5d5a4632" @@ -293,7 +298,7 @@ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.3.4.tgz#158ded712d09237c0d3428be60dc01ce8ebab9fb" integrity sha512-ZVRouDO5mbdCiDg4zCd3ZZABduRtpy4tCnB33Gh9upHe9tRzpiqbRSN1VTjrj/2g8u2c6MBi0YLNnNQpBYOiWg== -"@noble/secp256k1@1.5.5": +"@noble/secp256k1@1.5.5", "@noble/secp256k1@^1.3.0": version "1.5.5" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3" integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ== @@ -726,6 +731,59 @@ "@types/websocket" "^1.0.5" websocket "^1.0.34" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + "@scure/base@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.0.0.tgz#109fb595021de285f05a7db6806f2f48296fcee7" @@ -839,12 +897,10 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.3.tgz#a89798b3d5a8bd23ca56e855a9aee3e5a93bdaaa" integrity sha512-SWyMrjgdAUHNQmutvDcKablrJhkDLy4wunTme8oYLjKp41GnHGxMRXr2MQMvy/qy8H3LdzwQk9gH4hZ6T++H8g== -"@types/jsonwebtoken@^8.5.8": - version "8.5.8" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" - integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== - dependencies: - "@types/node" "*" +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== "@types/mime@^1": version "1.3.2" @@ -864,6 +920,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.15.tgz#97779282c09c09577120a2162e71d8380003590a" integrity sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA== +"@types/node@>=13.7.0": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + "@types/nodemailer@^6.4.4": version "6.4.4" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" @@ -1142,11 +1203,6 @@ bson@^4.2.2, bson@^4.6.1: dependencies: buffer "^5.6.0" -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1249,6 +1305,11 @@ character-parser@^2.2.0: dependencies: is-regex "^1.0.3" +class-is@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/class-is/-/class-is-1.1.0.tgz#9d3c0fba0440d211d843cec3dedfa48055005825" + integrity sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -1535,13 +1596,6 @@ duplexer@~0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ed2curve@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ed2curve/-/ed2curve-0.3.0.tgz#322b575152a45305429d546b071823a93129a05d" @@ -1574,6 +1628,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +err-code@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" + integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== + es5-ext@^0.10.35, es5-ext@^0.10.50: version "0.10.53" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" @@ -1653,6 +1712,11 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + express-fileupload@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.3.1.tgz#3238472def305b8cb4cc5936a953761d0c442011" @@ -2105,6 +2169,14 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +iso-random-stream@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/iso-random-stream/-/iso-random-stream-2.0.2.tgz#a24f77c34cfdad9d398707d522a6a0cc640ff27d" + integrity sha512-yJvs+Nnelic1L2vH2JzWvvPQFA4r7kSTnpST/+LkAQjSz0hos2oqLD+qIVi9Qk38Hoe7mNDt3j0S27R58MVjLQ== + dependencies: + events "^3.3.0" + readable-stream "^3.4.0" + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -2186,6 +2258,11 @@ jasmine@^4.0.2: glob "^7.1.6" jasmine-core "^4.0.0" +jose@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.6.0.tgz#f3ff007ddcbce462c091d3d41b7af2e35dec348c" + integrity sha512-0hNAkhMBNi4soKSAX4zYOFV+aqJlEz/4j4fregvasJzEVtjDChvWqRjPvHwLqr5hx28Ayr6bsOs1Kuj87V0O8w== + js-stringify@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -2228,22 +2305,6 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - jstransformer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" @@ -2252,23 +2313,6 @@ jstransformer@1.0.0: is-promise "^2.0.0" promise "^7.0.1" -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - kareem@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.3.tgz#a4432d7965a5bb06fc2b4eeae71317344c9a756a" @@ -2279,6 +2323,20 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +libp2p-crypto@^0.21.0: + version "0.21.2" + resolved "https://registry.yarnpkg.com/libp2p-crypto/-/libp2p-crypto-0.21.2.tgz#7f9875436f24ca3887b077210b217b702bd72916" + integrity sha512-EXFrhSpiHtJ+/L8xXDvQNK5VjUMG51u878jzZcaT5XhuN/zFg6PWJFnl/qB2Y2j7eMWnvCRP7Kp+ua2H36cG4g== + dependencies: + "@noble/ed25519" "^1.5.1" + "@noble/secp256k1" "^1.3.0" + err-code "^3.0.1" + iso-random-stream "^2.0.0" + multiformats "^9.4.5" + node-forge "^1.2.1" + protobufjs "^6.11.2" + uint8arrays "^3.0.0" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -2291,46 +2349,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -2363,6 +2386,11 @@ logion-api@^0.1.1: moment "^2.29.1" uuid "^8.3.2" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -2447,9 +2475,9 @@ minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^1.0.4: version "1.0.4" @@ -2539,6 +2567,11 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multiformats@^9.4.2, multiformats@^9.4.5: + version "9.6.4" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.4.tgz#5dce1f11a407dbb69aa612cb7e5076069bb759ca" + integrity sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg== + mz@^2.4.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -2592,6 +2625,11 @@ node-fetch@2.6.7, node-fetch@3.1.1, node-fetch@^2.6.1, node-fetch@^2.6.7, node-f dependencies: whatwg-url "^5.0.0" +node-forge@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -2848,6 +2886,17 @@ pause-stream@0.0.11: dependencies: through "~2.3" +peer-id@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/peer-id/-/peer-id-0.16.0.tgz#0913062cfa4378707fe69c949b5720b3efadbf32" + integrity sha512-EmL7FurFUduU9m1PS9cfJ5TAuCvxKQ7DKpfx3Yj6IKWyBRtosriFuOag/l3ni/dtPgPLwiA4R9IvpL7hsDLJuQ== + dependencies: + class-is "^1.1.0" + libp2p-crypto "^0.21.0" + multiformats "^9.4.5" + protobufjs "^6.10.2" + uint8arrays "^3.0.0" + pg-connection-string@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" @@ -2957,6 +3006,25 @@ propagate@^2.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== +protobufjs@^6.10.2, protobufjs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -3228,11 +3296,6 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -3763,6 +3826,13 @@ typescript@^4.5.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +uint8arrays@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"