diff --git a/package.json b/package.json index eae1f3d..af91e4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@logion/rest-api-core", - "version": "0.5.2", + "version": "0.6.0-1", "repository": { "type": "git", "url": "git+https://github.com/logion-network/logion-rest-api-core.git" @@ -26,7 +26,7 @@ "coverage": "nyc yarn run test" }, "dependencies": { - "@logion/authenticator": "^0.6.2", + "@logion/authenticator": "^0.7.0-1", "dinoloop": "^2.4.0", "express": "^4.18.2", "express-fileupload": "^1.4.0", @@ -39,7 +39,7 @@ }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@logion/node-api": "^0.31.0", + "@logion/node-api": "^0.31.2", "@tsconfig/node16": "^16.1.1", "@types/express": "^4.17.14", "@types/express-fileupload": "^1.4.1", diff --git a/src/AuthenticationController.ts b/src/AuthenticationController.ts index c3da6e7..40f2c54 100644 --- a/src/AuthenticationController.ts +++ b/src/AuthenticationController.ts @@ -33,6 +33,7 @@ export function fillInSpecForAuthenticationController(spec: OpenAPIV3.Document): AuthenticationController.signIn(spec); AuthenticationController.authenticate(spec); + AuthenticationController.authenticateV2(spec); } @injectable() @@ -114,6 +115,34 @@ export class AuthenticationController extends ApiController { return this.toAuthenticateResponseView(tokens); } + static authenticateV2(spec: OpenAPIV3.Document) { + const operationObject = requireDefined(spec.paths["/api/auth/{sessionId}/authenticate/v2"].post); + operationObject.summary = "Authenticate the given session"; + operationObject.description = "

The signature's resource is authentication, the operation login and the additional field is sessionId

