From 58a0a7e65799fbc00151a7e676a5cb7ac1e22435 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 10 May 2022 17:44:30 +0200 Subject: [PATCH 1/2] registration: add function to re-request email token --- src/interactive-auth.ts | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index f06369fe736..2732023d44c 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -203,6 +203,8 @@ export class InteractiveAuth { private chosenFlow: IFlow = null; private currentStage: string = null; + private emailAttempt = 1; + // if we are currently trying to submit an auth dict (which includes polling) // the promise the will resolve/reject when it completes private submitPromise: Promise = null; @@ -408,6 +410,34 @@ export class InteractiveAuth { this.emailSid = sid; } + /** + * Requests a new email token and sets the email sid for the validation session + */ + public requestEmailToken = async () => { + if (!this.requestingEmailToken) { + logger.trace("Requesting email token. Attempt: " + this.emailAttempt); + // If we've picked a flow with email auth, we send the email + // now because we want the request to fail as soon as possible + // if the email address is not valid (ie. already taken or not + // registered, depending on what the operation is). + this.requestingEmailToken = true; + try { + const requestTokenResult = await this.requestEmailTokenCallback( + this.inputs.emailAddress, + this.clientSecret, + this.emailAttempt++, + this.data.session, + ); + this.emailSid = requestTokenResult.sid; + logger.trace("Email token request succeeded"); + } finally { + this.requestingEmailToken = false; + } + } else { + logger.warn("Could not request email token: Already requesting"); + } + }; + /** * Fire off a request, and either resolve the promise, or call * startAuthStage. @@ -458,24 +488,9 @@ export class InteractiveAuth { return; } - if ( - !this.emailSid && - !this.requestingEmailToken && - this.chosenFlow.stages.includes(AuthType.Email) - ) { - // If we've picked a flow with email auth, we send the email - // now because we want the request to fail as soon as possible - // if the email address is not valid (ie. already taken or not - // registered, depending on what the operation is). - this.requestingEmailToken = true; + if (!this.emailSid && this.chosenFlow.stages.includes(AuthType.Email)) { try { - const requestTokenResult = await this.requestEmailTokenCallback( - this.inputs.emailAddress, - this.clientSecret, - 1, // TODO: Multiple send attempts? - this.data.session, - ); - this.emailSid = requestTokenResult.sid; + await this.requestEmailToken(); // NB. promise is not resolved here - at some point, doRequest // will be called again and if the user has jumped through all // the hoops correctly, auth will be complete and the request @@ -491,8 +506,6 @@ export class InteractiveAuth { // send the email, for whatever reason. this.attemptAuthDeferred.reject(e); this.attemptAuthDeferred = null; - } finally { - this.requestingEmailToken = false; } } } From 8e1f2f35ee067916a93ff6a35d764337dccf16bf Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 11 May 2022 13:07:38 +0200 Subject: [PATCH 2/2] registration: add tests for re-request email token function --- spec/unit/interactive-auth.spec.js | 105 +++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/spec/unit/interactive-auth.spec.js b/spec/unit/interactive-auth.spec.js index da2bf1917cb..6742d05909b 100644 --- a/spec/unit/interactive-auth.spec.js +++ b/spec/unit/interactive-auth.spec.js @@ -18,6 +18,8 @@ limitations under the License. import { logger } from "../../src/logger"; import { InteractiveAuth } from "../../src/interactive-auth"; import { MatrixError } from "../../src/http-api"; +import { sleep } from "../../src/utils"; +import { randomString } from "../../src/randomstring"; // Trivial client object to test interactive auth // (we do not need TestClient here) @@ -172,4 +174,107 @@ describe("InteractiveAuth", function() { expect(error.message).toBe('No appropriate authentication flow found'); }); }); + + describe("requestEmailToken", () => { + it("increases auth attempts", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + const requestEmailToken = jest.fn(); + requestEmailToken.mockImplementation(async () => ({ sid: "" })); + + const ia = new InteractiveAuth({ + matrixClient: new FakeClient(), + doRequest, stateUpdated, requestEmailToken, + }); + + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined); + }); + + it("increases auth attempts", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + const requestEmailToken = jest.fn(); + requestEmailToken.mockImplementation(async () => ({ sid: "" })); + + const ia = new InteractiveAuth({ + matrixClient: new FakeClient(), + doRequest, stateUpdated, requestEmailToken, + }); + + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined); + requestEmailToken.mockClear(); + await ia.requestEmailToken(); + expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined); + }); + + it("passes errors through", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + const requestEmailToken = jest.fn(); + requestEmailToken.mockImplementation(async () => { + throw new Error("unspecific network error"); + }); + + const ia = new InteractiveAuth({ + matrixClient: new FakeClient(), + doRequest, stateUpdated, requestEmailToken, + }); + + expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error"); + }); + + it("only starts one request at a time", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + const requestEmailToken = jest.fn(); + requestEmailToken.mockImplementation(() => sleep(500, { sid: "" })); + + const ia = new InteractiveAuth({ + matrixClient: new FakeClient(), + doRequest, stateUpdated, requestEmailToken, + }); + + await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]); + expect(requestEmailToken).toHaveBeenCalledTimes(1); + }); + + it("stores result in email sid", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + const requestEmailToken = jest.fn(); + const sid = randomString(24); + requestEmailToken.mockImplementation(() => sleep(500, { sid })); + + const ia = new InteractiveAuth({ + matrixClient: new FakeClient(), + doRequest, stateUpdated, requestEmailToken, + }); + + await ia.requestEmailToken(); + expect(ia.getEmailSid()).toEqual(sid); + }); + }); });