Skip to content

Commit d3f286a

Browse files
blackchoeysinghk97
authored andcommitted
[C#] feat: add teams sso bot auth sample (#945)
## Linked issues closes: #861 ## Details Add Teams SSO bot authentication sample based on the JS version. ## Attestation Checklist - [x] My code follows the style guidelines of this project - I have checked for/fixed spelling, linting, and other errors - I have commented my code for clarity - I have made corresponding changes to the documentation (we use [TypeDoc](https://typedoc.org/) to document our code) - My changes generate no new warnings - I have added tests that validates my changes, and provides sufficient test coverage. I have tested with: - Local testing - E2E testing in Teams - New and existing unit tests pass locally with my changes ### Additional information Needs to provide private version of C# SDK as the sample is based on latest code.
1 parent f78f522 commit d3f286a

26 files changed

+1265
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# TeamsFx files
2+
build
3+
appPackage/build
4+
env/.env.*.user
5+
env/.env.local
6+
appsettings.Development.json
7+
.deployment
8+
9+
# User-specific files
10+
*.user
11+
12+
# Build results
13+
[Dd]ebug/
14+
[Dd]ebugPublic/
15+
[Rr]elease/
16+
[Rr]eleases/
17+
x64/
18+
x86/
19+
bld/
20+
[Bb]in/
21+
[Oo]bj/
22+
[Ll]og/
23+
24+
# Notification local store
25+
.notification.localstore.json
26+
27+
# Visual Studio files
28+
.vs/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Bot.Builder.Integration.AspNet.Core;
2+
using Microsoft.Bot.Builder.TraceExtensions;
3+
using Microsoft.Bot.Connector.Authentication;
4+
5+
namespace BotAuth
6+
{
7+
public class AdapterWithErrorHandler : CloudAdapter
8+
{
9+
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<CloudAdapter> logger)
10+
: base(auth, logger)
11+
{
12+
OnTurnError = async (turnContext, exception) =>
13+
{
14+
// Log any leaked exception from the application.
15+
// NOTE: In production environment, you should consider logging this to
16+
// Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
17+
// to add telemetry capture to your bot.
18+
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
19+
20+
// Send a message to the user
21+
await turnContext.SendActivityAsync($"The bot encountered an unhandled error: {exception.Message}");
22+
await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code.");
23+
24+
// Send a trace activity
25+
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
26+
};
27+
}
28+
}
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectCapability Include="TeamsFx" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<None Remove="build/**/*" />
14+
<Content Remove="build/**/*" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="AdaptiveCards.Templating" Version="1.3.1" />
19+
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.21.1" />
20+
<PackageReference Include="Microsoft.Identity.Web.TokenCache" Version="2.16.0" />
21+
<PackageReference Include="Microsoft.Teams.AI" Version="1.0.*-*" />
22+
</ItemGroup>
23+
24+
<!-- Exclude Teams Toolkit files from build output, but can still be viewed from Solution Explorer -->
25+
<ItemGroup>
26+
<Content Remove="appPackage/**/*" />
27+
<None Include="appPackage/**/*" />
28+
<None Include="env/**/*" />
29+
<Content Remove="infra/**/*" />
30+
<None Include="infra/**/*" />
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<Folder Include="assets\" />
35+
</ItemGroup>
36+
37+
<!-- Exclude local settings from publish -->
38+
<ItemGroup>
39+
<Content Remove="appsettings.Development.json" />
40+
<Content Include="appsettings.Development.json">
41+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42+
<CopyToPublishDirectory>None</CopyToPublishDirectory>
43+
</Content>
44+
</ItemGroup>
45+
46+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.7.33906.173
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotAuth", "BotAuth.csproj", "{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
17+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
18+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.Build.0 = Release|Any CPU
19+
{D045C9A3-F421-4E8B-91D0-33A62C61DCCD}.Release|Any CPU.Deploy.0 = Release|Any CPU
20+
EndGlobalSection
21+
GlobalSection(SolutionProperties) = preSolution
22+
HideSolutionNode = FALSE
23+
EndGlobalSection
24+
GlobalSection(ExtensibilityGlobals) = postSolution
25+
SolutionGuid = {1A3065E4-A54D-45EE-BDCB-1BADCD6EA7CA}
26+
EndGlobalSection
27+
EndGlobal
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace BotAuth
2+
{
3+
public class ConfigOptions
4+
{
5+
public string BOT_ID { get; set; }
6+
public string BOT_PASSWORD { get; set; }
7+
public string BOT_DOMAIN { get; set; }
8+
public string AAD_APP_CLIENT_ID { get; set; }
9+
public string AAD_APP_CLIENT_SECRET { get; set; }
10+
public string AAD_APP_TENANT_ID { get; set; }
11+
public string AAD_APP_OAUTH_AUTHORITY_HOST { get; set; }
12+
13+
}
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.Bot.Builder;
3+
using Microsoft.Bot.Builder.Integration.AspNet.Core;
4+
5+
namespace BotAuth.Controllers
6+
{
7+
[Route("api/messages")]
8+
[ApiController]
9+
public class BotController : ControllerBase
10+
{
11+
private readonly IBotFrameworkHttpAdapter _adapter;
12+
private readonly IBot _bot;
13+
14+
public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
15+
{
16+
_adapter = adapter;
17+
_bot = bot;
18+
}
19+
20+
[HttpPost]
21+
public async Task PostAsync(CancellationToken cancellationToken = default)
22+
{
23+
await _adapter.ProcessAsync
24+
(
25+
Request,
26+
Response,
27+
_bot,
28+
cancellationToken
29+
);
30+
}
31+
}
32+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.Teams.AI.State;
2+
3+
namespace BotAuth.Model
4+
{
5+
// Extend the turn state by configuring custom strongly typed state classes.
6+
public class AppState : TurnState
7+
{
8+
public AppState() : base()
9+
{
10+
ScopeDefaults[CONVERSATION_SCOPE] = new ConversationState();
11+
}
12+
13+
/// <summary>
14+
/// Stores all the conversation-related state.
15+
/// </summary>
16+
public new ConversationState Conversation
17+
{
18+
get
19+
{
20+
TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);
21+
if (scope == null)
22+
{
23+
throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first.");
24+
}
25+
26+
return (ConversationState)scope.Value!;
27+
}
28+
set
29+
{
30+
TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);
31+
if (scope == null)
32+
{
33+
throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first.");
34+
}
35+
36+
scope.Replace(value!);
37+
}
38+
}
39+
}
40+
41+
// This class adds custom properties to the turn state which will be accessible in the activity handler methods.
42+
public class ConversationState : Record
43+
{
44+
private const string _countKey = "countKey";
45+
46+
public int MessageCount
47+
{
48+
get => Get<int>(_countKey);
49+
set => Set(_countKey, value);
50+
}
51+
}
52+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using Microsoft.Bot.Builder;
2+
using Microsoft.Bot.Builder.Integration.AspNet.Core;
3+
using Microsoft.Bot.Connector.Authentication;
4+
using Microsoft.Bot.Schema;
5+
using Microsoft.Identity.Client;
6+
using Microsoft.Teams.AI;
7+
using Microsoft.Identity.Web;
8+
using BotAuth;
9+
using BotAuth.Model;
10+
11+
var builder = WebApplication.CreateBuilder(args);
12+
13+
builder.Services.AddControllers();
14+
builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600));
15+
builder.Services.AddHttpContextAccessor();
16+
builder.Logging.AddConsole();
17+
18+
// Prepare Configuration for ConfigurationBotFrameworkAuthentication
19+
var config = builder.Configuration.Get<ConfigOptions>();
20+
builder.Configuration["MicrosoftAppType"] = "MultiTenant";
21+
builder.Configuration["MicrosoftAppId"] = config.BOT_ID;
22+
builder.Configuration["MicrosoftAppPassword"] = config.BOT_PASSWORD;
23+
24+
// Create the Bot Framework Authentication to be used with the Bot Adapter.
25+
builder.Services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();
26+
27+
// Create the Cloud Adapter with error handling enabled.
28+
// Note: some classes expect a BotAdapter and some expect a BotFrameworkHttpAdapter, so
29+
// register the same adapter instance for both types.
30+
builder.Services.AddSingleton<CloudAdapter, AdapterWithErrorHandler>();
31+
builder.Services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetService<CloudAdapter>());
32+
33+
// Create the storage to persist turn state
34+
builder.Services.AddSingleton<IStorage, MemoryStorage>();
35+
36+
builder.Services.AddSingleton<IConfidentialClientApplication>(sp =>
37+
{
38+
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.AAD_APP_CLIENT_ID)
39+
.WithClientSecret(config.AAD_APP_CLIENT_SECRET)
40+
.WithTenantId(config.AAD_APP_TENANT_ID)
41+
.WithLegacyCacheCompatibility(false)
42+
.Build();
43+
app.AddInMemoryTokenCache(); // For development purpose only, use distributed cache in production environment
44+
return app;
45+
});
46+
47+
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
48+
builder.Services.AddTransient<IBot>(sp =>
49+
{
50+
IStorage storage = sp.GetService<IStorage>();
51+
IConfidentialClientApplication msal = sp.GetService<IConfidentialClientApplication>();
52+
string signInLink = $"https://{config.BOT_DOMAIN}/auth-start.html";
53+
ApplicationOptions<AppState> applicationOptions = new()
54+
{
55+
Storage = storage,
56+
TurnStateFactory = () =>
57+
{
58+
return new AppState();
59+
},
60+
Authentication = new AuthenticationOptions<AppState>(
61+
new Dictionary<string, IAuthentication<AppState>>()
62+
{
63+
{ "graph", new TeamsSsoAuthentication<AppState>(new TeamsSsoSettings(new string[]{"User.Read"}, signInLink, msal)) }
64+
}
65+
)
66+
};
67+
68+
Application<AppState> app = new(applicationOptions);
69+
70+
// Listen for user to say "/reset" and then delete conversation state
71+
app.OnMessage("/reset", async (turnContext, turnState, cancellationToken) =>
72+
{
73+
turnState.DeleteConversationState();
74+
await turnContext.SendActivityAsync("Ok I've deleted the current conversation state", cancellationToken: cancellationToken);
75+
});
76+
77+
// Listen for user to say "/sigout" and then delete cached token
78+
app.OnMessage("/signout", async (context, state, cancellationToken) =>
79+
{
80+
await app.Authentication.SignOutUserAsync(context, state, cancellationToken: cancellationToken);
81+
82+
await context.SendActivityAsync("You have signed out");
83+
});
84+
85+
// Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS
86+
app.OnActivity(ActivityTypes.Message, async (turnContext, turnState, cancellationToken) =>
87+
{
88+
int count = turnState.Conversation.MessageCount;
89+
90+
// Increment count state.
91+
turnState.Conversation.MessageCount = ++count;
92+
93+
await turnContext.SendActivityAsync($"[{count}] you said: {turnContext.Activity.Text}", cancellationToken: cancellationToken);
94+
});
95+
96+
app.Authentication.Get("graph").OnUserSignInSuccess(async (context, state) =>
97+
{
98+
// Successfully logged in
99+
await context.SendActivityAsync("Successfully logged in");
100+
await context.SendActivityAsync($"Token string length: {state.Temp.AuthTokens["graph"].Length}");
101+
await context.SendActivityAsync($"This is what you said before the AuthFlow started: {context.Activity.Text}");
102+
});
103+
104+
app.Authentication.Get("graph").OnUserSignInFailure(async (context, state, ex) =>
105+
{
106+
// Failed to login
107+
await context.SendActivityAsync("Failed to login");
108+
await context.SendActivityAsync($"Error message: {ex.Message}");
109+
});
110+
111+
return app;
112+
});
113+
114+
var app = builder.Build();
115+
116+
if (app.Environment.IsDevelopment())
117+
{
118+
app.UseDeveloperExceptionPage();
119+
}
120+
121+
app.UseStaticFiles();
122+
app.UseRouting();
123+
app.UseEndpoints(endpoints =>
124+
{
125+
endpoints.MapControllers();
126+
});
127+
128+
app.Run();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"profiles": {
3+
"Microsoft Teams (browser)": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"launchUrl": "https://teams.microsoft.com/",
7+
"environmentVariables": {
8+
"ASPNETCORE_ENVIRONMENT": "Development"
9+
},
10+
"dotnetRunMessages": true,
11+
"applicationUrl": "http://localhost:5130",
12+
"hotReloadProfile": "aspnetcore"
13+
},
14+
"WSL": {
15+
"commandName": "WSL2",
16+
"launchBrowser": true,
17+
"launchUrl": "https://teams.microsoft.com/",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development",
20+
"ASPNETCORE_URLS": "http://localhost:5130"
21+
},
22+
"distributionName": ""
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)