fix: [#2398] Support Bots that use Single Tenant AAD Applications (#2457)

* include Single Tenant apps support

* change tenantId variable name

* fix unit tests

---------

Co-authored-by: JhontSouth <jhonatan.sandoval@southworks.com>
This commit is contained in:
Cecilia Avila 2024-05-23 02:15:41 +00:00 коммит произвёл GitHub
Родитель 012a664107
Коммит e554dc4231
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
15 изменённых файлов: 75 добавлений и 16 удалений

Просмотреть файл

@ -68,6 +68,7 @@ export class EmulatorCommands {
openBotViaUrlAction({
appId: endpoint.appId,
appPassword: endpoint.appPassword,
tenantId: endpoint.tenantId,
channelService: endpoint.channelService as ChannelService,
endpoint: endpoint.endpoint,
isFromBotFile: true,

Просмотреть файл

@ -110,6 +110,7 @@ export class BotSagas {
mode: action.payload.mode,
msaAppId: action.payload.appId,
msaPassword: action.payload.appPassword,
msaTenantId: action.payload.tenantId,
};
let res: Response = yield call([ConversationService, ConversationService.startConversation], serverUrl, payload);
if (!res.ok) {
@ -135,6 +136,7 @@ export class BotSagas {
speechKey: action.payload.speechKey,
speechRegion: action.payload.speechRegion,
user,
msaTenantId: action.payload.tenantId,
});
// add a document to the store so the livechat tab is rendered

Просмотреть файл

@ -256,6 +256,7 @@ describe('BotCreationDialog tests', () => {
endpoint: '',
id: expect.any(String),
name: '',
tenantId: '',
type: 'endpoint',
},
],

Просмотреть файл

@ -98,6 +98,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
appId: '',
appPassword: '',
endpoint: '',
tenantId: '',
}),
isAzureGov: false,
secret: '',
@ -121,7 +122,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
<Dialog className={dialogStyles.main} title="New bot configuration" cancel={this.onCancel}>
<div className={styles.botCreateForm}>
<TextField
value={this.state.bot.name}
value={bot.name}
data-prop="name"
onChange={this.onInputChange}
label={'Bot name'}
@ -134,7 +135,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
placeholder={endpointPlaceholder}
label={'Endpoint URL'}
required={true}
value={this.state.endpoint.endpoint}
value={endpoint.endpoint}
name={'create-bot-url'}
/>
{endpointWarning && <span className={styles.endpointWarning}>{endpointWarning}</span>}
@ -158,6 +159,13 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
value={endpoint.appPassword}
/>
</Row>
<TextField
name="tenantId"
label="Tenant ID"
onChange={this.onInputChange}
placeholder="Optional"
value={endpoint.tenantId}
/>
<Row align={RowAlignment.Bottom}>
<Checkbox label="Azure for US Government" checked={isAzureGov} onChange={this.onChannelServiceChange} />
<LinkButton
@ -350,6 +358,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
id: this.state.endpoint.id.trim(),
appId: this.state.endpoint.appId.trim(),
appPassword: this.state.endpoint.appPassword.trim(),
tenantId: this.state.endpoint.tenantId.trim(),
endpoint: this.state.endpoint.endpoint.trim(),
};
(endpoint as any).channelService = (this.state.endpoint as any).channelService;

Просмотреть файл

@ -227,6 +227,7 @@ describe('The OpenBotDialog', () => {
mode: 'livechat',
speechKey: 'i-am-a-speech-key',
speechRegion: 'westus',
tenantId: '',
});
});
@ -254,6 +255,7 @@ describe('The OpenBotDialog', () => {
mode: 'livechat',
speechKey: '',
speechRegion: '',
tenantId: '',
});
});

Просмотреть файл

