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.
|
* Handle MFA challenge: relay to Gateway (via callback) and wait for resolution.
|
||||||
* For code-based MFA: enter the code on the page.
|
* 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> {
|
private async _handleMfaChallenge(challenge: MfaChallenge): Promise<boolean> {
|
||||||
if (!this._onMfaChallenge) {
|
if (!this._onMfaChallenge) {
|
||||||
|
|
@ -343,6 +344,33 @@ export class AuthProcedure {
|
||||||
return false;
|
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);
|
const response = await this._onMfaChallenge(challenge);
|
||||||
if (response.action === 'timeout') {
|
if (response.action === 'timeout') {
|
||||||
this._logger.warn('MFA timed out - no user response');
|
this._logger.warn('MFA timed out - no user response');
|
||||||
|
|
@ -356,7 +384,6 @@ export class AuthProcedure {
|
||||||
return this._enterMfaCode(response.code);
|
return this._enterMfaCode(response.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For push/numberMatch: user confirms externally, MS redirects automatically
|
|
||||||
return this._waitForMfaResolution();
|
return this._waitForMfaResolution();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,8 +433,9 @@ export class AuthProcedure {
|
||||||
/**
|
/**
|
||||||
* Wait for MFA resolution: MS login page redirects away after successful approval.
|
* Wait for MFA resolution: MS login page redirects away after successful approval.
|
||||||
* Polls URL changes and checks for "Stay signed in?" or Teams redirect.
|
* 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 startUrl = this._page.url();
|
||||||
const maxWaitMs = 120000;
|
const maxWaitMs = 120000;
|
||||||
const pollMs = 2000;
|
const pollMs = 2000;
|
||||||
|
|
@ -416,7 +444,9 @@ export class AuthProcedure {
|
||||||
this._logger.info('Waiting for MFA resolution...');
|
this._logger.info('Waiting for MFA resolution...');
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
|
if (abortSignal?.aborted) return false;
|
||||||
await this._page.waitForTimeout(pollMs);
|
await this._page.waitForTimeout(pollMs);
|
||||||
|
if (abortSignal?.aborted) return false;
|
||||||
const currentUrl = this._page.url();
|
const currentUrl = this._page.url();
|
||||||
|
|
||||||
// URL changed = MFA resolved
|
// URL changed = MFA resolved
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue