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

feat: Add ASE channel validation. #4589

Merged
merged 5 commits into from
Dec 21, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ConnectorFactory,
ServiceClientCredentialsFactory,
UserTokenClient,
AseChannelValidation,
} from 'botframework-connector';

import {
Expand All @@ -26,6 +27,16 @@ import {

const TypedOptions = z
.object({
/**
* The ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
MicrosoftAppId: z.string(),

/**
* The tenant id assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
MicrosoftAppTenantId: z.string(),

/**
* (Optional) The OAuth URL used to get a token from OAuthApiClient. The "OAuthUrl" member takes precedence over this value.
*/
Expand Down Expand Up @@ -131,6 +142,7 @@ export class ConfigurationBotFrameworkAuthentication extends BotFrameworkAuthent
super();

try {
AseChannelValidation.init(botFrameworkAuthConfig);
const typedBotFrameworkAuthConfig = TypedOptions.nonstrict().parse(botFrameworkAuthConfig);

const {
Expand Down
44 changes: 29 additions & 15 deletions libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
GovernmentConstants,
JwtTokenValidation,
MicrosoftAppCredentials,
MicrosoftGovernmentAppCredentials,
SignInUrlResponse,
SimpleCredentialProvider,
SkillValidation,
Expand Down Expand Up @@ -254,11 +255,19 @@ export class BotFrameworkAdapter
);
this.credentialsProvider = new SimpleCredentialProvider(this.credentials.appId, '');
} else {
this.credentials = new MicrosoftAppCredentials(
this.settings.appId,
this.settings.appPassword || '',
this.settings.channelAuthTenant
);
if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
this.credentials = new MicrosoftGovernmentAppCredentials(
this.settings.appId,
this.settings.appPassword || '',
this.settings.channelAuthTenant
);
} else {
this.credentials = new MicrosoftAppCredentials(
this.settings.appId,
this.settings.appPassword || '',
this.settings.channelAuthTenant
);
}
this.credentialsProvider = new SimpleCredentialProvider(
this.credentials.appId,
this.settings.appPassword || ''
Expand All @@ -280,10 +289,6 @@ export class BotFrameworkAdapter
ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
}
if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
this.credentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
this.credentials.oAuthScope = GovernmentConstants.ToChannelFromBotOAuthScope;
}

// If a NodeWebSocketFactoryBase was passed in, set it on the BotFrameworkAdapter.
if (this.settings.webSocketFactory) {
Expand Down Expand Up @@ -1627,12 +1632,21 @@ export class BotFrameworkAdapter
this.settings.channelAuthTenant
);
} else {
credentials = new MicrosoftAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope);
}

if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
credentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
credentials.oAuthScope = oAuthScope || GovernmentConstants.ToChannelFromBotOAuthScope;
if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
credentials = new MicrosoftGovernmentAppCredentials(
appId,
appPassword,
this.settings.channelAuthTenant,
oAuthScope
);
} else {
credentials = new MicrosoftAppCredentials(
appId,
appPassword,
this.settings.channelAuthTenant,
oAuthScope
);
}
}

return credentials;
Expand Down
6 changes: 2 additions & 4 deletions libraries/botbuilder/src/botFrameworkHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
AppCredentials,
AuthenticationConstants,
ConversationConstants,
GovernmentConstants,
ICredentialProvider,
JwtTokenValidation,
MicrosoftAppCredentials,
MicrosoftGovernmentAppCredentials,
} from 'botframework-connector';

