UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

230 lines 10.4 kB
"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