"; + operationObject.requestBody = getRequestBody({ + description: "Authentication data", + view: "AuthenticateRequestView", + }); + operationObject.responses = getDefaultResponses("AuthenticateResponseView"); + setPathParameters(operationObject, { 'sessionId': "The ID of the session to authenticate" }); + } + + @HttpPost('/:sessionId/authenticate/v2') + @Async() + async authenticateV2( + authenticateRequest: AuthenticateRequestView, + sessionId: string + ): Promise { + + const { session, signatures } = await this.sessionAndSignatures(authenticateRequest, sessionId); + const { sessionManager, authenticator } = await this.authenticationService.authenticationSystem(); + + const signedSession = await sessionManager.signedSessionOrThrowV2(session, signatures); + const tokens = await authenticator.createTokens(signedSession, DateTime.now()); + + return this.toAuthenticateResponseView(tokens); + } + private async sessionAndSignatures( authenticateRequest: AuthenticateRequestView, sessionId: string diff --git a/test/AuthenticationController.spec.ts b/test/AuthenticationController.spec.ts index f9390f9..dcdb2aa 100644 --- a/test/AuthenticationController.spec.ts +++ b/test/AuthenticationController.spec.ts @@ -36,7 +36,7 @@ const TOKEN_ALICE = "some-fake-token-for-ALICE"; const TOKEN_BOB = "some-fake-token-for-BOB"; const SESSION_ID = "a4dade1d-f12c-414c-93f7-7f20ce1e2cb8"; -describe("AuthenticationController", () => { +describe("AuthenticationController (sign-in)", () => { it('should sign-in successfully', async () => { const app = setupApp(AuthenticationController, mockDependenciesForSignIn); @@ -55,6 +55,12 @@ describe("AuthenticationController", () => { }); }); +}) + +describe("AuthenticationController (authentication)", () => { + + const version = "V1"; + it('should authenticate successfully', async () => { const authenticateRequest: AuthenticateRequestView = { @@ -70,7 +76,7 @@ describe("AuthenticationController", () => { signedOn: TIMESTAMP, type: "POLKADOT", }; - const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container,true, true)); + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, true, true, version)); await request(app) .post(`/api/auth/${SESSION_ID}/authenticate`) .send(authenticateRequest) @@ -98,7 +104,7 @@ describe("AuthenticationController", () => { signedOn: TIMESTAMP, type: "POLKADOT", }; - const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container,false, true)); + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, false, true, version)); await request(app) .post(`/api/auth/${SESSION_ID}/authenticate`) .send(authenticateRequest) @@ -124,7 +130,7 @@ describe("AuthenticationController", () => { signedOn: TIMESTAMP, type: "POLKADOT", }; - const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container,true, false)); + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, true, false, version)); await request(app) .post(`/api/auth/${SESSION_ID}/authenticate`) .send(authenticateRequest) @@ -150,7 +156,7 @@ describe("AuthenticationController", () => { signedOn: TIMESTAMP, type: "POLKADOT", }; - const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container,false, true)); + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, false, true, version)); await request(app) .post(`/api/auth/${SESSION_ID}/authenticate`) .send(authenticateRequest) @@ -162,6 +168,119 @@ describe("AuthenticationController", () => { }) +}); + +describe("AuthenticationController (authentication v2)", () => { + + const version = "V2"; + + it('should authenticate successfully', async () => { + + const authenticateRequest: AuthenticateRequestView = { + signatures: {} + }; + authenticateRequest.signatures![ALICE.toKey()] = { + signature: "signature-ALICE", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + authenticateRequest.signatures![BOB.toKey()] = { + signature: "signature-BOB", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, true, true, version)); + await request(app) + .post(`/api/auth/${SESSION_ID}/authenticate/v2`) + .send(authenticateRequest) + .expect(200) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.tokens).toBeDefined(); + expect(response.body.tokens[ALICE.toKey()].value).toBe(TOKEN_ALICE); + expect(response.body.tokens[BOB.toKey()].value).toBe(TOKEN_BOB); + }); + }) + + it('should fail to authenticate on wrong signature', async () => { + + const authenticateRequest: AuthenticateRequestView = { + signatures: {} + }; + authenticateRequest.signatures![ALICE.toKey()] = { + signature: "signature-ALICE", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + authenticateRequest.signatures![BOB.toKey()] = { + signature: "signature-BOB", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, false, true, version)); + await request(app) + .post(`/api/auth/${SESSION_ID}/authenticate/v2`) + .send(authenticateRequest) + .expect(401) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.error).toBe("Invalid signature"); + }); + }) + + it('should fail to authenticate on missing session', async () => { + + const authenticateRequest: AuthenticateRequestView = { + signatures: {} + }; + authenticateRequest.signatures![ALICE.toKey()] = { + signature: "signature-ALICE", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + authenticateRequest.signatures![BOB.toKey()] = { + signature: "signature-BOB", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, true, false, version)); + await request(app) + .post(`/api/auth/${SESSION_ID}/authenticate/v2`) + .send(authenticateRequest) + .expect(401) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.error).toBe("Invalid session"); + }); + }) + + it('should fail to authenticate on wrong address type', async () => { + + const authenticateRequest: AuthenticateRequestView = { + signatures: {} + }; + authenticateRequest.signatures![`Unknown:${ ALICE.address }`] = { + signature: "signature-ALICE", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + authenticateRequest.signatures![`Unknown:${ BOB.address }`] = { + signature: "signature-BOB", + signedOn: TIMESTAMP, + type: "POLKADOT", + }; + const app = setupApp(AuthenticationController, (container) => mockDependenciesForAuth(container, false, true, version)); + await request(app) + .post(`/api/auth/${SESSION_ID}/authenticate/v2`) + .send(authenticateRequest) + .expect(401) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.error).toBe("Error: Unsupported key format"); + }); + }) + + }); function mockDependenciesForSignIn(container: Container): void { @@ -191,7 +310,7 @@ function mockDependenciesForSignIn(container: Container): void { .returns(() => Promise.resolve()); } -function mockDependenciesForAuth(container: Container, verifies: boolean, sessionExists:boolean): void { +function mockDependenciesForAuth(container: Container, verifies: boolean, sessionExists:boolean, version: "V1" | "V2"): void { const sessionAlice = new Mock(); sessionAlice.setup(instance => instance.createdOn).returns(DateTime.now().toJSDate()); @@ -238,10 +357,17 @@ function mockDependenciesForAuth(container: Container, verifies: boolean, sessio type: "POLKADOT", } ]; - sessionManager.setup(instance => instance.signedSessionOrThrow(It.IsAny(), It.IsAny())).returnsAsync({ - session: session.object(), - signatures - }); + if (version === "V1") { + sessionManager.setup(instance => instance.signedSessionOrThrow(It.IsAny(), It.IsAny())).returnsAsync({ + session: session.object(), + signatures + }); + } else { + sessionManager.setup(instance => instance.signedSessionOrThrowV2(It.IsAny(), It.IsAny())).returnsAsync({ + session: session.object(), + signatures + }); + } const tokens: Token[] = [ { type: "Polkadot", @@ -260,8 +386,17 @@ function mockDependenciesForAuth(container: Container, verifies: boolean, sessio args => args.session === session.object() && args.signatures === signatures ), It.IsAny())).returnsAsync(tokens); } else { - sessionManager.setup(instance => instance.signedSessionOrThrow) - .returns(() => { throw new UnauthorizedException({error: "Invalid signature"}) }); + if (version === "V1") { + sessionManager.setup(instance => instance.signedSessionOrThrow) + .returns(() => { + throw new UnauthorizedException({ error: "Invalid signature" }) + }); + } else { + sessionManager.setup(instance => instance.signedSessionOrThrowV2) + .returns(() => { + throw new UnauthorizedException({ error: "Invalid signature" }) + }); + } } const sessionRepository = new Mock(); diff --git a/yarn.lock b/yarn.lock index 874607b..73fc797 100644 --- a/yarn.lock +++ b/yarn.lock @@ -564,9 +564,9 @@ __metadata: languageName: node linkType: hard -"@logion/authenticator@npm:^0.6.2": - version: 0.6.2 - resolution: "@logion/authenticator@npm:0.6.2" +"@logion/authenticator@npm:^0.7.0-1": + version: 0.7.0-1 + resolution: "@logion/authenticator@npm:0.7.0-1" dependencies: "@ethersproject/transactions": ^5.7.0 "@multiversx/sdk-core": ^12.19.1 @@ -578,13 +578,13 @@ __metadata: web3-utils: ^4.2.1 peerDependencies: "@logion/node-api": 0.x - checksum: 46a5fbe4b671255c78c5bf4ff6799f0903783f2c2457023c8862e8360d9e6736cfb3a2a707325bf33f2b47f9ac92a2bdcf80b48c1f4b0fb80659e9a8e8c9d32e + checksum: 004f7f91d75cd7fee80886fce7dc3de6ab9d2e853f3126abb163d67c313dfd91958b8c39a2a71f0d73c1fe6b9e42057394bd90216ceaac333ac75aa100f5cb63 languageName: node linkType: hard -"@logion/node-api@npm:^0.31.0": - version: 0.31.0 - resolution: "@logion/node-api@npm:0.31.0" +"@logion/node-api@npm:^0.31.2": + version: 0.31.2 + resolution: "@logion/node-api@npm:0.31.2" dependencies: "@polkadot/api": ^11.0.2 "@polkadot/util": ^12.6.2 @@ -593,7 +593,7 @@ __metadata: bech32: ^2.0.0 fast-sha256: ^1.3.0 uuid: ^9.0.0 - checksum: 6aae4f45477cefcf253354ac26788a72e705d953f9e7a62f1fae2b92bb1ce9331bc850a967a5c5e1e3103dae448134ae2e1fc710a7408cebf88286b71b4a0ea2 + checksum: 9d3d41d250db88e49e17da8c3a5dd65271ea54f9ee06c87ac465c344d0e2ec48a4de0c5975745f21d1b8684d682b858d893512eda7a7d1f6d1f81c2518101e2f languageName: node linkType: hard @@ -602,8 +602,8 @@ __metadata: resolution: "@logion/rest-api-core@workspace:." dependencies: "@istanbuljs/nyc-config-typescript": ^1.0.2 - "@logion/authenticator": ^0.6.2 - "@logion/node-api": ^0.31.0 + "@logion/authenticator": ^0.7.0-1 + "@logion/node-api": ^0.31.2 "@tsconfig/node16": ^16.1.1 "@types/express": ^4.17.14 "@types/express-fileupload": ^1.4.1