Skip to content

Commit

Permalink
fix: [#2782] Migrate to MSAL from adal-node - Add MSAL support (#4543)
Browse files Browse the repository at this point in the history
* Add MSAL support

* Remove support old TS versions 3.5, 3.6, 3.7
  • Loading branch information
ceciliaavila authored Oct 10, 2023
1 parent 123cc88 commit fff65b4
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export class ServiceCollection {
* @returns this for chaining
*/
addInstance<InstanceType>(key: string, instance: InstanceType): this {
if (this.graph.hasNode(key)) {
this.graph.removeNode(key);
}

this.graph.addNode(key, [() => instance]);
return this;
}
Expand Down
1 change: 1 addition & 0 deletions libraries/botframework-connector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions libraries/botframework-connector/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ export * from './passwordServiceClientCredentialFactory';
export * from './skillValidation';
export * from './serviceClientCredentialsFactory';
export * from './userTokenClient';

export { MsalAppCredentials } from './msalAppCredentials';
export { MsalServiceClientCredentialsFactory } from './msalServiceClientCredentialsFactory';
136 changes: 136 additions & 0 deletions libraries/botframework-connector/src/auth/msalAppCredentials.ts
Original file line number Diff line number Diff line change
@@ -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<TokenResponse> {
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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return appId === this.appId;
}

/**
* @inheritdoc
*/
async isAuthenticationDisabled(): Promise<boolean> {
return !this.appId;
}

/**
* @inheritdoc
*/
async createCredentials(
appId: string,
audience: string,
loginEndpoint: string,
_validateAuthority: boolean
): Promise<ServiceClientCredentials> {
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);
}
}
2 changes: 1 addition & 1 deletion testing/consumer-test/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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), {
Expand Down
30 changes: 26 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -142,14 +142,22 @@
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==
dependencies:
"@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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down

0 comments on commit fff65b4

Please sign in to comment.