diff --git a/.github/workflows/ci-dotnet-samples.yml b/.github/workflows/ci-dotnet-samples.yml deleted file mode 100644 index 9ba9f00d2..000000000 --- a/.github/workflows/ci-dotnet-samples.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: ci-dotnet-samples - -env: - ROOT_FOLDER: BotBuilder-Samples/samples/ - -on: - workflow_dispatch: - pull_request: - branches: - - main - paths: - - "samples/**/*.cs" - -jobs: - generate: - name: detect and generate bot matrix - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - - steps: - - uses: actions/checkout@v3 - - - name: git diff - uses: technote-space/get-diff-action@v4 - with: - PATTERNS: samples/**/*.cs - ABSOLUTE: true - - - name: generate matrix - id: set-matrix - shell: pwsh - if: env.GIT_DIFF - run: | - function UpSearchFolder { - param ([String] $path, [String] $file) - - while ($path -and !(Test-Path (Join-Path $path $file))) { - $path = Split-Path $path -Parent - } - - return $path - } - - $paths = @("${{ env.GIT_DIFF_FILTERED }}" -replace "'", "" -split " ") - $rootFolder = "${{ env.ROOT_FOLDER }}" - - $result = $paths | ForEach-Object { UpSearchFolder -path $_ -file "*.csproj" } | Get-Unique | ForEach-Object { - $folder = $_ - return @{ - name = $folder.Substring($folder.IndexOf($rootFolder) + $rootFolder.Length); - folder = $folder; - } - } - - "Generated matrix:" - ConvertTo-Json @($result) - - $matrix = ConvertTo-Json -Compress @($result) - - echo "::set-output name=matrix::$($matrix)" - - build: - needs: generate - runs-on: ubuntu-latest - strategy: - matrix: - include: ${{fromJSON(needs.generate.outputs.matrix)}} - fail-fast: false - - name: bot - ${{ matrix.name }} - steps: - - uses: actions/checkout@v3 - - - name: use .net 8.0.x - uses: actions/setup-dotnet@v2 - with: - dotnet-version: "8.0.x" - - - name: dotnet restore - run: dotnet restore - working-directory: ${{ matrix.folder }} - - - name: dotnet build - run: dotnet build --configuration Release --no-restore --nologo --clp:NoSummary - working-directory: ${{ matrix.folder }} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/86.bot-authentication-fic.sln b/samples/csharp_dotnetcore/86.bot-authentication-fic/86.bot-authentication-fic.sln new file mode 100644 index 000000000..47cc1e5ca --- /dev/null +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/86.bot-authentication-fic.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthFederatedCredBot", "AuthFederatedCredBot.csproj", "{0D47DE58-8F94-4E24-9265-77074A356DBE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0D47DE58-8F94-4E24-9265-77074A356DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D47DE58-8F94-4E24-9265-77074A356DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D47DE58-8F94-4E24-9265-77074A356DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D47DE58-8F94-4E24-9265-77074A356DBE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B623C9C4-F8D5-4AAC-AA92-FC01B93080ED} + EndGlobalSection +EndGlobal diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/AdapterWithErrorHandler.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/AdapterWithErrorHandler.cs index 678ace38d..6761e30a1 100644 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/AdapterWithErrorHandler.cs +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/AdapterWithErrorHandler.cs @@ -1,5 +1,8 @@ -// Generated with Bot Builder V4 SDK Template for Visual Studio CoreBot v4.22.0 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.TraceExtensions; using Microsoft.Bot.Connector.Authentication; @@ -9,18 +12,36 @@ namespace Microsoft.BotBuilderSamples { public class AdapterWithErrorHandler : CloudAdapter { - public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) + public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger, ConversationState conversationState = default) : base(auth, logger) { OnTurnError = async (turnContext, exception) => { // Log any leaked exception from the application. + // NOTE: In production environment, you should consider logging this to + // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how + // to add telemetry capture to your bot. logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); // Send a message to the user await turnContext.SendActivityAsync("The bot encountered an error or bug."); await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); + if (conversationState != null) + { + try + { + // Delete the conversationState for the current conversation to prevent the + // bot from getting stuck in a error-loop caused by being in a bad state. + // ConversationState should be thought of as similar to "cookie-state" in a Web pages. + await conversationState.DeleteAsync(turnContext); + } + catch (Exception e) + { + logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}"); + } + } + // Send a trace activity, which will be displayed in the Bot Framework Emulator await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); }; diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/EchoFICBot.csproj b/samples/csharp_dotnetcore/86.bot-authentication-fic/AuthFederatedCredBot.csproj similarity index 86% rename from samples/csharp_dotnetcore/86.bot-authentication-fic/EchoFICBot.csproj rename to samples/csharp_dotnetcore/86.bot-authentication-fic/AuthFederatedCredBot.csproj index cd1119bfc..188389efd 100644 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/EchoFICBot.csproj +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/AuthFederatedCredBot.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/AuthBotFIC.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/AuthBotFIC.cs new file mode 100644 index 000000000..d84a14569 --- /dev/null +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/AuthBotFIC.cs @@ -0,0 +1,39 @@ +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.22.0 + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; + +namespace Microsoft.BotBuilderSamples.Bots +{ + public class AuthBotFIC : DialogBot where T : Dialog + { + public AuthBotFIC(ConversationState conversationState, UserState userState, T dialog, ILogger> logger) + : base(conversationState, userState, dialog, logger) + { + } + + protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) + { + foreach (var member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync(MessageFactory.Text("Welcome to AuthenticationBot using Federated Credentials. Type anything to get logged in. Type 'logout' to sign-out."), cancellationToken); + } + } + } + + protected override async Task OnTokenResponseEventAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + Logger.LogInformation("Running dialog with Token Response Event Activity."); + + // Run the Dialog with the new Token Response Event Activity. + await Dialog.RunAsync(turnContext, ConversationState.CreateProperty(nameof(DialogState)), cancellationToken); + } + } +} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/DialogBot.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/DialogBot.cs new file mode 100644 index 000000000..5136835d9 --- /dev/null +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/DialogBot.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; + +namespace Microsoft.BotBuilderSamples +{ + // This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots + // to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types + // each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity. + // The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation, + // and the requirement is that all BotState objects are saved at the end of a turn. + public class DialogBot : ActivityHandler where T : Dialog + { + protected readonly BotState ConversationState; + protected readonly Dialog Dialog; + protected readonly ILogger Logger; + protected readonly BotState UserState; + + public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger> logger) + { + ConversationState = conversationState; + UserState = userState; + Dialog = dialog; + Logger = logger; + } + + public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) + { + await base.OnTurnAsync(turnContext, cancellationToken); + + // Save any state changes that might have occurred during the turn. + await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); + await UserState.SaveChangesAsync(turnContext, false, cancellationToken); + } + + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + Logger.LogInformation("Running dialog with Message Activity."); + + // Run the Dialog with the new message Activity. + await Dialog.RunAsync(turnContext, ConversationState.CreateProperty(nameof(DialogState)), cancellationToken); + } + } +} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/EchoBot.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/EchoBot.cs deleted file mode 100644 index 55df82447..000000000 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/Bots/EchoBot.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.22.0 - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Schema; - -namespace Microsoft.BotBuilderSamples.Bots -{ - public class EchoBot : ActivityHandler - { - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - var replyText = $"Echo: {turnContext.Activity.Text}"; - await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken); - } - - protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) - { - var welcomeText = "Hello and welcome to Echo Bot Using Federated Identity Credentials !!"; - foreach (var member in membersAdded) - { - if (member.Id != turnContext.Activity.Recipient.Id) - { - await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken); - } - } - } - } -} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/LogoutDialog.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/LogoutDialog.cs new file mode 100644 index 000000000..449b4e23f --- /dev/null +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/LogoutDialog.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; + +namespace Microsoft.BotBuilderSamples +{ + public class LogoutDialog : ComponentDialog + { + public LogoutDialog(string id, string connectionName) + : base(id) + { + ConnectionName = connectionName; + } + + protected string ConnectionName { get; } + + protected override async Task OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = await InterruptAsync(innerDc, cancellationToken); + if (result != null) + { + return result; + } + + return await base.OnBeginDialogAsync(innerDc, options, cancellationToken); + } + + protected override async Task OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = await InterruptAsync(innerDc, cancellationToken); + if (result != null) + { + return result; + } + + return await base.OnContinueDialogAsync(innerDc, cancellationToken); + } + + private async Task InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken = default(CancellationToken)) + { + if (innerDc.Context.Activity.Type == ActivityTypes.Message) + { + var text = innerDc.Context.Activity.Text.ToLowerInvariant(); + + if (text == "logout") + { + // The UserTokenClient encapsulates the authentication processes. + var userTokenClient = innerDc.Context.TurnState.Get(); + await userTokenClient.SignOutUserAsync(innerDc.Context.Activity.From.Id, ConnectionName, innerDc.Context.Activity.ChannelId, cancellationToken).ConfigureAwait(false); + + await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken); + return await innerDc.CancelAllDialogsAsync(cancellationToken); + } + } + + return null; + } + } +} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/MainDialog.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/MainDialog.cs new file mode 100644 index 000000000..94a50458f --- /dev/null +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/Dialogs/MainDialog.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Microsoft.BotBuilderSamples +{ + public class MainDialog : LogoutDialog + { + protected readonly ILogger Logger; + + public MainDialog(IConfiguration configuration, ILogger logger) + : base(nameof(MainDialog), configuration["ConnectionName"]) + { + Logger = logger; + + AddDialog(new OAuthPrompt( + nameof(OAuthPrompt), + new OAuthPromptSettings + { + ConnectionName = ConnectionName, + Text = "Please Sign In", + Title = "Sign In", + Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5) + })); + + AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); + + AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] + { + PromptStepAsync, + LoginStepAsync, + DisplayTokenPhase1Async, + DisplayTokenPhase2Async, + })); + + // The initial child Dialog to run. + InitialDialogId = nameof(WaterfallDialog); + } + + private async Task PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken); + } + + private async Task LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + // Get the token from the previous step. Note that we could also have gotten the + // token directly from the prompt itself. There is an example of this in the next method. + var tokenResponse = (TokenResponse)stepContext.Result; + if (tokenResponse != null) + { + await stepContext.Context.SendActivityAsync(MessageFactory.Text("You are now logged in."), cancellationToken); + return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Would you like to view your token?") }, cancellationToken); + } + + await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken); + return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); + } + + private async Task DisplayTokenPhase1Async(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thank you."), cancellationToken); + + var result = (bool)stepContext.Result; + if (result) + { + // Call the prompt again because we need the token. The reasons for this are: + // 1. If the user is already logged in we do not need to store the token locally in the bot and worry + // about refreshing it. We can always just call the prompt again to get the token. + // 2. We never know how long it will take a user to respond. By the time the + // user responds the token may have expired. The user would then be prompted to login again. + // + // There is no reason to store the token locally in the bot because we can always just call + // the OAuth prompt to get the token or get a new token if needed. + return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), cancellationToken: cancellationToken); + } + + return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); + } + + private async Task DisplayTokenPhase2Async(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + var tokenResponse = (TokenResponse)stepContext.Result; + if (tokenResponse != null) + { + await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Here is your token {tokenResponse.Token}"), cancellationToken); + } + + return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); + } + } +} diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/README.md b/samples/csharp_dotnetcore/86.bot-authentication-fic/README.md index 887cec802..78b7c72e8 100644 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/README.md +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/README.md @@ -20,7 +20,7 @@ This bot has been created using [Bot Framework](https://dev.botframework.com/), ## To try this sample -- In a terminal, navigate to `EchoFICBot` +- In a terminal, navigate to `AuthFederatedCredBot` ```bash # change into project folder @@ -32,7 +32,7 @@ This bot has been created using [Bot Framework](https://dev.botframework.com/), - Launch Visual Studio - File -> Open -> Project/Solution - Navigate to `samples/csharp_dotnetcore/86.bot-authentication-fic` folder - - Select `EchoFICBot.csproj` file + - Select `AuthFederatedCredBot.csproj` file - Create an user assigned managed identity. - Record the client ID of the managed identity and add the same to appsettings.json. @@ -82,7 +82,7 @@ This bot has been created using [Bot Framework](https://dev.botframework.com/), - Launch Visual Studio - File -> Open -> Project/Solution - Navigate to `86.bot-authentication-fic` folder - - Select `EchoFICBot.csproj` file + - Select `AuthFederatedCredBot.csproj` file - Press `F5` to run the project ## Deploy the bot to Azure diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/Startup.cs b/samples/csharp_dotnetcore/86.bot-authentication-fic/Startup.cs index dec1902a1..c7059844a 100644 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/Startup.cs +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Connector.Authentication; +using Microsoft.BotBuilderSamples.Bots; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -41,10 +42,29 @@ namespace Microsoft.BotBuilderSamples // Create the Bot Adapter with error handling enabled. services.AddSingleton(); - // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. - services.AddTransient(); + // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) + services.AddSingleton(); + + // Create the User state. (Used in this bot's Dialog implementation.) + services.AddSingleton(); + + // Create the Conversation state. (Used by the Dialog system itself.) + services.AddSingleton(); + + // The Dialog that will be run by the bot. + services.AddSingleton(); + + // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. + services.AddTransient>(); + + services.AddHttpClient().AddControllers().AddNewtonsoftJson(options => + { + options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth; + }); + } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { diff --git a/samples/csharp_dotnetcore/86.bot-authentication-fic/appsettings.json b/samples/csharp_dotnetcore/86.bot-authentication-fic/appsettings.json index c165f1daf..80f7356e7 100644 --- a/samples/csharp_dotnetcore/86.bot-authentication-fic/appsettings.json +++ b/samples/csharp_dotnetcore/86.bot-authentication-fic/appsettings.json @@ -2,5 +2,6 @@ "MicrosoftAppType": "", "MicrosoftAppId": "", "MicrosoftAppClientId": "", - "MicrosoftAppTenantId": "" + "MicrosoftAppTenantId": "", + "ConnectionName": "" } diff --git a/samples/csharp_dotnetcore/csharp_dotnetcore.sln b/samples/csharp_dotnetcore/csharp_dotnetcore.sln index dd40b4a28..650f3a387 100644 --- a/samples/csharp_dotnetcore/csharp_dotnetcore.sln +++ b/samples/csharp_dotnetcore/csharp_dotnetcore.sln @@ -86,7 +86,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthCertificateBot", "84.bo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthSNIBot", "85.bot-authentication-sni\AuthSNIBot.csproj", "{95AA9E48-B2A4-40B2-B3D2-DA4B3C1AD652}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoFICBot", "86.bot-authentication-fic\EchoFICBot.csproj", "{14316F6B-1488-449C-B56E-C55A4236A059}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthFederatedCredBot", "86.bot-authentication-fic\AuthFederatedCredBot.csproj", "{14316F6B-1488-449C-B56E-C55A4236A059}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution