Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue: ClientAuthError: endpoints_resolution_error during ADB2C Login Process with Proxy Authentication #7144

Closed
1 of 2 tasks
alminvrb opened this issue Jun 5, 2024 · 2 comments
Labels
b2c Related to Azure B2C library-specific issues bug-unconfirmed A reported bug that needs to be investigated and confirmed msal-browser Related to msal-browser package Needs: Author Feedback Awaiting response from issue author no-issue-activity Issue author has not responded in 5 days public-client Issues regarding PublicClientApplications question Customer is asking for a clarification, use case or information.

Comments

@alminvrb
Copy link

alminvrb commented Jun 5, 2024

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

2.11.2

Wrapper Library

Not Applicable

Wrapper Library Version

None

Public or Confidential Client?

Public

Description

I am encountering a ClientAuthError during the ADB2C login process in my Electron application when Proxy authentication is enabled. The error message indicates that the endpoints could not be resolved.

After passing the "authorityMetadata" object manually (using JSON.stringify), the endpoints_resolution_error disappeared, but I encountered another error:
Failed to load loginWithADB2C window: Error: (-111) loading Failed to load loginWithADB2C window: Error: (-111) loading 'https://speechid.b2clogin.com/.../authorize?...'

If I open the same link in the browser, it shows the login window correctly.

This issue only occurs when Proxy authentication is enabled. When Proxy authentication is disabled, the application works correctly.

I also tried adding a custom network client with hardcoded proxy values but still experienced the same error.

Error Message

