Skip to content

Commit 41077fe

Browse files
martinlingstuylmilanholemans
authored andcommitted
Adds federated identity for GitHub Actions. Closes #6610
1 parent 71b45af commit 41077fe

File tree

5 files changed

+156
-4
lines changed

5 files changed

+156
-4
lines changed

docs/docs/cmd/login.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ m365 login [options]
2222
: ID of the tenant from which accounts should authenticate. Use `common` or `organization` for multitenant apps. Defaults to `common` if not specified and if the config value `tenantId` and the environment variable `CLIMICROSOFT365_TENANT` are also not set.
2323

2424
`-t, --authType [authType]`
25-
: The type of authentication to use. Allowed values `certificate`, `deviceCode`, `password`, `identity`, `browser`, `secret`. Default `deviceCode`
25+
: The type of authentication to use. Allowed values `certificate`, `deviceCode`, `password`, `identity`, `federatedIdentity`, `browser`, `secret`. Default `deviceCode`
2626

2727
`-u, --userName [userName]`
2828
: Name of the user to authenticate. Required when `authType` is set to `password`
@@ -78,6 +78,8 @@ When logging in to Microsoft 365 using the user name and password, next to the a
7878

7979
When logging in to Microsoft 365 using a certificate, the CLI for Microsoft 365 will store the contents of the certificate so that it can automatically re-authenticate if necessary. The contents of the certificate are removed by re-authenticating using the device code or by calling the [logout](logout.mdx) command.
8080

81+
Federated Identity is currently supported in GitHub Actions. To use this you need to set `authType` to `federatedIdentity` and specify `appId` and `tenant` to the values of the app registration you've configured a federated credential on.
82+
8183
To log in to Microsoft 365 using a certificate or secret, you will typically [create a custom Microsoft Entra application](../user-guide/using-own-identity.mdx). To use this application with the CLI for Microsoft 365, you will set the `CLIMICROSOFT365_ENTRAAPPID` environment variable to the application's ID and the `CLIMICROSOFT365_TENANT` environment variable to the ID of the Microsoft Entra tenant, where you created the Microsoft Entra application. Also, please make sure to read about [the caveats when using the certificate login option](../user-guide/cli-certificate-caveats.mdx).
8284

8385
Managed identity in Azure Cloud Shell is the identity of the user. It is neither system- nor user-assigned and it can't be configured. To log in to Microsoft 365 using managed identity in Azure Cloud Shell, set `authType` to `identity` and don't specify the `userName` option.
@@ -182,6 +184,12 @@ Log in to Microsoft 365 using a user-assigned managed identity with `clientId` s
182184
m365 login --authType identity --userName ac9fbed5-804c-4362-a369-21a4ec51109e
183185
```
184186

187+
Log in to Microsoft 365 using a federated identity. Can currently only be used in GitHub Actions.
188+
189+
```sh
190+
m365 login --authType federatedIdentity --appId eb96cfd0-abfa-4477-8d1d-c6dfde3efb19 --tenant 9a1a1bbd-e158-4cf5-967d-a53b8e85c628
191+
```
192+
185193
Log in to Microsoft 365 using the interactive browser authentication with `clientId` set in the configuration. Uses the identity of the current user.
186194

187195
```sh

src/Auth.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import request from './request.js';
1515
import { accessToken } from "./utils/accessToken.js";
1616
import { browserUtil } from "./utils/browserUtil.js";
1717
import { sinonUtil } from './utils/sinonUtil.js';
18+
import { CommandError } from "./Command.js";
1819

