diff --git a/Microsoft.Teams.sln b/Microsoft.Teams.sln index c18c56c8..f5a99502 100644 --- a/Microsoft.Teams.sln +++ b/Microsoft.Teams.sln @@ -78,8 +78,11 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.Dialogs", "Samples\Samples.Dialogs\Samples.Dialogs.csproj", "{D406AE11-285C-4AC8-862B-C755386D15F6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.MessageExtensions", "Samples\Samples.MessageExtensions\Samples.MessageExtensions.csproj", "{C4DDD35D-CCDC-4EBB-94F6-F2E4E4406AA8}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Teams.Plugins.External.McpClient.Tests", "Tests\Microsoft.Teams.Plugins.External.McpClient.Tests\Microsoft.Teams.Plugins.External.McpClient.Tests.csproj", "{783599E3-9841-4377-9590-4A5D9EC0023D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.AzureIdentity", "Samples\Samples.AzureIdentity\Samples.AzureIdentity.csproj", "{4FF887BF-A872-4752-94EB-610EF19800E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -486,6 +489,18 @@ Global {783599E3-9841-4377-9590-4A5D9EC0023D}.Release|x64.Build.0 = Release|Any CPU {783599E3-9841-4377-9590-4A5D9EC0023D}.Release|x86.ActiveCfg = Release|Any CPU {783599E3-9841-4377-9590-4A5D9EC0023D}.Release|x86.Build.0 = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|x64.Build.0 = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Debug|x86.Build.0 = Debug|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|Any CPU.Build.0 = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|x64.ActiveCfg = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|x64.Build.0 = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|x86.ActiveCfg = Release|Any CPU + {4FF887BF-A872-4752-94EB-610EF19800E2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -527,6 +542,7 @@ Global {D406AE11-285C-4AC8-862B-C755386D15F6} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} {C4DDD35D-CCDC-4EBB-94F6-F2E4E4406AA8} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} {783599E3-9841-4377-9590-4A5D9EC0023D} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {4FF887BF-A872-4752-94EB-610EF19800E2} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {378263F2-C2B2-4DB1-83CF-CA228AF03ABF} diff --git a/Samples/Samples.AzureIdentity/Program.cs b/Samples/Samples.AzureIdentity/Program.cs new file mode 100644 index 00000000..d8638cbf --- /dev/null +++ b/Samples/Samples.AzureIdentity/Program.cs @@ -0,0 +1,64 @@ +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Annotations; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddOpenApi(); +builder.Services.AddTransient(); + +var botClientId = builder.Configuration["AzureIdentity:BotClientId"] ?? ""; +var managedIdentityClientId = builder.Configuration["AzureIdentity:ManagedIdentityClientId"]; + +var managedIdentityId = string.IsNullOrEmpty(managedIdentityClientId) + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(managedIdentityClientId); + +var msalApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build(); + +var appOptions = new AppOptions +{ + Credentials = new TokenCredentials(botClientId, async (_, scopes) => + { + var scopesToUse = scopes.Length > 0 ? scopes : new[] { "https://api.botframework.com/.default" }; + var result = await msalApp.AcquireTokenForManagedIdentity(scopesToUse[0]).ExecuteAsync(); + return new TokenResponse { TokenType = "Bearer", AccessToken = result.AccessToken }; + }) +}; + +builder.AddTeams(appOptions).AddTeamsDevTools(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.UseTeams(); +app.Run(); + +[TeamsController] +public class Controller +{ + [Activity] + public async Task OnActivity(IContext context, [Context] IContext.Next next) + { + context.Log.Info($"Bot App ID: {context.AppId}"); + await next(); + } + + [Message] + public async Task OnMessage([Context] MessageActivity activity, [Context] IContext.Client client) + { + await client.Typing(); + await client.Send($"You said: '{activity.Text}'\n\nThis bot is authenticated using Azure Managed Identity!"); + } +} \ No newline at end of file diff --git a/Samples/Samples.AzureIdentity/Properties/launchSettings.json b/Samples/Samples.AzureIdentity/Properties/launchSettings.json new file mode 100644 index 00000000..c43effe1 --- /dev/null +++ b/Samples/Samples.AzureIdentity/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "openapi/v1.json", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:3978" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "openapi/v1.json", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7239;http://localhost:3978" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} diff --git a/Samples/Samples.AzureIdentity/README.md b/Samples/Samples.AzureIdentity/README.md new file mode 100644 index 00000000..6b627b35 --- /dev/null +++ b/Samples/Samples.AzureIdentity/README.md @@ -0,0 +1,331 @@ +# Azure Managed Identity Sample + +> **Note:** This sample demonstrates **ONLY** managed identity authentication. It does NOT use or require client secrets. All authentication is handled through Azure Managed Identity. + +This sample demonstrates how to authenticate a Teams bot using **Azure Managed Identity** with the **Microsoft.Identity.Client (MSAL)** library and the existing `TokenCredentials` class. This eliminates the need to store any credentials in your configuration. + +## Features + +- **User-Assigned Managed Identity**: Authenticate using a specific managed identity with a client ID +- **System-Assigned Managed Identity**: Authenticate using the system-assigned identity of your Azure resource +- **MSAL.NET Integration**: Uses Microsoft.Identity.Client (MSAL) for managed identity authentication + +## What is Managed Identity? + +Azure Managed Identity provides Azure services with an automatically managed identity in Microsoft Entra ID (formerly Azure AD). This identity can be used to authenticate to any service that supports Microsoft Entra authentication without storing credentials in your code. + +### Benefits: +- **No credentials in code**: This sample uses ONLY managed identity - no client secrets required +- **Automatic credential rotation**: Azure handles credential management automatically +- **Simplified deployment**: No secrets to manage or distribute +- **Enhanced security**: Zero risk of credential leaks since no credentials are stored + +## Prerequisites + +- .NET 9.0 SDK +- Azure subscription +- Azure Bot Service registration +- One of the following: + - Azure App Service, Azure Functions, or Azure Container Instances with Managed Identity enabled + - Azure Virtual Machine with Managed Identity enabled + - Azure Kubernetes Service (AKS) with Workload Identity configured + - Local development with Azure CLI or Visual Studio signed in (when using DefaultAzureCredential) + +## Project Structure + +``` +Samples.AzureIdentity/ +├── Program.cs # Main bot logic with Azure Identity configuration +├── Samples.AzureIdentity.csproj # Project file with SDK dependencies +├── appsettings.json # Configuration for managed identity +├── Properties/launchSettings.json # Launch configuration (port 3978) +└── README.md # This file +``` + +## Setup + +### 1. Azure Bot Registration + +1. Create an Azure Bot resource in the Azure Portal +2. Configure the messaging endpoint: `https://your-app-url/api/messages` +3. Note the Application (Client) ID + +### 2. Enable Managed Identity + +#### Option A: System-Assigned Managed Identity + +For Azure App Service or Azure Functions: +```bash +# Enable system-assigned managed identity +az webapp identity assign --name --resource-group +``` + +For Azure VM: +```bash +# Enable system-assigned managed identity +az vm identity assign --name --resource-group +``` + +#### Option B: User-Assigned Managed Identity + +1. Create a User-Assigned Managed Identity: +```bash +az identity create --name --resource-group +``` + +2. Note the Client ID from the output + +3. Assign it to your Azure resource: +```bash +# For App Service +az webapp identity assign --name --resource-group \ + --identities + +# For VM +az vm identity assign --name --resource-group \ + --identities +``` + +### 3. Grant Permissions to Managed Identity + +The managed identity needs permission to authenticate as your bot. Grant the identity the **Bot Service Contributor** role on the bot resource: + +```bash +# Get the principal ID of the managed identity +# For system-assigned: +principalId=$(az webapp identity show --name --resource-group --query principalId -o tsv) + +# For user-assigned: +principalId=$(az identity show --name --resource-group --query principalId -o tsv) + +# Grant the role assignment +az role assignment create --role "BotService Contributor" \ + --assignee-object-id $principalId \ + --scope /subscriptions//resourceGroups//providers/Microsoft.BotService/botServices/ +``` + +Alternatively, you can configure the managed identity's client ID directly in your bot's configuration in the Azure Portal. + +### 4. Update Configuration + +Update `appsettings.json` based on your authentication method: + +#### For System-Assigned Managed Identity: +```json +{ + "AzureIdentity": { + "BotClientId": "your-bot-application-id", + "ManagedIdentityClientId": "" + } +} +``` + +#### For User-Assigned Managed Identity: +```json +{ + "AzureIdentity": { + "BotClientId": "your-bot-application-id", + "ManagedIdentityClientId": "your-managed-identity-client-id" + } +} +``` + +### 5. Local Development Setup + +For local development with managed identity, you can: + +#### Option 1: Azure CLI +```bash +az login +az account set --subscription +``` + +#### Option 2: Visual Studio +Sign in to Visual Studio with an Azure account that has access to the bot + +#### Option 3: Environment Variables (for testing) +Set the `IDENTITY_ENDPOINT` and `IDENTITY_HEADER` environment variables to simulate managed identity locally (requires Azure resources) + +### 6. Dev Tunnels for Local Testing + +To test locally with Teams: + +1. Install dev tunnels: +```bash +winget install Microsoft.DevTunnels +``` + +2. Create and host a tunnel: +```bash +devtunnel create -a +devtunnel host -p 3978 +``` + +3. Update your Azure Bot messaging endpoint with the tunnel URL + +## Running the Sample + +### Locally (Development) + +```bash +# Navigate to the project directory +cd Samples/Samples.AzureIdentity + +# Run the bot +dotnet run +``` + +The bot will start on `http://localhost:3978` by default. + +### Deploy to Azure + +1. Build the project: +```bash +dotnet publish -c Release -o ./publish +``` + +2. Deploy to your Azure resource (App Service, Functions, etc.) + +3. Ensure the managed identity is configured correctly on the Azure resource + +## Usage + +Once the bot is running and configured in Teams: + +1. Send any message to the bot +2. The bot will echo your message back, confirming that it's authenticated using Azure Managed Identity + +Example: +``` +User: Hello bot! +Bot: You said: 'Hello bot!' + +This bot is authenticated using Azure Managed Identity instead of client secret! +``` + +## How It Works + +### Authentication Flow + +1. **Credential Creation**: The application creates an instance of `ManagedIdentityCredentials` based on configuration +2. **Token Acquisition**: When the bot needs to authenticate, the `ManagedIdentityCredentials` class uses Azure Identity SDK to acquire a token +3. **Automatic Token Management**: The Azure Identity SDK handles token caching and renewal automatically + +### Code Structure + +The key authentication setup happens in `Program.cs` using a minimal API style: + +```csharp +var managedIdentityId = string.IsNullOrEmpty(managedIdentityClientId) + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(managedIdentityClientId); + +var msalApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build(); + +var appOptions = new AppOptions +{ + Credentials = new TokenCredentials(botClientId, async (_, scopes) => + { + var scopesToUse = scopes.Length > 0 ? scopes : new[] { "https://api.botframework.com/.default" }; + var result = await msalApp.AcquireTokenForManagedIdentity(scopesToUse[0]).ExecuteAsync(); + return new TokenResponse { TokenType = "Bearer", AccessToken = result.AccessToken }; + }) +}; + +builder.AddTeams(appOptions); +``` + +### Using TokenCredentials with Microsoft.Identity.Client + +This sample demonstrates how to use the existing `TokenCredentials` class with the Microsoft.Identity.Client (MSAL) library. The `TokenCredentials` class accepts a `TokenFactory` delegate that allows you to provide custom token acquisition logic: + +```csharp +var managedIdentityId = string.IsNullOrEmpty(managedIdentityClientId) + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(managedIdentityClientId); + +var msalApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build(); + +var appOptions = new AppOptions +{ + Credentials = new TokenCredentials(botClientId, async (_, scopes) => + { + var scopesToUse = scopes.Length > 0 ? scopes : new[] { "https://api.botframework.com/.default" }; + var result = await msalApp.AcquireTokenForManagedIdentity(scopesToUse[0]).ExecuteAsync(); + return new TokenResponse { TokenType = "Bearer", AccessToken = result.AccessToken }; + }) +}; +``` + +The code uses MSAL's `ManagedIdentityApplicationBuilder` to create a managed identity application and acquire tokens. + +## Comparison: Managed Identity vs Client Secret + +> **Important:** This sample demonstrates ONLY the managed identity approach. The client secret approach is shown below for comparison purposes only to illustrate the security benefits of managed identity. + +### Traditional Approach (Client Secret) - NOT USED IN THIS SAMPLE +```json +{ + "Teams": { + "ClientId": "your-bot-application-id", + "ClientSecret": "your-bot-client-secret" // Sensitive! + } +} +``` + +### Managed Identity Approach - USED IN THIS SAMPLE +```json +{ + "AzureIdentity": { + "BotClientId": "your-bot-application-id", + "ManagedIdentityClientId": "your-managed-identity-client-id" + } +} +``` + +No client secret is stored in your configuration! + +**Note:** The `BotClientId` is the Application (Client) ID of your bot registration, which is not a secret and can be safely stored in configuration. This is NOT a client secret - it's a public identifier. + +## Troubleshooting + +### Issue: "ManagedIdentityCredential authentication unavailable" + +**Solution**: Ensure your Azure resource has managed identity enabled and you're running in an Azure environment (or use DefaultAzureCredential for local development). + +### Issue: "Authentication failed" when running locally + +**Solution**: When using DefaultAzureCredential: +1. Ensure you're signed in with Azure CLI: `az login` +2. Or signed in to Visual Studio with an Azure account +3. Or set environment variables for a service principal + +### Issue: "403 Forbidden" when authenticating + +**Solution**: Ensure the managed identity has the correct role assignments on the bot resource. + +### Issue: Bot receives 401 Unauthorized + +**Solution**: +1. Verify the managed identity client ID (if using user-assigned) +2. Check that the identity has access to the Bot Service +3. Ensure the bot's App ID is correctly configured + +## Additional Resources + +- [Azure Managed Identity Documentation](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/) +- [Managed identity with MSAL.NET](https://learn.microsoft.com/entra/msal/dotnet/advanced/managed-identity) +- [Microsoft.Identity.Client (MSAL) Documentation](https://learn.microsoft.com/entra/msal/dotnet/) +- [Teams AI SDK Documentation](https://microsoft.github.io/teams-ai) + +## Security Best Practices + +1. **Never commit credentials**: With managed identity, there are no secrets to commit +2. **Use User-Assigned Managed Identity**: For better control and reusability across resources +3. **Implement proper RBAC**: Grant only the minimum necessary permissions +4. **Rotate regularly**: If you must use service principals locally, rotate credentials regularly +5. **Use DefaultAzureCredential**: For seamless local development and production deployment + +## License + +This sample is licensed under the MIT License. See the LICENSE file in the repository root for more information. diff --git a/Samples/Samples.AzureIdentity/Samples.AzureIdentity.csproj b/Samples/Samples.AzureIdentity/Samples.AzureIdentity.csproj new file mode 100644 index 00000000..e5f19093 --- /dev/null +++ b/Samples/Samples.AzureIdentity/Samples.AzureIdentity.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Samples/Samples.AzureIdentity/appsettings.Development.json b/Samples/Samples.AzureIdentity/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Samples/Samples.AzureIdentity/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Samples/Samples.AzureIdentity/appsettings.json b/Samples/Samples.AzureIdentity/appsettings.json new file mode 100644 index 00000000..d0fad0c3 --- /dev/null +++ b/Samples/Samples.AzureIdentity/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "AzureIdentity": { + "BotClientId": "", + "ManagedIdentityClientId": "" + } +}