fix(mfa): race page polling with Gateway for push-based MFA types

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-03-01 10:57:08 +01:00
parent bcf099dec2
commit cb337ec377

View file

@ -335,7 +335,8 @@ export class AuthProcedure {
/**
* Handle MFA challenge: relay to Gateway (via callback) and wait for resolution.
* For code-based MFA: enter the code on the page.
* For push/number-match: wait for MS to redirect after user approves on phone.
* For push/number-match: race Gateway response with page polling since the user
* approves on their phone and MS redirects the page automatically.
*/
private async _handleMfaChallenge(challenge: MfaChallenge): Promise<boolean> {
if (!this._onMfaChallenge) {
@ -343,6 +344,33 @@ export class AuthProcedure {
return false;
}
const isPushBased = challenge.type === 'numberMatch' || challenge.type === 'pushApproval';
if (isPushBased) {
const abortSignal = { aborted: false };
const gatewayPromise = this._onMfaChallenge(challenge);
const pagePromise = this._waitForMfaResolution(abortSignal).then(resolved =>
({ action: resolved ? 'pageResolved' : 'timeout' } as { action: string; code?: string }),
);
const result = await Promise.race([gatewayPromise, pagePromise]);
abortSignal.aborted = true;
if (result.action === 'pageResolved') {
this._logger.info('MFA resolved via page change (user approved on phone)');
return true;
}
if (result.action === 'timeout') {
this._logger.warn('MFA timed out');
return false;
}
if (result.action === 'code' && result.code) {
return this._enterMfaCode(result.code);
}
return false;
}
// Code-based MFA: wait for Gateway response with the code
const response = await this._onMfaChallenge(challenge);
if (response.action === 'timeout') {
this._logger.warn('MFA timed out - no user response');
@ -356,7 +384,6 @@ export class AuthProcedure {
return this._enterMfaCode(response.code);
}
// For push/numberMatch: user confirms externally, MS redirects automatically
return this._waitForMfaResolution();
}
@ -406,8 +433,9 @@ export class AuthProcedure {
/**
* Wait for MFA resolution: MS login page redirects away after successful approval.
* Polls URL changes and checks for "Stay signed in?" or Teams redirect.
* @param abortSignal - optional signal to cancel polling early (used when racing with Gateway)
*/
private async _waitForMfaResolution(): Promise<boolean> {
private async _waitForMfaResolution(abortSignal?: { aborted: boolean }): Promise<boolean> {
const startUrl = this._page.url();
const maxWaitMs = 120000;
const pollMs = 2000;
@ -416,7 +444,9 @@ export class AuthProcedure {
this._logger.info('Waiting for MFA resolution...');
while (Date.now() < deadline) {
if (abortSignal?.aborted) return false;
await this._page.waitForTimeout(pollMs);
if (abortSignal?.aborted) return false;
const currentUrl = this._page.url();
// URL changed = MFA resolved