Skip to content

Commit 867a599

Browse files
authored
[Skills] add skill support in BotFrameworkAdapter (#1473)
* add skill support in BotFrameworkAdapter * address PR feedback
1 parent 8a32d65 commit 867a599

File tree

1 file changed

+105
-13
lines changed

1 file changed

+105
-13
lines changed

libraries/botbuilder/src/botFrameworkAdapter.ts

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { STATUS_CODES } from 'http';
1010
import * as os from 'os';
1111

1212
import { Activity, ActivityTypes, BotAdapter, BotCallbackHandlerKey, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, IUserTokenProvider, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core';
13-
import { AuthenticationConstants, ChannelValidation, ConnectorClient, EmulatorApiClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, AppCredentials, CertificateAppCredentials, SimpleCredentialProvider, TokenApiClient, TokenStatus, TokenApiModels } from 'botframework-connector';
13+
import { AuthenticationConfiguration, AuthenticationConstants, ChannelValidation, ClaimsIdentity, ConnectorClient, EmulatorApiClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, AppCredentials, CertificateAppCredentials, SimpleCredentialProvider, TokenApiClient, TokenStatus, TokenApiModels, SkillValidation } from 'botframework-connector';
1414
import { INodeBuffer, INodeSocket, IReceiveRequest, ISocket, IStreamingTransportServer, NamedPipeServer, NodeWebSocketFactory, NodeWebSocketFactoryBase, RequestHandler, StreamingResponse, WebSocketServer } from 'botframework-streaming';
1515

1616
import { StreamingHttpClient, TokenResolver } from './streaming';
@@ -155,6 +155,11 @@ export interface BotFrameworkAdapterSettings {
155155
* Optional. Certificate key to authenticate the appId against AAD.
156156
*/
157157
certificatePrivateKey?: string;
158+
159+
/**
160+
* Optional. Used to require specific endorsements and verify claims. Recommended for Skills.
161+
*/
162+
authConfig?: AuthenticationConfiguration;
158163
}
159164

160165
/**
@@ -232,6 +237,8 @@ export const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse');
232237
* ```
233238
*/
234239
export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvider, RequestHandler {
240+
public static readonly BotIdentityKey: string = 'BotIdentity';
241+
public static readonly ConnectorClientKey: string = 'ConnectorClient';
235242
protected readonly credentials: AppCredentials;
236243
protected readonly credentialsProvider: SimpleCredentialProvider;
237244
protected readonly settings: BotFrameworkAdapterSettings;
@@ -243,6 +250,8 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
243250
private streamingServer: IStreamingTransportServer;
244251
private webSocketFactory: NodeWebSocketFactoryBase;
245252

253+
private authConfiguration: AuthenticationConfiguration;
254+
246255
/**
247256
* Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class.
248257
*
@@ -280,6 +289,8 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
280289
this.settings.channelService = this.settings.channelService || process.env[AuthenticationConstants.ChannelService];
281290
this.settings.openIdMetadata = this.settings.openIdMetadata || process.env[AuthenticationConstants.BotOpenIdMetadataKey];
282291

292+
this.authConfiguration = this.settings.authConfig || new AuthenticationConfiguration();
293+
283294
if (this.settings.openIdMetadata) {
284295
ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
285296
GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
@@ -667,8 +678,7 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
667678
*
668679
* @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved.
669680
*/
670-
public async getTokenStatus(context: TurnContext, userId?: string, includeFilter?: string ): Promise<TokenStatus[]>
671-
{
681+
public async getTokenStatus(context: TurnContext, userId?: string, includeFilter?: string ): Promise<TokenStatus[]> {
672682
if (!userId && (!context.activity.from || !context.activity.from.id)) {
673683
throw new Error(`BotFrameworkAdapter.getTokenStatus(): missing from or from.id`);
674684
}
@@ -785,9 +795,14 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
785795
const authHeader: string = req.headers.authorization || req.headers.Authorization || '';
786796
await this.authenticateRequest(request, authHeader);
787797

798+
const identity = await this.authenticateRequestInternal(request, authHeader);
799+
788800
// Process received activity
789801
status = 500;
790802
const context: TurnContext = this.createContext(request);
803+
context.turnState.set(BotFrameworkAdapter.BotIdentityKey, identity);
804+
const connectorClient = await this.createConnectorClientWithIdentity(request.serviceUrl, identity);
805+
context.turnState.set(BotFrameworkAdapter.ConnectorClientKey, connectorClient);
791806
context.turnState.set(BotCallbackHandlerKey, logic);
792807
await this.runMiddleware(context, logic);
793808

@@ -968,25 +983,87 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
968983
* Override this in a derived class to create a mock connector client for unit testing.
969984
*/
970985
public createConnectorClient(serviceUrl: string): ConnectorClient {
971-
if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) {
986+
return this.createConnectorClientInternal(serviceUrl, this.credentials);
987+
}
988+
989+
/**
990+
* Create a ConnectorClient with a ClaimsIdentity.
991+
* @remarks
992+
* If the ClaimsIdentity contains the claims for a Skills request, create a ConnectorClient for use with Skills.
993+
* @param serviceUrl
994+
* @param identity ClaimsIdentity
995+
*/
996+
public async createConnectorClientWithIdentity(serviceUrl: string, identity: ClaimsIdentity): Promise<ConnectorClient> {
997+
if (!identity) {
998+
throw new Error('BotFrameworkAdapter.createConnectorClientWithScope(): invalid identity parameter.');
999+
}
1000+
1001+
const botAppId = identity.getClaimValue(AuthenticationConstants.AudienceClaim) ||
1002+
identity.getClaimValue(AuthenticationConstants.AppIdClaim);
1003+
1004+
// Anonymous claims and non-skill claims should fall through without modifying the scope.
1005+
let credentials: AppCredentials = this.credentials;
1006+
1007+
// If the request is for skills, we need to create an AppCredentials instance with
1008+
// the correct scope for communication between the caller and the skill.
1009+
if (botAppId && SkillValidation.isSkillClaim(identity.claims)) {
1010+
const scope = JwtTokenValidation.getAppIdFromClaims(identity.claims);
1011+
if (this.credentials.oAuthScope === scope) {
1012+
// Do nothing, the current credentials and its scope are valid for the skill.
1013+
// i.e. the adatper instance is pre-configured to talk with one skill.
1014+
} else {
1015+
// Since the scope is different, we will create a new instance of the AppCredentials
1016+
// so this.credentials.oAuthScope isn't overridden.
1017+
credentials = await this.buildCredentials(botAppId, scope);
1018+
1019+
if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
1020+
credentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
1021+
// Not sure that this code is correct because the scope was set earlier.
1022+
credentials.oAuthScope = GovernmentConstants.ToChannelFromBotOAuthScope;
1023+
}
1024+
}
1025+
}
1026+
1027+
const client: ConnectorClient = this.createConnectorClientInternal(serviceUrl, credentials);
1028+
return client;
1029+
}
9721030

1031+
/**
1032+
* @private
1033+
* @param serviceUrl The client's service URL.
1034+
* @param credentials AppCredentials instance to construct the ConnectorClient with.
1035+
*/
1036+
private createConnectorClientInternal(serviceUrl: string, credentials: AppCredentials): ConnectorClient {
1037+
if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) {
9731038
// Check if we have a streaming server. Otherwise, requesting a connector client
9741039
// for a non-existent streaming connection results in an error
9751040
if (!this.streamingServer) {
9761041
throw new Error(`Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`)
9771042
}
9781043

9791044
return new ConnectorClient(
980-
this.credentials,
1045+
credentials,
9811046
{
9821047
baseUri: serviceUrl,
9831048
userAgent: USER_AGENT,
9841049
httpClient: new StreamingHttpClient(this.streamingServer)
9851050
});
9861051
}
9871052

988-
const client: ConnectorClient = new ConnectorClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} );
989-
return client;
1053+
return new ConnectorClient(credentials, { baseUri: serviceUrl, userAgent: USER_AGENT });
1054+
}
1055+
1056+
/**
1057+
*
1058+
* @remarks
1059+
* @param appId
1060+
* @param oAuthScope
1061+
*/
1062+
protected async buildCredentials(appId: string, oAuthScope: string = ''): Promise<AppCredentials> {
1063+
// There is no cache for AppCredentials in JS as opposed to C#.
1064+
// Instead of retrieving an AppCredentials from the Adapter instance, generate a new one
1065+
const appPassword = await this.credentialsProvider.getAppPassword(appId);
1066+
return new MicrosoftAppCredentials(appId, appPassword, oAuthScope);
9901067
}
9911068

9921069
/**
@@ -998,22 +1075,37 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide
9981075
* Override this in a derived class to create a mock OAuth API client for unit testing.
9991076
*/
10001077
protected createTokenApiClient(serviceUrl: string): TokenApiClient {
1001-
const client = new TokenApiClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} );
1078+
const client = new TokenApiClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT });
10021079
return client;
10031080
}
10041081