1920
class MockTokenStorage implements TokenStorage {
2021
public async get(): Promise<string> {
@@ -70,6 +71,7 @@ describe('Auth', () => {
7071
const identityTenantId = '9bc3ab49-b65d-410a-85ad-de819febfddd';
7172
const appId = '9bc3ab49-b65d-410a-85ad-de819febfddc';
7273
const tenant = '9bc3ab49-b65d-410a-85ad-de819febfddd';
74+
const federatedIdentityAudience = 'api://AzureADTokenExchange';
7375
const activeConnection: Connection = { name: identityId, identityId, identityName, active: true, appId, tenant, authType: AuthType.DeviceCode, certificateType: CertificateType.Unknown, accessTokens: {}, cloudType: CloudType.Public, identityTenantId: identityTenantId, deactivate: () => { } };
7476
const base64EncodedPemCert = 'QmFnIEF0dHJpYnV0ZXMNCiAgICBsb2NhbEtleUlEOiBDQyBGNCBGMiBBMyBDMyBEMiAwOSBDNSAxMiBCMyA3MiA0QiBCOCA4MyBBNSA0NyA0QyAwOSAyMSBEQyANCnN1YmplY3Q9QyA9IEFVLCBTVCA9IFNvbWUtU3RhdGUsIE8gPSBJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQNCg0KaXNzdWVyPUMgPSBBVSwgU1QgPSBTb21lLVN0YXRlLCBPID0gSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkDQoNCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRGF6Q0NBbE9nQXdJQkFnSVVXb25VNFM0RTcxRjVZMU5zU0xYbUlhZ1dkNVl3RFFZSktvWklodmNOQVFFTA0KQlFBd1JURUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTQ0KR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nVUhSNUlFeDBaREFlRncweE9UQTNNVEl5TVRVek1qbGFGdzB5TURBMw0KTVRFeU1UVXpNamxhTUVVeEN6QUpCZ05WQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFdw0KSHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQg0KQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNsa01lQXlKbTJkMy95aEV0NHZGYjYrYjEyUGxRSDB4VGx1a1BoK2xScg0KOXJDNk5DM3dObnoySm5vbE1HclhuZVp2TlN5czFONVpSTm0yTjhQdy9QOExxeHJSenFFOFBNVC96NnN1UFhSUg0KWm5hZ2xaUklXb0NNR25pRVlDZVJHZnI4R2JpUXcwYlZEeXFuSnJaZjByS0pHbnZUNlY3QmpUdFloRWIzeXhoNA0KSmNUSnIrVDl0OEFYaldmemt6alBZdklxYmhha3FxcHd1SEVPYkh4T201cHVERTFBNVJOZm8wamcrTmZtVko5VQ0KMWR1RjVzdmE2NVQ5Q1RtdEdlbVNlUGlzWmgxZmhoOS94QmJwTCs0RUJWUXZqdEZXWk5zMVJHMW9QUllscmpzaQ0KTXFsaHNUdjhDZXI5cWUxcVNTdHFjMmJsc3hGek1zNmxZOHAvUHIrYm5uR3pBZ01CQUFHalV6QlJNQjBHQTFVZA0KRGdRV0JCU203cWFreXQwY2xxN0lnRFRWdkUrWEpaNFU5akFmQmdOVkhTTUVHREFXZ0JTbTdxYWt5dDBjbHE3SQ0KZ0RUVnZFK1hKWjRVOWpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBYQ0KQnVqTytveU0yL0Q0SzNpS3lqVDVzbHF2UFVlVzFrZVVXYVdSVDZXRTY0VkFPbTlPZzU1bkIyOE5TSVVXampXMA0KdTJEUHF3SzJiOEFXalEveWp3S3NUMXVTdzcyQ0VEY2o3SkE1VXA5UWpBa0hIZmFoQWtOd0o5M0llcmFBdTEyVQ0KN25FRDdIN20yeGZscDVwM0dadzNHUE0rZmpBaDZLOUZIRDI0bWdGUTh4b2JPQSttVEVvV2ZIVVQrZ1pUMGxYdQ0KazFrVTJVelVOd2dwc3c4V04wNFFzWU5XcFF5d3ppUWtuZTQzNW5tdmxZOGZRc2hPSnErK0JCS0thd0xEcjk3bA0KRTBYQUxEZDZlVVhQenZ5OU1xZlozeUswRmUzMy8zbnZnUnE4QWZ3azRsbzhac2ZYWUlSTXA3b3BER0VmaUZmNQ0KM3JTTGxSZG9TNDQ4OVFZRnAyYUQNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NCkJhZyBBdHRyaWJ1dGVzDQogICAgbG9jYWxLZXlJRDogQ0MgRjQgRjIgQTMgQzMgRDIgMDkgQzUgMTIgQjMgNzIgNEIgQjggODMgQTUgNDcgNEMgMDkgMjEgREMgDQpLZXkgQXR0cmlidXRlczogPE5vIEF0dHJpYnV0ZXM+DQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0NCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ2xrTWVBeUptMmQzL3kNCmhFdDR2RmI2K2IxMlBsUUgweFRsdWtQaCtsUnI5ckM2TkMzd05uejJKbm9sTUdyWG5lWnZOU3lzMU41WlJObTINCk44UHcvUDhMcXhyUnpxRThQTVQvejZzdVBYUlJabmFnbFpSSVdvQ01HbmlFWUNlUkdmcjhHYmlRdzBiVkR5cW4NCkpyWmYwcktKR252VDZWN0JqVHRZaEViM3l4aDRKY1RKcitUOXQ4QVhqV2Z6a3pqUFl2SXFiaGFrcXFwd3VIRU8NCmJIeE9tNXB1REUxQTVSTmZvMGpnK05mbVZKOVUxZHVGNXN2YTY1VDlDVG10R2VtU2VQaXNaaDFmaGg5L3hCYnANCkwrNEVCVlF2anRGV1pOczFSRzFvUFJZbHJqc2lNcWxoc1R2OENlcjlxZTFxU1N0cWMyYmxzeEZ6TXM2bFk4cC8NClByK2Jubkd6QWdNQkFBRUNnZ0VBUjRsMytqZ3kybmxseWtiSlNXQ3ZnSCs2RWtZNkRxdHd3eFlwVUpIV09sUDcNCjVtaTNWS3htY0FFT0U5V0l4S05RTnNyV0E5TnlRMFlSZjc4MnBZRGJQcEp1NHlxUjFqSTN1SVJsWlhSZU52RzcNCjNnVGpiaVBVbVRTeTBCZXY0TzFGMmZuUEdwV1ZuR2VTT1dqcnNobWExTXlocGwyV2VMRHFiSU96R2t3aHhYOXkNClRhRFd5MjErbDFpNVNGWUZTdHdXOWlhOXRORTFTTTU4WnpQWk0yK0NDdHhQVEFBQXRJRmZXUVdTbnhodUxMenMNCjNyVDRVOGNLZzJITVBXb29rOS9peWxsa0xEVXBPanhJR2tHWXdheDVnR2xvR0xZYWVoelc5Q3hobzgvc3A4WjUNCkVNNVFvczVJSTF2K21pNHhHa0RTdW4rbDYzcDN5Nm54T3pqM1h1MzRlUUtCZ1FEUDNtRWttN2lVaTlhRUxweXYNCkIxeDFlRFR2UmEwcllZMHZUaXFrYzhyUGc0NU1uOUNWRWZqdnV3YkN4M21tTExabThqZVY3ZTFHWjZJeXYreEUNCmcxeFkrUTd0RUlCb1FwWThlemg0UVYvMXRkZkhiUzNPcGdIbHVqMGd5MWxqT2QrbkxzS2RNQWRlYVF3Uy9WK2MNCk51Sks0Y3oyQWl6UXU1dHQ4WHdoOGdvU0Z3S0JnUURMNXRjZnF0VmdMQWJmMnJQbEhBLzdNcU1sWGpqNUQ0ejkNCjZmTWlCVDdOWHlYUGx6a2pJQkxOdG9OWlBCVTFzeERFb2tiNUtyTlhLTUtIaU9nTkQ0cWtDYkdnRFk2WUdaS3cNCkg4bDlLWDBaM2pwcEp0TURvQ21yQW9hSmZTUXNreGJXSDd4VlFGVzdPVWQ0dHMxZ3FDbTBUTFVxeW9lcW1EK3INCmg3WFlaa2RxeFFLQmdBK2NpZnN2M3NyNVBhRXJ4d1MyTHRGN3Q2NElzNXJBZHRRSXNOY3RBeHhXcXdkQ01XNGcNCnJXdUR4bHcya3dKUjlWa0I4LzdFb2I5WjVTcWVrMllKMzVPbkVPSHBEVnZITkhWU1k4bFVUNXFxajR3Z3ZRSDYNCkljWlpHR0l3STRSNlFqdlNIVGVrOWNpM1p2cStJTUlndFJvZW4wQVNwYjcvZUFybnlnVGFvcnI5QW9HQkFJT3QNCllOSEhqaUtjYkJnV2NjU01tZGw4T3hXL3dvVTlRSzBkYjNGUjk5dkREWFVCVU5uWk5hdDVxVnR3VExZd0hLMFANCnEwdndBbjlRQ0VoazVvN0FzYVQ3eWFUMS9GZEhkSTZmQ0l6MnhSNTJnRHcxNFdIZkJlbTFLTk1UYU5BTWNWdjQNCmhMUjlacUFRL3BIN1k2aC9FT2VwL2ZsVGI4ZUFxT1dLTDZvL2F2R05Bb0dCQUlHc0c1VExuSmlPU044SUtGU04NCmJmK3IrNkhWL2R6MkluNjhSR255MTB0OGpwbUpPbGgrdXRncGtvOXI2Y09uWGY4VHM2SFAveTBtbDl5YXhvMlANCm52c2wwcFlseFQxQy9taXJaZWxYKzFaQTltdFpHT2RxbzZhdVZUM1drcXBpb3c2WUtzbzl2Z2RHWmRWRUxiMEINCnUvdyt4UjBvN21aSEpwVEdmS09KdE53MQ0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQ0K';
7577
const base64EncodedPfxCert = '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';
@@ -1381,6 +1383,66 @@ describe('Auth', () => {
13811383
assert.strictEqual((requestStub.lastCall.args[0] as any).headers['x-anonymous'], true);
13821384
});
13831385

1386+
it('calls api with correct params using federated identity flow from GitHub Action', async () => {
1387+
process.env = {
1388+
ACTIONS_ID_TOKEN_REQUEST_URL: 'https://pipelinesghubeus2.actions.githubusercontent.com/P0Hx2m9SFWNP5AeJ8UVAwq97ADEUmaXrYvynThDBYkADVfIDh4/00000000-0000-0000-0000-000000000000/_apis/distributedtask/hubs/Actions/plans/485ea249-6e9d-4c8d-8853-c57725625712/jobs/97ef1aa6-b90f-40f0-81a4-9120b120ceaa/idtoken?api-version=2.0',
1389+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'eyJ0eXAiOiJKV1QiLCJ-GitHubFederationToken-...'
1390+
};
1391+
const federatedIdentityClientAssertion = 'eyJ0eXAiOiJKV1QiLCJ-GitHubFederationClientAssertion-...';
1392+
const accessToken = 'eyJ0eXAiOiJKV1QiLCJ-EntraIDAccessToken...';
1393+
1394+
const requestGetStub = sinon.stub(request, 'get').callsFake(async (opts) => {
1395+
if (opts.url === `${process.env.ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${encodeURIComponent(federatedIdentityAudience)}`) {
1396+
return {
1397+
"value": federatedIdentityClientAssertion
1398+
};
1399+
}
1400+
throw { error: { "error": "Invalid GET Request" } };
1401+
});
1402+
1403+
const requestPostStub = sinon.stub(request, 'post').callsFake(async (opts) => {
1404+
if (opts.url === `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`) {
1405+
return {
1406+
"access_token": accessToken,
1407+
"client_id": "a04566df-9a65-4e90-ae3d-574572a16423",
1408+
"expires_in": "86399",
1409+
"expires_on": "1587847593",
1410+
"ext_expires_in": "86399",
1411+
"not_before": "1587760893",
1412+
"resource": "https://contoso.sharepoint.com/",
1413+
"token_type": "Bearer"
1414+
};
1415+
}
1416+
1417+
throw { error: { "error": "Invalid POST Request" } };
1418+
});
1419+
1420+
auth.connection.authType = AuthType.FederatedIdentity;
1421+
auth.connection.appId = appId;
1422+
auth.connection.tenant = tenant;
1423+
1424+
const ensuredAccessToken = await auth.ensureAccessToken(resource, logger, true);
1425+
assert.strictEqual(ensuredAccessToken, accessToken);
1426+
assert(requestGetStub.calledOnce);
1427+
assert.strictEqual(requestGetStub.firstCall.args[0].headers?.Authorization, `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`);
1428+
assert(requestPostStub.calledOnce);
1429+
assert.strictEqual(requestPostStub.firstCall.args[0].data.indexOf(`client_id=${appId}`) > -1, true);
1430+
assert.strictEqual(requestPostStub.firstCall.args[0].data.indexOf(`client_assertion=${federatedIdentityClientAssertion}`) > -1, true);
1431+
});
1432+
1433+
it('fails when not using federated identity from GitHub Action', async () => {
1434+
process.env = {
1435+
ACTIONS_ID_TOKEN_REQUEST_URL: '',
1436+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1437+
};
1438+
1439+
auth.connection.authType = AuthType.FederatedIdentity;
1440+
auth.connection.appId = appId;
1441+
auth.connection.tenant = tenant;
1442+
1443+
await assert.rejects(auth.ensureAccessToken(resource, logger, true), new CommandError('Federated identity is currently only supported in GitHub Actions.'));
1444+
});
1445+
13841446
it('returns access token if persisting connection fails', async () => {
13851447
sinonUtil.restore(auth.storeConnectionInfo);
13861448
sinon.stub(auth as any, 'getPublicClient').returns(publicApplication);

src/Auth.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { msalCachePlugin } from './auth/msalCachePlugin.js';
1111
import { Logger } from './cli/Logger.js';
1212
import { cli } from './cli/cli.js';
1313
import { ConnectionDetails } from './m365/commands/ConnectionDetails.js';
14-
import request from './request.js';
14+
import request, { CliRequestOptions } from './request.js';
1515
import { settingsNames } from './settingsNames.js';
1616
import * as accessTokenUtil from './utils/accessToken.js';
1717
import { browserUtil } from './utils/browserUtil.js';
@@ -104,6 +104,7 @@ export enum AuthType {
104104
Password = 'password',
105105
Certificate = 'certificate',
106106
Identity = 'identity',
107+
FederatedIdentity = 'federatedIdentity',
107108
Browser = 'browser',
108109
Secret = 'secret'
109110
}
@@ -225,7 +226,8 @@ export class Auth {
225226
// wasn't specified
226227
if (this.connection.authType !== AuthType.Certificate &&
227228
this.connection.authType !== AuthType.Secret &&
228-
this.connection.authType !== AuthType.Identity) {
229+
this.connection.authType !== AuthType.Identity &&
230+
this.connection.authType !== AuthType.FederatedIdentity) {
229231
this.clientApplication = await this.getPublicClient(logger, debug);
230232
if (this.clientApplication) {
231233
const accounts = await this.clientApplication.getTokenCache().getAllAccounts();
@@ -250,6 +252,9 @@ export class Auth {
250252
case AuthType.Identity:
251253
getTokenPromise = this.ensureAccessTokenWithIdentity.bind(this);
252254
break;
255+
case AuthType.FederatedIdentity:
256+
getTokenPromise = this.ensureAccessTokenWithFederatedIdentity.bind(this);
257+
break;
253258
case AuthType.Browser:
254259
getTokenPromise = this.ensureAccessTokenWithBrowser.bind(this);
255260
break;
@@ -693,6 +698,68 @@ export class Auth {
693698
}
694699
}
695700

701+
private async ensureAccessTokenWithFederatedIdentity(resource: string, logger: Logger, debug: boolean): Promise<AccessToken | null> {
702+
if (debug) {
703+
await logger.logToStderr('Trying to retrieve access token using federated identity...');
704+
}
705+
706+
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL || !process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) {
707+
throw new CommandError('Federated identity is currently only supported in GitHub Actions.');
708+
}
709+
710+
if (debug) {
711+
await logger.logToStderr('ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN env variables found. The context is GitHub Actions...');
712+
}
713+
714+
const federationToken = await this.getFederationTokenFromGithub(logger, debug);
715+
716+
const queryParams = [
717+
'grant_type=client_credentials',
718+
`scope=${encodeURIComponent(`${resource}/.default`)}`,
719+
`client_id=${this.connection.appId}`,
720+
`client_assertion_type=${encodeURIComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer')}`,
721+
`client_assertion=${federationToken}`
722+
];
723+
724+
const requestOptions: CliRequestOptions = {
725+
url: `https://login.microsoftonline.com/${this.connection.tenant}/oauth2/v2.0/token`,
726+
headers: {
727+
accept: 'application/json',
728+
'x-anonymous': true,
729+
'Content-Type': 'application/x-www-form-urlencoded'
730+
},
731+
data: queryParams.join('&'),
732+
responseType: 'json'
733+
};
734+
735+
const accessTokenResponse = await request.post<{ access_token: string; expires_on: string }>(requestOptions);
736+
737+
return {
738+
accessToken: accessTokenResponse.access_token,
739+
expiresOn: new Date(parseInt(accessTokenResponse.expires_on) * 1000)
740+
};
741+
}
742+
743+
private async getFederationTokenFromGithub(logger: Logger, debug: boolean): Promise<string> {
744+
if (debug) {
745+
await logger.logToStderr('Retrieving GitHub federation token...');
746+
}
747+
748+
const requestOptions: CliRequestOptions = {
749+
url: `${process.env.ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${encodeURIComponent('api://AzureADTokenExchange')}`,
750+
headers: {
751+
Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
752+
accept: 'application/json',
753+
'x-anonymous': true
754+
},
755+
responseType: 'json'
756+
};
757+
758+
const accessTokenResponse = await request.get<{ value: string }>(requestOptions);
759+
760+
return accessTokenResponse.value;
761+
}
762+
696763
private async ensureAccessTokenWithSecret(resource: string, logger: Logger, debug: boolean, fetchNew: boolean): Promise<AccessToken | null> {
697764
this.clientApplication = await this.getConfidentialClient(logger, debug, undefined, undefined, this.connection.secret);
698765
return (this.clientApplication as Msal.ConfidentialClientApplication).acquireTokenByClientCredential({
@@ -787,7 +854,8 @@ export class Auth {
787854
// When using an application identity, there is no account in the MSAL TokenCache
788855
if (this.connection.authType !== AuthType.Certificate &&
789856
this.connection.authType !== AuthType.Secret &&
790-
this.connection.authType !== AuthType.Identity) {
857+
this.connection.authType !== AuthType.Identity &&
858+
this.connection.authType !== AuthType.FederatedIdentity) {
791859
this.clientApplication = await this.getPublicClient(logger, debug);
792860

793861
if (this.clientApplication) {

0 commit comments

Comments
 (0)