import { USER_AGENT } from './botFrameworkAdapter';
Expand Down Expand Up @@ -158,9 +158,7 @@ export class BotFrameworkHttpClient implements BotFrameworkClient {
protected async buildCredentials(appId: string, oAuthScope?: string): Promise<AppCredentials> {
const appPassword = await this.credentialProvider.getAppPassword(appId);
if (JwtTokenValidation.isGovernment(this.channelService)) {
const appCredentials = new MicrosoftAppCredentials(appId, appPassword, undefined, oAuthScope);
appCredentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
return appCredentials;
return new MicrosoftGovernmentAppCredentials(appId, appPassword, undefined, oAuthScope);
} else {
return new MicrosoftAppCredentials(appId, appPassword, undefined, oAuthScope);
}
Expand Down
24 changes: 16 additions & 8 deletions libraries/botframework-connector/src/auth/appCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,11 @@ export abstract class AppCredentials implements ServiceClientCredentials {
* @param channelAuthTenant Optional. The oauth token tenant.
* @param oAuthScope The scope for the token.
*/
constructor(
appId: string,
channelAuthTenant?: string,
oAuthScope: string = AuthenticationConstants.ToBotFromChannelTokenIssuer
) {
constructor(appId: string, channelAuthTenant?: string, oAuthScope: string = null) {
this.appId = appId;
this.tenant = channelAuthTenant;
this.oAuthEndpoint = AuthenticationConstants.ToChannelFromBotLoginUrlPrefix + this.tenant;
this.oAuthScope = oAuthScope;
this.oAuthEndpoint = this.GetToChannelFromBotLoginUrlPrefix() + this.tenant;
this.oAuthScope = oAuthScope && oAuthScope.length > 0 ? oAuthScope : this.GetToChannelFromBotOAuthScope();
}

/**
Expand All @@ -69,7 +65,7 @@ export abstract class AppCredentials implements ServiceClientCredentials {
* Sets tenant to be used for channel authentication.
*/
private set tenant(value: string) {
this._tenant = value && value.length > 0 ? value : AuthenticationConstants.DefaultChannelAuthTenant;
this._tenant = value && value.length > 0 ? value : this.GetDefaultChannelAuthTenant();
}

/**
Expand Down Expand Up @@ -191,6 +187,18 @@ export abstract class AppCredentials implements ServiceClientCredentials {
}
}

protected GetToChannelFromBotOAuthScope(): string {
return AuthenticationConstants.ToChannelFromBotOAuthScope;
}

protected GetToChannelFromBotLoginUrlPrefix(): string {
return AuthenticationConstants.ToChannelFromBotLoginUrlPrefix;
}

protected GetDefaultChannelAuthTenant(): string {
return AuthenticationConstants.DefaultChannelAuthTenant;
}

protected abstract refreshToken(): Promise<AuthenticatorResult>;

/**
Expand Down
164 changes: 164 additions & 0 deletions libraries/botframework-connector/src/auth/aseChannelValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* @module botframework-connector
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/* eslint-disable @typescript-eslint/no-namespace */

import { ClaimsIdentity } from './claimsIdentity';
import { AuthenticationConstants } from './authenticationConstants';
import { AuthenticationConfiguration } from './authenticationConfiguration';
import { GovernmentConstants } from './governmentConstants';
import { ICredentialProvider } from './credentialProvider';
import { JwtTokenExtractor } from './jwtTokenExtractor';
import { JwtTokenValidation } from './jwtTokenValidation';
import { AuthenticationError } from './authenticationError';
import { SimpleCredentialProvider } from './credentialProvider';
import { StatusCodes } from 'botframework-schema';
import { BetweenBotAndAseChannelTokenValidationParameters } from './tokenValidationParameters';

/**
* @deprecated Use `ConfigurationBotFrameworkAuthentication` instead to perform AseChannel validation.
* Validates and Examines JWT tokens from the Bot Framework AseChannel
*/
export namespace AseChannelValidation {
const ChannelId = 'AseChannel';
let _creadentialProvider: ICredentialProvider;
let _channelService: string;
export let MetadataUrl: string;

/**
* init authentication from user .env configuration.
*
* @param configuration The user .env configuration.
*/
export function init(configuration: any) {
const appId = configuration.MicrosoftAppId;
const tenantId = configuration.MicrosoftAppTenantId;
_channelService = configuration.ChannelService;
MetadataUrl =
_channelService !== undefined && JwtTokenValidation.isGovernment(_channelService)
? GovernmentConstants.ToBotFromEmulatorOpenIdMetadataUrl
: AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl;

_creadentialProvider = new SimpleCredentialProvider(appId, '');

const tenantIds: string[] = [
tenantId,
'f8cdef31-a31e-4b4a-93e4-5f571e91255a', // US Gov MicrosoftServices.onmicrosoft.us
'd6d49420-f39b-4df7-a1dc-d59a935871db', // Public botframework.com
];
const validIssuers: string[] = [];
tenantIds.forEach((tmpId: string) => {
validIssuers.push(`https://sts.windows.net/${tmpId}/`); // Auth Public/US Gov, 1.0 token
validIssuers.push(`https://login.microsoftonline.com/${tmpId}/v2.0`); // Auth Public, 2.0 token
validIssuers.push(`https://login.microsoftonline.us/${tmpId}/v2.0`); // Auth for US Gov, 2.0 token
});
BetweenBotAndAseChannelTokenValidationParameters.issuer = validIssuers;
}

/**
* Determines if a given Auth header is from the Bot Framework AseChannel
*
* @param {string} channelId The channelId.
* @returns {boolean} True, if the token was issued by the AseChannel. Otherwise, false.
*/
export function isTokenFromAseChannel(channelId: string): boolean {
return channelId === ChannelId;
}

/**
* Validate the incoming Auth Header as a token sent from the Bot Framework AseChannel.
* A token issued by the Bot Framework will FAIL this check. Only AseChannel tokens will pass.
*
* @param {string} authHeader The raw HTTP header in the format: 'Bearer [longString]'
* @param {AuthenticationConfiguration} authConfig The authentication configuration.
* @returns {Promise<ClaimsIdentity>} A valid ClaimsIdentity.
*/
export async function authenticateAseChannelToken(
authHeader: string,
authConfig: AuthenticationConfiguration = new AuthenticationConfiguration()
): Promise<ClaimsIdentity> {
const tokenExtractor: JwtTokenExtractor = new JwtTokenExtractor(
BetweenBotAndAseChannelTokenValidationParameters,
MetadataUrl,
AuthenticationConstants.AllowedSigningAlgorithms
);

const identity: ClaimsIdentity = await tokenExtractor.getIdentityFromAuthHeader(
authHeader,
ChannelId,
authConfig.requiredEndorsements
);
if (!identity) {
// No valid identity. Not Authorized.
throw new AuthenticationError('Unauthorized. No valid identity.', StatusCodes.UNAUTHORIZED);
}

if (!identity.isAuthenticated) {
// The token is in some way invalid. Not Authorized.
throw new AuthenticationError('Unauthorized. Is not authenticated', StatusCodes.UNAUTHORIZED);
}

// Now check that the AppID in the claimset matches
// what we're looking for. Note that in a multi-tenant bot, this value
// comes from developer code that may be reaching out to a service, hence the
// Async validation.
const versionClaim: string = identity.getClaimValue(AuthenticationConstants.VersionClaim);
if (versionClaim === null) {
throw new AuthenticationError(
'Unauthorized. "ver" claim is required on Emulator Tokens.',
StatusCodes.UNAUTHORIZED
);
}

let appId = '';

// The Emulator, depending on Version, sends the AppId via either the
// appid claim (Version 1) or the Authorized Party claim (Version 2).
if (!versionClaim || versionClaim === '1.0') {
// either no Version or a version of "1.0" means we should look for
// the claim in the "appid" claim.
const appIdClaim: string = identity.getClaimValue(AuthenticationConstants.AppIdClaim);
if (!appIdClaim) {
// No claim around AppID. Not Authorized.
throw new AuthenticationError(
'Unauthorized. "appid" claim is required on Emulator Token version "1.0".',
StatusCodes.UNAUTHORIZED
);
}

appId = appIdClaim;
} else if (versionClaim === '2.0') {
// Emulator, "2.0" puts the AppId in the "azp" claim.
const appZClaim: string = identity.getClaimValue(AuthenticationConstants.AuthorizedParty);
if (!appZClaim) {
// No claim around AppID. Not Authorized.
throw new AuthenticationError(
'Unauthorized. "azp" claim is required on Emulator Token version "2.0".',
StatusCodes.UNAUTHORIZED
);
}

appId = appZClaim;
} else {
// Unknown Version. Not Authorized.
throw new AuthenticationError(
`Unauthorized. Unknown Emulator Token version "${versionClaim}".`,
StatusCodes.UNAUTHORIZED
);
}

if (!(await _creadentialProvider.isValidAppId(appId))) {
throw new AuthenticationError(
`Unauthorized. Invalid AppId passed on token: ${appId}`,
StatusCodes.UNAUTHORIZED
);
}

return identity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,21 @@ export namespace GovernmentConstants {

/**
* TO CHANNEL FROM BOT: Login URL
*
* DEPRECATED: DO NOT USE
*/
export const ToChannelFromBotLoginUrl = 'https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us';

/**
* TO CHANNEL FROM BOT: Login URL prefix
*/
export const ToChannelFromBotLoginUrlPrefix = 'https://login.microsoftonline.us/';

/**
* TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication
*/
export const DefaultChannelAuthTenant = 'MicrosoftServices.onmicrosoft.us';

/**
* TO CHANNEL FROM BOT: OAuth scope to request
*/
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 @@ -22,6 +22,7 @@ export * from './claimsIdentity';
export * from './connectorFactory';
export * from './credentialProvider';
export * from './emulatorValidation';
export * from './aseChannelValidation';
export * from './endorsementsValidator';
export * from './enterpriseChannelValidation';
export * from './governmentChannelValidation';
Expand All @@ -32,9 +33,11 @@ export * from './managedIdentityAppCredentials';
export * from './managedIdentityAuthenticator';
export * from './managedIdentityServiceClientCredentialsFactory';
export * from './microsoftAppCredentials';
export * from './microsoftGovernmentAppCredentials';
export * from './passwordServiceClientCredentialFactory';
export * from './serviceClientCredentialsFactory';
export * from './skillValidation';
export * from './tokenValidationParameters';
export * from './userTokenClient';

export { MsalAppCredentials } from './msalAppCredentials';
Expand Down