@ -111,10 +111,19 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
constructor(props: OpenBotDialogProps) {
super(props);
const { appId = '', appPassword = '', botUrl = '', isAzureGov = false, isDebug = false, mode = 'livechat' } = props;
const {
appId = '',
appPassword = '',
botUrl = '',
isAzureGov = false,
isDebug = false,
mode = 'livechat',
tenantId,
} = props;
this.state = {
appId,
appPassword,
tenantId,
botUrl,
isAzureGov,
isDebug,
@ -132,6 +141,7 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
botUrl,
appId,
appPassword,
tenantId,
mode,
isDebug,
isAzureGov,
@ -180,6 +190,13 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
value={appPassword}
/>
</Row>
<TextField
name="tenantId"
label="Tenant ID"
onChange={this.onInputChange}
placeholder="Optional"
value={tenantId}
/>
{!isDebug && (
<Row className={openBotStyles.multiInputRow}>
<TextField

Просмотреть файл

@ -50,6 +50,7 @@ const mapDispatchToProps = (dispatch: (action: Action) => void): OpenBotDialogPr
const {
appId = '',
appPassword = '',
tenantId = '',
botUrl = '',
mode = 'livechat-url',
isAzureGov,
@ -63,6 +64,7 @@ const mapDispatchToProps = (dispatch: (action: Action) => void): OpenBotDialogPr
openBotViaUrlAction({
appId,
appPassword,
tenantId,
endpoint: botUrl,
mode,
channelService: isAzureGov ? 'azureusgovernment' : 'public',

Просмотреть файл

@ -34,6 +34,7 @@
export const authentication = {
channelService: 'https://dev.botframework.com/',
tokenEndpoint: 'https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token',
tokenEndpointSingleTenant: 'https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token',
openIdMetadata: 'https://login.microsoftonline.com/botframework.com/v2.0/.well-known/openid-configuration',
botTokenAudience: 'https://api.botframework.com',
};
@ -53,6 +54,7 @@ export const v31Authentication = {
};
export const v32Authentication = {
tokenIssuerSingleTenant: 'https://sts.windows.net/{tenant-id}/',
tokenIssuerV1: 'https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/',
tokenIssuerV2: 'https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0',
};

Просмотреть файл

@ -45,22 +45,22 @@ export function createGetBotEndpointHandler(state: ServerState) {
if (request.jwt && request.jwt.appid) {
request.botEndpoint = endpoints.getByAppId(request.jwt.appid);
} else {
const { bot, botUrl, channelServiceType, msaAppId, msaPassword } = req.body;
const { bot, botUrl, channelServiceType, msaAppId, msaPassword, options, msaTenantId } = req.body;
let endpoint = endpoints.get(botUrl);
if (!endpoint) {
const channelService =
channelServiceType === 'azureusgovernment'
? usGovernmentAuthentication.channelService
: authentication.channelService;
// create endpoint
endpoint = endpoints.set(
bot.id,
new BotEndpoint(bot.id, bot.id, botUrl, msaAppId, msaPassword, false, channelService)
new BotEndpoint(bot.id, bot.id, botUrl, msaAppId, msaPassword, false, channelService, options, msaTenantId)
);
} else {
endpoint.msaAppId = msaAppId;
endpoint.msaPassword = msaPassword;
endpoint.tenantId = msaTenantId;
}
request.botEndpoint = endpoint;
}

Просмотреть файл

@ -51,7 +51,7 @@ import { getActivitiesForConversation } from './handlers/getActivitiesForConvers
export function mountConversationsRoutes(emulatorServer: EmulatorRestServer) {
const { server, state } = emulatorServer;
const verifyBotFramework = createBotFrameworkAuthenticationMiddleware(emulatorServer.options.fetch);
const verifyBotFramework = createBotFrameworkAuthenticationMiddleware(emulatorServer.options.fetch, state);
const jsonBodyParser = createJsonBodyParserMiddleware();
const fetchConversation = createGetConversationHandler(state);

Просмотреть файл

@ -41,14 +41,22 @@ import {
v32Authentication,
} from '../../constants/authEndpoints';
import { OpenIdMetadata } from '../../utils/openIdMetadata';
import { ConversationAPIPathParameters } from '../channel/conversations/types/conversationAPIPathParameters';
import { ServerState } from '../../state/serverState';
export function createBotFrameworkAuthenticationMiddleware(fetch: any) {
export function createBotFrameworkAuthenticationMiddleware(fetch: any, state?: ServerState) {
const openIdMetadata = new OpenIdMetadata(fetch, authentication.openIdMetadata);
const usGovOpenIdMetadata = new OpenIdMetadata(fetch, usGovernmentAuthentication.openIdMetadata);
return async (req: Restify.Request, res: Restify.Response) => {
const authorization = req.header('Authorization');
const conversationParameters: ConversationAPIPathParameters = req.params;
let conversation;
if (conversationParameters?.conversationId && state) {
conversation = state.conversations.conversationById(conversationParameters.conversationId);
}
if (!authorization) {
return;
}
@ -118,7 +126,9 @@ export function createBotFrameworkAuthenticationMiddleware(fetch: any) {
let issuer;
if (decoded.payload.ver === '1.0') {
if (conversation?.botEndpoint.tenantId) {
issuer = v32Authentication.tokenIssuerSingleTenant.replace('{tenant-id}', conversation?.botEndpoint.tenantId);
} else if (decoded.payload.ver === '1.0') {
issuer = v32Authentication.tokenIssuerV1;
} else if (decoded.payload.ver === '2.0') {
issuer = v32Authentication.tokenIssuerV2;

Просмотреть файл

@ -48,6 +48,7 @@ export class BotEndpoint {
public appId?: string;
public appPassword?: string;
public speechAuthenticationToken?: SpeechAuthenticationToken;
public tenantId?: string;
constructor(
public id?: string,
@ -57,10 +58,12 @@ export class BotEndpoint {
public msaPassword?: string,
public use10Tokens?: boolean,
public channelService?: string,
private _options?: BotEndpointOptions
private _options?: BotEndpointOptions,
public msaTenantId?: string
) {
this.appId = msaAppId;
this.appPassword = msaPassword;
this.tenantId = msaTenantId;
}
private willTokenExpireWithin(millisecondsToExpire: number): boolean {
@ -126,7 +129,8 @@ export class BotEndpoint {
} catch (e) {
return {
status: e.status,
message: "The bot's Microsoft App ID or Microsoft App Password is incorrect.",
message:
"The bot's Microsoft App ID, Microsoft App Password, or Microsoft Tenant ID (Single Tenant apps) is incorrect.",
};
}
}
@ -149,11 +153,17 @@ export class BotEndpoint {
return this.accessToken;
}
let tokenEndpoint;
// Refresh access token
const tokenEndpoint: string =
this.channelService === usGovernmentAuthentication.channelService
? usGovernmentAuthentication.tokenEndpoint
: authentication.tokenEndpoint;
if (this.channelService === usGovernmentAuthentication.channelService) {
tokenEndpoint = usGovernmentAuthentication.tokenEndpoint;
} else if (this.tenantId) {
tokenEndpoint = authentication.tokenEndpointSingleTenant.replace('{tenant-id}', this.tenantId);
} else {
tokenEndpoint = authentication.tokenEndpoint;
}
const resp = await this._options.fetch(tokenEndpoint, {
method: 'POST',
body: new URLSearchParams({

Просмотреть файл

@ -57,7 +57,8 @@ export class EndpointSet {
botEndpoint.channelService,
{
fetch: this._fetch,
}
},
botEndpoint.tenantId
);
this._endpoints[id] = botEndpointInstance;

Просмотреть файл

@ -58,6 +58,7 @@ interface StartConversationPayload {
mode: EmulatorMode;
msaAppId?: string;
msaPassword?: string;
msaTenantId?: string;
}
export class ConversationService {

Просмотреть файл

@ -36,6 +36,7 @@ export interface BotEndpoint {
botUrl: string;
msaAppId: string;
msaPassword: string;
msaTenantId?: string;
use10Tokens?: boolean;
channelService?: string;
}