From 021429c1fa6465b447461232162b29bc7b9a1f41 Mon Sep 17 00:00:00 2001 From: Joel Mut <62260472+sw-joelmut@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:02:31 -0300 Subject: [PATCH] port: [#4632] Support Federated Identity Credential (#4765) * Add FIC support * Fix lint * Add appId validation --- .../src/auth/federatedAppCredentials.ts | 91 +++++++++++++++++++ ...ederatedServiceClientCredentialsFactory.ts | 66 ++++++++++++++ .../botframework-connector/src/auth/index.ts | 4 +- 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 libraries/botframework-connector/src/auth/federatedAppCredentials.ts create mode 100644 libraries/botframework-connector/src/auth/federatedServiceClientCredentialsFactory.ts diff --git a/libraries/botframework-connector/src/auth/federatedAppCredentials.ts b/libraries/botframework-connector/src/auth/federatedAppCredentials.ts new file mode 100644 index 0000000000..39029e8c88 --- /dev/null +++ b/libraries/botframework-connector/src/auth/federatedAppCredentials.ts @@ -0,0 +1,91 @@ +/** + * @module botframework-connector + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ConfidentialClientApplication, ManagedIdentityApplication } from '@azure/msal-node'; +import { ok } from 'assert'; +import { AppCredentials } from './appCredentials'; +import { AuthenticatorResult } from './authenticatorResult'; +import { MsalAppCredentials } from './msalAppCredentials'; + +/** + * Federated Credentials auth implementation. + */ +export class FederatedAppCredentials extends AppCredentials { + private credentials: MsalAppCredentials; + private managedIdentityClientAssertion: ManagedIdentityApplication; + private clientAudience: string; + + /** + * Initializes a new instance of the [FederatedAppCredentials](xref:botframework-connector.FederatedAppCredentials) class. + * + * @param {string} appId App ID for the Application. + * @param {string} clientId Client ID for the managed identity assigned to the bot. + * @param {string} channelAuthTenant Tenant ID of the Azure AD tenant where the bot is created. + * * **Required** for SingleTenant app types. + * * **Optional** for MultiTenant app types. **Note**: '_botframework.com_' is the default tenant when no value is provided. + * + * More information: https://learn.microsoft.com/en-us/security/zero-trust/develop/identity-supported-account-types. + * @param {string} oAuthScope **Optional**. The scope for the token. + * @param {string} clientAudience **Optional**. The Audience used in the Client's Federated Credential. **Default** (_api://AzureADTokenExchange_). + */ + constructor( + appId: string, + clientId: string, + channelAuthTenant?: string, + oAuthScope?: string, + clientAudience?: string + ) { + super(appId, channelAuthTenant, oAuthScope); + + ok(appId?.trim(), 'FederatedAppCredentials.constructor(): missing appId.'); + + this.clientAudience = clientAudience ?? 'api://AzureADTokenExchange'; + this.managedIdentityClientAssertion = new ManagedIdentityApplication({ + managedIdentityIdParams: { userAssignedClientId: clientId }, + }); + } + + /** + * @inheritdoc + */ + async getToken(forceRefresh = false): Promise { + this.credentials ??= new MsalAppCredentials( + this.createClientApplication(await this.fetchExternalToken(forceRefresh)), + this.oAuthEndpoint, + 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.'); + } + + private createClientApplication(clientAssertion: string) { + return new ConfidentialClientApplication({ + auth: { + clientId: this.appId, + authority: this.oAuthEndpoint, + clientAssertion, + }, + }); + } + + private async fetchExternalToken(forceRefresh = false): Promise { + const response = await this.managedIdentityClientAssertion.acquireToken({ + resource: this.clientAudience, + forceRefresh, + }); + return response.accessToken; + } +} diff --git a/libraries/botframework-connector/src/auth/federatedServiceClientCredentialsFactory.ts b/libraries/botframework-connector/src/auth/federatedServiceClientCredentialsFactory.ts new file mode 100644 index 0000000000..6f0df67a81 --- /dev/null +++ b/libraries/botframework-connector/src/auth/federatedServiceClientCredentialsFactory.ts @@ -0,0 +1,66 @@ +/** + * @module botframework-connector + */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ok } from 'assert'; +import type { ServiceClientCredentials } from '@azure/core-http'; +import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; +import { FederatedAppCredentials } from './federatedAppCredentials'; + +/** + * A Federated Credentials implementation of the [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) interface. + */ +export class FederatedServiceClientCredentialsFactory extends ServiceClientCredentialsFactory { + /** + * Initializes a new instance of the [FederatedServiceClientCredentialsFactory](xref:botframework-connector.FederatedServiceClientCredentialsFactory) class. + * + * @param {string} appId App ID for the Application. + * @param {string} clientId Client ID for the managed identity assigned to the bot. + * @param {string} tenantId Tenant ID of the Azure AD tenant where the bot is created. + * * **Required** for SingleTenant app types. + * * **Optional** for MultiTenant app types. **Note**: '_botframework.com_' is the default tenant when no value is provided. + * + * More information: https://learn.microsoft.com/en-us/security/zero-trust/develop/identity-supported-account-types. + * @param {string} clientAudience **Optional**. The Audience used in the Client's Federated Credential. **Default** (_api://AzureADTokenExchange_). + */ + constructor( + private appId: string, + private clientId: string, + private tenantId?: string, + private clientAudience?: string + ) { + super(); + + ok(appId?.trim(), 'FederatedServiceClientCredentialsFactory.constructor(): missing appId.'); + ok(clientId?.trim(), 'FederatedServiceClientCredentialsFactory.constructor(): missing clientId.'); + } + + /** + * @inheritdoc + */ + async isValidAppId(appId = ''): Promise { + return appId === this.appId; + } + + /** + * @inheritdoc + */ + async isAuthenticationDisabled(): Promise { + // Auth is always enabled for FIC. + return; + } + + /** + * @inheritdoc + */ + async createCredentials(appId: string, audience: string): Promise { + ok( + await this.isValidAppId(appId), + 'FederatedServiceClientCredentialsFactory.createCredentials(): Invalid App ID.' + ); + + return new FederatedAppCredentials(this.appId, this.clientId, this.tenantId, audience, this.clientAudience); + } +} diff --git a/libraries/botframework-connector/src/auth/index.ts b/libraries/botframework-connector/src/auth/index.ts index 5367b217b3..34834a591c 100644 --- a/libraries/botframework-connector/src/auth/index.ts +++ b/libraries/botframework-connector/src/auth/index.ts @@ -8,6 +8,7 @@ export * from './allowedCallersClaimsValidator'; export * from './appCredentials'; +export * from './aseChannelValidation'; export * from './authenticateRequestResult'; export * from './authenticationConfiguration'; export * from './authenticationConstants'; @@ -22,9 +23,10 @@ export * from './claimsIdentity'; export * from './connectorFactory'; export * from './credentialProvider'; export * from './emulatorValidation'; -export * from './aseChannelValidation'; export * from './endorsementsValidator'; export * from './enterpriseChannelValidation'; +export * from './federatedAppCredentials'; +export * from './federatedServiceClientCredentialsFactory'; export * from './governmentChannelValidation'; export * from './governmentConstants'; export * from './jwtTokenProviderFactory';