Skip to content

Commit

Permalink
feat: add additional BotFrameworkAuthentication classes for CloudAdap…
Browse files Browse the repository at this point in the history
…ter (#3698)

* add additional BotFrameworkAuthentication classes for CloudAdapter

* Add Alexa to Channels enum for #3603

* fix export, USER_AGENT and style issue

* make path in Configuration.get optional, apply other PR feedback

* address additional PR feedback, minor cleanup

* update adaptive-runtime.Configuration to reflect contract

* cleanup typing, add unit tests, refactor existing code

* fix linting error in types.ts
  • Loading branch information
stevengum authored and Josh Gummersall committed Jun 18, 2021
1 parent 2bc15c6 commit 68bebf0
Show file tree
Hide file tree
Showing 30 changed files with 1,518 additions and 61 deletions.
1 change: 1 addition & 0 deletions libraries/botbuilder-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"botbuilder-stdlib": "4.1.6",
"botframework-connector": "4.1.6",
"botframework-schema": "4.1.6",
"runtypes": "~6.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Activity } from 'botframework-schema';
import {
AuthenticateRequestResult,
AuthenticationConfiguration,
AuthenticationConstants,
BotFrameworkAuthentication,
BotFrameworkAuthenticationFactory,
BotFrameworkClient,
ClaimsIdentity,
ConnectorClientOptions,
ConnectorFactory,
ServiceClientCredentialsFactory,
UserTokenClient,
} from 'botframework-connector';
import { Configuration } from 'botbuilder-dialogs-adaptive-runtime-core';
import {
ConfigurationServiceClientCredentialFactory,
ConfigurationServiceClientCredentialFactoryOptions,
} from './configurationServiceClientCredentialFactory';
import * as t from 'runtypes';
import { ValidationError } from 'runtypes';

const TypedOptions = t.Record({
/**
* (Optional) The OAuth URL used to get a token from OAuthApiClient. The "OAuthUrl" member takes precedence over this value.
*/
[AuthenticationConstants.OAuthUrlKey]: t.String.nullable().optional(),

/**
* (Optional) The OpenID metadata document used for authenticating tokens coming from the channel. The "ToBotFromChannelOpenIdMetadataUrl" member takes precedence over this value.
*/
[AuthenticationConstants.BotOpenIdMetadataKey]: t.String.nullable().optional(),

/**
* A string used to indicate if which cloud the bot is operating in (e.g. Public Azure or US Government).
*
* @remarks
* A `null` or `''` value indicates Public Azure, whereas [GovernmentConstants.ChannelService](xref:botframework-connector.GovernmentConstants.ChannelService) indicates the bot is operating in the US Government cloud.
*
* Other values result in a custom authentication configuration derived from the values passed in on the [ConfigurationBotFrameworkAuthenticationOptions](xef:botbuilder-core.ConfigurationBotFrameworkAuthenticationOptions) instance.
*/
[AuthenticationConstants.ChannelService]: t.String.nullable().optional(),

/**
* Flag indicating whether or not to validate the address.
*/
ValidateAuthority: t.String.Or(t.Boolean),

/**
* The Login URL used to specify the tenant from which the bot should obtain access tokens from.
*/
ToChannelFromBotLoginUrl: t.String,

/**
* The Oauth scope to request.
*
* @remarks
* This value is used when fetching a token to indicate the ultimate recipient or `audience` of an activity sent using these credentials.
*/
ToChannelFromBotOAuthScope: t.String,

/**
* The Token issuer for signed requests to the channel.
*/
ToBotFromChannelTokenIssuer: t.String,

/**
* The OAuth URL used to get a token from OAuthApiClient.
*/
OAuthUrl: t.String,

/**
* The OpenID metadata document used for authenticating tokens coming from the channel.
*/
ToBotFromChannelOpenIdMetadataUrl: t.String,

/**
* The The OpenID metadata document used for authenticating tokens coming from the Emulator.
*/
ToBotFromEmulatorOpenIdMetadataUrl: t.String,

/**
* A value for the CallerId.
*/
CallerId: t.String,
});

/**
* Contains settings used to configure a [ConfigurationBotFrameworkAuthentication](xref:botbuilder-core.ConfigurationBotFrameworkAuthentication) instance.
*/
export type ConfigurationBotFrameworkAuthenticationOptions = t.Static<typeof TypedOptions>;

/**
* Creates a [BotFrameworkAuthentication](xref:botframework-connector.BotFrameworkAuthentication) instance from an object with the authentication values or a [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance.
*/
export class ConfigurationBotFrameworkAuthentication extends BotFrameworkAuthentication {
private readonly inner: BotFrameworkAuthentication;

/**
* Initializes a new instance of the [ConfigurationBotFrameworkAuthentication](xref:botbuilder-core.ConfigurationBotFrameworkAuthentication) class.
*
* @param botFrameworkAuthConfig A [ConfigurationBotFrameworkAuthenticationOptions](xref:botbuilder-core.ConfigurationBotFrameworkAuthenticationOptions) object.
* @param credentialsFactory A [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) instance.
* @param authConfiguration A [Configuration](xref:botframework-connector.AuthenticationConfiguration) object.
* @param botFrameworkClientFetch A custom Fetch implementation to be used in the [BotFrameworkClient](xref:botframework-connector.BotFrameworkClient).
* @param connectorClientOptions A [ConnectorClientOptions](xref:botframework-connector.ConnectorClientOptions) object.
*/
constructor(
botFrameworkAuthConfig: Partial<ConfigurationBotFrameworkAuthenticationOptions>,
credentialsFactory?: ServiceClientCredentialsFactory,
authConfiguration?: AuthenticationConfiguration,
botFrameworkClientFetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>,
connectorClientOptions: ConnectorClientOptions = {}
) {
super();
try {
TypedOptions.check(botFrameworkAuthConfig);
} catch (err) {
// Throw a new error with the validation details prominently featured.
if (err instanceof ValidationError && err.details) {
throw new Error(JSON.stringify(err.details, undefined, 2));
}
throw err;
}
const {
ChannelService,
ToChannelFromBotLoginUrl,
ToChannelFromBotOAuthScope,
ToBotFromChannelTokenIssuer,
ToBotFromEmulatorOpenIdMetadataUrl,
CallerId,
} = botFrameworkAuthConfig;

const ToBotFromChannelOpenIdMetadataUrl =
botFrameworkAuthConfig.ToBotFromChannelOpenIdMetadataUrl ??
botFrameworkAuthConfig[AuthenticationConstants.BotOpenIdMetadataKey];
const OAuthUrl = botFrameworkAuthConfig.OAuthUrl ?? botFrameworkAuthConfig[AuthenticationConstants.OAuthUrlKey];
let ValidateAuthority = true;
try {
ValidateAuthority = Boolean(JSON.parse(botFrameworkAuthConfig.ValidateAuthority as string));
} catch (_err) {
// no-op
}

this.inner = BotFrameworkAuthenticationFactory.create(
ChannelService,
ValidateAuthority,
ToChannelFromBotLoginUrl,
ToChannelFromBotOAuthScope,
ToBotFromChannelTokenIssuer,
OAuthUrl,
ToBotFromChannelOpenIdMetadataUrl,
ToBotFromEmulatorOpenIdMetadataUrl,
CallerId,
credentialsFactory ??
new ConfigurationServiceClientCredentialFactory(
botFrameworkAuthConfig as ConfigurationServiceClientCredentialFactoryOptions
),
authConfiguration ?? ({} as AuthenticationConfiguration),
botFrameworkClientFetch,
connectorClientOptions
);
}

authenticateChannelRequest(authHeader: string): Promise<ClaimsIdentity> {
return this.inner.authenticateChannelRequest(authHeader);
}

authenticateRequest(activity: Activity, authHeader: string): Promise<AuthenticateRequestResult> {
return this.inner.authenticateRequest(activity, authHeader);
}

authenticateStreamingRequest(authHeader: string, channelIdHeader: string): Promise<AuthenticateRequestResult> {
return this.inner.authenticateStreamingRequest(authHeader, channelIdHeader);
}

createBotFrameworkClient(): BotFrameworkClient {
return this.inner.createBotFrameworkClient();
}

createConnectorFactory(claimsIdentity: ClaimsIdentity): ConnectorFactory {
return this.inner.createConnectorFactory(claimsIdentity);
}

createUserTokenClient(claimsIdentity: ClaimsIdentity): Promise<UserTokenClient> {
return this.inner.createUserTokenClient(claimsIdentity);
}
}

/**
* Creates a new instance of the [ConfigurationBotFrameworkAuthentication](xref:botbuilder-core.ConfigurationBotFrameworkAuthentication) class.
*
* @remarks
* The [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance provided to the constructor should
* have the desired authentication values available at the root, using the properties of [ConfigurationBotFrameworkAuthenticationOptions](xref:botbuilder-core.ConfigurationBotFrameworkAuthenticationOptions) as its keys.
* @param configuration A [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance.
* @param credentialsFactory A [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) instance.
* @param authConfiguration A [Configuration](xref:botframework-connector.AuthenticationConfiguration) object.
* @param botFrameworkClientFetch A custom Fetch implementation to be used in the [BotFrameworkClient](xref:botframework-connector.BotFrameworkClient).
* @param connectorClientOptions A [ConnectorClientOptions](xref:botframework-connector.ConnectorClientOptions) object.
* @returns A [ConfigurationBotFrameworkAuthentication](xref:botbuilder-core.ConfigurationBotFrameworkAuthentication) instance.
*/
export function createBotFrameworkAuthenticationFromConfiguration(
configuration: Configuration,
credentialsFactory: ServiceClientCredentialsFactory = null,
authConfiguration: AuthenticationConfiguration = null,
botFrameworkClientFetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>,
connectorClientOptions: ConnectorClientOptions = {}
): BotFrameworkAuthentication {
const botFrameworkAuthConfig = configuration?.get<ConfigurationBotFrameworkAuthenticationOptions>();
return new ConfigurationBotFrameworkAuthentication(
botFrameworkAuthConfig,
credentialsFactory,
authConfiguration,
botFrameworkClientFetch,
connectorClientOptions
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Configuration } from 'botbuilder-dialogs-adaptive-runtime-core';
import { PasswordServiceClientCredentialFactory } from 'botframework-connector';
import * as t from 'runtypes';
import { ValidationError } from 'runtypes';

const TypedConfig = t.Record({
/**
* The ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
MicrosoftAppId: t.String.nullable().optional(),

/**
* The password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
MicrosoftAppPassword: t.String.nullable().optional(),
});

/**
* Contains settings used to configure a [ConfigurationServiceClientCredentialFactory](xref:botbuilder-core.ConfigurationServiceClientCredentialFactory) instance.
*/
export type ConfigurationServiceClientCredentialFactoryOptions = t.Static<typeof TypedConfig>;

const isValidationErrorWithDetails = (val: unknown): val is ValidationError => {
return !!(val as ValidationError)?.details;
};

/**
* ServiceClientCredentialsFactory that uses a [ConfigurationServiceClientCredentialFactoryOptions](xref:botbuilder-core.ConfigurationServiceClientCredentialFactoryOptions) or a [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance to build ServiceClientCredentials with an AppId and App Password.
*/
export class ConfigurationServiceClientCredentialFactory extends PasswordServiceClientCredentialFactory {
/**
* Initializes a new instance of the [ConfigurationServiceClientCredentialFactory](xref:botbuilder-core.ConfigurationServiceClientCredentialFactory) class.
*
* @param factoryOptions A [ConfigurationServiceClientCredentialFactoryOptions](xref:botbuilder-core.ConfigurationServiceClientCredentialFactoryOptions) object.
*/
constructor(factoryOptions: ConfigurationServiceClientCredentialFactoryOptions) {
super(null, null);
try {
const { MicrosoftAppId, MicrosoftAppPassword } = TypedConfig.check(factoryOptions);
this.appId = MicrosoftAppId ?? this.appId;
this.password = MicrosoftAppPassword ?? this.password;
} catch (err) {
if (isValidationErrorWithDetails(err)) {
const e = new Error(JSON.stringify(err.details, undefined, 2));
e.stack = err.stack;
throw e;
}
throw err;
}
}
}

/**
* Creates a new instance of the [ConfigurationServiceClientCredentialFactory](xref:botbuilder-core.ConfigurationServiceClientCredentialFactory) class.
*
* @remarks
* The [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance provided to the constructor should
* have the desired authentication values available at the root, using the properties of [ConfigurationServiceClientCredentialFactoryOptions](xref:botbuilder-core.ConfigurationServiceClientCredentialFactoryOptions) as its keys.
* @param configuration A [Configuration](xref:botbuilder-dialogs-adaptive-runtime-core.Configuration) instance.
* @returns A [ConfigurationServiceClientCredentialFactory](xref:botbuilder-core.ConfigurationServiceClientCredentialFactory) instance.
*/
export function createServiceClientCredentialFactoryFromConfiguration(
configuration: Configuration
): ConfigurationServiceClientCredentialFactory {
const factoryOptions = configuration.get<ConfigurationServiceClientCredentialFactoryOptions>();
return new ConfigurationServiceClientCredentialFactory(factoryOptions);
}
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export * from './botTelemetryClient';
export * from './browserStorage';
export * from './cardFactory';
export * from './componentRegistration';
export * from './configurationBotFrameworkAuthentication';
export * from './configurationServiceClientCredentialFactory';
export * from './conversationState';
export * from './coreAppCredentials';
export * from './extendedUserTokenProvider';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const assert = require('assert');
const { AuthenticationConstants } = require('botframework-connector');
const {
CallerIdConstants,
ConfigurationBotFrameworkAuthentication,
createBotFrameworkAuthenticationFromConfiguration,
} = require('../');

describe('ConfigurationBotFrameworkAuthentication', function () {
class TestConfiguration {
static DefaultConfig = {
// [AuthenticationConstants.ChannelService]: undefined,
ValidateAuthority: true,
ToChannelFromBotLoginUrl: AuthenticationConstants.ToChannelFromBotLoginUrl,
ToChannelFromBotOAuthScope: AuthenticationConstants.ToChannelFromBotOAuthScope,
ToBotFromChannelTokenIssuer: AuthenticationConstants.ToBotFromChannelTokenIssuer,
ToBotFromEmulatorOpenIdMetadataUrl: AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl,
CallerId: CallerIdConstants.PublicAzureChannel,
ToBotFromChannelOpenIdMetadataUrl: AuthenticationConstants.ToBotFromChannelOpenIdMetadataUrl,
OAuthUrl: AuthenticationConstants.OAuthUrl,
// [AuthenticationConstants.OAuthUrlKey]: 'test',
[AuthenticationConstants.BotOpenIdMetadataKey]: null,
};

constructor(config = {}) {
this.configuration = Object.assign({}, TestConfiguration.DefaultConfig, config);
}

get(_path) {
return this.configuration;
}

set(_path, _val) {}
}

it('constructor should work', function () {
const bfAuth = new ConfigurationBotFrameworkAuthentication(TestConfiguration.DefaultConfig);
assert(bfAuth.inner);
});

it('createBotFrameworkAuthenticationFromConfiguration should work', function () {
const config = new TestConfiguration();
const bfAuth = createBotFrameworkAuthenticationFromConfiguration(config);
assert(bfAuth.inner);
});
});
Loading

0 comments on commit 68bebf0

Please sign in to comment.