private async Task <bool> DeduplicatedTokenExchangeIdAsync(ITurnContext turnContext, CancellationToken cancellationToken) { // Create a StoreItem with Etag of the unique 'signin/tokenExchange' request var storeItem = new TokenStoreItem { ETag = (turnContext.Activity.Value as JObject).Value <string>("id") }; var storeItems = new Dictionary <string, object> { { TokenStoreItem.GetStorageKey(turnContext), storeItem } }; try { // Writing the IStoreItem with ETag of unique id will succeed only once await _storage.WriteAsync(storeItems, cancellationToken).ConfigureAwait(false); } catch (Exception ex) // Memory storage throws a generic exception with a Message of 'Etag conflict. [other error info]' // CosmosDbPartitionedStorage throws: ex.Message.Contains("pre-condition is not met") when(ex.Message.StartsWith("Etag conflict", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("pre-condition is not met")) { // Do NOT proceed processing this message, some other thread or machine already has processed it. // Send 200 invoke response. await SendInvokeResponseAsync(turnContext, cancellationToken : cancellationToken).ConfigureAwait(false); return(false); } return(true); }
/// <summary> /// Determines if a "signin/tokenExchange" should be processed by this caller. /// /// If a token exchange is unsuccessful, an InvokeResponse of PreconditionFailed is sent. /// </summary> /// <param name="turnContext"><see cref="ITurnContext"/> for this specific activity.</param> /// <param name="cancellationToken"><see cref="CancellationToken"/> for this specific process.</param> /// <returns>True if the bot should continue processing this TokenExchange request.</returns> public async Task <bool> ShouldProcessTokenExchange(ITurnContext turnContext, CancellationToken cancellationToken) { if (turnContext.Activity.Name != SignInConstants.TokenExchangeOperationName) { throw new InvalidOperationException("Only 'signin/tokenExchange' invoke activities can be procssed by TokenExchangeHelper."); } if (!await this.ExchangedTokenAsync(turnContext, cancellationToken).ConfigureAwait(false)) { // If the TokenExchange is NOT successful, the response will have already been sent by ExchangedTokenAsync return(false); } // If a user is signed into multiple Teams clients, the Bot might receive a "signin/tokenExchange" from each client. // Each token exchange request for a specific user login will have an identical Activity.Value.Id. // Only one of these token exchange requests should be processe by the bot. For a distributed bot in production, // this requires a distributed storage to ensure only one token exchange is processed. // This example utilizes Bot Framework IStorage's ETag implementation for token exchange activity deduplication. // Create a StoreItem with Etag of the unique 'signin/tokenExchange' request var storeItem = new TokenStoreItem { ETag = (turnContext.Activity.Value as JObject).Value <string>("id") }; var storeItems = new Dictionary <string, object> { { TokenStoreItem.GetStorageKey(turnContext), storeItem } }; try { // Writing the IStoreItem with ETag of unique id will succeed only once await _storage.WriteAsync(storeItems, cancellationToken).ConfigureAwait(false); } catch (Exception ex) // Memory storage throws a generic exception with a Message of 'Etag conflict. [other error info]' when(ex.Message.StartsWith("Etag conflict")) { // CosmosDbPartitionedStorage throws: ex.Message.Contains("pre-condition is not met") // Do NOT send this message on to be processed, some other thread or machine already has processed it. // TODO: Should send 200 invoke response here??? return(false); } return(true); }