[2024-05-29 15:47:37.405] [error] (Login) ClientAuthError: endpoints_resolution_error: Error: could not resolve endpoints. Please check network and try again. Detail: ClientAuthError: openid_config_error: Could not retrieve endpoints. Check your authority and verify the .well-known/openid-configuration endpoint returns the required endpoints. Attempted to retrieve endpoints from: https://domain/domain/b2c_1a_******signin/v2.0/.well-known/openid-configuration
at ClientAuthError.AuthError [as constructor] (http://localhost:3001/main_window/index.js:560:24)
at new ClientAuthError (http://localhost:3001/main_window/index.js:796:28)
at Function.ClientAuthError.createEndpointDiscoveryIncompleteError (http://localhost:3001/main_window/index.js:833:16)
at Function. (http://localhost:3001/main_window/index.js:6245:47)
at step (http://localhost:3001/main_window/index.js:213:23)
at Object.throw (http://localhost:300

MSAL Logs

ClientAuthError: endpoints_resolution_error: Error: could not resolve endpoints. Please check network and try again. Detail: ClientAuthError: openid_config_error: Could not retrieve endpoints. Check your authority and verify the .well-known/openid-configuration endpoint returns the required endpoints. Attempted to retrieve endpoints from: https://domain/domain/b2c_1a_******signin/v2.0/.well-known/openid-configuration

Network Trace (Preferrably Fiddler)

  • Sent
  • Pending

MSAL Configuration

constructor(adb2cConfiguration: IADB2CConfiguration) {
        try {
            this.logger.logEnter();
            this.adb2cConfig = adb2cConfiguration;
            this.onRedirectNavigate.bind(this);
            this.msalLoggerCallback.bind(this);
        } catch (err) {
            this.logger.error(err);
        } finally {
            this.logger.logExit();
        }
    }

    initialize(): void {
        try {
            this.logger.logEnter();
            this.msalInstance = new msal.PublicClientApplication(this.createMSALConfig());
        } catch (err) {
            this.logger.error(err);
        } finally {
            this.logger.logExit();
        }
    }

    private createMSALConfig(): msal.Configuration {
        try {
            this.logger.logEnter();
            const b2cPolicies: any = {
                names: {
                    signUpSignIn: this.adb2cConfig.SignInUpName,
                },
                authorities: {
                    signUpSignIn: {
                        authority: `${this.adb2cConfig.Protocol}://${this.adb2cConfig.TenantName}.b2clogin.com/${this.adb2cConfig.TenantName}.onmicrosoft.com/${this.adb2cConfig.SignInUpName}`,
                    },
                },
            };
            this.logger.verbose(`SignUpSignIn authority: '${b2cPolicies.authorities.signUpSignIn.authority}'`);

            const msalConfig: msal.Configuration = {
                auth: <msal.BrowserAuthOptions>{
                    authorityMetadata: JSON.stringify(authorityMetadata),
                    clientId: this.adb2cConfig.ClientID,
                    authority: b2cPolicies.authorities.signUpSignIn.authority,
                    validateAuthority: false,
                    redirectUri: this.adb2cConfig.RedirectURL,
                    knownAuthorities: [`${this.adb2cConfig.TenantName}.b2clogin.com`],
                },
                cache: <msal.CacheOptions>{
                    cacheLocation: 'sessionStorage',
                    storeAuthStateInCookie: false,
                },
                system: {
                    networkClient: HttpClientAxios,
                    loggerOptions: {
                        loggerCallback: this.msalLoggerCallback.bind(this),
                        piiLoggingEnabled: false,
                    },
                },
            };
            this.logger.verbose(`Constructed MSAL config:
            ${JSONHelper.serializeObjectToJSONString(msalConfig)}`);
            return msalConfig;
        } catch (err) {
            this.logger.error(err);
        } finally {
            this.logger.logExit();
        }
    }

Relevant Code Snippets

public async createMainWindow(initializeOtherManagersFunction: (mainWindow: BrowserWindow) => void): Promise<void> {
        try {
            return new Promise<void>((resolve, reject) => {
                try {
                    this.logger.logPromiseEnter(this.createMainWindow.name);
                    this.logger.info('Creating Main window...');
                    this.logger.debug('Initializing main window creation');
                    // Create the main browser window hosting the vue router.
                    var mainWindow = new BrowserWindow({
                        width: 500,
                        height: 800,
                        frame: false,
                        show: false,
                        //opacity: 0,
                        transparent: true,
                        resizable: false,
                        maximizable: false,
                        fullscreenable: false,
                        webPreferences: {
                            nodeIntegration: true,
                            backgroundThrottling: false,
                            //preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, //hardcoded webpack based preload URL
                            enableRemoteModule: true,
                            devTools: true,
                            webSecurity: !isDevelopment,
                            contextIsolation: false,
                        },
                    });

                    ws.addNewWindow('main', mainWindow, MAIN_WINDOW_WEBPACK_ENTRY);

                    this.logger.debug('Main browser window created');

                    this.GetMainWindow().on('minimize', () => {
                        this.logger.verbose(`MainWindow minimized!`);
                    });

                    initializeOtherManagersFunction(this.GetMainWindow());

                    //set up event handlers
                    ipcMain.handle(IpcEventNameConstants.SetAlwaysOnTop, (event: IpcMainInvokeEvent, newValue: boolean): void => {
                        try {
                            this.logger.logEnter();
                            this.logger.verbose(`Setting MainWindow always on top to: ${newValue}`);
                            this.GetMainWindow().setAlwaysOnTop(newValue);
                            return;
                        } catch (err) {
                            this.logger.error(err);
                            return;
                        } finally {
                            this.logger.logExit();
                        }
                    });

                    ipcMain.handle(IpcEventNameConstants.IgnoreMouseEvents, (event: IpcMainInvokeEvent, forwardEvents: boolean = true): void => {
                        try {
                            this.logger.logEnter();
                            this.GetMainWindow()?.setIgnoreMouseEvents(true, { forward: forwardEvents });
                            return;
                        } catch (err) {
                            this.logger.error(err);
                            return;
                        } finally {
                            this.logger.logExit();
                        }
                    });

                    ipcMain.handle(IpcEventNameConstants.RestoreMouseEvents, (event: IpcMainInvokeEvent, args: any): void => {
                        try {
                            this.logger.logEnter();
                            this.GetMainWindow()?.setIgnoreMouseEvents(false);
                            return;
                        } catch (err) {
                            this.logger.error(err);
                            return;
                        } finally {
                            this.logger.logExit();
                        }
                    });

                    this.logger.debug('Event handlers set up for main window');

                    ipcMain.handle(IpcEventNameConstants.IsMessageBoxShownChanged, (event: IpcMainInvokeEvent, newValue: boolean): void => {
                        try {
                            this.logger.logEnter();
                            this.isMessageBoxShown = newValue;
                            this.GetMainWindow().webContents.send(IpcEventNameConstants.IsMessageBoxShownChanged, newValue);
                        } catch (err) {
                            this.logger.error(err);
                        } finally {
                            this.logger.logExit();
                        }
                    });

                    ipcMain.on(IpcEventNameConstants.LoginSuccessful, (event: IpcMainEvent, args: any): void => {
                        try {
                            this.logger.logEnter();
                            this.logger.verbose(`LoginSuccessful, closing windows used only during the login...`);
                            ws.getWindow('loginWithADB2C', false)?.close();
                            this.adb2cLoginWindowShouldBeVisible = false;
                            ws.destroyWindow('loginWithADB2C', false);
                            ws.getWindow('userSelector')?.close();
                            ws.destroyWindow('userSelector');
                            this.logger.verbose(`Login related windows closed.`);
                        } catch (err) {
                            this.logger.error(err);
                        } finally {
                            this.logger.logExit();
                        }
                    });

                    //We need to create the messageBox window as soon as possible to be able to display error messages if necessary
                    this.createMessageBoxWindow()
                        .then(() => {
                            this.logger.logThenEnter(this.createMessageBoxWindow.name);
                            // and load the index.html of the app.
                            ws.loadWithRetry('main')
                                .then(() => {
                                    //Create and preload secondary windows
                                    this.createVoiceCommandsWindow();
                                    this.createAboutWindow();
                                    this.createUserSelectorWindow();
                                    this.createUploadToAuthorUserSelectorWindow();
                                    this.createUploadToTypistOrUserSelectorWindow();
                                    this.logger.debug('Secondary windows created and preloaded');

                                    // Emitted when the window is closed.
                                    this.GetMainWindow().on('closed', () => {
                                        this.logger.debug(`MainWindow close called!`);
                                        // Dereference the window object, usually you would store windows
                                        // in an array if your app supports multi windows, this is the time
                                        // when you should delete the corresponding element.
                                        ws.destroyWindow('main');
                                    });
                                    resolve();
                                })
                                .catch((error) => {
                                    this.logger.error(`FAILED TO LOAD MAIN WINDOW, exiting application`);
                                    this.logger.error(error);
                                    app.quit();
                                });
                            this.logger.logThenExit(this.createMessageBoxWindow.name);
                        })
                        .catch((error) => {
                            this.logger.error(error);
                            reject(error);
                        });
                } catch (err) {
                    this.logger.error(err);
                    reject(err);
                } finally {
                    this.logger.logPromiseExit(this.createMainWindow.name);
                }
            });
        } catch (err) {
            this.logger.error(err);
        } finally {
            this.logger.logExit();
        }
    }

    private openADB2CLoginWindow(args: IADB2CWindowData): void {
        this.adb2cLoginSuccessful = false; //reset flag
        this.logger.debug('Opening ADB2C login window');
        if (ws.getWindow('loginWithADB2C', false) == undefined || ws.getWindow('loginWithADB2C', false) == null) {
            this.logger.info(`Creating ADB2C Login window...`);
            var loginWithADB2CWindow = new BrowserWindow({
                width: 600,
                height: 600,
                show: false,
                //transparent: true,
                //opacity: 0,
                resizable: false,
                modal: true,
                maximizable: false,
                fullscreenable: false,
                autoHideMenuBar: true,
                parent: ws.getWindow('splashScreen'),
                webPreferences: {
                    nodeIntegration: true,
                    devTools: true,
                },
            });

            ws.addNewWindow('loginWithADB2C', loginWithADB2CWindow, args.url);
            this.logger.debug('ADB2C login window created');

            ws.getWindow('loginWithADB2C').on('minimize', () => {
                try {
                    this.logger.logEnter();
                    this.logger.verbose(`LoginWithADB2CWindow minimized!`);

                    ws.getWindow('loginWithADB2C').hide();
                    ws.getWindow('splashScreen').minimize();
                } catch (err) {
                    this.logger.error(err);
                } finally {
                    this.logger.logExit();
                }
            });

            //In order to support ADB2C login, we have to catch the redirect URL that would be loaded from the ADB2C login package in the BrowserWindow it is running in.
            //At the time of writing this code, there is no official support for electron applications, and since electron doesn't support 'popup windows', whe javascript MSAL sdk cannot be used either
            //This is a workaround where we parse out the generated JWT token from the redirect URL, and use that to log in and communicate with the * API
            ws.getWindow('loginWithADB2C')?.webContents?.session?.webRequest?.onBeforeRequest((details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.Response) => void) => {
                var preventRedirect: boolean = false;
                try {
                    this.logger.logEnter(`${'loginWithADB2C'} - onBeforeRequest`);
                    this.logger.verbose(`${'loginWithADB2C'} - onBeforeRequest triggered`);
                    if (details !== undefined && details.url !== undefined) {
                        var url = details.url;
                        this.logger.verbose(`Request captured to: ${url}`);
                        if (url.includes('processexec.com') || url.includes('process.com')) {
                            preventRedirect = true;
                            shell.openExternal(url);
                        } else if (url.includes(this.adb2cConfig.RedirectURL)) {
                            if (url.includes(ID_TOKEN_URL_PARAM)) {
                                //Success redirect
                                var index = url.indexOf(ID_TOKEN_URL_PARAM);
                                var token = url.substr(index + ID_TOKEN_URL_PARAM.length);

                                //If something comes after the token, remove it
                                if (token.includes('&')) {
                                    var andIndex = token.indexOf('&');
                                    token = token.substr(0, andIndex + 1);
                                }
                                this.logger.verbose(`PreLogin Token: ${token}`);
                                this.adb2cLoginSuccessful = true;
                                this.fadeWindow('out', ws.getWindow('loginWithADB2C'), 0.1, 20, () => {
                                    this.adb2cLoginWindowShouldBeVisible = false;
                                    var windowInstance = ws.getWindowInstance('loginWithADB2C');
                                    windowInstance.window.hide();
                                    windowInstance.url = 'about:blank';
                                    ws.load('loginWithADB2C'); //Make sure that no auto refresh requests are fired with the same redirect URL while the window is still alive
                                });
                                this.GetMainWindow().webContents.send(IpcEventNameConstants.ADB2CLoginResult, { isSuccessful: true, preLoginToken: token } as IADB2CLoginResult);
                            } else if (url.includes(ERROR_URL_PARAM)) {
                                //Failed / error redirect
                                this.logger.error(`Error redirect received with URL: ${url}`);
                                this.loadADB2CLoginAndShowWindow();
                            } else {
                                //Unknown redirect
                                this.logger.error(`Unknown redirect URL scheme detected: ${url}`);
                                this.GetMainWindow().webContents.send(IpcEventNameConstants.ADB2CLoginResult, { isSuccessful: false, errorMessageID: 'UnknownRedirectURLScheme' } as IADB2CLoginResult);
                            }
                        }
                    }
                } catch (err) {
                    this.logger.error(err);
                } finally {
                    this.logger.logExit(`${'loginWithADB2C'} - onBeforeRequest`);
                    this.logger.verbose(`PreventRedirect: ${preventRedirect}`);
                    if (preventRedirect) {
                        callback({
                            cancel: false,
                            redirectURL: ws.getWindowInstance('loginWithADB2C').url,
                        });
                    } else {
                        callback({}); //Always need to call the callback to not cancel the request.
                    }
                }
            });

            this.logger.debug('Event handlers set up for ADB2C login window');

            //Handle close depending on the login success
            ws.getWindow('loginWithADB2C').on('close', () => {
                this.adb2cLoginWindowShouldBeVisible = false;
                if (!this.adb2cLoginSuccessful) {
                    this.logger.debug('Handling close of ADB2C login window based on login success');
                    try {
                        ws.getWindow('splashScreen')?.close();
                    } catch (err) {
                        this.logger.error(err);
                    }

                    try {
                        ws.getWindow('main')?.close();
                    } catch (err) {
                        this.logger.error(err);
                    }
                }
            });
        } else {
            ws.getWindowInstance('loginWithADB2C').url = args.url;
        }

        this.logger.debug('Cleared load for ADB2C login URL');

        //Clear load for the ADB2C Login URL
        this.loadADB2CLoginAndShowWindow();
    }

    private loadADB2CLoginAndShowWindow() {
        try {
            this.logger.logEnter();
            this.logger.debug('Attempting to load loginWithADB2C window');

            ws.load('loginWithADB2C')
                .then(() => {
                    try {
                        this.logger.debug('loginWithADB2C window loaded successfully');

                        this.adb2cLoginWindowShouldBeVisible = true;
                        this.logger.logThenEnter(this.loadADB2CLoginAndShowWindow.name);
                        var window = ws.getWindow('loginWithADB2C');
                        window.show();
                        this.fadeWindow('in', window, 0.1, 20, () => {
                            this.checkForUpdates();
                        });
                    } catch (err) {
                        this.logger.error(err);
                    } finally {
                        this.logger.logThenExit(this.loadADB2CLoginAndShowWindow.name);
                    }
                })
                .catch((error) => {
                    console.log(`Failed to load loginWithADB2C window: ${error}`);

                    this.logger.error(`Failed to load loginWithADB2C window: ${error}`);
                });
        } catch (err) {
            this.logger.error(err);
        } finally {
            this.logger.logExit();
        }
    }

Reproduction Steps

  1. Enable proxy authentication.
  2. Run the application.
  3. The application will get stuck on the splash screen and the MSAL login window will not be shown.

Expected Behavior

The MSAL login window should be shown, even when Proxy authentication is enabled.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Other

Regression

2.11.2

Source

External (Customer)

@alminvrb alminvrb added bug-unconfirmed A reported bug that needs to be investigated and confirmed question Customer is asking for a clarification, use case or information. labels Jun 5, 2024
@github-actions github-actions bot added b2c Related to Azure B2C library-specific issues msal-browser Related to msal-browser package public-client Issues regarding PublicClientApplications labels Jun 5, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added the Needs: Attention 👋 Awaiting response from the MSAL.js team label Jun 5, 2024
@sameerag
Copy link
Member

@alminvrb A lot changed since 2.x msal js. Did you try moving to 3.x versions and still see this issue?

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs: Author Feedback Awaiting response from issue author and removed Needs: Attention 👋 Awaiting response from the MSAL.js team labels Jun 18, 2024
Copy link
Contributor

@alminvrb This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.

@microsoft-github-policy-service microsoft-github-policy-service bot added the no-issue-activity Issue author has not responded in 5 days label Jun 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
b2c Related to Azure B2C library-specific issues bug-unconfirmed A reported bug that needs to be investigated and confirmed msal-browser Related to msal-browser package Needs: Author Feedback Awaiting response from issue author no-issue-activity Issue author has not responded in 5 days public-client Issues regarding PublicClientApplications question Customer is asking for a clarification, use case or information.
Projects
None yet
Development

No branches or pull requests

2 participants