diff --git a/libraries/botbuilder/package.json b/libraries/botbuilder/package.json index 50cd274dff..257891e736 100644 --- a/libraries/botbuilder/package.json +++ b/libraries/botbuilder/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@azure/ms-rest-js": "^2.7.0", + "@azure/msal-node": "^1.2.0", "axios": "^0.25.0", "botbuilder-core": "4.1.6", "botbuilder-stdlib": "4.1.6", diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index 96421354f1..8ab7ff957d 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -3,12 +3,14 @@ const nock = require('nock'); const sinon = require('sinon'); const { BotFrameworkAdapter, TeamsInfo, CloudAdapter } = require('../'); const { Conversations } = require('botframework-connector/lib/connectorApi/operations'); -const { MicrosoftAppCredentials, ConnectorClient } = require('botframework-connector'); +const { MicrosoftAppCredentials, ConnectorClient, MsalAppCredentials } = require('botframework-connector'); const { TurnContext, MessageFactory, ActionTypes, Channels } = require('botbuilder-core'); +const { ConfidentialClientApplication } = require('@azure/msal-node'); class TeamsInfoAdapter extends BotFrameworkAdapter { constructor() { super({ appId: 'appId', appPassword: 'appPassword' }); + this.credentials = getCredentials(); } } @@ -181,11 +183,27 @@ const teamActivity = { }, }; -describe('TeamsInfo', function () { - const connectorClient = new ConnectorClient(new MicrosoftAppCredentials('abc', '123'), { - baseUri: 'https://smba.trafficmanager.net/amer/', +function getBearerToken() { + const tokenType = 'Bearer'; + const accessToken = 'access_token'; + + return `${tokenType} ${accessToken}`; +} + +function getCredentials() { + const accessToken = getBearerToken().split(' ')[1]; + const confidential = new ConfidentialClientApplication({ + auth: { + clientId: 'appId', + clientSecret: 'appPassword', + }, }); + sinon.stub(confidential, 'acquireTokenByClientCredential').returns({ accessToken, expiresOn: new Date() }); + return new MsalAppCredentials(confidential, 'appId'); +} + +describe('TeamsInfo', function () { beforeEach(function () { nock.cleanAll(); }); @@ -194,18 +212,9 @@ describe('TeamsInfo', function () { nock.cleanAll(); }); - // Sets up nock expectation for an oauth token call, returning the expected auth header - // and the nock expectation - const nockOauth = () => { - const tokenType = 'Bearer'; - const accessToken = 'access_token'; - - const expectation = nock('https://login.microsoftonline.com') - .post(/\/oauth2\/token/) - .reply(200, { access_token: accessToken, token_type: tokenType }); - - return { expectedAuthHeader: `${tokenType} ${accessToken}`, expectation }; - }; + const connectorClient = new ConnectorClient(getCredentials(), { + baseUri: 'https://smba.trafficmanager.net/amer/', + }); describe('sendMessageToTeamsChannel()', function () { it('should work with correct information', async function () { @@ -216,7 +225,7 @@ describe('TeamsInfo', function () { 'resourceresponseid', ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchNewConversation = nock('https://smba.trafficmanager.net/amer') .post('/v3/conversations') @@ -229,7 +238,6 @@ describe('TeamsInfo', function () { const response = await TeamsInfo.sendMessageToTeamsChannel(context, msg, teamChannelId); - assert(fetchOauthToken.isDone()); assert(fetchNewConversation.isDone()); assert(Array.isArray(response)); @@ -338,7 +346,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/teams/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/conversations') @@ -349,7 +357,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const channels = await TeamsInfo.getTeamChannels(context); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert(Array.isArray(channels)); @@ -378,7 +385,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/teams/19%3AChannelIdgeneralChannelId%40thread.skype/conversations') @@ -389,7 +396,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const channels = await TeamsInfo.getTeamChannels(context, '19:ChannelIdgeneralChannelId@thread.skype'); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert(Array.isArray(channels)); @@ -430,7 +436,7 @@ describe('TeamsInfo', function () { type: 'standard', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchTeamDetailsExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/teams/19%3AgeneralChannelIdgeneralChannelId%40thread.skype') @@ -441,7 +447,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedTeamDetails = await TeamsInfo.getTeamDetails(context); - assert(fetchOauthToken.isDone()); assert(fetchTeamDetailsExpectation.isDone()); assert(fetchedTeamDetails, `teamDetails should not be falsey: ${teamDetails}`); @@ -458,7 +463,7 @@ describe('TeamsInfo', function () { aadGroupId: 'Team-aadGroupId', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchTeamDetailsExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/teams/19%3AChannelIdgeneralChannelId%40thread.skype') @@ -472,7 +477,6 @@ describe('TeamsInfo', function () { '19:ChannelIdgeneralChannelId@thread.skype' ); - assert(fetchOauthToken.isDone()); assert(fetchTeamDetailsExpectation.isDone()); assert(fetchedTeamDetails, `teamDetails should not be falsey: ${teamDetails}`); @@ -497,7 +501,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/a%3AoneOnOneConversationId/members') @@ -508,7 +512,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMembers = await TeamsInfo.getMembers(context); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert.deepStrictEqual( @@ -551,7 +554,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/19%3AgroupChatId%40thread.v2/members') @@ -562,7 +565,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMembers = await TeamsInfo.getMembers(context); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert.deepStrictEqual( @@ -595,7 +597,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/members') @@ -606,7 +608,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMembers = await TeamsInfo.getMembers(context); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert.deepStrictEqual( @@ -641,7 +642,7 @@ describe('TeamsInfo', function () { tenantId: 'tenantId-Guid', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/a%3AoneOnOneConversationId/members/29%3AUser-One-Id') @@ -652,7 +653,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMember = await TeamsInfo.getMember(context, oneOnOneActivity.from.id); - assert(fetchOauthToken.isDone()); assert(fetchExpectation.isDone()); assert.deepStrictEqual(fetchedMember, member); @@ -670,7 +670,7 @@ describe('TeamsInfo', function () { tenantId: 'tenantId-Guid', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/members/29%3AUser-One-Id') @@ -681,7 +681,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMember = await TeamsInfo.getMember(context, teamActivity.from.id); - assert(fetchOauthToken.isDone()); assert(fetchExpectation.isDone()); assert.deepStrictEqual(fetchedMember, member); @@ -724,7 +723,7 @@ describe('TeamsInfo', function () { }, }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v1/meetings/19%3AmeetingId/participants/User-aadObjectId?tenantId=tenantId-Guid') @@ -733,7 +732,6 @@ describe('TeamsInfo', function () { const fetchedParticipant = await TeamsInfo.getMeetingParticipant(context); - assert(fetchOauthToken.isDone()); assert(fetchExpectation.isDone()); assert.deepStrictEqual(fetchedParticipant, participant); @@ -774,7 +772,7 @@ describe('TeamsInfo', function () { }, }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v1/meetings/19%3AmeetingId') @@ -783,7 +781,6 @@ describe('TeamsInfo', function () { const fetchedDetails = await TeamsInfo.getMeetingInfo(context); - assert(fetchOauthToken.isDone()); assert(fetchExpectation.isDone()); assert.deepStrictEqual(fetchedDetails, details); @@ -818,7 +815,7 @@ describe('TeamsInfo', function () { }, }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v1/meetings/meeting-id') @@ -827,7 +824,6 @@ describe('TeamsInfo', function () { const fetchedDetails = await TeamsInfo.getMeetingInfo(context, details.details.id); - assert(fetchOauthToken.isDone()); assert(fetchExpectation.isDone()); assert.deepStrictEqual(fetchedDetails, details); @@ -888,7 +884,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/members') @@ -899,7 +895,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMembers = await TeamsInfo.getTeamMembers(context); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert.deepStrictEqual( @@ -932,7 +927,7 @@ describe('TeamsInfo', function () { }, ]; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') .get('/v3/conversations/19%3AChannelIdgeneralChannelId%40thread.skype/members') @@ -943,7 +938,6 @@ describe('TeamsInfo', function () { context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); const fetchedMembers = await TeamsInfo.getTeamMembers(context, '19:ChannelIdgeneralChannelId@thread.skype'); - assert(fetchOauthToken.isDone()); assert(fetchChannelListExpectation.isDone()); assert.deepStrictEqual( @@ -991,7 +985,7 @@ describe('TeamsInfo', function () { }, }; const meetingId = 'randomGUID'; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') .post(`/v1/meetings/${meetingId}/notification`, notification) @@ -1003,14 +997,13 @@ describe('TeamsInfo', function () { // if notification object wasn't passed as request body, test would fail await TeamsInfo.sendMeetingNotification(context, notification, meetingId); - assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); }); it('should return an empty object if a 202 status code was returned', async function () { const notification = {}; const meetingId = 'randomGUID'; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') .post(`/v1/meetings/${meetingId}/notification`, notification) @@ -1025,7 +1018,6 @@ describe('TeamsInfo', function () { meetingId ); - assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); const isEmptyObject = (obj) => Object.keys(obj).length == 0; @@ -1035,7 +1027,7 @@ describe('TeamsInfo', function () { it('should return a MeetingNotificationResponse if a 207 status code was returned', async function () { const notification = {}; const meetingId = 'randomGUID'; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const recipientsFailureInfo = { recipientsFailureInfo: [ @@ -1065,7 +1057,6 @@ describe('TeamsInfo', function () { meetingId ); - assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); assert.deepEqual(sendTeamsMeetingNotification, recipientsFailureInfo); @@ -1074,7 +1065,7 @@ describe('TeamsInfo', function () { it('should return standard error response if a 4xx status code was returned', async function () { const notification = {}; const meetingId = 'randomGUID'; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; @@ -1096,14 +1087,13 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); }); it('should throw an error if an empty meeting id is provided', async function () { const notification = {}; const emptyMeetingId = ''; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') .post(`/v1/meetings/${emptyMeetingId}/notification`, notification) @@ -1123,13 +1113,12 @@ describe('TeamsInfo', function () { } assert(isErrorThrown); - assert(fetchOauthToken.isDone() === false); assert(sendTeamsMeetingNotificationExpectation.isDone() === false); }); it('should get the meeting id from the context object if no meeting id is provided', async function () { const notification = {}; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const context = new TestContext(teamActivity); @@ -1145,7 +1134,6 @@ describe('TeamsInfo', function () { await TeamsInfo.sendMeetingNotification(context, notification); - assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); }); }); @@ -1177,7 +1165,7 @@ describe('TeamsInfo', function () { tenantId: 'randomGUID', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToListOfUsersExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/users', content) @@ -1189,12 +1177,11 @@ describe('TeamsInfo', function () { await TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, members); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfUsersExpectation.isDone()); }); it('should return operation id if a 201 status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToListOfUsersExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/users') @@ -1206,13 +1193,12 @@ describe('TeamsInfo', function () { const operationId = await TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, members); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfUsersExpectation.isDone()); assert(operationId, { operationId: '1' }); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; @@ -1234,7 +1220,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfUsersExpectation.isDone()); }); @@ -1273,7 +1258,7 @@ describe('TeamsInfo', function () { tenantId: 'randomGUID', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToAllUsersInTenantExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/tenant', content) @@ -1285,12 +1270,11 @@ describe('TeamsInfo', function () { await TeamsInfo.sendMessageToAllUsersInTenant(context, activity, tenantId); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTenantExpectation.isDone()); }); it('should return operation id if a 201 status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToAllUsersInTenantExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/tenant') @@ -1302,13 +1286,12 @@ describe('TeamsInfo', function () { const operationId = await TeamsInfo.sendMessageToAllUsersInTenant(context, activity, tenantId); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTenantExpectation.isDone()); assert(operationId, { operationId: '1' }); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; @@ -1330,7 +1313,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTenantExpectation.isDone()); }); @@ -1364,7 +1346,7 @@ describe('TeamsInfo', function () { teamId: 'teamRandomGUID', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToAllUsersInTeamExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/team', content) @@ -1376,12 +1358,11 @@ describe('TeamsInfo', function () { await TeamsInfo.sendMessageToAllUsersInTeam(context, activity, tenantId, teamId); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTeamExpectation.isDone()); }); it('should return operation id if a 201 status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToAllUsersInTeamExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/team') @@ -1393,13 +1374,12 @@ describe('TeamsInfo', function () { const operationId = await TeamsInfo.sendMessageToAllUsersInTeam(context, activity, tenantId, teamId); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTeamExpectation.isDone()); assert(operationId, { operationId: '1' }); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; @@ -1421,7 +1401,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(sendMessageToAllUsersInTeamExpectation.isDone()); }); @@ -1474,7 +1453,7 @@ describe('TeamsInfo', function () { tenantId: 'randomGUID', }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToListOfChannelsExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/channels', content) @@ -1486,12 +1465,11 @@ describe('TeamsInfo', function () { await TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, members); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfChannelsExpectation.isDone()); }); it('should return operation id if a 201 status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const sendMessageToListOfChannelsExpectation = nock('https://smba.trafficmanager.net/amer') .post('/v3/batch/conversation/channels') @@ -1503,13 +1481,12 @@ describe('TeamsInfo', function () { const operationId = await TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, members); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfChannelsExpectation.isDone()); assert(operationId, { operationId: '1' }); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; @@ -1531,7 +1508,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(sendMessageToListOfChannelsExpectation.isDone()); }); @@ -1570,7 +1546,7 @@ describe('TeamsInfo', function () { totalEntriesCount: 5, }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const getOperationStateExpectation = nock('https://smba.trafficmanager.net/amer') .get(`/v3/batch/conversation/${operationId}`) @@ -1582,14 +1558,13 @@ describe('TeamsInfo', function () { const operationStateDetails = await TeamsInfo.getOperationState(context, operationId); - assert(fetchOauthToken.isDone()); assert(getOperationStateExpectation.isDone()); assert.deepStrictEqual(operationStateDetails, operationState); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; const getOperationStateExpectation = nock('https://smba.trafficmanager.net/amer') @@ -1610,7 +1585,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(getOperationStateExpectation.isDone()); }); @@ -1641,7 +1615,7 @@ describe('TeamsInfo', function () { ], }; - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const getFailedEntriesExpectation = nock('https://smba.trafficmanager.net/amer') .get(`/v3/batch/conversation/failedentries/${operationId}`) @@ -1653,14 +1627,13 @@ describe('TeamsInfo', function () { const failedEntriesResponse = await TeamsInfo.getFailedEntries(context, operationId); - assert(fetchOauthToken.isDone()); assert(getFailedEntriesExpectation.isDone()); assert.deepStrictEqual(failedEntriesResponse, failedEntries); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; const getFailedEntriesExpectation = nock('https://smba.trafficmanager.net/amer') @@ -1681,7 +1654,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(getFailedEntriesExpectation.isDone()); }); @@ -1694,7 +1666,7 @@ describe('TeamsInfo', function () { const operationId = 'amerOperationId'; it('should finish operation with correct operationId in parameters', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const cancelOperationExpectation = nock('https://smba.trafficmanager.net/amer') .delete(`/v3/batch/conversation/${operationId}`) @@ -1706,12 +1678,11 @@ describe('TeamsInfo', function () { await TeamsInfo.cancelOperation(context, operationId); - assert(fetchOauthToken.isDone()); assert(cancelOperationExpectation.isDone()); }); it('should return standard error response if a 4xx status code was returned', async function () { - const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const expectedAuthHeader = getBearerToken(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; const cancelOperationExpectation = nock('https://smba.trafficmanager.net/amer') @@ -1732,7 +1703,6 @@ describe('TeamsInfo', function () { assert(isErrorThrown); - assert(fetchOauthToken.isDone()); assert(cancelOperationExpectation.isDone()); }); diff --git a/libraries/botframework-connector/package.json b/libraries/botframework-connector/package.json index 0f98647b4c..fb77db9772 100644 --- a/libraries/botframework-connector/package.json +++ b/libraries/botframework-connector/package.json @@ -30,7 +30,6 @@ "@azure/identity": "^2.0.4", "@azure/ms-rest-js": "^2.7.0", "@azure/msal-node": "^1.2.0", - "adal-node": "0.2.3", "axios": "^0.25.0", "base64url": "^3.0.0", "botbuilder-stdlib": "4.1.6", diff --git a/libraries/botframework-connector/src/auth/appCredentials.ts b/libraries/botframework-connector/src/auth/appCredentials.ts index e3823219a8..a646f2a5fd 100644 --- a/libraries/botframework-connector/src/auth/appCredentials.ts +++ b/libraries/botframework-connector/src/auth/appCredentials.ts @@ -7,15 +7,16 @@ */ import * as msrest from '@azure/ms-rest-js'; -import * as adal from 'adal-node'; +import { ConfidentialClientApplication } from '@azure/msal-node'; import { AuthenticationConstants } from './authenticationConstants'; +import { AuthenticatorResult } from './authenticatorResult'; /** - * General AppCredentials auth implementation and cache. Supports any ADAL client credential flow. + * General AppCredentials auth implementation and cache. * Subclasses can implement refreshToken to acquire the token. */ export abstract class AppCredentials implements msrest.ServiceClientCredentials { - private static readonly cache: Map = new Map(); + private static readonly cache: Map = new Map(); appId: string; @@ -23,8 +24,7 @@ export abstract class AppCredentials implements msrest.ServiceClientCredentials private _oAuthScope: string; private _tenant: string; tokenCacheKey: string; - protected refreshingToken: Promise | null = null; - protected authenticationContext: adal.AuthenticationContext; + protected clientApplication: ConfidentialClientApplication; // Protects against JSON.stringify leaking secrets private toJSON(): unknown { @@ -104,7 +104,6 @@ export abstract class AppCredentials implements msrest.ServiceClientCredentials // aadApiVersion is set to '1.5' to avoid the "spn:" concatenation on the audience claim // For more info, see https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/issues/128 this._oAuthEndpoint = value; - this.authenticationContext = new adal.AuthenticationContext(value, true, undefined, '1.5'); } /** @@ -168,12 +167,10 @@ export abstract class AppCredentials implements msrest.ServiceClientCredentials async getToken(forceRefresh = false): Promise { if (!forceRefresh) { // check the global cache for the token. If we have it, and it's valid, we're done. - const oAuthToken: adal.TokenResponse = AppCredentials.cache.get(this.tokenCacheKey); - if (oAuthToken) { - // we have the token. Is it valid? - if (oAuthToken.expirationTime > Date.now()) { - return oAuthToken.accessToken; - } + const oAuthToken = AppCredentials.cache.get(this.tokenCacheKey); + // Check if the token is not expired. + if (oAuthToken && oAuthToken.expiresOn > new Date()) { + return oAuthToken.accessToken; } } @@ -181,28 +178,19 @@ export abstract class AppCredentials implements msrest.ServiceClientCredentials // 1. The user requested it via the forceRefresh parameter // 2. We have it, but it's expired // 3. We don't have it in the cache. - const res: adal.TokenResponse = await this.refreshToken(); - this.refreshingToken = null; + const res = await this.refreshToken(); if (res && res.accessToken) { - // `res` is equalivent to the results from the cached promise `this.refreshingToken`. - // Because the promise has been cached, we need to see if the body has been read. - // If the body has not been read yet, we can call res.json() to get the access_token. - // If the body has been read, the OAuthResponse for that call should have been cached already, - // in which case we can return the cache from there. If a cached OAuthResponse does not exist, - // call getToken() again to retry the authentication process. - - // Subtract 5 minutes from expires_in so they'll we'll get a - // new token before it expires. - res.expirationTime = Date.now() + res.expiresIn * 1000 - 300000; + // Subtract 5 minutes from expiresOn so they'll we'll get a new token before it expires. + res.expiresOn.setMinutes(res.expiresOn.getMinutes() - 5); AppCredentials.cache.set(this.tokenCacheKey, res); return res.accessToken; } else { - throw new Error('Authentication: No response or error received from ADAL.'); + throw new Error('Authentication: No response or error received from MSAL.'); } } - protected abstract refreshToken(): Promise; + protected abstract refreshToken(): Promise; /** * @private diff --git a/libraries/botframework-connector/src/auth/authenticatorResult.ts b/libraries/botframework-connector/src/auth/authenticatorResult.ts new file mode 100644 index 0000000000..12e087efe5 --- /dev/null +++ b/libraries/botframework-connector/src/auth/authenticatorResult.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Contains tokens and metadata upon successful completion of an acquireToken call. + */ +export interface AuthenticatorResult { + /** + * The value of the access token resulting from an authentication process. + */ + accessToken: string; + /** + * The date and time of expiration relative to Coordinated Universal Time (UTC). + */ + expiresOn: Date; +} diff --git a/libraries/botframework-connector/src/auth/certificateAppCredentials.ts b/libraries/botframework-connector/src/auth/certificateAppCredentials.ts index b8130010cd..ec9737ef41 100644 --- a/libraries/botframework-connector/src/auth/certificateAppCredentials.ts +++ b/libraries/botframework-connector/src/auth/certificateAppCredentials.ts @@ -6,8 +6,10 @@ * Licensed under the MIT License. */ -import * as adal from 'adal-node'; +import { ConfidentialClientApplication } from '@azure/msal-node'; import { AppCredentials } from './appCredentials'; +import { AuthenticatorResult } from './authenticatorResult'; +import { MsalAppCredentials } from './msalAppCredentials'; /** * CertificateAppCredentials auth implementation @@ -15,6 +17,9 @@ import { AppCredentials } from './appCredentials'; export class CertificateAppCredentials extends AppCredentials { certificateThumbprint: string; certificatePrivateKey: string; + x5c: string; + + private credentials: MsalAppCredentials; /** * Initializes a new instance of the [CertificateAppCredentials](xref:botframework-connector.CertificateAppCredentials) class. @@ -24,37 +29,55 @@ export class CertificateAppCredentials extends AppCredentials { * @param certificatePrivateKey A PEM encoded certificate private key. * @param channelAuthTenant Optional. The oauth token tenant. * @param oAuthScope Optional. The scope for the token. + * @param x5c Optional. Enables application developers to achieve easy certificates roll-over in Azure AD: + * set this parameter to send the public certificate (BEGIN CERTIFICATE) to Azure AD, so that Azure AD can use it to validate the subject name based on a trusted issuer policy. */ constructor( appId: string, certificateThumbprint: string, certificatePrivateKey: string, channelAuthTenant?: string, - oAuthScope?: string + oAuthScope?: string, + x5c?: string ) { super(appId, channelAuthTenant, oAuthScope); this.certificateThumbprint = certificateThumbprint; this.certificatePrivateKey = certificatePrivateKey; + this.x5c = x5c; + } + + /** + * @inheritdoc + */ + async getToken(forceRefresh = false): Promise { + this.credentials ??= new MsalAppCredentials( + this.createClientApplication(), + this.appId, + this.oAuthEndpoint, + this.oAuthScope + ); + return this.credentials.getToken(forceRefresh); + } + + /** + * @inheritdoc + */ + protected refreshToken(): Promise { + // This will never be executed because we are using MsalAppCredentials.getToken underneath. + throw new Error('Method not implemented.'); } - protected async refreshToken(): Promise { - if (!this.refreshingToken) { - this.refreshingToken = new Promise((resolve, reject) => { - this.authenticationContext.acquireTokenWithClientCertificate( - this.oAuthScope, - this.appId, - this.certificatePrivateKey, - this.certificateThumbprint, - function (err, tokenResponse) { - if (err) { - reject(err); - } else { - resolve(tokenResponse as adal.TokenResponse); - } - } - ); - }); - } - return this.refreshingToken; + private createClientApplication() { + return new ConfidentialClientApplication({ + auth: { + clientId: this.appId, + authority: this.oAuthEndpoint, + clientCertificate: { + thumbprint: this.certificateThumbprint, + privateKey: this.certificatePrivateKey, + x5c: this.x5c, + }, + }, + }); } } diff --git a/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts b/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts index 67314aa6ef..65b0682751 100644 --- a/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts +++ b/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts @@ -19,6 +19,7 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre private readonly certificateThumbprint: string; private readonly certificatePrivateKey: string; private readonly tenantId: string | null; + private readonly x5c: string | null; /** * Initializes a new instance of the CertificateServiceClientCredentialsFactory class. @@ -27,8 +28,16 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre * @param certificateThumbprint A hex encoded thumbprint of the certificate. * @param certificatePrivateKey A PEM encoded certificate private key. * @param tenantId Optional. The oauth token tenant. + * @param x5c Optional. Enables application developers to achieve easy certificates roll-over in Azure AD: + * set this parameter to send the public certificate (BEGIN CERTIFICATE) to Azure AD, so that Azure AD can use it to validate the subject name based on a trusted issuer policy. */ - constructor(appId: string, certificateThumbprint: string, certificatePrivateKey: string, tenantId?: string) { + constructor( + appId: string, + certificateThumbprint: string, + certificatePrivateKey: string, + tenantId?: string, + x5c?: string + ) { super(); ok(appId?.trim(), 'CertificateServiceClientCredentialsFactory.constructor(): missing appId.'); ok( @@ -44,6 +53,7 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre this.certificateThumbprint = certificateThumbprint; this.certificatePrivateKey = certificatePrivateKey; this.tenantId = tenantId; + this.x5c = x5c; } /** @@ -75,7 +85,8 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre this.certificateThumbprint, this.certificatePrivateKey, this.tenantId, - audience + audience, + this.x5c ); } } diff --git a/libraries/botframework-connector/src/auth/index.ts b/libraries/botframework-connector/src/auth/index.ts index 077849d488..5d78f5bc5c 100644 --- a/libraries/botframework-connector/src/auth/index.ts +++ b/libraries/botframework-connector/src/auth/index.ts @@ -8,10 +8,11 @@ export * from './allowedCallersClaimsValidator'; export * from './appCredentials'; +export * from './authenticateRequestResult'; export * from './authenticationConfiguration'; export * from './authenticationConstants'; export * from './authenticationError'; -export * from './authenticateRequestResult'; +export * from './authenticatorResult'; export * from './botFrameworkAuthentication'; export * from './botFrameworkAuthenticationFactory'; export * from './certificateAppCredentials'; @@ -32,8 +33,8 @@ export * from './managedIdentityAuthenticator'; export * from './managedIdentityServiceClientCredentialsFactory'; export * from './microsoftAppCredentials'; export * from './passwordServiceClientCredentialFactory'; -export * from './skillValidation'; export * from './serviceClientCredentialsFactory'; +export * from './skillValidation'; export * from './userTokenClient'; export { MsalAppCredentials } from './msalAppCredentials'; diff --git a/libraries/botframework-connector/src/auth/managedIdentityAppCredentials.ts b/libraries/botframework-connector/src/auth/managedIdentityAppCredentials.ts index bfb31fbc36..cc19c1b482 100644 --- a/libraries/botframework-connector/src/auth/managedIdentityAppCredentials.ts +++ b/libraries/botframework-connector/src/auth/managedIdentityAppCredentials.ts @@ -6,11 +6,11 @@ * Licensed under the MIT License. */ +import { ok } from 'assert'; import { AppCredentials } from './appCredentials'; import type { IJwtTokenProviderFactory } from './jwtTokenProviderFactory'; import { ManagedIdentityAuthenticator } from './managedIdentityAuthenticator'; -import { TokenResponse } from 'adal-node'; -import { ok } from 'assert'; +import { AuthenticatorResult } from './authenticatorResult'; /** * Managed Service Identity auth implementation. @@ -40,14 +40,11 @@ export class ManagedIdentityAppCredentials extends AppCredentials { /** * @inheritdoc */ - protected async refreshToken(): Promise { + protected async refreshToken(): Promise { const token = await this.authenticator.getToken(); return { accessToken: token.token, expiresOn: new Date(token.expiresOnTimestamp), - tokenType: 'Bearer', - expiresIn: (token.expiresOnTimestamp - Date.now()) / 1000, - resource: this.oAuthScope, }; } } diff --git a/libraries/botframework-connector/src/auth/microsoftAppCredentials.ts b/libraries/botframework-connector/src/auth/microsoftAppCredentials.ts index 5a44c03be3..b98e57355a 100644 --- a/libraries/botframework-connector/src/auth/microsoftAppCredentials.ts +++ b/libraries/botframework-connector/src/auth/microsoftAppCredentials.ts @@ -6,18 +6,9 @@ * Licensed under the MIT License. */ -import * as adal from 'adal-node'; import { AppCredentials } from './appCredentials'; - -// Determines if an unknown value is of adal.ErrorResponse type -function isErrorResponse(value: unknown): value is adal.ErrorResponse { - if (value) { - const { error, errorDescription } = value as adal.ErrorResponse; - return error != null && errorDescription != null; - } - - return false; -} +import { AuthenticatorResult } from './authenticatorResult'; +import { MsalAppCredentials } from './msalAppCredentials'; /** * MicrosoftAppCredentials auth implementation @@ -28,6 +19,8 @@ export class MicrosoftAppCredentials extends AppCredentials { */ static readonly Empty = new MicrosoftAppCredentials(null, null); + private credentials: MsalAppCredentials; + /** * Initializes a new instance of the [MicrosoftAppCredentials](xref:botframework-connector.MicrosoftAppCredentials) class. * @@ -40,26 +33,19 @@ export class MicrosoftAppCredentials extends AppCredentials { super(appId, channelAuthTenant, oAuthScope); } - protected async refreshToken(): Promise { - if (!this.refreshingToken) { - this.refreshingToken = new Promise((resolve, reject): void => { - this.authenticationContext.acquireTokenWithClientCredentials( - this.oAuthScope, - this.appId, - this.appPassword, - (err, tokenResponse) => { - if (err) { - reject(err); - } else if (isErrorResponse(tokenResponse)) { - reject(new Error(tokenResponse.error)); - } else { - resolve(tokenResponse); - } - } - ); - }); - } + /** + * @inheritdoc + */ + async getToken(forceRefresh = false): Promise { + this.credentials ??= new MsalAppCredentials(this.appId, this.appPassword, this.oAuthEndpoint, this.oAuthScope); + return this.credentials.getToken(forceRefresh); + } - return this.refreshingToken; + /** + * @inheritdoc + */ + protected refreshToken(): Promise { + // This will never be executed because we are using MsalAppCredentials.getToken underneath. + throw new Error('Method not implemented.'); } } diff --git a/libraries/botframework-connector/src/auth/msalAppCredentials.ts b/libraries/botframework-connector/src/auth/msalAppCredentials.ts index f64d8ed1b3..4792a8c42d 100644 --- a/libraries/botframework-connector/src/auth/msalAppCredentials.ts +++ b/libraries/botframework-connector/src/auth/msalAppCredentials.ts @@ -4,9 +4,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { AppCredentials } from './appCredentials'; import { ConfidentialClientApplication, NodeAuthOptions } from '@azure/msal-node'; -import { TokenResponse } from 'adal-node'; +import { AppCredentials } from './appCredentials'; +import { AuthenticatorResult } from './authenticatorResult'; export interface Certificate { thumbprint: string; @@ -22,8 +22,6 @@ export class MsalAppCredentials extends AppCredentials { */ static Empty = new MsalAppCredentials(); - private readonly clientApplication?: ConfidentialClientApplication; - /** * Create an MsalAppCredentials instance using a confidential client application. * @@ -102,7 +100,7 @@ export class MsalAppCredentials extends AppCredentials { /** * @inheritdoc */ - protected async refreshToken(): Promise { + protected async refreshToken(): Promise { if (!this.clientApplication) { throw new Error('getToken should not be called for empty credentials.'); } @@ -123,14 +121,9 @@ export class MsalAppCredentials extends AppCredentials { throw new Error('Authentication: No access token received from MSAL.'); } - const expiresIn = (token.expiresOn.getTime() - Date.now()) / 1000; - return { accessToken: token.accessToken, expiresOn: token.expiresOn, - tokenType: token.tokenType, - expiresIn: expiresIn, - resource: this.oAuthScope, }; } } diff --git a/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts b/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts index f7bcc1af3d..388cd62b52 100644 --- a/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts +++ b/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts @@ -4,7 +4,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as adal from 'adal-node'; import type { ServiceClientCredentials } from '@azure/ms-rest-js'; import { AuthenticationConstants } from './authenticationConstants'; import { GovernmentConstants } from './governmentConstants'; @@ -171,6 +170,5 @@ class PrivateCloudAppCredentials extends MicrosoftAppCredentials { // aadApiVersion is set to '1.5' to avoid the "spn:" concatenation on the audience claim // For more info, see https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/issues/128 this.__oAuthEndpoint = value; - this.authenticationContext = new adal.AuthenticationContext(value, this.validateAuthority, undefined, '1.5'); } } diff --git a/libraries/botframework-connector/tests/appCredentials.test.js b/libraries/botframework-connector/tests/appCredentials.test.js index ef818fae3f..77f75dd5e9 100644 --- a/libraries/botframework-connector/tests/appCredentials.test.js +++ b/libraries/botframework-connector/tests/appCredentials.test.js @@ -84,12 +84,7 @@ describe('AppCredentials', function () { it('should fail to get a token with an appId and no appPassword', async function () { const tokenGenerator = new MicrosoftAppCredentials(APP_ID); - await assert.rejects( - tokenGenerator.getToken(true), - // e.message evaluation per adal-node@0.2.1: - // https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/blob/eeff5215bd7a6629edbd1d71450a0db68f029838/lib/authentication-context.js#L277 - new Error('The clientSecret parameter is required.') - ); + await assert.rejects(tokenGenerator.getToken(true), { errorCode: 'invalid_client_credential' }); }); }); }); diff --git a/libraries/botframework-connector/tests/auth/jwtTokenValidation.test.js b/libraries/botframework-connector/tests/auth/jwtTokenValidation.test.js index 09497056dc..261f2a5742 100644 --- a/libraries/botframework-connector/tests/auth/jwtTokenValidation.test.js +++ b/libraries/botframework-connector/tests/auth/jwtTokenValidation.test.js @@ -3,6 +3,7 @@ const assert = require('assert'); const url = require('url'); +const sinon = require('sinon'); const { JwtTokenExtractor } = require('../../lib/auth/jwtTokenExtractor'); const { jwt, oauth } = require('botbuilder-test-utils'); @@ -14,8 +15,9 @@ const { GovernmentConstants, JwtTokenValidation, SimpleCredentialProvider, - MicrosoftAppCredentials, + MsalAppCredentials, } = require('../..'); +const { ConfidentialClientApplication } = require('@azure/msal-node'); describe('JwtTokenValidation', function () { beforeEach(function () { @@ -26,6 +28,17 @@ describe('JwtTokenValidation', function () { oauth.mocha(); describe('authenticateRequest', function () { + function defaultCredentials(appId, appPassword, tenant, accessToken) { + const credentials = new ConfidentialClientApplication({ + auth: { + clientId: appId, + clientSecret: appPassword, + }, + }); + + sinon.stub(credentials, 'acquireTokenByClientCredential').returns({ accessToken, expiresOn: new Date() }); + return new MsalAppCredentials(credentials, appId); + } const authenticateRequest = async ({ appId = 'appId', appPassword = 'password', @@ -34,7 +47,7 @@ describe('JwtTokenValidation', function () { makeActivity = () => ({}), makeAuthHeader = (tokenType, accessToken) => `${tokenType} ${accessToken}`, makeClaims = (appId, defaultClaims) => defaultClaims, - makeCredentials = (appId, appPassword, tenant) => new MicrosoftAppCredentials(appId, appPassword, tenant), + makeCredentials = defaultCredentials, makeProvider = (appId, appPassword) => new SimpleCredentialProvider(appId, appPassword), metadata, parameters, @@ -49,33 +62,25 @@ describe('JwtTokenValidation', function () { metadata: url.parse(metadata), }); - const credentials = makeCredentials(appId, appPassword, tenant); const activity = makeActivity(); - - // Stub oauth calls with nock. The received token is the signed claims generated by the caller. - const { verify: verifyOauth, tokenType } = oauth.stub({ - accessToken: sign( - makeClaims(appId, { - [AuthenticationConstants.ServiceUrlClaim]: activity.serviceUrl, - [AuthenticationConstants.VersionClaim]: version, - tid: tenant, - }) - ), - tenant, - }); + const token = sign( + makeClaims(appId, { + [AuthenticationConstants.ServiceUrlClaim]: activity.serviceUrl, + [AuthenticationConstants.VersionClaim]: version, + tid: tenant, + }) + ); + const credentials = makeCredentials(appId, appPassword, tenant, token); // Fetch "stubbed" token const accessToken = await credentials.getToken(true); - // Ensure all oauth expectations are met - verifyOauth(); - const provider = makeProvider(appId, appPassword); // Do not await so that we support expecting resolution or rejection const promise = JwtTokenValidation.authenticateRequest( activity, - makeAuthHeader(tokenType, accessToken), + makeAuthHeader('Bearer', accessToken), provider, channelService ); diff --git a/package.json b/package.json index 6d2586cc74..df2b4ab5c6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "update-versions": "yarn workspace botbuilder-repo-utils update-versions" }, "resolutions": { - "adal-node": "0.2.3", "async": "3.2.3", "minimist": "^1.2.6", "mixme": "0.5.2", diff --git a/tools/framework/suite-base.js b/tools/framework/suite-base.js index 5d46aed4fb..355b29b9e5 100644 --- a/tools/framework/suite-base.js +++ b/tools/framework/suite-base.js @@ -14,7 +14,6 @@ // limitations under the License. // - var fs = require('fs'); var os = require('os'); var path = require('path'); @@ -23,8 +22,8 @@ var _ = require('underscore'); var util = require('util'); var uuid = require('uuid'); var msRest = require('@azure/ms-rest-js'); -var identity = require("@azure/identity"); -var {Environment} = require("@azure/ms-rest-azure-env") +var identity = require('@azure/identity'); +var { Environment } = require('@azure/ms-rest-azure-env'); var MicrosoftAppCredentials = require('botframework-connector/lib/auth/microsoftAppCredentials'); var TokenApiClient = require('botframework-connector/lib/tokenApi/tokenApiClient'); var FileTokenCache = require('../util/fileTokenCache'); @@ -32,7 +31,7 @@ var MockTokenCache = require('./mock-token-cache'); var nockHelper = require('./nock-helper'); var ResourceManagementClient = require('../resourceManagement/lib/resource/resourceManagementClient'); var async = require('async'); -var adal = require('adal-node'); +var msal = require('@azure/msal-node'); var DEFAULT_ADAL_CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'; var DEFAULT_ADAL_TENANT_ID = 'd4058e97-3782-4874-bc12-c975407af782'; @@ -75,7 +74,8 @@ function SuiteBase(mochaSuiteObject, testPrefix, env, libraryPath) { this.username = process.env['AZURE_USERNAME'] || 'username@example.com'; this.password = process.env['AZURE_PASSWORD'] || 'dummypassword'; this.secret = process.env['CLIENT_SECRET'] || 'dummysecret'; - this.tokenCache = new adal.MemoryCache(); + const clientApplication = new msal.PublicClientApplication({auth: {}}); + this.tokenCache = clientApplication.getTokenCache(); this._setCredentials(); //subscriptionId should be recorded for playback diff --git a/tools/package.json b/tools/package.json index 6c152a12c1..43e73ead0a 100644 --- a/tools/package.json +++ b/tools/package.json @@ -39,7 +39,7 @@ "uuid": "^3.3.2" }, "devDependencies": { - "adal-node": "^0.1.28", + "@azure/msal-node": "^1.2.0", "async": "^2.6.1", "colors": "1.1.2", "fs-extra": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index dc535f7c14..228c1fdc04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2643,7 +2643,7 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" -"@xmldom/xmldom@0.8.6", "@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.8.6": +"@xmldom/xmldom@0.8.6", "@xmldom/xmldom@^0.8.6": version "0.8.6" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg== @@ -2757,20 +2757,6 @@ acorn@^8.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== -adal-node@0.2.3, adal-node@^0.1.28: - version "0.2.3" - resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.2.3.tgz#87ed3dbed344f6e114e36bf18fe1c4e7d3cc6069" - integrity sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ== - dependencies: - "@xmldom/xmldom" "^0.7.0" - async "^2.6.3" - axios "^0.21.1" - date-utils "*" - jws "3.x.x" - underscore ">= 1.3.1" - uuid "^3.1.0" - xpath.js "~1.1.0" - adm-zip@0.4.16: version "0.4.16" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" @@ -3208,7 +3194,7 @@ async-listener@^0.6.0: semver "^5.3.0" shimmer "^1.1.0" -async@3.2.3, async@^1.4.0, async@^1.5.2, async@^2.6.1, async@^2.6.3, async@^3.2.3: +async@3.2.3, async@^1.4.0, async@^1.5.2, async@^2.6.1, async@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== @@ -4769,11 +4755,6 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -date-utils@*: - version "1.2.21" - resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" - integrity sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q= - dateformat@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" @@ -8412,7 +8393,7 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@3.x.x, jws@^3.2.2: +jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -13302,7 +13283,7 @@ undeclared-identifiers@^1.1.2: simple-concat "^1.0.0" xtend "^4.0.1" -underscore@1.13.1, "underscore@>= 1.3.1", underscore@^1.13.1: +underscore@1.13.1, underscore@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== @@ -13534,7 +13515,7 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: +uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -13998,11 +13979,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xpath.js@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" - integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ== - xpath@^0.0.32: version "0.0.32" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"