10051082
/**
1006-
* Allows for the overriding of authentication in unit tests.
1083+
* Allows for the overriding of authentication in unit tests.
1084+
* @param request Received request.
1085+
* @param authHeader Received authentication header.
1086+
*/
1087+
protected async authenticateRequest(request: Partial<Activity>, authHeader: string): Promise<void> {
1088+
const claims = await this.authenticateRequestInternal(request, authHeader);
1089+
if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); }
1090+
}
1091+
1092+
/**
1093+
* @ignore
1094+
* @private
1095+
* Returns the actual ClaimsIdentity from the JwtTokenValidation.authenticateRequest() call.
1096+
* @remarks
1097+
* This method is used instead of authenticateRequest() in processActivity() to obtain the ClaimsIdentity for caching in the TurnContext.turnState.
1098+
*
10071099
* @param request Received request.
10081100
* @param authHeader Received authentication header.
10091101
*/
1010-
protected async authenticateRequest(request: Partial<Activity>, authHeader: string): Promise<void> {
1011-
const claims = await JwtTokenValidation.authenticateRequest(
1102+
private authenticateRequestInternal(request: Partial<Activity>, authHeader: string): Promise<ClaimsIdentity> {
1103+
return JwtTokenValidation.authenticateRequest(
10121104
request as Activity, authHeader,
10131105
this.credentialsProvider,
1014-
this.settings.channelService
1106+
this.settings.channelService,
1107+
this.authConfiguration
10151108
);
1016-
if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); }
10171109
}
10181110

10191111
/**

0 commit comments

Comments
 (0)