From fff65b426da2c61eb98554bdc93f884b7fefc589 Mon Sep 17 00:00:00 2001 From: Cecilia Avila <44245136+ceciliaavila@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:02:03 -0300 Subject: [PATCH] fix: [#2782] Migrate to MSAL from adal-node - Add MSAL support (#4543) * Add MSAL support * Remove support old TS versions 3.5, 3.6, 3.7 --- README.md | 1 + .../src/serviceCollection.ts | 4 + libraries/botframework-connector/package.json | 1 + .../botframework-connector/src/auth/index.ts | 3 + .../src/auth/msalAppCredentials.ts | 136 ++++++++++++++++++ .../msalServiceClientCredentialsFactory.ts | 84 +++++++++++ testing/consumer-test/run.ts | 2 +- yarn.lock | 30 +++- 8 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 libraries/botframework-connector/src/auth/msalAppCredentials.ts create mode 100644 libraries/botframework-connector/src/auth/msalServiceClientCredentialsFactory.ts diff --git a/README.md b/README.md index 7cc09217c5..a5aed55dea 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ If you want to debug an issue, would like to [contribute](#Contributing-and-our- - [Git](https://git-scm.com/downloads) - [Node.js](https://nodejs.org/en/) - [Yarn 1.x](https://classic.yarnpkg.com/) +- [TypeScript](https://www.typescriptlang.org/) version >= 3.8 - Your favorite code-editor for example [VS Code](https://code.visualstudio.com/) ### Clone diff --git a/libraries/botbuilder-dialogs-adaptive-runtime-core/src/serviceCollection.ts b/libraries/botbuilder-dialogs-adaptive-runtime-core/src/serviceCollection.ts index 944c68d777..a6a2679e37 100644 --- a/libraries/botbuilder-dialogs-adaptive-runtime-core/src/serviceCollection.ts +++ b/libraries/botbuilder-dialogs-adaptive-runtime-core/src/serviceCollection.ts @@ -69,6 +69,10 @@ export class ServiceCollection { * @returns this for chaining */ addInstance(key: string, instance: InstanceType): this { + if (this.graph.hasNode(key)) { + this.graph.removeNode(key); + } + this.graph.addNode(key, [() => instance]); return this; } diff --git a/libraries/botframework-connector/package.json b/libraries/botframework-connector/package.json index 35b1acecd9..0f98647b4c 100644 --- a/libraries/botframework-connector/package.json +++ b/libraries/botframework-connector/package.json @@ -29,6 +29,7 @@ "dependencies": { "@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", diff --git a/libraries/botframework-connector/src/auth/index.ts b/libraries/botframework-connector/src/auth/index.ts index 9a75f7386d..077849d488 100644 --- a/libraries/botframework-connector/src/auth/index.ts +++ b/libraries/botframework-connector/src/auth/index.ts @@ -35,3 +35,6 @@ export * from './passwordServiceClientCredentialFactory'; export * from './skillValidation'; export * from './serviceClientCredentialsFactory'; export * from './userTokenClient'; + +export { MsalAppCredentials } from './msalAppCredentials'; +export { MsalServiceClientCredentialsFactory } from './msalServiceClientCredentialsFactory'; diff --git a/libraries/botframework-connector/src/auth/msalAppCredentials.ts b/libraries/botframework-connector/src/auth/msalAppCredentials.ts new file mode 100644 index 0000000000..f64d8ed1b3 --- /dev/null +++ b/libraries/botframework-connector/src/auth/msalAppCredentials.ts @@ -0,0 +1,136 @@ +/** + * @module botframework-connector + */ +// 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'; + +export interface Certificate { + thumbprint: string; + privateKey: string; +} + +/** + * An implementation of AppCredentials that uses @azure/msal-node to fetch tokens. + */ +export class MsalAppCredentials extends AppCredentials { + /** + * A reference used for Empty auth scenarios + */ + static Empty = new MsalAppCredentials(); + + private readonly clientApplication?: ConfidentialClientApplication; + + /** + * Create an MsalAppCredentials instance using a confidential client application. + * + * @param clientApplication An @azure/msal-node ConfidentialClientApplication instance. + * @param appId The application ID. + * @param authority The authority to use for fetching tokens + * @param scope The oauth scope to use when fetching tokens. + */ + constructor(clientApplication: ConfidentialClientApplication, appId: string, authority: string, scope: string); + + /** + * Create an MsalAppCredentials instance using a confidential client application. + * + * @param appId The application ID. + * @param appPassword The application password. + * @param authority The authority to use for fetching tokens + * @param scope The oauth scope to use when fetching tokens. + */ + constructor(appId: string, appPassword: string, authority: string, scope: string); + + /** + * Create an MsalAppCredentials instance using a confidential client application. + * + * @param appId The application ID. + * @param certificate The client certificate details. + * @param authority The authority to use for fetching tokens + * @param scope The oauth scope to use when fetching tokens. + */ + constructor(appId: string, certificate: Certificate, authority: string, scope: string); + + /** + * @internal + */ + constructor(); + + /** + * @internal + */ + constructor( + maybeClientApplicationOrAppId?: ConfidentialClientApplication | string, + maybeAppIdOrAppPasswordOrCertificate?: string | Certificate, + maybeAuthority?: string, + maybeScope?: string + ) { + const appId = + typeof maybeClientApplicationOrAppId === 'string' + ? maybeClientApplicationOrAppId + : typeof maybeAppIdOrAppPasswordOrCertificate === 'string' + ? maybeAppIdOrAppPasswordOrCertificate + : undefined; + + super(appId, undefined, maybeScope); + + if (typeof maybeClientApplicationOrAppId !== 'string') { + this.clientApplication = maybeClientApplicationOrAppId; + } else { + const auth: NodeAuthOptions = { + authority: maybeAuthority, + clientId: appId, + }; + + auth.clientCertificate = + typeof maybeAppIdOrAppPasswordOrCertificate !== 'string' + ? maybeAppIdOrAppPasswordOrCertificate + : undefined; + + auth.clientSecret = + typeof maybeAppIdOrAppPasswordOrCertificate === 'string' + ? maybeAppIdOrAppPasswordOrCertificate + : undefined; + + this.clientApplication = new ConfidentialClientApplication({ auth }); + } + } + + /** + * @inheritdoc + */ + protected async refreshToken(): Promise { + if (!this.clientApplication) { + throw new Error('getToken should not be called for empty credentials.'); + } + + const scopePostfix = '/.default'; + let scope = this.oAuthScope; + if (!scope.endsWith(scopePostfix)) { + scope = `${scope}${scopePostfix}`; + } + + const token = await this.clientApplication.acquireTokenByClientCredential({ + scopes: [scope], + skipCache: true, + }); + + const { accessToken } = token ?? {}; + if (typeof accessToken !== 'string') { + 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/msalServiceClientCredentialsFactory.ts b/libraries/botframework-connector/src/auth/msalServiceClientCredentialsFactory.ts new file mode 100644 index 0000000000..1f183cbb78 --- /dev/null +++ b/libraries/botframework-connector/src/auth/msalServiceClientCredentialsFactory.ts @@ -0,0 +1,84 @@ +/** + * @module botframework-connector + */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ConfidentialClientApplication } from '@azure/msal-node'; +import { MsalAppCredentials } from './msalAppCredentials'; +import { ServiceClientCredentials } from '@azure/ms-rest-js'; +import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; +import { AuthenticationConstants } from './authenticationConstants'; +import { GovernmentConstants } from './governmentConstants'; + +/** + * An implementation of ServiceClientCredentialsFactory that generates MsalAppCredentials + */ +export class MsalServiceClientCredentialsFactory implements ServiceClientCredentialsFactory { + private readonly appId: string; + + /** + * Create an MsalServiceClientCredentialsFactory instance using runtime configuration and an + * `@azure/msal-node` `ConfidentialClientApplication`. + * + * @param appId App ID for validation. + * @param clientApplication An `@azure/msal-node` `ConfidentialClientApplication` instance. + */ + constructor(appId: string, private readonly clientApplication: ConfidentialClientApplication) { + this.appId = appId; + } + + /** + * @inheritdoc + */ + async isValidAppId(appId: string): Promise { + return appId === this.appId; + } + + /** + * @inheritdoc + */ + async isAuthenticationDisabled(): Promise { + return !this.appId; + } + + /** + * @inheritdoc + */ + async createCredentials( + appId: string, + audience: string, + loginEndpoint: string, + _validateAuthority: boolean + ): Promise { + if (await this.isAuthenticationDisabled()) { + return MsalAppCredentials.Empty; + } + + if (!(await this.isValidAppId(appId))) { + throw new Error('Invalid appId.'); + } + + const normalizedEndpoint = loginEndpoint.toLowerCase(); + + if (normalizedEndpoint.startsWith(AuthenticationConstants.ToChannelFromBotLoginUrlPrefix)) { + return new MsalAppCredentials( + this.clientApplication, + appId, + undefined, + audience || AuthenticationConstants.ToBotFromChannelTokenIssuer + ); + } + + if (normalizedEndpoint === GovernmentConstants.ToChannelFromBotLoginUrl.toLowerCase()) { + return new MsalAppCredentials( + this.clientApplication, + appId, + GovernmentConstants.ToChannelFromBotLoginUrl, + audience || GovernmentConstants.ToChannelFromBotOAuthScope + ); + } + + return new MsalAppCredentials(this.clientApplication, appId, loginEndpoint, audience); + } +} diff --git a/testing/consumer-test/run.ts b/testing/consumer-test/run.ts index 49dec91814..ffbbf83016 100644 --- a/testing/consumer-test/run.ts +++ b/testing/consumer-test/run.ts @@ -5,7 +5,7 @@ import { promisify } from 'util'; const execp = promisify(exec); -const versions = ['3.5', '3.6', '3.7', '3.8', '3.9', '4.0', '4.1', '4.2', '4.3']; +const versions = ['3.8', '3.9', '4.0', '4.1', '4.2', '4.3']; (async () => { const flags = minimist(process.argv.slice(2), { diff --git a/yarn.lock b/yarn.lock index fffe24cc41..a39a8a3233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,9 +72,9 @@ xml2js "^0.5.0" "@azure/core-lro@^2.2.0": - version "2.5.3" - resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.5.3.tgz#6bb74e76dd84071d319abf7025e8abffef091f91" - integrity sha512-ubkOf2YCnVtq7KqEJQqAI8dDD5rH1M6OP5kW0KO/JQyTaxLA0N0pjFWvvaysCj9eHMNBcuuoZXhhl0ypjod2DA== + version "2.5.4" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.5.4.tgz#b21e2bcb8bd9a8a652ff85b61adeea51a8055f90" + integrity sha512-3GJiMVH7/10bulzOKGrrLeG/uCBH/9VtxqaMcB9lIqAeamI/xYQSHJL/KcsLDuH+yTjYpro/u6D/MuRe4dN70Q== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-util" "^1.2.0" @@ -142,7 +142,7 @@ dependencies: tslib "^2.0.0" -"@azure/core-util@^1.1.1", "@azure/core-util@^1.2.0": +"@azure/core-util@^1.1.1": version "1.3.2" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.3.2.tgz#3f8cfda1e87fac0ce84f8c1a42fcd6d2a986632d" integrity sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ== @@ -150,6 +150,14 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" +"@azure/core-util@^1.2.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.5.0.tgz#ffe49c3e867044da67daeb8122143fa065e1eb0e" + integrity sha512-GZBpVFDtQ/15hW1OgBcRdT4Bl7AEpcEZqLfbAvOtm1CQUncKWiYapFHVD588hmlV27NbOOtSm3cnLF3lvoHi4g== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + "@azure/cosmos@3.10.0": version "3.10.0" resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.10.0.tgz#ec11828e380a656f689357b51e8f3f451d78640d" @@ -235,6 +243,11 @@ dependencies: "@azure/msal-common" "^5.0.0" +"@azure/msal-common@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-13.3.0.tgz#dfa39810e0fbce6e07ca85a2cf305da58d30b7c9" + integrity sha512-/VFWTicjcJbrGp3yQP7A24xU95NiDMe23vxIU1U6qdRPFsprMDNUohMudclnd+WSHE4/McqkZs/nUU3sAKkVjg== + "@azure/msal-common@^4.5.1": version "4.5.1" resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-4.5.1.tgz#f35af8b634ae24aebd0906deb237c0db1afa5826" @@ -249,6 +262,15 @@ dependencies: debug "^4.1.1" +"@azure/msal-node@^1.2.0": + version "1.18.3" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.18.3.tgz#e265556d4db0340590eeab5341469fb6740251d0" + integrity sha512-lI1OsxNbS/gxRD4548Wyj22Dk8kS7eGMwD9GlBZvQmFV8FJUXoXySL1BiNzDsHUE96/DS/DHmA+F73p1Dkcktg== + dependencies: + "@azure/msal-common" "13.3.0" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + "@azure/msal-node@^1.3.0": version "1.3.1" resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.3.1.tgz#55c8915c9bc5222dbe152ffd67f9357b83461fde"