Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#2782] Migrate to MSAL from adal-node - Add MSAL support #4543

Merged
merged 2 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]":
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/[email protected]":
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
Loading