fix: auth join via 'Sign in' link on pre-join page (correct Teams flow: launcher -> pre-join -> sign in -> auth pre-join -> join)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-16 11:55:01 +01:00
parent 1972f698b4
commit b07910410e

View file

@ -133,193 +133,104 @@ export class BotOrchestrator {
// Launch browser // Launch browser
await this._launchBrowser(); await this._launchBrowser();
// Authenticate with Microsoft if requested // Update JoinProcedure with correct auth state
this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate);
this._setState('navigating');
// STEP 1: Navigate to meeting URL and click "Continue on this browser"
// This is the same for both authenticated and anonymous joins.
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
// STEP 2: For authenticated joins, click "Sign in" on the pre-join page
// instead of entering a name. The "Sign in" link is at the bottom of the
// anonymous pre-join page. Clicking it triggers the Microsoft login flow,
// which redirects back to an authenticated pre-join page within Teams v2.
if (authenticate) { if (authenticate) {
this._logger.info('Authenticated join: looking for "Sign in" link on pre-join page...');
// Wait for the pre-join page to load
await this._page!.waitForTimeout(3000);
// Dismiss mic/camera permission overlay if present
// The "Continue without audio or video" modal may appear here
await this._joinProcedure.dismissBrowserPermissionModals();
// Find and click the "Sign in" link at the bottom of the pre-join page
const signInSelectors = [
'a:has-text("Sign in")',
'button:has-text("Sign in")',
'a:has-text("Anmelden")',
'button:has-text("Anmelden")',
'a[href*="login"]',
];
let signInClicked = false;
for (const sel of signInSelectors) {
try {
const link = await this._page!.$(sel);
if (link) {
await link.click();
this._logger.info(`Clicked "Sign in" link: ${sel}`);
signInClicked = true;
break;
}
} catch { /* continue */ }
}
if (!signInClicked) {
// Fallback: try to find "Sign in" by evaluating text content
signInClicked = await this._page!.evaluate(() => {
const links = document.querySelectorAll('a, button');
for (let i = 0; i < links.length; i++) {
const el = links[i] as HTMLElement;
const text = el.innerText?.trim().toLowerCase() || '';
if (text === 'sign in' || text === 'anmelden') {
el.click();
return true;
}
}
return false;
});
if (signInClicked) {
this._logger.info('Clicked "Sign in" via DOM evaluation');
}
}
if (signInClicked) {
// Wait for Microsoft login page to load
await this._page!.waitForTimeout(3000);
// Perform Microsoft login (email, password, stay signed in)
const { AuthProcedure } = await import('./authProcedure'); const { AuthProcedure } = await import('./authProcedure');
const authProcedure = new AuthProcedure(this._page!, this._logger); const authProcedure = new AuthProcedure(this._page!, this._logger);
const authSuccess = await authProcedure.authenticateWithMicrosoft( const authSuccess = await authProcedure.authenticateWithMicrosoft(
this._options.botAccountEmail!, this._options.botAccountEmail!,
this._options.botAccountPassword! this._options.botAccountPassword!
); );
if (!authSuccess) {
throw new Error('Microsoft authentication failed');
}
// CRITICAL: After auth, navigate to Teams web app first to establish if (authSuccess) {
// a Teams session. Without this, Teams redirects to anonymous mode this._logger.info('Authentication via "Sign in" link succeeded');
// when navigating directly to the meeting URL. // After auth, Teams redirects back to the authenticated pre-join page
this._logger.info('Establishing Teams session after auth...'); // within Teams v2 (/v2/) -- wait for it to load
try {
await this._page!.goto('https://teams.microsoft.com', {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
// Wait for Teams v2 to fully load and establish the auth session
// Teams v2 does multiple redirects (OAuth callback -> v2 app) which takes time
try {
await this._page!.waitForSelector('[data-tid="app-layout"], [data-tid="left-rail"], [class*="teams-"]', {
timeout: 15000
});
this._logger.info('Teams v2 app loaded (UI elements detected)');
} catch {
// Timeout waiting for UI elements, but auth session may still be established
await this._page!.waitForTimeout(5000); await this._page!.waitForTimeout(5000);
}
const teamsUrl = this._page!.url();
this._logger.info(`Teams session established at: ${teamsUrl.substring(0, 80)}...`);
} catch (teamsNavError) {
this._logger.warn(`Teams session establishment failed (non-fatal): ${teamsNavError}`);
}
}
// Update JoinProcedure with correct auth state const postAuthUrl = this._page!.url();
this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate); this._logger.info(`Post-auth URL: ${postAuthUrl.substring(0, 80)}`);
this._setState('navigating'); // Verify we're on the authenticated pre-join page
if (authenticate) {
// AUTHENTICATED JOIN: Use the Teams v2 "Join a meeting" form.
// The Teams v2 app (already loaded after auth) has a "Join a meeting" page
// with Meeting ID and Passcode fields. This keeps the user authenticated
// and avoids the external launcher which always redirects to anonymous mode.
this._logger.info(`Authenticated: joining meeting via Teams v2 "Join a meeting" form...`);
// Parse the meeting URL to extract meeting code and passcode
const { parseMeetingUrl } = await import('./meetingUrlParser');
const parsed = parseMeetingUrl(this._meetingUrl);
const meetingId = parsed.meetingId || '';
const passcode = parsed.passcode || '';
this._logger.info(`Meeting ID: ${meetingId}, Passcode: ${passcode ? '***' : '(none)'}`);
let joinedViaForm = false;
try {
// Navigate to the Teams v2 "Join a meeting" page
// This page is available within the authenticated Teams v2 app
await this._page!.goto('https://teams.microsoft.com/v2/', {
waitUntil: 'domcontentloaded',
timeout: 20000,
});
await this._page!.waitForTimeout(3000);
// Look for "Join a meeting" link/button in the Teams v2 sidebar or header
const joinMeetingSelectors = [
'button:has-text("Join a meeting")',
'a:has-text("Join a meeting")',
'button:has-text("Join with an ID")',
'a:has-text("Join with an ID")',
'[data-tid="join-meeting-button"]',
'button:has-text("An Besprechung teilnehmen")',
];
let foundJoinPage = false;
for (const sel of joinMeetingSelectors) {
try {
const btn = await this._page!.$(sel);
if (btn) {
await btn.click();
this._logger.info(`Clicked: ${sel}`);
await this._page!.waitForTimeout(2000);
foundJoinPage = true;
break;
}
} catch { /* continue */ }
}
// Check if we're on the "Join a meeting" form page
const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
if (pageText.includes('Join now')) {
if (pageText.includes('Meeting ID') || pageText.includes('Join a meeting') || pageText.includes('Besprechungs-ID')) { this._logger.info('On authenticated pre-join page with "Join now" button');
this._logger.info('On "Join a meeting" form page - filling in meeting details');
// Fill in the Meeting ID field
const meetingIdSelectors = [
'input[placeholder*="Meeting ID" i]',
'input[placeholder*="Besprechungs-ID" i]',
'input[aria-label*="Meeting ID" i]',
'input[aria-label*="Besprechungs-ID" i]',
'input[name*="meetingId" i]',
'input[type="text"]:first-of-type',
];
for (const sel of meetingIdSelectors) {
try {
const input = await this._page!.$(sel);
if (input) {
await input.click();
await input.fill(meetingId);
this._logger.info(`Filled Meeting ID: ${meetingId}`);
break;
}
} catch { /* continue */ }
}
// Fill in the Passcode field (if we have one)
if (passcode) {
const passcodeSelectors = [
'input[placeholder*="passcode" i]',
'input[placeholder*="Passcode" i]',
'input[placeholder*="Kennung" i]',
'input[aria-label*="passcode" i]',
'input[aria-label*="Kennung" i]',
'input[name*="passcode" i]',
'input[type="text"]:nth-of-type(2)',
'input[type="password"]',
];
for (const sel of passcodeSelectors) {
try {
const input = await this._page!.$(sel);
if (input) {
await input.click();
await input.fill(passcode);
this._logger.info('Filled Passcode');
break;
}
} catch { /* continue */ }
}
}
// Click "Join meeting" button
await this._page!.waitForTimeout(500);
const joinBtnSelectors = [
'button:has-text("Join meeting")',
'button:has-text("An Besprechung teilnehmen")',
'button:has-text("Beitreten")',
'button[data-tid="join-meeting-submit"]',
];
for (const sel of joinBtnSelectors) {
try {
const btn = await this._page!.$(sel);
if (btn) {
await btn.click();
this._logger.info(`Clicked: ${sel}`);
joinedViaForm = true;
break;
}
} catch { /* continue */ }
}
if (joinedViaForm) {
// Wait for the pre-join/meeting page to load
await this._page!.waitForTimeout(5000);
this._logger.info('Submitted meeting join form - waiting for pre-join page');
}
}
} catch (formError) {
this._logger.warn(`Teams v2 form join failed: ${formError}`);
}
if (!joinedViaForm) {
this._logger.warn('Teams v2 form join did not work - falling back to launcher flow');
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
}
} else { } else {
// ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params) this._logger.warn(`Post-auth page content: ${pageText.substring(0, 200)}`);
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); }
} else {
this._logger.warn('Authentication via "Sign in" failed - continuing as anonymous');
}
} else {
this._logger.warn('Could not find "Sign in" link - continuing as anonymous');
}
} }
// Set virtual background if configured (must be done on pre-join screen, before "Join now") // Set virtual background if configured (must be done on pre-join screen, before "Join now")