From d64b065a6e4bf08fc0821c53d5d67d11e927bbb9 Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Tue, 20 Oct 2026 12:45:40 -0400 Subject: [PATCH 1/8] Ask Studio for the Entra MFA access token when the user-to-machine OAuth module is exposed. Studio builds that do not expose the module keep the authorization-code exchange. Co-authored-by: Cursor --- forward_engineering/api/testConnection.js | 5 +- forward_engineering/config.json | 1 + reverse_engineering/api.js | 12 +- reverse_engineering/config.json | 1 + reverse_engineering/connectionState.js | 4 +- .../databaseService/databaseService.js | 3 +- .../databaseService/helpers/connection.js | 45 +-- reverse_engineering/helpers/mfaAccessToken.js | 106 +++++++ .../helpers/mfaAccessToken.spec.js | 268 ++++++++++++++++++ 9 files changed, 399 insertions(+), 46 deletions(-) create mode 100644 reverse_engineering/helpers/mfaAccessToken.js create mode 100644 reverse_engineering/helpers/mfaAccessToken.spec.js diff --git a/forward_engineering/api/testConnection.js b/forward_engineering/api/testConnection.js index 0b25832..d45838e 100644 --- a/forward_engineering/api/testConnection.js +++ b/forward_engineering/api/testConnection.js @@ -1,13 +1,14 @@ const { logInfo } = require('../../reverse_engineering/helpers/logInfo'); +const { isUserToMachineOauthAvailable } = require('../../reverse_engineering/helpers/mfaAccessToken'); const { getExternalBrowserUrl, connect } = require('../../reverse_engineering/api'); async function testConnection(connectionInfo, logger, callback, app) { try { logInfo('Test connection', connectionInfo, logger); - if (connectionInfo.authMethod === 'Azure Active Directory (MFA)') { + if (connectionInfo.authMethod === 'Azure Active Directory (MFA)' && !isUserToMachineOauthAvailable({ app })) { await getExternalBrowserUrl(connectionInfo, logger, callback); } else { - await connect(connectionInfo, logger); + await connect(connectionInfo, logger, undefined, app); } callback(null); } catch (error) { diff --git a/forward_engineering/config.json b/forward_engineering/config.json index d535db8..d2c0c27 100644 --- a/forward_engineering/config.json +++ b/forward_engineering/config.json @@ -16,6 +16,7 @@ ] }, "externalBrowserPort": 8080, + "userToMachineOAuth": true, "combinedContainers": true, "level": { "container": true, diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index 9324bec..a51506b 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -12,6 +12,7 @@ const { getCollectionsRelationships, } = require('./reverseEngineeringService/reverseEngineeringService'); const { logInfo } = require('./helpers/logInfo'); +const { isUserToMachineOauthAvailable } = require('./helpers/mfaAccessToken'); const filterRelationships = require('./helpers/filterRelationships'); const getOptionsFromConnectionInfo = require('./helpers/getOptionsFromConnectionInfo'); const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo'); @@ -22,7 +23,7 @@ module.exports = { async connect(connectionInfo, logger, callback, app) { const client = getClient(); if (!client) { - await setClient(connectionInfo, logger); + await setClient(connectionInfo, logger, app); return getClient(); } @@ -37,10 +38,13 @@ module.exports = { async testConnection(connectionInfo, logger, callback, app) { try { logInfo('Test connection', connectionInfo, logger); - if (connectionInfo.authMethod === 'Azure Active Directory (MFA)') { + if ( + connectionInfo.authMethod === 'Azure Active Directory (MFA)' && + !isUserToMachineOauthAvailable({ app }) + ) { await this.getExternalBrowserUrl(connectionInfo, logger, callback); } else { - await this.connect(connectionInfo, logger); + await this.connect(connectionInfo, logger, undefined, app); } callback(null); } catch (error) { @@ -85,7 +89,7 @@ module.exports = { async getDbCollectionsNames(connectionInfo, logger, callback, app) { try { logInfo('Retrieving databases and tables information', connectionInfo, logger); - const client = await this.connect(connectionInfo, logger); + const client = await this.connect(connectionInfo, logger, undefined, app); if (!client.config.database) { throw new Error('No database specified'); } diff --git a/reverse_engineering/config.json b/reverse_engineering/config.json index 21cea71..58abd95 100644 --- a/reverse_engineering/config.json +++ b/reverse_engineering/config.json @@ -16,5 +16,6 @@ } ], "externalBrowserPort": 8080, + "userToMachineOAuth": true, "helpUrl": "https://hackolade.com/help/ConnecttoSynapse.html" } diff --git a/reverse_engineering/connectionState.js b/reverse_engineering/connectionState.js index 7b2c208..be6f767 100644 --- a/reverse_engineering/connectionState.js +++ b/reverse_engineering/connectionState.js @@ -4,9 +4,9 @@ module.exports = { _client: null, _connectionInfo: {}, getClient: () => this._client, - setClient: async (connectionInfo, logger) => { + setClient: async (connectionInfo, logger, app) => { this._connectionInfo = connectionInfo; - this._client = await getConnectionClient(connectionInfo, logger); + this._client = await getConnectionClient(connectionInfo, logger, app); }, getConnectionInfo: () => this._connectionInfo, clearClient: () => (this._client = null), diff --git a/reverse_engineering/databaseService/databaseService.js b/reverse_engineering/databaseService/databaseService.js index e585f22..4bd297d 100644 --- a/reverse_engineering/databaseService/databaseService.js +++ b/reverse_engineering/databaseService/databaseService.js @@ -15,7 +15,7 @@ const { parseProcedure } = require('./helpers/parsers/parseProcedure'); const QUERY_REQUEST_TIMEOUT = 60000; -const getConnectionClient = async (connectionInfo, logger) => { +const getConnectionClient = async (connectionInfo, logger, app) => { const hostName = getHostName(connectionInfo.host); const userName = isEmail(connectionInfo.userName) && hostName @@ -48,6 +48,7 @@ const getConnectionClient = async (connectionInfo, logger) => { clientId, redirectUri, logger, + app, }); return connection.connect(); diff --git a/reverse_engineering/databaseService/helpers/connection.js b/reverse_engineering/databaseService/helpers/connection.js index e5a069e..a6b069c 100644 --- a/reverse_engineering/databaseService/helpers/connection.js +++ b/reverse_engineering/databaseService/helpers/connection.js @@ -1,8 +1,6 @@ -const { hckFetch } = require('@hackolade/fetch'); const sql = require('mssql'); const { logAuthTokenInfo, logConnectionHostAndUsername } = require('../../helpers/logInfo'); -const { prepareError } = require('./errorService'); -const { parseResponse } = require('../../helpers/parseResponse'); +const { getMfaAccessToken } = require('../../helpers/mfaAccessToken'); class Connection { constructor({ logger }) { @@ -51,13 +49,11 @@ class UsernamePasswordConnection extends Connection { } class AzureActiveDirectoryMFAConnection extends Connection { - constructor({ connectionInfo, commonConfig, tenantId, clientId, redirectUri, logger }) { + constructor({ connectionInfo, commonConfig, logger, app }) { super({ logger }); this.connectionInfo = connectionInfo; this.commonConfig = commonConfig; - this.tenantId = tenantId; - this.clientId = clientId; - this.redirectUri = redirectUri; + this.app = app; } async connect() { @@ -80,36 +76,11 @@ class AzureActiveDirectoryMFAConnection extends Connection { } async #getToken() { - try { - const urlParams = new URLSearchParams(); - - urlParams.append('code', this.connectionInfo?.externalBrowserQuery?.code || ''); - urlParams.append('client_id', this.clientId); - urlParams.append('redirect_uri', this.redirectUri); - urlParams.append('grant_type', 'authorization_code'); - urlParams.append('code_verifier', this.connectionInfo?.proofKey); - - const options = { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'http://localhost', - }, - body: urlParams, - }; - - const response = await hckFetch( - `https://login.microsoftonline.com/organizations/oauth2/v2.0/token`, - options, - ); - const responseData = await parseResponse(response); - - return responseData?.access_token || ''; - } catch (error) { - this.logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA auth error'); - return ''; - } + return getMfaAccessToken({ + app: this.app, + connectionInfo: this.connectionInfo, + logger: this.logger, + }); } } diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js new file mode 100644 index 0000000..d945b5f --- /dev/null +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -0,0 +1,106 @@ +'use strict'; + +const { hckFetch } = require('@hackolade/fetch'); + +const { parseResponse } = require('./parseResponse'); + +const OAUTH_MODULE = '@hackolade/oauth'; +const OAUTH_NOT_EXPOSED_MESSAGE = `The module ${OAUTH_MODULE} is not exposed by the application to the plugins!`; +const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; +const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token'; +const REDIRECT_URI = 'http://localhost:8080'; +const REDIRECT_PORT = 8080; + +const getAuthorizeTenant = ({ connectionInfo = {} } = {}) => + connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; + +const getAuthorizationQuery = ({ connectionInfo = {} } = {}) => { + const authorizationQuery = { + prompt: 'select_account', + response_mode: 'query', + resource: 'https://database.windows.net/', + }; + + if (!connectionInfo.loginHint) { + return authorizationQuery; + } + + return { + ...authorizationQuery, + login_hint: connectionInfo.loginHint, + }; +}; + +const exchangeAuthorizationCode = async ({ connectionInfo, logger } = {}) => { + try { + const urlParams = new URLSearchParams(); + + urlParams.append('code', connectionInfo?.externalBrowserQuery?.code || ''); + urlParams.append('client_id', ENTRA_CLIENT_ID); + urlParams.append('redirect_uri', REDIRECT_URI); + urlParams.append('grant_type', 'authorization_code'); + urlParams.append('code_verifier', connectionInfo?.proofKey); + + const response = await hckFetch(TOKEN_ENDPOINT, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Origin': 'http://localhost', + }, + body: urlParams, + }); + const responseData = await parseResponse(response); + + return responseData?.access_token || ''; + } catch (error) { + logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA auth error'); + + return ''; + } +}; + +const loadOauthModule = ({ app } = {}) => { + try { + return app.require(OAUTH_MODULE); + } catch (error) { + if (error?.message === OAUTH_NOT_EXPOSED_MESSAGE) { + return null; + } + + throw error; + } +}; + +const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { + const oauth = loadOauthModule({ app }); + + if (!oauth) { + return exchangeAuthorizationCode({ connectionInfo, logger }); + } + + const { accessToken } = await oauth.getUserToMachineAccessToken({ + authorizationEndpoint: `https://login.microsoftonline.com/${getAuthorizeTenant({ connectionInfo })}/oauth2/authorize`, + tokenEndpoint: TOKEN_ENDPOINT, + clientId: ENTRA_CLIENT_ID, + port: REDIRECT_PORT, + failOnPortConflict: true, + authorizationQuery: getAuthorizationQuery({ connectionInfo }), + headers: { + Origin: 'http://localhost', + }, + }); + + if (!accessToken) { + throw new Error('Entra token response did not include an access token.'); + } + + return accessToken; +}; + +const isUserToMachineOauthAvailable = ({ app } = {}) => Boolean(loadOauthModule({ app })); + +module.exports = { + getMfaAccessToken, + isUserToMachineOauthAvailable, +}; diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js new file mode 100644 index 0000000..bfa82dd --- /dev/null +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -0,0 +1,268 @@ +'use strict'; + +const { registerHooks } = require('node:module'); +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); + +globalThis.entraMfaFetch = async () => { + throw new Error('hckFetch should not be called'); +}; + +registerHooks({ + load(url, context, nextLoad) { + if (!url.includes('@hackolade/fetch')) { + return nextLoad(url, context); + } + + return { + format: 'commonjs', + source: 'exports.hckFetch = (...args) => globalThis.entraMfaFetch(...args);', + shortCircuit: true, + }; + }, +}); + +const { getMfaAccessToken } = require('./mfaAccessToken'); + +const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; +const OAUTH_NOT_EXPOSED_MESSAGE = 'The module @hackolade/oauth is not exposed by the application to the plugins!'; + +describe('getMfaAccessToken', { concurrency: false }, () => { + it('asks Studio for a user-to-machine token with the Entra authorize request', async () => { + const calls = []; + const app = { + require(name) { + assert.equal(name, '@hackolade/oauth'); + + return { + async getUserToMachineAccessToken(options) { + calls.push(options); + + return { accessToken: 'sql-access-token' }; + }, + }; + }, + }; + + const token = await getMfaAccessToken({ + app, + connectionInfo: { + connectionTenantId: 'saved-directory', + tenantId: 'control-plane-tenant', + loginHint: 'user name@example.com', + }, + logger: { log() {} }, + }); + + assert.equal(token, 'sql-access-token'); + assert.equal('scope' in calls[0], false); + assert.deepEqual(calls, [ + { + authorizationEndpoint: 'https://login.microsoftonline.com/saved-directory/oauth2/authorize', + tokenEndpoint: 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token', + clientId: ENTRA_CLIENT_ID, + port: 8080, + failOnPortConflict: true, + authorizationQuery: { + prompt: 'select_account', + response_mode: 'query', + resource: 'https://database.windows.net/', + login_hint: 'user name@example.com', + }, + headers: { + Origin: 'http://localhost', + }, + }, + ]); + }); + + it('uses the control-plane tenant when the connection tenant is empty and omits a missing login hint', async () => { + const calls = []; + const app = { + require() { + return { + async getUserToMachineAccessToken(options) { + calls.push(options); + + return { accessToken: 'sql-access-token' }; + }, + }; + }, + }; + + await getMfaAccessToken({ + app, + connectionInfo: { + connectionTenantId: '', + tenantId: 'control-plane-tenant', + }, + logger: { log() {} }, + }); + + assert.equal( + calls[0].authorizationEndpoint, + 'https://login.microsoftonline.com/control-plane-tenant/oauth2/authorize', + ); + assert.deepEqual(calls[0].authorizationQuery, { + prompt: 'select_account', + response_mode: 'query', + resource: 'https://database.windows.net/', + }); + }); + + it('uses the common tenant when no tenant id is set', async () => { + const calls = []; + const app = { + require() { + return { + async getUserToMachineAccessToken(options) { + calls.push(options); + + return { accessToken: 'sql-access-token' }; + }, + }; + }, + }; + + await getMfaAccessToken({ + app, + connectionInfo: {}, + logger: { log() {} }, + }); + + assert.equal(calls[0].authorizationEndpoint, 'https://login.microsoftonline.com/common/oauth2/authorize'); + }); + + it('rejects when Studio does not return an access token', async () => { + const app = { + require() { + return { + async getUserToMachineAccessToken() { + return {}; + }, + }; + }, + }; + + await assert.rejects( + () => + getMfaAccessToken({ + app, + connectionInfo: { connectionTenantId: 'saved-directory' }, + logger: { log() {} }, + }), + { message: 'Entra token response did not include an access token.' }, + ); + }); + + it('rejects when loading the OAuth module fails for another reason', async () => { + const failure = new Error('oauth crashed'); + const app = { + require() { + throw failure; + }, + }; + + await assert.rejects( + () => + getMfaAccessToken({ + app, + connectionInfo: {}, + logger: { log() {} }, + }), + error => error === failure, + ); + }); + + it('rejects when the OAuth module fails to return a token', async () => { + const failure = new Error('Entra rejected the login'); + const app = { + require() { + return { + async getUserToMachineAccessToken() { + throw failure; + }, + }; + }, + }; + + await assert.rejects( + () => + getMfaAccessToken({ + app, + connectionInfo: {}, + logger: { log() {} }, + }), + error => error === failure, + ); + }); + + it('exchanges the authorization code and proof key when the OAuth module is not exposed', async () => { + const requests = []; + globalThis.entraMfaFetch = async (url, options) => { + requests.push({ url, body: String(options.body) }); + + return { + status: 200, + json: async () => ({ access_token: 'exchanged-token' }), + }; + }; + const app = { + require() { + throw new Error(OAUTH_NOT_EXPOSED_MESSAGE); + }, + }; + + const token = await getMfaAccessToken({ + app, + connectionInfo: { + externalBrowserQuery: { code: 'auth-code' }, + proofKey: 'proof-key', + }, + logger: { log() {} }, + }); + + assert.equal(token, 'exchanged-token'); + assert.deepEqual(requests, [ + { + url: 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token', + body: 'code=auth-code&client_id=0dc36597-bc44-49f8-a4a7-ae5401959b85&redirect_uri=http%3A%2F%2Flocalhost%3A8080&grant_type=authorization_code&code_verifier=proof-key', + }, + ]); + }); + + it('logs a code exchange failure and returns an empty token', async () => { + const failure = new Error('network down'); + const entries = []; + globalThis.entraMfaFetch = async () => { + throw failure; + }; + const app = { + require() { + throw new Error(OAUTH_NOT_EXPOSED_MESSAGE); + }, + }; + + const token = await getMfaAccessToken({ + app, + connectionInfo: { + externalBrowserQuery: { code: 'auth-code' }, + proofKey: 'proof-key', + }, + logger: { + log(level, data, message) { + entries.push({ level, data, message }); + }, + }, + }); + + assert.equal(token, ''); + assert.deepEqual(entries, [ + { + level: 'error', + data: { message: 'network down', stack: failure.stack, error: failure }, + message: 'MFA auth error', + }, + ]); + }); +}); From 9accdac4f275524cab1a001680bd52c9463ebac9 Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Tue, 20 Oct 2026 13:07:19 -0400 Subject: [PATCH 2/8] Pass the Studio application into apply-to-instance so MFA can request a user-to-machine token or fall back to the code exchange. Share the external-browser decision in the MFA helper and call the token helper directly from the connection. Co-authored-by: Cursor --- forward_engineering/api/testConnection.js | 4 +- .../helpers/applyToInstanceHelper.js | 2 +- reverse_engineering/api.js | 7 +--- .../databaseService/helpers/connection.js | 14 +++---- reverse_engineering/helpers/mfaAccessToken.js | 10 ++++- .../helpers/mfaAccessToken.spec.js | 40 ++++++++++++++++++- 6 files changed, 57 insertions(+), 20 deletions(-) diff --git a/forward_engineering/api/testConnection.js b/forward_engineering/api/testConnection.js index d45838e..748ad5a 100644 --- a/forward_engineering/api/testConnection.js +++ b/forward_engineering/api/testConnection.js @@ -1,11 +1,11 @@ const { logInfo } = require('../../reverse_engineering/helpers/logInfo'); -const { isUserToMachineOauthAvailable } = require('../../reverse_engineering/helpers/mfaAccessToken'); +const { shouldUseExternalBrowser } = require('../../reverse_engineering/helpers/mfaAccessToken'); const { getExternalBrowserUrl, connect } = require('../../reverse_engineering/api'); async function testConnection(connectionInfo, logger, callback, app) { try { logInfo('Test connection', connectionInfo, logger); - if (connectionInfo.authMethod === 'Azure Active Directory (MFA)' && !isUserToMachineOauthAvailable({ app })) { + if (shouldUseExternalBrowser({ app, connectionInfo })) { await getExternalBrowserUrl(connectionInfo, logger, callback); } else { await connect(connectionInfo, logger, undefined, app); diff --git a/forward_engineering/helpers/applyToInstanceHelper.js b/forward_engineering/helpers/applyToInstanceHelper.js index 9ece971..a80409f 100644 --- a/forward_engineering/helpers/applyToInstanceHelper.js +++ b/forward_engineering/helpers/applyToInstanceHelper.js @@ -7,7 +7,7 @@ const applyToInstance = async (connectionInfo, logger, app) => { const async = app.require('async'); try { - const client = await connect(connectionInfo, logger); + const client = await connect(connectionInfo, logger, undefined, app); if (!client.config.database) { throw new Error('No database specified'); } diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index a51506b..ca89811 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -12,7 +12,7 @@ const { getCollectionsRelationships, } = require('./reverseEngineeringService/reverseEngineeringService'); const { logInfo } = require('./helpers/logInfo'); -const { isUserToMachineOauthAvailable } = require('./helpers/mfaAccessToken'); +const { shouldUseExternalBrowser } = require('./helpers/mfaAccessToken'); const filterRelationships = require('./helpers/filterRelationships'); const getOptionsFromConnectionInfo = require('./helpers/getOptionsFromConnectionInfo'); const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo'); @@ -38,10 +38,7 @@ module.exports = { async testConnection(connectionInfo, logger, callback, app) { try { logInfo('Test connection', connectionInfo, logger); - if ( - connectionInfo.authMethod === 'Azure Active Directory (MFA)' && - !isUserToMachineOauthAvailable({ app }) - ) { + if (shouldUseExternalBrowser({ app, connectionInfo })) { await this.getExternalBrowserUrl(connectionInfo, logger, callback); } else { await this.connect(connectionInfo, logger, undefined, app); diff --git a/reverse_engineering/databaseService/helpers/connection.js b/reverse_engineering/databaseService/helpers/connection.js index a6b069c..44e8052 100644 --- a/reverse_engineering/databaseService/helpers/connection.js +++ b/reverse_engineering/databaseService/helpers/connection.js @@ -57,7 +57,11 @@ class AzureActiveDirectoryMFAConnection extends Connection { } async connect() { - const token = await this.#getToken(); + const token = await getMfaAccessToken({ + app: this.app, + connectionInfo: this.connectionInfo, + logger: this.logger, + }); logAuthTokenInfo({ token, logger: this.logger }); logConnectionHostAndUsername({ authMethod: this.connectionInfo.authMethod, logger: this.logger }); return sql.connect({ @@ -74,14 +78,6 @@ class AzureActiveDirectoryMFAConnection extends Connection { }, }); } - - async #getToken() { - return getMfaAccessToken({ - app: this.app, - connectionInfo: this.connectionInfo, - logger: this.logger, - }); - } } class AzureActiveDirectoryUsernamePasswordConnection extends Connection { diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index d945b5f..115cc8f 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -98,9 +98,15 @@ const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { return accessToken; }; -const isUserToMachineOauthAvailable = ({ app } = {}) => Boolean(loadOauthModule({ app })); +const shouldUseExternalBrowser = ({ app, connectionInfo } = {}) => { + if (connectionInfo.authMethod !== 'Azure Active Directory (MFA)') { + return false; + } + + return !loadOauthModule({ app }); +}; module.exports = { getMfaAccessToken, - isUserToMachineOauthAvailable, + shouldUseExternalBrowser, }; diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js index bfa82dd..afdbdf7 100644 --- a/reverse_engineering/helpers/mfaAccessToken.spec.js +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -22,7 +22,7 @@ registerHooks({ }, }); -const { getMfaAccessToken } = require('./mfaAccessToken'); +const { getMfaAccessToken, shouldUseExternalBrowser } = require('./mfaAccessToken'); const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; const OAUTH_NOT_EXPOSED_MESSAGE = 'The module @hackolade/oauth is not exposed by the application to the plugins!'; @@ -266,3 +266,41 @@ describe('getMfaAccessToken', { concurrency: false }, () => { ]); }); }); + +describe('shouldUseExternalBrowser', { concurrency: false }, () => { + const mfaConnectionInfo = { authMethod: 'Azure Active Directory (MFA)' }; + + it('opens SQL when the OAuth module is present', () => { + const app = { + require() { + return {}; + }, + }; + + assert.equal(shouldUseExternalBrowser({ app, connectionInfo: mfaConnectionInfo }), false); + }); + + it('keeps the external browser when the OAuth module is not exposed', () => { + const app = { + require() { + throw new Error(OAUTH_NOT_EXPOSED_MESSAGE); + }, + }; + + assert.equal(shouldUseExternalBrowser({ app, connectionInfo: mfaConnectionInfo }), true); + }); + + it('surfaces any other failure while loading the OAuth module', () => { + const failure = new Error('oauth crashed'); + const app = { + require() { + throw failure; + }, + }; + + assert.throws( + () => shouldUseExternalBrowser({ app, connectionInfo: mfaConnectionInfo }), + error => error === failure, + ); + }); +}); From f2d0353d96fe2f18ab028cabcf91a579154fcb44 Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Tue, 20 Oct 2026 14:05:03 -0400 Subject: [PATCH 3/8] adapt the strategy --- forward_engineering/config.json | 1 - reverse_engineering/api.js | 7 +++- reverse_engineering/config.json | 1 - reverse_engineering/helpers/mfaAccessToken.js | 12 ++++-- .../helpers/mfaAccessToken.spec.js | 42 ++++++++++++++++++- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/forward_engineering/config.json b/forward_engineering/config.json index d2c0c27..d535db8 100644 --- a/forward_engineering/config.json +++ b/forward_engineering/config.json @@ -16,7 +16,6 @@ ] }, "externalBrowserPort": 8080, - "userToMachineOAuth": true, "combinedContainers": true, "level": { "container": true, diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index ca89811..fd35595 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -12,7 +12,7 @@ const { getCollectionsRelationships, } = require('./reverseEngineeringService/reverseEngineeringService'); const { logInfo } = require('./helpers/logInfo'); -const { shouldUseExternalBrowser } = require('./helpers/mfaAccessToken'); +const { canRequestUserToMachineAccessToken, shouldUseExternalBrowser } = require('./helpers/mfaAccessToken'); const filterRelationships = require('./helpers/filterRelationships'); const getOptionsFromConnectionInfo = require('./helpers/getOptionsFromConnectionInfo'); const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo'); @@ -60,6 +60,11 @@ module.exports = { }, async getExternalBrowserUrl(connectionInfo, logger, cb, app) { + if (canRequestUserToMachineAccessToken({ app, connectionInfo })) { + cb(null, {}); + return; + } + logInfo('Get external browser URL', connectionInfo, logger); const verifier = randomstring.generate(32); const base64Digest = crypto.createHash('sha256').update(verifier).digest('base64'); diff --git a/reverse_engineering/config.json b/reverse_engineering/config.json index 58abd95..21cea71 100644 --- a/reverse_engineering/config.json +++ b/reverse_engineering/config.json @@ -16,6 +16,5 @@ } ], "externalBrowserPort": 8080, - "userToMachineOAuth": true, "helpUrl": "https://hackolade.com/help/ConnecttoSynapse.html" } diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index 115cc8f..0579e59 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -98,15 +98,21 @@ const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { return accessToken; }; -const shouldUseExternalBrowser = ({ app, connectionInfo } = {}) => { - if (connectionInfo.authMethod !== 'Azure Active Directory (MFA)') { +const isEntraMfa = ({ connectionInfo } = {}) => connectionInfo?.authMethod === 'Azure Active Directory (MFA)'; + +const canRequestUserToMachineAccessToken = ({ app, connectionInfo } = {}) => { + if (!isEntraMfa({ connectionInfo })) { return false; } - return !loadOauthModule({ app }); + return Boolean(loadOauthModule({ app })); }; +const shouldUseExternalBrowser = ({ app, connectionInfo } = {}) => + isEntraMfa({ connectionInfo }) && !canRequestUserToMachineAccessToken({ app, connectionInfo }); + module.exports = { getMfaAccessToken, + canRequestUserToMachineAccessToken, shouldUseExternalBrowser, }; diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js index afdbdf7..2141cbd 100644 --- a/reverse_engineering/helpers/mfaAccessToken.spec.js +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -9,8 +9,18 @@ globalThis.entraMfaFetch = async () => { }; registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier !== '@hackolade/fetch') { + return nextResolve(specifier, context); + } + + return { + url: 'file:///hackolade-fetch-stub', + shortCircuit: true, + }; + }, load(url, context, nextLoad) { - if (!url.includes('@hackolade/fetch')) { + if (url !== 'file:///hackolade-fetch-stub') { return nextLoad(url, context); } @@ -22,7 +32,11 @@ registerHooks({ }, }); -const { getMfaAccessToken, shouldUseExternalBrowser } = require('./mfaAccessToken'); +const { + getMfaAccessToken, + canRequestUserToMachineAccessToken, + shouldUseExternalBrowser, +} = require('./mfaAccessToken'); const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; const OAUTH_NOT_EXPOSED_MESSAGE = 'The module @hackolade/oauth is not exposed by the application to the plugins!'; @@ -267,6 +281,30 @@ describe('getMfaAccessToken', { concurrency: false }, () => { }); }); +describe('canRequestUserToMachineAccessToken', { concurrency: false }, () => { + const mfaConnectionInfo = { authMethod: 'Azure Active Directory (MFA)' }; + + it('uses the user-to-machine call when the OAuth module is present', () => { + const app = { + require() { + return {}; + }, + }; + + assert.equal(canRequestUserToMachineAccessToken({ app, connectionInfo: mfaConnectionInfo }), true); + }); + + it('keeps the external browser when the OAuth module is not exposed', () => { + const app = { + require() { + throw new Error(OAUTH_NOT_EXPOSED_MESSAGE); + }, + }; + + assert.equal(canRequestUserToMachineAccessToken({ app, connectionInfo: mfaConnectionInfo }), false); + }); +}); + describe('shouldUseExternalBrowser', { concurrency: false }, () => { const mfaConnectionInfo = { authMethod: 'Azure Active Directory (MFA)' }; From 15ac147cbeba55eef8fb78387dc3892d040d8e5a Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Thu, 22 Oct 2026 10:31:10 -0400 Subject: [PATCH 4/8] Do not hardcode azure entra id parameters --- reverse_engineering/api.js | 7 +-- .../databaseService/databaseService.js | 8 +++- .../helpers/entraOAuthConfig.js | 46 +++++++++++++++++++ reverse_engineering/helpers/mfaAccessToken.js | 38 +++++---------- .../helpers/mfaAccessToken.spec.js | 21 +++++++-- 5 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 reverse_engineering/helpers/entraOAuthConfig.js diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index fd35595..65902d2 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -13,6 +13,7 @@ const { } = require('./reverseEngineeringService/reverseEngineeringService'); const { logInfo } = require('./helpers/logInfo'); const { canRequestUserToMachineAccessToken, shouldUseExternalBrowser } = require('./helpers/mfaAccessToken'); +const { getEntraOAuthConfig, resolveAuthorizeTenant } = require('./helpers/entraOAuthConfig'); const filterRelationships = require('./helpers/filterRelationships'); const getOptionsFromConnectionInfo = require('./helpers/getOptionsFromConnectionInfo'); const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo'); @@ -69,14 +70,14 @@ module.exports = { const verifier = randomstring.generate(32); const base64Digest = crypto.createHash('sha256').update(verifier).digest('base64'); const challenge = base64url.fromBase64(base64Digest); - const tenantId = connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; - const clientId = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; + const tenantId = resolveAuthorizeTenant({ connectionInfo }); + const { productionClientId, authorizationEndpoint } = getEntraOAuthConfig({ app, tenantId }); const loginHint = connectionInfo.loginHint ? `login_hint=${encodeURIComponent(connectionInfo.loginHint)}&` : ''; const redirectUrl = `http://localhost:${connectionInfo.redirectPort || 8080}`; cb(null, { proofKey: verifier, - url: `https://login.microsoftonline.com/${tenantId}/oauth2/authorize?${loginHint}code_challenge_method=S256&code_challenge=${challenge}&response_type=code&response_mode=query&client_id=${clientId}&redirect_uri=${redirectUrl}&prompt=select_account&resource=https://database.windows.net/`, + url: `${authorizationEndpoint}?${loginHint}code_challenge_method=S256&code_challenge=${challenge}&response_type=code&response_mode=query&client_id=${productionClientId}&redirect_uri=${redirectUrl}&prompt=select_account&resource=https://database.windows.net/`, }); }, diff --git a/reverse_engineering/databaseService/databaseService.js b/reverse_engineering/databaseService/databaseService.js index 4bd297d..710d3af 100644 --- a/reverse_engineering/databaseService/databaseService.js +++ b/reverse_engineering/databaseService/databaseService.js @@ -4,6 +4,7 @@ const { getObjectsFromDatabase, getNewConnectionClientByDb } = require('./helper const getSampleDocSize = require('../helpers/getSampleDocSize'); const { logAuthTokenInfo } = require('../helpers/logInfo'); const { getConnection } = require('./helpers/connection'); +const { getEntraOAuthConfig, resolveAuthorizeTenant } = require('../helpers/entraOAuthConfig'); const { progress } = require('../helpers/logInfo'); const { getPartitionsSubQueryForRetrievingTheTablesSelectedByTheUser, @@ -21,7 +22,7 @@ const getConnectionClient = async (connectionInfo, logger, app) => { isEmail(connectionInfo.userName) && hostName ? `${connectionInfo.userName}@${hostName}` : connectionInfo.userName; - const tenantId = connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; + const tenantId = resolveAuthorizeTenant({ connectionInfo }); const queryRequestTimeout = Number(connectionInfo.queryRequestTimeout) || QUERY_REQUEST_TIMEOUT; const commonConfig = { @@ -36,7 +37,10 @@ const getConnectionClient = async (connectionInfo, logger, app) => { password: connectionInfo.userPassword, }; - const clientId = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; + const clientId = + connectionInfo.authMethod === 'Azure Active Directory (Username / Password)' + ? getEntraOAuthConfig({ app }).productionClientId + : undefined; const redirectUri = 'http://localhost:8080'; const connection = getConnection({ diff --git a/reverse_engineering/helpers/entraOAuthConfig.js b/reverse_engineering/helpers/entraOAuthConfig.js new file mode 100644 index 0000000..ad357c5 --- /dev/null +++ b/reverse_engineering/helpers/entraOAuthConfig.js @@ -0,0 +1,46 @@ +'use strict'; + +const OAUTH_MODULE = '@hackolade/oauth'; +const OAUTH_NOT_EXPOSED_MESSAGE = `The module ${OAUTH_MODULE} is not exposed by the application to the plugins!`; +const FALLBACK_PRODUCTION_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; +const FALLBACK_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token'; +const FALLBACK_AUTHORIZE_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize'; + +const loadOauthModule = ({ app } = {}) => { + try { + return app.require(OAUTH_MODULE); + } catch (error) { + if (error?.message === OAUTH_NOT_EXPOSED_MESSAGE) { + return null; + } + + throw error; + } +}; + +const resolveAuthorizeTenant = ({ connectionInfo = {} } = {}) => + connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; + +const getLegacyEntraOAuthConfig = ({ tenantId } = {}) => ({ + productionClientId: FALLBACK_PRODUCTION_CLIENT_ID, + authorizationEndpoint: tenantId + ? `https://login.microsoftonline.com/${tenantId}/oauth2/authorize` + : FALLBACK_AUTHORIZE_ENDPOINT, + tokenEndpoint: FALLBACK_TOKEN_ENDPOINT, +}); + +const getEntraOAuthConfig = ({ app, tenantId } = {}) => { + const oauth = loadOauthModule({ app }); + + if (!oauth) { + return getLegacyEntraOAuthConfig({ tenantId }); + } + + return oauth.getEntraIdOAuthConfig(tenantId === undefined ? {} : { tenantId }); +}; + +module.exports = { + loadOauthModule, + resolveAuthorizeTenant, + getEntraOAuthConfig, +}; diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index 0579e59..a6721df 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -3,17 +3,11 @@ const { hckFetch } = require('@hackolade/fetch'); const { parseResponse } = require('./parseResponse'); +const { getEntraOAuthConfig, loadOauthModule, resolveAuthorizeTenant } = require('./entraOAuthConfig'); -const OAUTH_MODULE = '@hackolade/oauth'; -const OAUTH_NOT_EXPOSED_MESSAGE = `The module ${OAUTH_MODULE} is not exposed by the application to the plugins!`; -const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; -const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token'; const REDIRECT_URI = 'http://localhost:8080'; const REDIRECT_PORT = 8080; -const getAuthorizeTenant = ({ connectionInfo = {} } = {}) => - connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; - const getAuthorizationQuery = ({ connectionInfo = {} } = {}) => { const authorizationQuery = { prompt: 'select_account', @@ -31,17 +25,18 @@ const getAuthorizationQuery = ({ connectionInfo = {} } = {}) => { }; }; -const exchangeAuthorizationCode = async ({ connectionInfo, logger } = {}) => { +const exchangeAuthorizationCode = async ({ app, connectionInfo, logger } = {}) => { try { + const { productionClientId, tokenEndpoint } = getEntraOAuthConfig({ app }); const urlParams = new URLSearchParams(); urlParams.append('code', connectionInfo?.externalBrowserQuery?.code || ''); - urlParams.append('client_id', ENTRA_CLIENT_ID); + urlParams.append('client_id', productionClientId); urlParams.append('redirect_uri', REDIRECT_URI); urlParams.append('grant_type', 'authorization_code'); urlParams.append('code_verifier', connectionInfo?.proofKey); - const response = await hckFetch(TOKEN_ENDPOINT, { + const response = await hckFetch(tokenEndpoint, { method: 'POST', headers: { 'Accept': 'application/json', @@ -60,29 +55,20 @@ const exchangeAuthorizationCode = async ({ connectionInfo, logger } = {}) => { } }; -const loadOauthModule = ({ app } = {}) => { - try { - return app.require(OAUTH_MODULE); - } catch (error) { - if (error?.message === OAUTH_NOT_EXPOSED_MESSAGE) { - return null; - } - - throw error; - } -}; - const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { const oauth = loadOauthModule({ app }); if (!oauth) { - return exchangeAuthorizationCode({ connectionInfo, logger }); + return exchangeAuthorizationCode({ app, connectionInfo, logger }); } + const { productionClientId, authorizationEndpoint, tokenEndpoint } = oauth.getEntraIdOAuthConfig({ + tenantId: resolveAuthorizeTenant({ connectionInfo }), + }); const { accessToken } = await oauth.getUserToMachineAccessToken({ - authorizationEndpoint: `https://login.microsoftonline.com/${getAuthorizeTenant({ connectionInfo })}/oauth2/authorize`, - tokenEndpoint: TOKEN_ENDPOINT, - clientId: ENTRA_CLIENT_ID, + authorizationEndpoint, + tokenEndpoint, + clientId: productionClientId, port: REDIRECT_PORT, failOnPortConflict: true, authorizationQuery: getAuthorizationQuery({ connectionInfo }), diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js index 2141cbd..4855bf6 100644 --- a/reverse_engineering/helpers/mfaAccessToken.spec.js +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -32,15 +32,21 @@ registerHooks({ }, }); -const { - getMfaAccessToken, - canRequestUserToMachineAccessToken, - shouldUseExternalBrowser, -} = require('./mfaAccessToken'); +const { getMfaAccessToken, canRequestUserToMachineAccessToken, shouldUseExternalBrowser } = require('./mfaAccessToken'); const ENTRA_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; const OAUTH_NOT_EXPOSED_MESSAGE = 'The module @hackolade/oauth is not exposed by the application to the plugins!'; +const getEntraIdOAuthConfig = ({ tenantId } = {}) => ({ + productionClientId: ENTRA_CLIENT_ID, + testingClientId: 'f28311c1-422d-4c77-8bbb-ff2fa1b84b7a', + authorizationEndpoint: tenantId + ? `https://login.microsoftonline.com/${tenantId}/oauth2/authorize` + : 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize', + tokenEndpoint: 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token', + adminConsentBase: 'https://login.microsoftonline.com/organizations/adminconsent', +}); + describe('getMfaAccessToken', { concurrency: false }, () => { it('asks Studio for a user-to-machine token with the Entra authorize request', async () => { const calls = []; @@ -49,6 +55,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { assert.equal(name, '@hackolade/oauth'); return { + getEntraIdOAuthConfig, async getUserToMachineAccessToken(options) { calls.push(options); @@ -95,6 +102,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { const app = { require() { return { + getEntraIdOAuthConfig, async getUserToMachineAccessToken(options) { calls.push(options); @@ -129,6 +137,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { const app = { require() { return { + getEntraIdOAuthConfig, async getUserToMachineAccessToken(options) { calls.push(options); @@ -151,6 +160,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { const app = { require() { return { + getEntraIdOAuthConfig, async getUserToMachineAccessToken() { return {}; }, @@ -193,6 +203,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { const app = { require() { return { + getEntraIdOAuthConfig, async getUserToMachineAccessToken() { throw failure; }, From 8ecd670b485fd9943740222b61b39bb9f6d4acb6 Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Thu, 22 Oct 2026 10:31:26 -0400 Subject: [PATCH 5/8] 0.2.36 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 910417f..82b6a4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Synapse", - "version": "0.2.35", + "version": "0.2.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Synapse", - "version": "0.2.35", + "version": "0.2.36", "dependencies": { "@hackolade/fetch": "1.3.0", "base64url": "3.0.1", diff --git a/package.json b/package.json index 060e267..7d9d4b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Synapse", - "version": "0.2.35", + "version": "0.2.36", "author": "hackolade", "engines": { "hackolade": "6.1.2", @@ -80,4 +80,4 @@ "uuid": "14.0.0", "brace-expansion": "5.0.12" } -} \ No newline at end of file +} From 6996a88e25ab99a88d77b2064a912fba13d6c77a Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Thu, 22 Oct 2026 11:31:13 -0400 Subject: [PATCH 6/8] Rename login function --- reverse_engineering/helpers/mfaAccessToken.js | 8 +++++--- .../helpers/mfaAccessToken.spec.js | 16 +++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index a6721df..99f6545 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -65,15 +65,17 @@ const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { const { productionClientId, authorizationEndpoint, tokenEndpoint } = oauth.getEntraIdOAuthConfig({ tenantId: resolveAuthorizeTenant({ connectionInfo }), }); - const { accessToken } = await oauth.getUserToMachineAccessToken({ + const { accessToken } = await oauth.browserLoginFlow({ authorizationEndpoint, tokenEndpoint, clientId: productionClientId, port: REDIRECT_PORT, failOnPortConflict: true, authorizationQuery: getAuthorizationQuery({ connectionInfo }), - headers: { - Origin: 'http://localhost', + tokenRequestOptions: { + headers: { + Origin: 'http://localhost', + }, }, }); diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js index 4855bf6..7f02cfd 100644 --- a/reverse_engineering/helpers/mfaAccessToken.spec.js +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -56,7 +56,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { return { getEntraIdOAuthConfig, - async getUserToMachineAccessToken(options) { + async browserLoginFlow(options) { calls.push(options); return { accessToken: 'sql-access-token' }; @@ -90,8 +90,10 @@ describe('getMfaAccessToken', { concurrency: false }, () => { resource: 'https://database.windows.net/', login_hint: 'user name@example.com', }, - headers: { - Origin: 'http://localhost', + tokenRequestOptions: { + headers: { + Origin: 'http://localhost', + }, }, }, ]); @@ -103,7 +105,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { require() { return { getEntraIdOAuthConfig, - async getUserToMachineAccessToken(options) { + async browserLoginFlow(options) { calls.push(options); return { accessToken: 'sql-access-token' }; @@ -138,7 +140,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { require() { return { getEntraIdOAuthConfig, - async getUserToMachineAccessToken(options) { + async browserLoginFlow(options) { calls.push(options); return { accessToken: 'sql-access-token' }; @@ -161,7 +163,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { require() { return { getEntraIdOAuthConfig, - async getUserToMachineAccessToken() { + async browserLoginFlow() { return {}; }, }; @@ -204,7 +206,7 @@ describe('getMfaAccessToken', { concurrency: false }, () => { require() { return { getEntraIdOAuthConfig, - async getUserToMachineAccessToken() { + async browserLoginFlow() { throw failure; }, }; From 86e31bffee22e4159e42264c8246daaf3206ce6b Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Thu, 22 Oct 2026 11:42:59 -0400 Subject: [PATCH 7/8] no need to add origin header anymore --- reverse_engineering/helpers/entraOAuthConfig.js | 2 ++ reverse_engineering/helpers/mfaAccessToken.js | 9 ++------- reverse_engineering/helpers/mfaAccessToken.spec.js | 6 +----- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/reverse_engineering/helpers/entraOAuthConfig.js b/reverse_engineering/helpers/entraOAuthConfig.js index ad357c5..bfc29ff 100644 --- a/reverse_engineering/helpers/entraOAuthConfig.js +++ b/reverse_engineering/helpers/entraOAuthConfig.js @@ -5,6 +5,7 @@ const OAUTH_NOT_EXPOSED_MESSAGE = `The module ${OAUTH_MODULE} is not exposed by const FALLBACK_PRODUCTION_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; const FALLBACK_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token'; const FALLBACK_AUTHORIZE_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize'; +const FALLBACK_DESKTOP_TOKEN_REQUEST_HEADERS = { Origin: 'http://localhost' }; const loadOauthModule = ({ app } = {}) => { try { @@ -27,6 +28,7 @@ const getLegacyEntraOAuthConfig = ({ tenantId } = {}) => ({ ? `https://login.microsoftonline.com/${tenantId}/oauth2/authorize` : FALLBACK_AUTHORIZE_ENDPOINT, tokenEndpoint: FALLBACK_TOKEN_ENDPOINT, + desktopTokenRequestHeaders: FALLBACK_DESKTOP_TOKEN_REQUEST_HEADERS, }); const getEntraOAuthConfig = ({ app, tenantId } = {}) => { diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index 99f6545..1065928 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -27,7 +27,7 @@ const getAuthorizationQuery = ({ connectionInfo = {} } = {}) => { const exchangeAuthorizationCode = async ({ app, connectionInfo, logger } = {}) => { try { - const { productionClientId, tokenEndpoint } = getEntraOAuthConfig({ app }); + const { productionClientId, tokenEndpoint, desktopTokenRequestHeaders } = getEntraOAuthConfig({ app }); const urlParams = new URLSearchParams(); urlParams.append('code', connectionInfo?.externalBrowserQuery?.code || ''); @@ -41,7 +41,7 @@ const exchangeAuthorizationCode = async ({ app, connectionInfo, logger } = {}) = headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'http://localhost', + ...desktopTokenRequestHeaders, }, body: urlParams, }); @@ -72,11 +72,6 @@ const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { port: REDIRECT_PORT, failOnPortConflict: true, authorizationQuery: getAuthorizationQuery({ connectionInfo }), - tokenRequestOptions: { - headers: { - Origin: 'http://localhost', - }, - }, }); if (!accessToken) { diff --git a/reverse_engineering/helpers/mfaAccessToken.spec.js b/reverse_engineering/helpers/mfaAccessToken.spec.js index 7f02cfd..ae32970 100644 --- a/reverse_engineering/helpers/mfaAccessToken.spec.js +++ b/reverse_engineering/helpers/mfaAccessToken.spec.js @@ -45,6 +45,7 @@ const getEntraIdOAuthConfig = ({ tenantId } = {}) => ({ : 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize', tokenEndpoint: 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token', adminConsentBase: 'https://login.microsoftonline.com/organizations/adminconsent', + desktopTokenRequestHeaders: { Origin: 'http://localhost' }, }); describe('getMfaAccessToken', { concurrency: false }, () => { @@ -90,11 +91,6 @@ describe('getMfaAccessToken', { concurrency: false }, () => { resource: 'https://database.windows.net/', login_hint: 'user name@example.com', }, - tokenRequestOptions: { - headers: { - Origin: 'http://localhost', - }, - }, }, ]); }); From 8e25cba6fef2d714605e47903fda75657b8b45a6 Mon Sep 17 00:00:00 2001 From: massintha azamoum Date: Thu, 22 Oct 2026 12:04:26 -0400 Subject: [PATCH 8/8] await was missing --- reverse_engineering/api.js | 2 +- reverse_engineering/databaseService/databaseService.js | 2 +- reverse_engineering/helpers/entraOAuthConfig.js | 2 +- reverse_engineering/helpers/mfaAccessToken.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index 65902d2..2ecbdc7 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -71,7 +71,7 @@ module.exports = { const base64Digest = crypto.createHash('sha256').update(verifier).digest('base64'); const challenge = base64url.fromBase64(base64Digest); const tenantId = resolveAuthorizeTenant({ connectionInfo }); - const { productionClientId, authorizationEndpoint } = getEntraOAuthConfig({ app, tenantId }); + const { productionClientId, authorizationEndpoint } = await getEntraOAuthConfig({ app, tenantId }); const loginHint = connectionInfo.loginHint ? `login_hint=${encodeURIComponent(connectionInfo.loginHint)}&` : ''; const redirectUrl = `http://localhost:${connectionInfo.redirectPort || 8080}`; diff --git a/reverse_engineering/databaseService/databaseService.js b/reverse_engineering/databaseService/databaseService.js index 710d3af..93127d5 100644 --- a/reverse_engineering/databaseService/databaseService.js +++ b/reverse_engineering/databaseService/databaseService.js @@ -39,7 +39,7 @@ const getConnectionClient = async (connectionInfo, logger, app) => { const clientId = connectionInfo.authMethod === 'Azure Active Directory (Username / Password)' - ? getEntraOAuthConfig({ app }).productionClientId + ? (await getEntraOAuthConfig({ app })).productionClientId : undefined; const redirectUri = 'http://localhost:8080'; diff --git a/reverse_engineering/helpers/entraOAuthConfig.js b/reverse_engineering/helpers/entraOAuthConfig.js index bfc29ff..0d3ee3b 100644 --- a/reverse_engineering/helpers/entraOAuthConfig.js +++ b/reverse_engineering/helpers/entraOAuthConfig.js @@ -31,7 +31,7 @@ const getLegacyEntraOAuthConfig = ({ tenantId } = {}) => ({ desktopTokenRequestHeaders: FALLBACK_DESKTOP_TOKEN_REQUEST_HEADERS, }); -const getEntraOAuthConfig = ({ app, tenantId } = {}) => { +const getEntraOAuthConfig = async ({ app, tenantId } = {}) => { const oauth = loadOauthModule({ app }); if (!oauth) { diff --git a/reverse_engineering/helpers/mfaAccessToken.js b/reverse_engineering/helpers/mfaAccessToken.js index 1065928..14b5893 100644 --- a/reverse_engineering/helpers/mfaAccessToken.js +++ b/reverse_engineering/helpers/mfaAccessToken.js @@ -27,7 +27,7 @@ const getAuthorizationQuery = ({ connectionInfo = {} } = {}) => { const exchangeAuthorizationCode = async ({ app, connectionInfo, logger } = {}) => { try { - const { productionClientId, tokenEndpoint, desktopTokenRequestHeaders } = getEntraOAuthConfig({ app }); + const { productionClientId, tokenEndpoint, desktopTokenRequestHeaders } = await getEntraOAuthConfig({ app }); const urlParams = new URLSearchParams(); urlParams.append('code', connectionInfo?.externalBrowserQuery?.code || ''); @@ -62,7 +62,7 @@ const getMfaAccessToken = async ({ app, connectionInfo, logger } = {}) => { return exchangeAuthorizationCode({ app, connectionInfo, logger }); } - const { productionClientId, authorizationEndpoint, tokenEndpoint } = oauth.getEntraIdOAuthConfig({ + const { productionClientId, authorizationEndpoint, tokenEndpoint } = await oauth.getEntraIdOAuthConfig({ tenantId: resolveAuthorizeTenant({ connectionInfo }), }); const { accessToken } = await oauth.browserLoginFlow({