Skip to content

Commit

Permalink
[C#] feat: add teams sso bot auth sample
Browse files Browse the repository at this point in the history
  • Loading branch information
blackchoey committed Nov 30, 2023
1 parent 035e49e commit e803676
Show file tree
Hide file tree
Showing 26 changed files with 1,265 additions and 0 deletions.
28 changes: 28 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
29 changes: 29 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/AdapterWithErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -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<CloudAdapter> 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");
};
}
}
}
46 changes: 46 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/BotAuth.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectCapability Include="TeamsFx" />
</ItemGroup>

<ItemGroup>
<None Remove="build/**/*" />
<Content Remove="build/**/*" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AdaptiveCards.Templating" Version="1.3.1" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.21.1" />
<PackageReference Include="Microsoft.Identity.Web.TokenCache" Version="2.16.0" />
<PackageReference Include="Microsoft.Teams.AI" Version="1.0.*-*" />
</ItemGroup>

<!-- Exclude Teams Toolkit files from build output, but can still be viewed from Solution Explorer -->
<ItemGroup>
<Content Remove="appPackage/**/*" />
<None Include="appPackage/**/*" />
<None Include="env/**/*" />
<Content Remove="infra/**/*" />
<None Include="infra/**/*" />
</ItemGroup>

<ItemGroup>
<Folder Include="assets\" />
</ItemGroup>

<!-- Exclude local settings from publish -->
<ItemGroup>
<Content Remove="appsettings.Development.json" />
<Content Include="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>None</CopyToPublishDirectory>
</Content>
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/BotAuth.sln
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/Config.cs
Original file line number Diff line number Diff line change
@@ -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; }

}
}
32 changes: 32 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/Controllers/BotController.cs
Original file line number Diff line number Diff line change
@@ -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
);
}
}
}
52 changes: 52 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/Model/AppState.cs
Original file line number Diff line number Diff line change
@@ -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();
}

/// <summary>
/// Stores all the conversation-related state.
/// </summary>
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<int>(_countKey);
set => Set(_countKey, value);
}
}
}
128 changes: 128 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/Program.cs
Original file line number Diff line number Diff line change
@@ -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<ConfigOptions>();
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<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

// 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<CloudAdapter, AdapterWithErrorHandler>();
builder.Services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetService<CloudAdapter>());

// Create the storage to persist turn state
builder.Services.AddSingleton<IStorage, MemoryStorage>();

builder.Services.AddSingleton<IConfidentialClientApplication>(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<IBot>(sp =>
{
IStorage storage = sp.GetService<IStorage>();
IConfidentialClientApplication msal = sp.GetService<IConfidentialClientApplication>();
string signInLink = $"https://{config.BOT_DOMAIN}/auth-start.html";
ApplicationOptions<AppState> applicationOptions = new()
{
Storage = storage,
TurnStateFactory = () =>
{
return new AppState();
},
Authentication = new AuthenticationOptions<AppState>(
new Dictionary<string, IAuthentication<AppState>>()
{
{ "graph", new TeamsSsoAuthentication<AppState>(new TeamsSsoSettings(new string[]{"User.Read"}, signInLink, msal)) }
}
)
};

Application<AppState> 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();
25 changes: 25 additions & 0 deletions dotnet/samples/06.auth.teamsSSO.bot/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -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": ""
}
}
}
Loading

0 comments on commit e803676

Please sign in to comment.