diff --git a/dotnet/samples/06.auth.teamsSSO.bot/.gitignore b/dotnet/samples/06.auth.teamsSSO.bot/.gitignore new file mode 100644 index 000000000..9028eddca --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/.gitignore @@ -0,0 +1,28 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +.deployment + +# User-specific files +*.user + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Notification local store +.notification.localstore.json + +# Visual Studio files +.vs/ \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/AdapterWithErrorHandler.cs b/dotnet/samples/06.auth.teamsSSO.bot/AdapterWithErrorHandler.cs new file mode 100644 index 000000000..bb66fe516 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/AdapterWithErrorHandler.cs @@ -0,0 +1,29 @@ +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.TraceExtensions; +using Microsoft.Bot.Connector.Authentication; + +namespace BotAuth +{ + public class AdapterWithErrorHandler : CloudAdapter + { + public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) + : 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 unhandled error: {exception.Message}"); + await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); + + // Send a trace activity + await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); + }; + } + } +} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.csproj b/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.csproj new file mode 100644 index 000000000..13a9e6d1b --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.csproj @@ -0,0 +1,46 @@ + + + + net6.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + None + + + + diff --git a/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.sln b/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.sln new file mode 100644 index 000000000..9e3664dfd --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/BotAuth.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33906.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotAuth", "BotAuth.csproj", "{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.Build.0 = Release|Any CPU + {D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1A3065E4-A54D-45EE-BDCB-1BADCD6EA7CA} + EndGlobalSection +EndGlobal diff --git a/dotnet/samples/06.auth.teamsSSO.bot/Config.cs b/dotnet/samples/06.auth.teamsSSO.bot/Config.cs new file mode 100644 index 000000000..df47f7886 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/Config.cs @@ -0,0 +1,14 @@ +namespace BotAuth +{ + public class ConfigOptions + { + public string BOT_ID { get; set; } + public string BOT_PASSWORD { get; set; } + public string BOT_DOMAIN { get; set; } + public string AAD_APP_CLIENT_ID { get; set; } + public string AAD_APP_CLIENT_SECRET { get; set; } + public string AAD_APP_TENANT_ID { get; set; } + public string AAD_APP_OAUTH_AUTHORITY_HOST { get; set; } + + } +} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/Controllers/BotController.cs b/dotnet/samples/06.auth.teamsSSO.bot/Controllers/BotController.cs new file mode 100644 index 000000000..37658feb2 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/Controllers/BotController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace BotAuth.Controllers +{ + [Route("api/messages")] + [ApiController] + public class BotController : ControllerBase + { + private readonly IBotFrameworkHttpAdapter _adapter; + private readonly IBot _bot; + + public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) + { + _adapter = adapter; + _bot = bot; + } + + [HttpPost] + public async Task PostAsync(CancellationToken cancellationToken = default) + { + await _adapter.ProcessAsync + ( + Request, + Response, + _bot, + cancellationToken + ); + } + } +} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/Model/AppState.cs b/dotnet/samples/06.auth.teamsSSO.bot/Model/AppState.cs new file mode 100644 index 000000000..3621e1d2e --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/Model/AppState.cs @@ -0,0 +1,52 @@ +using Microsoft.Teams.AI.State; + +namespace BotAuth.Model +{ + // Extend the turn state by configuring custom strongly typed state classes. + public class AppState : TurnState + { + public AppState() : base() + { + ScopeDefaults[CONVERSATION_SCOPE] = new ConversationState(); + } + + /// + /// Stores all the conversation-related state. + /// + public new ConversationState Conversation + { + get + { + TurnStateEntry scope = GetScope(CONVERSATION_SCOPE); + if (scope == null) + { + throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first."); + } + + return (ConversationState)scope.Value!; + } + set + { + TurnStateEntry scope = GetScope(CONVERSATION_SCOPE); + if (scope == null) + { + throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first."); + } + + scope.Replace(value!); + } + } + } + + // This class adds custom properties to the turn state which will be accessible in the activity handler methods. + public class ConversationState : Record + { + private const string _countKey = "countKey"; + + public int MessageCount + { + get => Get(_countKey); + set => Set(_countKey, value); + } + } +} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/Program.cs b/dotnet/samples/06.auth.teamsSSO.bot/Program.cs new file mode 100644 index 000000000..c4a686a00 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/Program.cs @@ -0,0 +1,128 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Microsoft.Identity.Client; +using Microsoft.Teams.AI; +using Microsoft.Identity.Web; +using BotAuth; +using BotAuth.Model; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); +builder.Services.AddHttpContextAccessor(); +builder.Logging.AddConsole(); + +// Prepare Configuration for ConfigurationBotFrameworkAuthentication +var config = builder.Configuration.Get(); +builder.Configuration["MicrosoftAppType"] = "MultiTenant"; +builder.Configuration["MicrosoftAppId"] = config.BOT_ID; +builder.Configuration["MicrosoftAppPassword"] = config.BOT_PASSWORD; + +// Create the Bot Framework Authentication to be used with the Bot Adapter. +builder.Services.AddSingleton(); + +// Create the Cloud Adapter with error handling enabled. +// Note: some classes expect a BotAdapter and some expect a BotFrameworkHttpAdapter, so +// register the same adapter instance for both types. +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetService()); + +// Create the storage to persist turn state +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(sp => +{ + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.AAD_APP_CLIENT_ID) + .WithClientSecret(config.AAD_APP_CLIENT_SECRET) + .WithTenantId(config.AAD_APP_TENANT_ID) + .WithLegacyCacheCompatibility(false) + .Build(); + app.AddInMemoryTokenCache(); // For development purpose only, use distributed cache in production environment + return app; +}); + +// Create the bot as a transient. In this case the ASP Controller is expecting an IBot. +builder.Services.AddTransient(sp => +{ + IStorage storage = sp.GetService(); + IConfidentialClientApplication msal = sp.GetService(); + string signInLink = $"https://{config.BOT_DOMAIN}/auth-start.html"; + ApplicationOptions applicationOptions = new() + { + Storage = storage, + TurnStateFactory = () => + { + return new AppState(); + }, + Authentication = new AuthenticationOptions( + new Dictionary>() + { + { "graph", new TeamsSsoAuthentication(new TeamsSsoSettings(new string[]{"User.Read"}, signInLink, msal)) } + } + ) + }; + + Application app = new(applicationOptions); + + // Listen for user to say "/reset" and then delete conversation state + app.OnMessage("/reset", async (turnContext, turnState, cancellationToken) => + { + turnState.DeleteConversationState(); + await turnContext.SendActivityAsync("Ok I've deleted the current conversation state", cancellationToken: cancellationToken); + }); + + // Listen for user to say "/sigout" and then delete cached token + app.OnMessage("/signout", async (context, state, cancellationToken) => + { + await app.Authentication.SignOutUserAsync(context, state, cancellationToken: cancellationToken); + + await context.SendActivityAsync("You have signed out"); + }); + + // Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS + app.OnActivity(ActivityTypes.Message, async (turnContext, turnState, cancellationToken) => + { + int count = turnState.Conversation.MessageCount; + + // Increment count state. + turnState.Conversation.MessageCount = ++count; + + await turnContext.SendActivityAsync($"[{count}] you said: {turnContext.Activity.Text}", cancellationToken: cancellationToken); + }); + + app.Authentication.Get("graph").OnUserSignInSuccess(async (context, state) => + { + // Successfully logged in + await context.SendActivityAsync("Successfully logged in"); + await context.SendActivityAsync($"Token string length: {state.Temp.AuthTokens["graph"].Length}"); + await context.SendActivityAsync($"This is what you said before the AuthFlow started: {context.Activity.Text}"); + }); + + app.Authentication.Get("graph").OnUserSignInFailure(async (context, state, ex) => + { + // Failed to login + await context.SendActivityAsync("Failed to login"); + await context.SendActivityAsync($"Error message: {ex.Message}"); + }); + + return app; +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +app.Run(); diff --git a/dotnet/samples/06.auth.teamsSSO.bot/Properties/launchSettings.json b/dotnet/samples/06.auth.teamsSSO.bot/Properties/launchSettings.json new file mode 100644 index 000000000..720ed0b33 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "profiles": { + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "hotReloadProfile": "aspnetcore" + }, + "WSL": { + "commandName": "WSL2", + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5130" + }, + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/README.md b/dotnet/samples/06.auth.teamsSSO.bot/README.md new file mode 100644 index 000000000..a49d67185 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/README.md @@ -0,0 +1,31 @@ +# Teams Conversation Bot + +Teams AI Conversation Bot sample for Teams. + +This sample shows how to incorporate basic conversational flow with SSO into a Teams application. +It also illustrates a few of the Teams specific calls you can make from your bot. + +This sample depends on Teams SSO and gives you more flexibility on how to configure AAD, like using a client certificate. There is no need to create an OAuth Connection in Azure Bot Service to run this sample. + +## Set up instructions + +All the samples in the C# .NET SDK can be set up in the same way: You can find the step by step instructions here: + [Setup Instructions](../README.md). + +## Interacting with the Bot + +At this point you should have set up the bot and installed it in Teams. You can interact with the bot by sending it a message. + +Here's a sample interaction with the bot: + +![Sample interaction](assets/helloworld.png) + +You can reset the message count by sending the bot the message `/reset`. + +![Reset interaction](assets/reset.png) + +## Deploy to Azure + +You can use Teams Toolkit for Visual Studio or CLI to host the bot in Azure. The sample includes Bicep templates in the `/infra` directory which are used by the tools to create resources in Azure. + +You can find deployment instructions [here](../README.md#deploy-to-azure). \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/aad.manifest.json b/dotnet/samples/06.auth.teamsSSO.bot/aad.manifest.json new file mode 100644 index 000000000..fac9a4319 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/aad.manifest.json @@ -0,0 +1,101 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "BotAuth-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://botid-${{BOT_ID}}" + ], + "replyUrlsWithType": [ + { + "url": "https://${{BOT_DOMAIN}}/auth-end.html", + "type": "Web" + } + ] +} \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/appPackage/color.png b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/color.png new file mode 100644 index 000000000..4ab158588 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/color.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67c7c063ba4dc41c977080c1f1fa17c897e1c72ec4a6412ed5e681b5d4cb9680 +size 1066 diff --git a/dotnet/samples/06.auth.teamsSSO.bot/appPackage/manifest.json b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/manifest.json new file mode 100644 index 000000000..0fcb1c93c --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/manifest.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", + "manifestVersion": "1.16", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "BotAuth-${{TEAMSFX_ENV}}", + "full": "Full name for BotAuth" + }, + "description": { + "short": "Short description of BotAuth", + "full": "Full description of BotAuth" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false, + "commandLists": [ + { + "scopes": [ + "personal", + "team", + "groupchat" + ], + "commands": [] + } + ] + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{BOT_DOMAIN}}" + ], + "webApplicationInfo": { + "id": "${{BOT_ID}}", + "resource": "api://botid-${{BOT_ID}}" + } +} \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/appPackage/outline.png b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/outline.png new file mode 100644 index 000000000..458549f6d --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/appPackage/outline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1ddc76f79027d9c0300689721649ce1f1950271a5fc4ca50ae56545228fb566 +size 249 diff --git a/dotnet/samples/06.auth.teamsSSO.bot/appsettings.json b/dotnet/samples/06.auth.teamsSSO.bot/appsettings.json new file mode 100644 index 000000000..6d2093d20 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "BOT_ID": "", + "BOT_PASSWORD": "", + "BOT_DOMAIN": "", + "AAD_APP_CLIENT_ID": "", + "AAD_APP_CLIENT_SECRET": "", + "AAD_APP_TENANT_ID": "", + "AAD_APP_OAUTH_AUTHORITY_HOST": "" +} \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/assets/helloworld.png b/dotnet/samples/06.auth.teamsSSO.bot/assets/helloworld.png new file mode 100644 index 000000000..4507d87b9 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/assets/helloworld.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b85dc0ec138d7a680e48d6cb6c169c7d21e1bb456c584971b41176724947711 +size 52183 diff --git a/dotnet/samples/06.auth.teamsSSO.bot/assets/reset.png b/dotnet/samples/06.auth.teamsSSO.bot/assets/reset.png new file mode 100644 index 000000000..cb608b200 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/assets/reset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:891653223bbfcbd4ff561cfba30b74815178ac5ff42b33946a696170b769d18d +size 40284 diff --git a/dotnet/samples/06.auth.teamsSSO.bot/env/.env.dev b/dotnet/samples/06.auth.teamsSSO.bot/env/.env.dev new file mode 100644 index 000000000..4d51098b6 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/env/.env.dev @@ -0,0 +1,23 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= +TEAMS_APP_TENANT_ID= +AAD_APP_CLIENT_ID= +AAD_APP_OBJECT_ID= +AAD_APP_TENANT_ID= +AAD_APP_OAUTH_AUTHORITY= +AAD_APP_OAUTH_AUTHORITY_HOST= +AAD_APP_ACCESS_AS_USER_PERMISSION_ID= \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.bicep b/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.bicep new file mode 100644 index 000000000..0b9c3de8d --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.bicep @@ -0,0 +1,81 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location + +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +@secure() +param aadAppClientSecret string + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + ftpsState: 'FtpsOnly' + } + } +} + +resource webAppSettings 'Microsoft.Web/sites/config@2021-02-01' = { + name: '${webAppName}/appsettings' + properties: { + WEBSITE_RUN_FROM_PACKAGE: '1' + BOT_ID: botAadAppClientId + BOT_PASSWORD: botAadAppClientSecret + BOT_DOMAIN: webApp.properties.defaultHostName + AAD_APP_CLIENT_ID: aadAppClientId + AAD_APP_CLIENT_SECRET: aadAppClientSecret + AAD_APP_TENANT_ID: aadAppTenantId + AAD_APP_OAUTH_AUTHORITY_HOST: aadAppOauthAuthorityHost + RUNNING_ON_AZURE: '1' + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName diff --git a/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.parameters.json b/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.parameters.json new file mode 100644 index 000000000..ffffffff5 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/infra/azure.parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "BotAuth${{RESOURCE_SUFFIX}}" + }, + "botAadAppClientId": { + "value": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "BotAuth" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppClientSecret": { + "value": "${{SECRET_AAD_APP_CLIENT_SECRET}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/azurebot.bicep b/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/azurebot.bicep new file mode 100644 index 000000000..ab67c7a56 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param botAadAppClientId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: botAadAppClientId + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/readme.md b/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/readme.md new file mode 100644 index 000000000..d5416243c --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.local.yml b/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.local.yml new file mode 100644 index 000000000..0072eadaa --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.local.yml @@ -0,0 +1,119 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +provision: + # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. + name: BotAuth-aad + # If the value is false, the action will not generate client secret for you + generateClientSecret: true + # Authenticate users with a Microsoft work or school account in your organization's Azure AD tenant (for example, single tenant). + signInAudience: "AzureADMyOrg" + # Write the information of created resources into environment file for the specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file + clientSecret: SECRET_AAD_APP_CLIENT_SECRET + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: BotAuth-${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Azure Active Directory application for bot. + - uses: botAadApp/create + with: + # The Azure Active Directory application's display name + name: BotAuth-${{TEAMSFX_ENV}} + writeToEnvironmentFile: + # The Azure Active Directory application's client id created for bot. + botId: BOT_ID + # The Azure Active Directory application's client secret created for bot. + botPassword: SECRET_BOT_PASSWORD + + # Generate runtime appsettings to JSON file + - uses: file/createOrUpdateJsonFile + with: + target: ./appsettings.Development.json + content: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_DOMAIN: ${{BOT_DOMAIN}} + AAD_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} + AAD_APP_CLIENT_SECRET: ${{SECRET_AAD_APP_CLIENT_SECRET}} + AAD_APP_TENANT_ID: ${{AAD_APP_TENANT_ID}} + AAD_APP_OAUTH_AUTHORITY_HOST: ${{AAD_APP_OAUTH_AUTHORITY_HOST}} + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: BotAuth + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will be replaced before apply to AAD app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Create or update debug profile in lauchsettings file + - uses: file/createOrUpdateJsonFile + with: + target: ./Properties/launchSettings.json + content: + profiles: + Microsoft Teams (browser): + commandName: "Project" + dotnetRunMessages: true + launchBrowser: true + launchUrl: "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + applicationUrl: "http://localhost:5130" + environmentVariables: + ASPNETCORE_ENVIRONMENT: "Development" + hotReloadProfile: "aspnetcore" diff --git a/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.yml b/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.yml new file mode 100644 index 000000000..7fde72300 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/teamsapp.yml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + + # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. + name: BotAuth-aad + # If the value is false, the action will not generate client secret for you + generateClientSecret: true + # Authenticate users with a Microsoft work or school account in your organization's Azure AD tenant (for example, single tenant). + signInAudience: "AzureADMyOrg" + # Write the information of created resources into environment file for the specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file + clientSecret: SECRET_AAD_APP_CLIENT_SECRET + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: BotAuth-${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Azure Active Directory application for bot. + - uses: botAadApp/create + with: + # The Azure Active Directory application's display name + name: BotAuth-${{TEAMSFX_ENV}} + writeToEnvironmentFile: + # The Azure Active Directory application's client id created for bot. + botId: BOT_ID + # The Azure Active Directory application's client secret created for bot. + botPassword: SECRET_BOT_PASSWORD + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-tab + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will be replaced before apply to AAD app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsfx deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained + + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # deploy base folder + artifactFolder: bin/Release/net6.0/win-x86/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} diff --git a/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-end.html b/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-end.html new file mode 100644 index 000000000..15b343986 --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-end.html @@ -0,0 +1,63 @@ + + + Login End Page + + + + + +
+ + + \ No newline at end of file diff --git a/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-start.html b/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-start.html new file mode 100644 index 000000000..beffc10ee --- /dev/null +++ b/dotnet/samples/06.auth.teamsSSO.bot/wwwroot/auth-start.html @@ -0,0 +1,177 @@ + + + + + Login Start Page + + + + + + + \ No newline at end of file