From b07910410e0142419805b65bb0ba346cd383b9f6 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 16 Feb 2026 11:55:01 +0100
Subject: [PATCH] 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
---
src/bot/orchestrator.ts | 253 +++++++++++++---------------------------
1 file changed, 82 insertions(+), 171 deletions(-)
diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts
index e3b1b80..0b2b84f 100644
--- a/src/bot/orchestrator.ts
+++ b/src/bot/orchestrator.ts
@@ -133,193 +133,104 @@ export class BotOrchestrator {
// Launch browser
await this._launchBrowser();
- // Authenticate with Microsoft if requested
- if (authenticate) {
- const { AuthProcedure } = await import('./authProcedure');
- const authProcedure = new AuthProcedure(this._page!, this._logger);
- const authSuccess = await authProcedure.authenticateWithMicrosoft(
- this._options.botAccountEmail!,
- this._options.botAccountPassword!
- );
- if (!authSuccess) {
- throw new Error('Microsoft authentication failed');
- }
-
- // CRITICAL: After auth, navigate to Teams web app first to establish
- // a Teams session. Without this, Teams redirects to anonymous mode
- // when navigating directly to the meeting URL.
- this._logger.info('Establishing Teams session after auth...');
- 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);
- }
- 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
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) {
- // 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...`);
+ this._logger.info('Authenticated join: looking for "Sign in" link on pre-join page...');
- // 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 || '';
+ // Wait for the pre-join page to load
+ await this._page!.waitForTimeout(3000);
- this._logger.info(`Meeting ID: ${meetingId}, Passcode: ${passcode ? '***' : '(none)'}`);
+ // Dismiss mic/camera permission overlay if present
+ // The "Continue without audio or video" modal may appear here
+ await this._joinProcedure.dismissBrowserPermissionModals();
- let joinedViaForm = false;
+ // 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"]',
+ ];
- 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,
+ 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);
- // 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")',
- ];
+ // Perform Microsoft login (email, password, stay signed in)
+ const { AuthProcedure } = await import('./authProcedure');
+ const authProcedure = new AuthProcedure(this._page!, this._logger);
+ const authSuccess = await authProcedure.authenticateWithMicrosoft(
+ this._options.botAccountEmail!,
+ this._options.botAccountPassword!
+ );
- 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 */ }
+ if (authSuccess) {
+ this._logger.info('Authentication via "Sign in" link succeeded');
+ // After auth, Teams redirects back to the authenticated pre-join page
+ // within Teams v2 (/v2/) -- wait for it to load
+ await this._page!.waitForTimeout(5000);
+
+ const postAuthUrl = this._page!.url();
+ this._logger.info(`Post-auth URL: ${postAuthUrl.substring(0, 80)}`);
+
+ // Verify we're on the authenticated pre-join page
+ const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
+ if (pageText.includes('Join now')) {
+ this._logger.info('On authenticated pre-join page with "Join now" button');
+ } else {
+ this._logger.warn(`Post-auth page content: ${pageText.substring(0, 200)}`);
+ }
+ } else {
+ this._logger.warn('Authentication via "Sign in" failed - continuing as anonymous');
}
-
- // Check if we're on the "Join a meeting" form page
- const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
-
- if (pageText.includes('Meeting ID') || pageText.includes('Join a meeting') || pageText.includes('Besprechungs-ID')) {
- 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}`);
+ } else {
+ this._logger.warn('Could not find "Sign in" link - continuing as anonymous');
}
-
- if (!joinedViaForm) {
- this._logger.warn('Teams v2 form join did not work - falling back to launcher flow');
- await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
- }
-
- } else {
- // ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params)
- await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
}
// Set virtual background if configured (must be done on pre-join screen, before "Join now")