UNPKG

8.28 kBJavaScriptView Raw
1"use strict";
2// Copyright (c) Microsoft Corporation.
3// Licensed under the MIT License.
4var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
5 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
6 return new (P || (P = Promise))(function (resolve, reject) {
7 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
8 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
9 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
10 step((generator = generator.apply(thisArg, _arguments || [])).next());
11 });
12};
13Object.defineProperty(exports, "__esModule", { value: true });
14exports.TeamsSSOTokenExchangeMiddleware = void 0;
15const z = require("zod");
16const botbuilder_core_1 = require("botbuilder-core");
17function getStorageKey(context) {
18 var _a;
19 const activity = context.activity;
20 const channelId = activity.channelId;
21 if (!channelId) {
22 throw new Error('invalid activity. Missing channelId');
23 }
24 const conversationId = (_a = activity.conversation) === null || _a === void 0 ? void 0 : _a.id;
25 if (!conversationId) {
26 throw new Error('invalid activity. Missing conversation.id');
27 }
28 const value = activity.value;
29 if (!(value === null || value === void 0 ? void 0 : value.id)) {
30 throw new Error('Invalid signin/tokenExchange. Missing activity.value.id.');
31 }
32 return `${channelId}/${conversationId}/${value.id}`;
33}
34function sendInvokeResponse(context, body = null, status = botbuilder_core_1.StatusCodes.OK) {
35 return __awaiter(this, void 0, void 0, function* () {
36 yield context.sendActivity({
37 type: botbuilder_core_1.ActivityTypes.InvokeResponse,
38 value: { body, status },
39 });
40 });
41}
42const ExchangeToken = z.custom((val) => typeof val.exchangeToken === 'function', { message: 'ExtendedUserTokenProvider' });
43/**
44 * If the activity name is signin/tokenExchange, this middleware will attempt to
45 * exchange the token, and deduplicate the incoming call, ensuring only one
46 * exchange request is processed.
47 *
48 * If a user is signed into multiple Teams clients, the Bot could receive a
49 * "signin/tokenExchange" from each client. Each token exchange request for a
50 * specific user login will have an identical activity.value.id.
51 *
52 * Only one of these token exchange requests should be processed by the bot.
53 * The others return [StatusCodes.PRECONDITION_FAILED](xref:botframework-schema:StatusCodes.PRECONDITION_FAILED).
54 * For a distributed bot in production, this requires distributed storage
55 * ensuring only one token exchange is processed. This middleware supports
56 * CosmosDb storage found in botbuilder-azure, or MemoryStorage for local development.
57 */
58class TeamsSSOTokenExchangeMiddleware {
59 /**
60 * Initializes a new instance of the TeamsSSOTokenExchangeMiddleware class.
61 *
62 * @param storage The [Storage](xref:botbuilder-core.Storage) to use for deduplication
63 * @param oAuthConnectionName The connection name to use for the single sign on token exchange
64 */
65 constructor(storage, oAuthConnectionName) {
66 this.storage = storage;
67 this.oAuthConnectionName = oAuthConnectionName;
68 if (!storage) {
69 throw new TypeError('`storage` parameter is required');
70 }
71 if (!oAuthConnectionName) {
72 throw new TypeError('`oAuthConnectionName` parameter is required');
73 }
74 }
75 /**
76 * Called each time the bot receives a new request.
77 *
78 * @param context Context for current turn of conversation with the user.
79 * @param next Function to call to continue execution to the next step in the middleware chain.
80 */
81 onTurn(context, next) {
82 return __awaiter(this, void 0, void 0, function* () {
83 if (context.activity.channelId === botbuilder_core_1.Channels.Msteams && context.activity.name === botbuilder_core_1.tokenExchangeOperationName) {
84 // If the TokenExchange is NOT successful, the response will have already been sent by exchangedToken
85 if (!(yield this.exchangedToken(context))) {
86 return;
87 }
88 // Only one token exchange should proceed from here. Deduplication is performed second because in the case
89 // of failure due to consent required, every caller needs to receive a response
90 if (!(yield this.deduplicatedTokenExchangeId(context))) {
91 // If the token is not exchangeable, do not process this activity further.
92 return;
93 }
94 }
95 yield next();
96 });
97 }
98 deduplicatedTokenExchangeId(context) {
99 var _a, _b;
100 return __awaiter(this, void 0, void 0, function* () {
101 // Create a StoreItem with Etag of the unique 'signin/tokenExchange' request
102 const storeItem = {
103 eTag: (_a = context.activity.value) === null || _a === void 0 ? void 0 : _a.id,
104 };
105 try {
106 // Writing the IStoreItem with ETag of unique id will succeed only once
107 yield this.storage.write({
108 [getStorageKey(context)]: storeItem,
109 });
110 }
111 catch (err) {
112 const message = (_b = err.message) === null || _b === void 0 ? void 0 : _b.toLowerCase();
113 // Do NOT proceed processing this message, some other thread or machine already has processed it.
114 // Send 200 invoke response.
115 if (message.includes('etag conflict') || message.includes('precondition is not met')) {
116 yield sendInvokeResponse(context);
117 return false;
118 }
119 throw err;
120 }
121 return true;
122 });
123 }
124 exchangedToken(context) {
125 return __awaiter(this, void 0, void 0, function* () {
126 let tokenExchangeResponse;
127 const tokenExchangeRequest = context.activity.value;
128 try {
129 const userTokenClient = context.turnState.get(context.adapter.UserTokenClientKey);
130 const exchangeToken = ExchangeToken.safeParse(context.adapter);
131 if (userTokenClient) {
132 tokenExchangeResponse = yield userTokenClient.exchangeToken(context.activity.from.id, this.oAuthConnectionName, context.activity.channelId, { token: tokenExchangeRequest.token });
133 }
134 else if (exchangeToken.success) {
135 tokenExchangeResponse = yield exchangeToken.data.exchangeToken(context, this.oAuthConnectionName, context.activity.from.id, { token: tokenExchangeRequest.token });
136 }
137 else {
138 new Error('Token Exchange is not supported by the current adapter.');
139 }
140 }
141 catch (_err) {
142 // Ignore Exceptions
143 // If token exchange failed for any reason, tokenExchangeResponse above stays null,
144 // and hence we send back a failure invoke response to the caller.
145 }
146 if (!(tokenExchangeResponse === null || tokenExchangeResponse === void 0 ? void 0 : tokenExchangeResponse.token)) {
147 // The token could not be exchanged (which could be due to a consent requirement)
148 // Notify the sender that PreconditionFailed so they can respond accordingly.
149 const invokeResponse = {
150 id: tokenExchangeRequest.id,
151 connectionName: this.oAuthConnectionName,
152 failureDetail: 'The bot is unable to exchange token. Proceed with regular login.',
153 };
154 yield sendInvokeResponse(context, invokeResponse, botbuilder_core_1.StatusCodes.PRECONDITION_FAILED);
155 return false;
156 }
157 return true;
158 });
159 }
160}
161exports.TeamsSSOTokenExchangeMiddleware = TeamsSSOTokenExchangeMiddleware;
162//# sourceMappingURL=teamsSSOTokenExchangeMiddleware.js.map
\No newline at end of file