fix(mfa): race page polling with Gateway for push-based MFA types
Made-with: Cursor
This commit is contained in:
parent
bcf099dec2
commit
cb337ec377
1 changed files with 33 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue