@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
230 lines • 10.4 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.TeamsSsoPrompt = void 0;
const botbuilder_dialogs_1 = require("botbuilder-dialogs");
const botbuilder_1 = require("botbuilder");
const uuid_1 = require("uuid");
const invokeResponseType = 'invokeResponse';
class TokenExchangeInvokeResponse {
/**
* Response id
*/
id;
/**
* Detailed error message
*/
failureDetail;
constructor(id, failureDetail) {
this.id = id;
this.failureDetail = failureDetail;
}
}
/**
* @internal
*
* Creates a new prompt that leverage Teams Single Sign On (SSO) support for bot to automatically sign in user and
* help receive oauth token, asks the user to consent if needed.
*/
class TeamsSsoPrompt extends botbuilder_dialogs_1.Dialog {
settingName;
settings;
msal;
/**
* Creates a new instance of TeamsSsoPrompt.
* @param {string} dialogId - The ID of the dialog.
* @param {string} settingName - The name of the setting.
* @param {TeamsSsoSettings} settings - The settings for Teams SSO.
* @param {ConfidentialClientApplication} msal - The MSAL (Microsoft Authentication Library) object.
*/
constructor(dialogId, settingName, settings, msal) {
super(dialogId);
this.settingName = settingName;
this.settings = settings;
this.msal = msal;
this.validateScopesType(settings.scopes);
}
/**
* Called when a prompt dialog is pushed onto the dialog stack and is being activated.
* @remarks
* If the task is successful, the result indicates whether the prompt is still
* active after the turn has been processed by the prompt.
* @param {any} dc - The DialogContext for the current turn of the conversation.
* @param {any} options - The options for the dialog
* @returns {Promise<any>} A `Promise` representing the result of current turn.
*/
async beginDialog(dc, options) {
const default_timeout = 900000;
let timeout = default_timeout;
if (this.settings.timeout) {
if (typeof this.settings.timeout != 'number') {
const errorMsg = 'type of timeout property in teamsBotSsoPromptSettings should be number.';
throw new Error(errorMsg);
}
if (this.settings.timeout <= 0) {
const errorMsg = 'value of timeout property in teamsBotSsoPromptSettings should be positive.';
throw new Error(errorMsg);
}
timeout = this.settings.timeout;
}
if (this.settings.endOnInvalidMessage === undefined) {
this.settings.endOnInvalidMessage = true;
}
const state = dc.activeDialog?.state;
state.state = {};
state.options = {};
state.expires = new Date().getTime() + timeout;
const token = await this.acquireTokenFromCache(dc.context);
if (token) {
const tokenResponse = {
connectionName: '', // No connection name is avaiable in this implementation
token: token.accessToken,
expiration: token.expiresOn?.toISOString() ?? ''
};
return await dc.endDialog(tokenResponse);
}
// Cannot get token from cache, send OAuth card to get SSO token
await this.sendOAuthCardAsync(dc.context);
return botbuilder_dialogs_1.Dialog.EndOfTurn;
}
/**
* Called when a prompt dialog is the active dialog and the user replied with a new activity.
* @remarks
* If the task is successful, the result indicates whether the dialog is still
* active after the turn has been processed by the dialog.
* @param {any} dc The DialogContext for the current turn of the conversation.
* @returns {Promise<any>} A `Promise` representing the result of the turn after the dialog has processed the activity.
*/
async continueDialog(dc) {
const state = dc.activeDialog?.state;
const isMessage = dc.context.activity.type === botbuilder_1.ActivityTypes.Message;
const isTimeoutActivityType = isMessage || this.isTeamsVerificationInvoke(dc.context) || this.isTokenExchangeRequestInvoke(dc.context);
const hasTimedOut = isTimeoutActivityType && new Date().getTime() > state.expires;
if (hasTimedOut) {
return await dc.endDialog(undefined);
}
else {
if (this.isTeamsVerificationInvoke(dc.context) || this.isTokenExchangeRequestInvoke(dc.context)) {
const recognized = await this.recognizeToken(dc);
if (recognized.succeeded) {
return await dc.endDialog(recognized.value);
}
}
else if (isMessage && this.settings.endOnInvalidMessage) {
return await dc.endDialog(undefined);
}
return botbuilder_dialogs_1.Dialog.EndOfTurn;
}
}
async recognizeToken(dc) {
const context = dc.context;
let tokenResponse;
if (this.isTokenExchangeRequestInvoke(context)) {
// Received activity is not a token exchange request
if (!(context.activity.value && this.isTokenExchangeRequest(context.activity.value))) {
const warningMsg = 'The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value. This is required to be sent with the InvokeActivity.';
await context.sendActivity(this.getTokenExchangeInvokeResponse(botbuilder_1.StatusCodes.BAD_REQUEST, warningMsg));
}
else {
const ssoToken = context.activity.value.token;
let exchangedToken;
try {
exchangedToken = await this.msal.acquireTokenOnBehalfOf({
oboAssertion: ssoToken,
scopes: this.settings.scopes
});
if (exchangedToken) {
await context.sendActivity(this.getTokenExchangeInvokeResponse(botbuilder_1.StatusCodes.OK, '', context.activity.value.id));
tokenResponse = {
connectionName: '',
token: exchangedToken.accessToken,
expiration: exchangedToken.expiresOn?.toISOString() ?? ''
};
}
}
catch (error) {
const warningMsg = 'The bot is unable to exchange token. Ask for user consent.';
await context.sendActivity(this.getTokenExchangeInvokeResponse(botbuilder_1.StatusCodes.PRECONDITION_FAILED, warningMsg, context.activity.value.id));
}
}
}
else if (this.isTeamsVerificationInvoke(context)) {
await this.sendOAuthCardAsync(dc.context);
await context.sendActivity({ type: invokeResponseType, value: { status: botbuilder_1.StatusCodes.OK } });
}
return tokenResponse !== undefined ? { succeeded: true, value: tokenResponse } : { succeeded: false };
}
async acquireTokenFromCache(context) {
if (context.activity.from.aadObjectId) {
try {
const account = await this.msal.getTokenCache().getAccountByLocalId(context.activity.from.aadObjectId);
if (account) {
const silentRequest = {
account: account,
scopes: this.settings.scopes
};
return await this.msal.acquireTokenSilent(silentRequest);
}
}
catch (error) {
return null;
}
}
return null;
}
getTokenExchangeInvokeResponse(status, failureDetail, id) {
const invokeResponse = {
type: invokeResponseType,
value: { status, body: new TokenExchangeInvokeResponse(id, failureDetail) }
};
return invokeResponse;
}
async sendOAuthCardAsync(context) {
const signInResource = await this.getSignInResource();
const card = botbuilder_1.CardFactory.oauthCard('', 'Teams SSO Sign In', 'Sign In', signInResource.signInLink, signInResource.tokenExchangeResource);
card.content.buttons[0].type = botbuilder_1.ActionTypes.Signin;
const msg = botbuilder_1.MessageFactory.attachment(card);
// Send prompt
await context.sendActivity(msg);
}
async getSignInResource() {
const clientId = this.settings.msalConfig.auth.clientId;
const scope = encodeURI(this.settings.scopes.join(' '));
const authority = this.settings.msalConfig.auth.authority ?? 'https://login.microsoftonline.com/common/';
const tenantId = authority.match(/https:\/\/[^\/]+\/([^\/]+)\/?/)?.[1];
const signInLink = `${this.settings.signInLink}?scope=${scope}&clientId=${clientId}&tenantId=${tenantId}`;
const tokenExchangeResource = {
id: `${(0, uuid_1.v4)()}-${this.settingName}`
};
return {
signInLink: signInLink,
tokenExchangeResource: tokenExchangeResource
};
}
validateScopesType(value) {
// empty array
if (Array.isArray(value) && value.length === 0) {
return;
}
// string array
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string')) {
return;
}
const errorMsg = 'The type of scopes is not valid, it must be string array';
throw new Error(errorMsg);
}
isTeamsVerificationInvoke(context) {
const activity = context.activity;
return activity.type === botbuilder_1.ActivityTypes.Invoke && activity.name === botbuilder_1.verifyStateOperationName;
}
isTokenExchangeRequestInvoke(context) {
const activity = context.activity;
return activity.type === botbuilder_1.ActivityTypes.Invoke && activity.name === botbuilder_1.tokenExchangeOperationName;
}
isTokenExchangeRequest(obj) {
return Object.prototype.hasOwnProperty.call(obj, 'token');
}
}
exports.TeamsSsoPrompt = TeamsSsoPrompt;
//# sourceMappingURL=TeamsBotSsoPrompt.js.map