From 17207350354e73b9ad097659b6ce176118041555 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 16 Feb 2026 14:58:17 +0100
Subject: [PATCH] fix: use exact Teams inline modal selectors (emailInput,
authLoginDialogNextButton) for hybrid auth flow
Co-authored-by: Cursor
---
src/bot/authProcedure.ts | 226 ++++++++++++++++-----------------------
1 file changed, 95 insertions(+), 131 deletions(-)
diff --git a/src/bot/authProcedure.ts b/src/bot/authProcedure.ts
index 713c135..f699e3f 100644
--- a/src/bot/authProcedure.ts
+++ b/src/bot/authProcedure.ts
@@ -3,17 +3,21 @@ import { Logger } from 'winston';
/**
* AuthProcedure - Handles Microsoft account authentication in the browser.
- *
- * Supports TWO login flows:
- *
- * 1. **Inline modal flow** (when clicking "Sign in" on Teams pre-join page):
- * - An inline modal appears on the same Teams page (no URL change)
- * - Email input → Next → Password input → Sign in → Stay signed in? → Yes
- * - Then redirect chain to teams.microsoft.com/v2/ with "Join now"
- *
- * 2. **Direct navigation flow** (standalone login):
- * - Navigate to login.microsoftonline.com
- * - Standard Microsoft login form
+ *
+ * The login flow when clicking "Sign in" on the Teams pre-join page is a HYBRID:
+ *
+ * 1. Teams shows an INLINE MODAL on the light-meetings page (no URL change):
+ * - Email input: input#emailOrPhonerm (type="text", data-testid="emailInput")
+ * - Next button: button[data-testid="authLoginDialogNextButton"] (starts disabled!)
+ *
+ * 2. After clicking Next, Teams REDIRECTS to login.microsoftonline.com:
+ * - Password input: input#i0118 (name="passwd", type="password")
+ * - Sign in button: input#idSIButton9 (type="submit", value="Sign in")
+ *
+ * 3. "Stay signed in?" prompt on login.microsoftonline.com:
+ * - Yes button: input#idSIButton9 (value="Yes")
+ *
+ * 4. Redirect chain back to teams.microsoft.com/v2/ with "Join now" button.
*/
const _LOGIN_URL = 'https://login.microsoftonline.com';
@@ -27,13 +31,6 @@ export class AuthProcedure {
this._logger = logger;
}
- /**
- * Authenticate with Microsoft using email + password.
- *
- * @param skipNavigation - When true, assumes the login form is already loading
- * (e.g. after clicking "Sign in" on Teams pre-join page). Does NOT navigate.
- * @returns true if authentication was successful, false otherwise
- */
async authenticateWithMicrosoft(email: string, password: string, skipNavigation = false): Promise {
try {
this._logger.info(`Authenticating as ${email}...`);
@@ -45,71 +42,59 @@ export class AuthProcedure {
timeout: 30000,
});
} else {
- this._logger.info('Inline auth flow: waiting for login form to appear on current page...');
+ this._logger.info('Inline auth flow: waiting for Teams login modal...');
}
- // Step 1: Wait for email input field.
- // Works for both the Teams inline modal and the Microsoft login portal.
- const emailInput = await this._waitForEmailInput();
+ // Step 1: Wait for email input
+ const emailInput = await this._waitForEmailInput(skipNavigation);
if (!emailInput) {
return false;
}
- // Step 2: Enter email and click Next
+ // Step 2: Enter email
await emailInput.fill(email);
this._logger.info('Email entered');
- const nextClicked = await this._clickNextButton();
- if (!nextClicked) {
- this._logger.warn('Could not find Next button, pressing Enter');
- await this._page.keyboard.press('Enter');
- }
+ // Step 3: Click Next
+ await this._clickNextButton(skipNavigation);
- // Step 3: Wait for password input
+ // Step 4: Wait for password input (on login.microsoftonline.com after redirect)
const passwordInput = await this._waitForPasswordInput();
if (!passwordInput) {
return false;
}
- // Step 4: Enter password and click Sign in
+ // Step 5: Enter password and click Sign in
await passwordInput.fill(password);
this._logger.info('Password entered');
-
- const signInClicked = await this._clickSignInButton();
- if (!signInClicked) {
- this._logger.warn('Could not find Sign in button, pressing Enter');
- await this._page.keyboard.press('Enter');
- }
+ await this._clickSignInButton();
await this._page.waitForTimeout(3000);
- // Step 5: Check for MFA
- const mfaDetected = await this._detectMfa();
- if (mfaDetected) {
+ // Step 6: Check for MFA
+ if (await this._detectMfa()) {
this._logger.error('MFA prompt detected - cannot authenticate automatically.');
return false;
}
- // Step 6: Check for password error
- const pwdError = await this._page.$('#passwordError, [data-tid="error"]');
+ // Step 7: Check for password error
+ const pwdError = await this._page.$('#passwordError');
if (pwdError) {
const errorText = await pwdError.textContent();
this._logger.error(`Login error after password: ${errorText}`);
return false;
}
- // Step 7: Handle "Stay signed in?" prompt
+ // Step 8: Handle "Stay signed in?" prompt
await this._handleStaySignedIn();
- // Auth succeeded - the redirect chain will be handled by the caller
this._logger.info(`Successfully authenticated as ${email}`);
return true;
} catch (error) {
const errorMessage = String(error);
- // "Execution context was destroyed" means page navigated (login redirect)
if (errorMessage.includes('Execution context was destroyed') ||
errorMessage.includes('execution context')) {
- this._logger.info('Page navigated during auth (execution context destroyed) - treating as success');
+ this._logger.info('Page navigated during auth (context destroyed) - treating as success');
return true;
}
this._logger.error(`Authentication failed: ${error}`);
@@ -117,62 +102,48 @@ export class AuthProcedure {
}
}
- /**
- * Wait for an email/username input field to appear.
- * Uses broad selectors that work on both the Teams inline modal
- * and the Microsoft login portal.
- */
- private async _waitForEmailInput() {
- const selectors = [
- 'input[type="email"]',
- 'input[name="loginfmt"]',
- 'input[name="login"]',
- 'input[name="email"]',
- 'input[autocomplete="username"]',
- ];
- const combinedSelector = selectors.join(', ');
-
+ private async _waitForEmailInput(isInlineModal: boolean) {
this._logger.info('Waiting for email input field...');
- try {
- const element = await this._page.waitForSelector(combinedSelector, {
- timeout: 30000,
- state: 'visible',
- });
- const url = this._page.url();
- const matchedTag = await element?.evaluate(el => `${el.tagName}[type=${el.getAttribute('type')}, name=${el.getAttribute('name')}]`);
- this._logger.info(`Found email input: ${matchedTag} on URL: ${url.substring(0, 80)}`);
- return element;
- } catch {
- // Fallback: look for any visible text input that could be an email field
- this._logger.info('Primary email selectors failed, trying fallback (any visible text input)...');
+
+ if (isInlineModal) {
try {
- const fallback = await this._page.waitForSelector(
- 'input[type="text"]:visible, input:not([type]):visible',
- { timeout: 10000, state: 'visible' }
+ const element = await this._page.waitForSelector(
+ 'input[data-testid="emailInput"], input#emailOrPhonerm',
+ { timeout: 20000, state: 'visible' }
);
- if (fallback) {
- const url = this._page.url();
- this._logger.info(`Found fallback text input on URL: ${url.substring(0, 80)}`);
- return fallback;
+ if (element) {
+ this._logger.info('Found Teams inline modal email input (data-testid="emailInput")');
+ return element;
}
} catch {
- // Log page state for debugging
- await this._logPageState('email input not found');
+ this._logger.warn('Teams inline modal email input not found');
+ await this._logPageState('inline modal email input not found');
}
}
+
+ try {
+ const element = await this._page.waitForSelector(
+ 'input[name="loginfmt"], input[type="email"]',
+ { timeout: 15000, state: 'visible' }
+ );
+ if (element) {
+ this._logger.info('Found MS login portal email input');
+ return element;
+ }
+ } catch {
+ await this._logPageState('email input not found (all selectors failed)');
+ }
+
this._logger.error('Could not find email input field');
return null;
}
- /**
- * Wait for the password input field to appear.
- */
private async _waitForPasswordInput() {
this._logger.info('Waiting for password input field...');
try {
const element = await this._page.waitForSelector(
- 'input[type="password"], input[name="passwd"], input[name="password"]',
- { timeout: 15000, state: 'visible' }
+ 'input[name="passwd"], input#i0118, input[type="password"]',
+ { timeout: 20000, state: 'visible' }
);
const url = this._page.url();
this._logger.info(`Found password input on URL: ${url.substring(0, 80)}`);
@@ -184,18 +155,28 @@ export class AuthProcedure {
return null;
}
- /**
- * Click the "Next" button after entering email.
- * Works on both Teams inline modal and Microsoft login portal.
- */
- private async _clickNextButton(): Promise {
+ private async _clickNextButton(isInlineModal: boolean): Promise {
+ if (isInlineModal) {
+ const teamsNextSelector = 'button[data-testid="authLoginDialogNextButton"]';
+ try {
+ await this._page.waitForSelector(
+ `${teamsNextSelector}:not([disabled])`,
+ { timeout: 10000, state: 'visible' }
+ );
+ await this._page.click(teamsNextSelector);
+ this._logger.info('Clicked Teams inline modal Next button');
+ await this._page.waitForTimeout(3000);
+ return;
+ } catch {
+ this._logger.warn('Teams inline modal Next button not found/enabled, trying fallbacks...');
+ }
+ }
+
const selectors = [
'input[type="submit"]',
'button[type="submit"]',
'button:has-text("Next")',
'button:has-text("Weiter")',
- 'input[value="Next"]',
- 'input[value="Weiter"]',
];
for (const selector of selectors) {
@@ -205,27 +186,25 @@ export class AuthProcedure {
await button.click();
this._logger.info(`Clicked Next: ${selector}`);
await this._page.waitForTimeout(2000);
- return true;
+ return;
}
} catch {
// Continue
}
}
- return false;
+
+ this._logger.warn('No Next button found, pressing Enter');
+ await this._page.keyboard.press('Enter');
+ await this._page.waitForTimeout(2000);
}
- /**
- * Click the "Sign in" button after entering password.
- * Works on both Teams inline modal and Microsoft login portal.
- */
- private async _clickSignInButton(): Promise {
+ private async _clickSignInButton(): Promise {
const selectors = [
+ 'input#idSIButton9',
'input[type="submit"]',
'button[type="submit"]',
'button:has-text("Sign in")',
'button:has-text("Anmelden")',
- 'input[value="Sign in"]',
- 'input[value="Anmelden"]',
];
for (const selector of selectors) {
@@ -234,18 +213,17 @@ export class AuthProcedure {
if (button && await button.isVisible()) {
await button.click();
this._logger.info(`Clicked Sign in: ${selector}`);
- return true;
+ return;
}
} catch {
// Continue
}
}
- return false;
+
+ this._logger.warn('No Sign in button found, pressing Enter');
+ await this._page.keyboard.press('Enter');
}
- /**
- * Detect MFA prompts (authenticator app, SMS, phone call).
- */
private async _detectMfa(): Promise {
const mfaSelectors = [
'#idDiv_SAOTCAS_Description',
@@ -255,28 +233,21 @@ export class AuthProcedure {
'text=Approve sign in request',
'text=Enter code',
'text=Verify your identity',
- 'text=Approve sign-in request',
];
for (const selector of mfaSelectors) {
try {
- const element = await this._page.$(selector);
- if (element) {
- return true;
- }
+ if (await this._page.$(selector)) return true;
} catch {
- // Continue checking
+ // Continue
}
}
return false;
}
- /**
- * Handle the "Stay signed in?" prompt after successful login.
- */
private async _handleStaySignedIn(): Promise {
try {
- const staySignedInSelectors = [
+ const selectors = [
'input#idSIButton9',
'button#idSIButton9',
'input[value="Yes"]',
@@ -285,7 +256,7 @@ export class AuthProcedure {
'button:has-text("Ja")',
];
- for (const selector of staySignedInSelectors) {
+ for (const selector of selectors) {
try {
const button = await this._page.$(selector);
if (button) {
@@ -298,32 +269,25 @@ export class AuthProcedure {
// Continue
}
}
-
this._logger.debug('No "Stay signed in" prompt detected');
} catch (error) {
this._logger.debug(`Stay signed in handling: ${error}`);
}
}
- /**
- * Log current page state for debugging when a selector is not found.
- */
private async _logPageState(context: string): Promise {
try {
const url = this._page.url();
const title = await this._page.title();
- const bodyText = await this._page.evaluate(() =>
- document.body?.innerText?.substring(0, 300) || '(empty)'
- );
const inputs = await this._page.evaluate(() => {
const allInputs = document.querySelectorAll('input');
- return Array.from(allInputs).map(i =>
- `${i.tagName}[type=${i.type}, name=${i.name}, placeholder=${i.placeholder}]`
- ).join(', ');
+ return Array.from(allInputs).map(i => {
+ const tid = i.getAttribute('data-testid') || '';
+ return `[type=${i.type},name=${i.name},id=${i.id},ph=${i.placeholder},tid=${tid}]`;
+ }).join(', ');
});
- this._logger.warn(`Page state (${context}): URL=${url.substring(0, 80)}, Title=${title}`);
- this._logger.warn(`Inputs on page: ${inputs.substring(0, 300)}`);
- this._logger.warn(`Page text: ${bodyText.substring(0, 200)}`);
+ this._logger.warn(`Page state (${context}): URL=${url.substring(0, 100)}, Title=${title}`);
+ this._logger.warn(`Inputs: ${inputs.substring(0, 400)}`);
} catch (err) {
this._logger.warn(`Could not log page state: ${err}`);
}