Skip to content

Commit 923ff4b

Browse files
authored
registration: add function to re-request email token (#2357)
1 parent 49dd76b commit 923ff4b

File tree

2 files changed

+137
-19
lines changed

2 files changed

+137
-19
lines changed

spec/unit/interactive-auth.spec.js

+105
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ limitations under the License.
1818
import { logger } from "../../src/logger";
1919
import { InteractiveAuth } from "../../src/interactive-auth";
2020
import { MatrixError } from "../../src/http-api";
21+
import { sleep } from "../../src/utils";
22+
import { randomString } from "../../src/randomstring";
2123

2224
// Trivial client object to test interactive auth
2325
// (we do not need TestClient here)
@@ -172,4 +174,107 @@ describe("InteractiveAuth", function() {
172174
expect(error.message).toBe('No appropriate authentication flow found');
173175
});
174176
});
177+
178+
describe("requestEmailToken", () => {
179+
it("increases auth attempts", async () => {
180+
const doRequest = jest.fn();
181+
const stateUpdated = jest.fn();
182+
const requestEmailToken = jest.fn();
183+
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
184+
185+
const ia = new InteractiveAuth({
186+
matrixClient: new FakeClient(),
187+
doRequest, stateUpdated, requestEmailToken,
188+
});
189+
190+
await ia.requestEmailToken();
191+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
192+
requestEmailToken.mockClear();
193+
await ia.requestEmailToken();
194+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
195+
requestEmailToken.mockClear();
196+
await ia.requestEmailToken();
197+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
198+
requestEmailToken.mockClear();
199+
await ia.requestEmailToken();
200+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
201+
requestEmailToken.mockClear();
202+
await ia.requestEmailToken();
203+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
204+
});
205+
206+
it("increases auth attempts", async () => {
207+
const doRequest = jest.fn();
208+
const stateUpdated = jest.fn();
209+
const requestEmailToken = jest.fn();
210+
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
211+
212+
const ia = new InteractiveAuth({
213+
matrixClient: new FakeClient(),
214+
doRequest, stateUpdated, requestEmailToken,
215+
});
216+
217+
await ia.requestEmailToken();
218+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
219+
requestEmailToken.mockClear();
220+
await ia.requestEmailToken();
221+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
222+
requestEmailToken.mockClear();
223+
await ia.requestEmailToken();
224+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
225+
requestEmailToken.mockClear();
226+
await ia.requestEmailToken();
227+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
228+
requestEmailToken.mockClear();
229+
await ia.requestEmailToken();
230+
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
231+
});
232+
233+
it("passes errors through", async () => {
234+
const doRequest = jest.fn();
235+
const stateUpdated = jest.fn();
236+
const requestEmailToken = jest.fn();
237+
requestEmailToken.mockImplementation(async () => {
238+
throw new Error("unspecific network error");
239+
});
240+
241+
const ia = new InteractiveAuth({
242+
matrixClient: new FakeClient(),
243+
doRequest, stateUpdated, requestEmailToken,
244+
});
245+
246+
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
247+
});
248+
249+
it("only starts one request at a time", async () => {
250+
const doRequest = jest.fn();
251+
const stateUpdated = jest.fn();
252+
const requestEmailToken = jest.fn();
253+
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
254+
255+
const ia = new InteractiveAuth({
256+
matrixClient: new FakeClient(),
257+
doRequest, stateUpdated, requestEmailToken,
258+
});
259+
260+
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
261+
expect(requestEmailToken).toHaveBeenCalledTimes(1);
262+
});
263+
264+
it("stores result in email sid", async () => {
265+
const doRequest = jest.fn();
266+
const stateUpdated = jest.fn();
267+
const requestEmailToken = jest.fn();
268+
const sid = randomString(24);
269+
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
270+
271+
const ia = new InteractiveAuth({
272+
matrixClient: new FakeClient(),
273+
doRequest, stateUpdated, requestEmailToken,
274+
});
275+
276+
await ia.requestEmailToken();
277+
expect(ia.getEmailSid()).toEqual(sid);
278+
});
279+
});
175280
});

src/interactive-auth.ts

+32-19
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ export class InteractiveAuth {
203203
private chosenFlow: IFlow = null;
204204
private currentStage: string = null;
205205

206+
private emailAttempt = 1;
207+
206208
// if we are currently trying to submit an auth dict (which includes polling)
207209
// the promise the will resolve/reject when it completes
208210
private submitPromise: Promise<void> = null;
@@ -408,6 +410,34 @@ export class InteractiveAuth {
408410
this.emailSid = sid;
409411
}
410412

413+
/**
414+
* Requests a new email token and sets the email sid for the validation session
415+
*/
416+
public requestEmailToken = async () => {
417+
if (!this.requestingEmailToken) {
418+
logger.trace("Requesting email token. Attempt: " + this.emailAttempt);
419+
// If we've picked a flow with email auth, we send the email
420+
// now because we want the request to fail as soon as possible
421+
// if the email address is not valid (ie. already taken or not
422+
// registered, depending on what the operation is).
423+
this.requestingEmailToken = true;
424+
try {
425+
const requestTokenResult = await this.requestEmailTokenCallback(
426+
this.inputs.emailAddress,
427+
this.clientSecret,
428+
this.emailAttempt++,
429+
this.data.session,
430+
);
431+
this.emailSid = requestTokenResult.sid;
432+
logger.trace("Email token request succeeded");
433+
} finally {
434+
this.requestingEmailToken = false;
435+
}
436+
} else {
437+
logger.warn("Could not request email token: Already requesting");
438+
}
439+
};
440+
411441
/**
412442
* Fire off a request, and either resolve the promise, or call
413443
* startAuthStage.
@@ -458,24 +488,9 @@ export class InteractiveAuth {
458488
return;
459489
}
460490

461-
if (
462-
!this.emailSid &&
463-
!this.requestingEmailToken &&
464-
this.chosenFlow.stages.includes(AuthType.Email)
465-
) {
466-
// If we've picked a flow with email auth, we send the email
467-
// now because we want the request to fail as soon as possible
468-
// if the email address is not valid (ie. already taken or not
469-
// registered, depending on what the operation is).
470-
this.requestingEmailToken = true;
491+
if (!this.emailSid && this.chosenFlow.stages.includes(AuthType.Email)) {
471492
try {
472-
const requestTokenResult = await this.requestEmailTokenCallback(
473-
this.inputs.emailAddress,
474-
this.clientSecret,
475-
1, // TODO: Multiple send attempts?
476-
this.data.session,
477-
);
478-
this.emailSid = requestTokenResult.sid;
493+
await this.requestEmailToken();
479494
// NB. promise is not resolved here - at some point, doRequest
480495
// will be called again and if the user has jumped through all
481496
// the hoops correctly, auth will be complete and the request
@@ -491,8 +506,6 @@ export class InteractiveAuth {
491506
// send the email, for whatever reason.
492507
this.attemptAuthDeferred.reject(e);
493508
this.attemptAuthDeferred = null;
494-
} finally {
495-
this.requestingEmailToken = false;
496509
}
497510
}
498511
}

0 commit comments

Comments
 (0)