Skip to content

Commit

Permalink
Add support for InTune Single-Sign-On
Browse files Browse the repository at this point in the history
This add support for communicating with the Microsoft Authentication
Broker over its DBus interface in order to retrieve an authentication
cookie, that can be used to automatically login the user currently
logged-in via InTune. This also adds support for MFA and Conditional
Access, which allows use of Teams outside of corporate network in case
the organization has chosen to only allow access from registered
devices.

Behind the scene this uses the same mechanism as Microsoft Edge on
Linux: upon loading a website from login.microsoftonline.com the URL
is passed to the authentication broker in order to prepare a token
based on the PRT (Primary Refresh Token). The returned refresh token
is passed to the server via the 'X-Ms-Refreshtokencredential' HTTP
header. With this token in place the server will skip any interactive
prompts and generate a proper OAuth authentication token. Since the
PRT is tied to the device credentials, the resulting refresh token
carries the MFA attribute, which causes it to be accepted even if the
Conditional Access policy mandates strong, device-based
authentication.

Signed-off-by: Krzysztof Nowicki <[email protected]>
  • Loading branch information
Krzysztof Nowicki committed Jun 3, 2024
1 parent 221dafe commit df06898
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 5 deletions.
2 changes: 2 additions & 0 deletions app/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Here is the list of available arguments and its usage:
| spellCheckerLanguages | Array of languages to use with Electron's spell checker | [] |
| ssoBasicAuthUser | Login that will be sent for basic_auth SSO login. | string |
| ssoBasicAuthPasswordCommand | Command to execute, grab stdout and use it as a password for basic_auth SSO login. | string |
| ssoIntuneEnabled | Enable InTune Single-Sign-On | false
| ssoIntuneAuthUser | User (e-mail) to be used for InTune SSO login. | string |
| trayIconEnabled | Enable tray icon | true |
| url | Microsoft Teams URL | string |
| useMutationTitleLogic | Use MutationObserver to update counter from title | true |
Expand Down
10 changes: 10 additions & 0 deletions app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ function argv(configPath, appVersion) {
describe: 'Command to execute to retrieve password for SSO basic auth.',
type: 'string'
},
ssoInTuneEnabled: {
default: false,
describe: 'Enable Single-Sign-On using Microsoft InTune.',
type: 'boolean'
},
ssoInTuneAuthUser: {
default: '',
describe: 'User (e-mail) to use for InTune SSO.',
type: 'string'
},
trayIconEnabled: {
default: true,
describe: 'Enable tray icon',
Expand Down
85 changes: 85 additions & 0 deletions app/intune/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const dbus = require('dbus-native');
const { LucidLog } = require('lucid-log');

var sessionBus = dbus.sessionBus();

var intuneAccount = null;

var brokerService = sessionBus.getService('com.microsoft.identity.broker1');

exports.initSso = function initIntuneSso(logger, ssoIntuneAuthUser) {
logger.debug("Initializing InTune SSO");
brokerService.getInterface(
'/com/microsoft/identity/broker1',
'com.microsoft.identity.Broker1', function(err, broker) {
if (err) {
logger.warn('Failed to find microsoft-identity-broker DBus interface');
} else {
broker.getAccounts('0.0', '', JSON.stringify({'clientId': '88200948-af09-45a1-9c03-53cdcc75c183', 'redirectUri':'urn:ietf:oob'}), function(err, resp) {
if (err) {
logger.warn('Failed to communicate with microsoft-identity-broker');
} else {
response = JSON.parse(resp);
if ('error' in response) {
logger.warn('Failed to retrieve InTune account list: ' + response.error.context);
} else {
if (ssoIntuneAuthUser == '') {
intuneAccount = response.accounts[0];
logger.debug('Using first available InTune account (' + intuneAccount.username + ')');
} else {
for (account in response.accounts) {
if (account.username == ssoIntuneAuthUser) {
intuneAccount = account;
logger.debug('Found matching InTune account (' + intuneAccount.username + ')');
break;
}
}
if (intuneAccount == null) {
logger.warn('Failed to find matching InTune account for ' + ssoIntuneAuthUser + '.');
}
}
}
}
});
}
});
}

exports.setupUrlFilter = function setupUrlFilter(filter) {
filter.urls.push('https://login.microsoftonline.com/*');
}

exports.isSsoUrl = function isSsoUrl(url) {
return intuneAccount != null && url.startsWith('https://login.microsoftonline.com/');
}

exports.addSsoCookie = function addIntuneSsoCookie(logger, detail, callback) {
logger.debug('Retrieving InTune SSO cookie');
if (intuneAccount == null) {
logger.info("InTune SSO not active");
callback({
requestHeaders: detail.requestHeaders
});
} else {
brokerService.getInterface(
'/com/microsoft/identity/broker1',
'com.microsoft.identity.Broker1', function(err, broker) {
broker.acquirePrtSsoCookie('0.0', '', JSON.stringify({'ssoUrl':detail.url, 'account':intuneAccount, 'authParameters':{'authority':'https://login.microsoftonline.com/common/'}}), function(err, resp) {
response = JSON.parse(resp);
if ('error' in response) {
logger.warn('Failed to retrieve Intune SSO cookie: ' + response.error.context);
callback({
requestHeaders: detail.requestHeaders
});
} else {
logger.debug('Adding SSO credential');
detail.requestHeaders['X-Ms-Refreshtokencredential'] = response['cookieContent'];
callback({
requestHeaders: detail.requestHeaders
});
}
});
});
}
}

20 changes: 15 additions & 5 deletions app/mainAppWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TrayIconChooser = require('../browser/tools/trayIconChooser');
const { AppConfiguration } = require('../appConfiguration');
const connMgr = require('../connectionManager');
const fs = require('fs');
const intune = require('../intune');

/**
* @type {TrayIconChooser}
Expand Down Expand Up @@ -65,6 +66,10 @@ exports.onAppReady = async function onAppReady(configGroup) {
levels: config.appLogLevels.split(',')
});

if (config.ssoInTuneEnabled) {
intune.initSso(logger, config.ssoInTuneAuthUser);
}

window = await createWindow();

if (config.trayIconEnabled) {
Expand Down Expand Up @@ -323,12 +328,16 @@ function setImgSrcSecurityPolicy(policies) {
* @param {Electron.BeforeSendResponse} callback
*/
function onBeforeSendHeadersHandler(detail, callback) {
if (detail.url.startsWith(customBGServiceUrl.href)) {
detail.requestHeaders['Access-Control-Allow-Origin'] = '*';
if (intune.isSsoUrl(detail.url)) {
intune.addSsoCookie(logger, detail, callback);
} else {
if (detail.url.startsWith(customBGServiceUrl.href)) {
detail.requestHeaders['Access-Control-Allow-Origin'] = '*';
}
callback({
requestHeaders: detail.requestHeaders
});
}
callback({
requestHeaders: detail.requestHeaders
});
}

/**
Expand Down Expand Up @@ -409,6 +418,7 @@ function addEventHandlers() {

function getWebRequestFilterFromURL() {
const filter = customBGServiceUrl.protocol === 'http:' ? { urls: ['http://*/*'] } : { urls: ['https://*/*'] };
intune.setupUrlFilter(filter);
return filter;
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"release": "electron-builder"
},
"dependencies": {
"dbus-native": "0.4.0",
"@electron/remote": "^2.1.2",
"electron-is-dev": "2.0.0",
"electron-store": "8.2.0",
Expand Down
Loading

0 comments on commit df06898

Please sign in to comment.