Skip to content

Commit

Permalink
Add bot authentication support for asp net core 2 authentication midd…
Browse files Browse the repository at this point in the history
…leware (#3687)

* Add bot authentication support for asp net core 2 authentication pipeline

* Remove app id and password from aspnetcore2 sample
  • Loading branch information
msft-shahins authored and Tom Laird-McConnell committed Oct 30, 2017
1 parent b6dfd9c commit ba60581
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.Bot.Connector
{
public static class BotAuthenticationExtensions
{
public static AuthenticationBuilder AddBotAuthentication(this AuthenticationBuilder builder, string microsoftAppId, string microsoftAppPassword)
=> builder.AddBotAuthentication(new StaticCredentialProvider(microsoftAppId, microsoftAppPassword));

public static AuthenticationBuilder AddBotAuthentication(this AuthenticationBuilder builder, ICredentialProvider credentialProvider)
=> builder.AddBotAuthentication(JwtBearerDefaults.AuthenticationScheme, displayName: "botAuthenticator", configureOptions: options =>
{
options.CredentialProvider = credentialProvider;
options.Events = new JwtBearerEvents();
});
public static AuthenticationBuilder AddBotAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<BotAuthenticationOptions> configureOptions)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<BotAuthenticationOptions>, JwtBearerPostConfigureOptions>());
return builder.AddScheme<BotAuthenticationOptions, BotAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Bot.Connector
{
/// <summary>
/// Bot authentication hanlder used by <see cref="BotAuthenticationMiddleware"/>.
/// </summary>
public class BotAuthenticationHandler : AuthenticationHandler<BotAuthenticationOptions>
{
public BotAuthenticationHandler(IOptionsMonitor<BotAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
this.Events = options.CurrentValue.Events;
}

/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new JwtBearerEvents Events
{
get { return (JwtBearerEvents)base.Events; }
set { base.Events = value; }
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (await Options.CredentialProvider.IsAuthenticationDisabledAsync())
{
var principal = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Bot") }));

var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal
};
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}

string token = null;
try
{
// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);

// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}

// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;

if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers["Authorization"];

// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}

if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}

// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}

// If no token found, no further work possible
// and Authentication is not disabled fail
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.Fail("No JwtToken is present and BotAuthentication is enabled!");
}
var authenticator = new BotAuthenticator(Options.CredentialProvider, Options.OpenIdConfiguration, Options.DisableEmulatorTokens);
var identityToken = await authenticator.TryAuthenticateAsync(Options.Challenge, token, CancellationToken.None);

if (identityToken.Authenticated)
{
Logger.TokenValidationSucceeded();

identityToken.Identity.AddClaim(new Claim(ClaimTypes.Role, "Bot"));
var principal = new ClaimsPrincipal(identityToken.Identity);
Context.User = principal;

var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = new JwtSecurityToken(token)
};

await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}

if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}

tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
else
{
Logger.TokenValidationFailed(token, null);
return AuthenticateResult.Fail($"Failed to authenticate JwtToken {token}");
}

}
catch (Exception ex)
{
Logger.ErrorProcessingMessage(ex);

var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};

await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}

throw;
}
}
}

internal static class LoggingExtensions
{
private static Action<ILogger, string, Exception> _tokenValidationFailed;
private static Action<ILogger, Exception> _tokenValidationSucceeded;
private static Action<ILogger, Exception> _errorProcessingMessage;

static LoggingExtensions()
{
_tokenValidationFailed = LoggerMessage.Define<string>(
eventId: 1,
logLevel: LogLevel.Information,
formatString: "Failed to validate the token {Token}.");
_tokenValidationSucceeded = LoggerMessage.Define(
eventId: 2,
logLevel: LogLevel.Information,
formatString: "Successfully validated the token.");
_errorProcessingMessage = LoggerMessage.Define(
eventId: 3,
logLevel: LogLevel.Error,
formatString: "Exception occurred while processing message.");
}

public static void TokenValidationFailed(this ILogger logger, string token, Exception ex)
{
_tokenValidationFailed(logger, token, ex);
}

public static void TokenValidationSucceeded(this ILogger logger)
{
_tokenValidationSucceeded(logger, null);
}

public static void ErrorProcessingMessage(this ILogger logger, Exception ex)
{
_errorProcessingMessage(logger, ex);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace Microsoft.Bot.Connector
{
public sealed class BotAuthenticationOptions : JwtBearerOptions
{

/// <summary>
/// The <see cref="ICredentialProvider"/> used for authentication.
/// </summary>
public ICredentialProvider CredentialProvider { set; get; }

/// <summary>
/// The OpenId configuation.
/// </summary>
public string OpenIdConfiguration { set; get; } = JwtConfig.ToBotFromChannelOpenIdMetadataUrl;

/// <summary>
/// Flag indicating if emulator tokens should be disabled.
/// </summary>
public bool DisableEmulatorTokens { set; get; } = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Microsoft.Bot.Connector.AspNetCore\TrustServiceUrlAttribute.cs" Link="TrustServiceUrlAttribute.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Bot.Connector.NetCore\Microsoft.Bot.Connector.NetCore.csproj" />
</ItemGroup>

</Project>
20 changes: 19 additions & 1 deletion CSharp/Microsoft.Bot.Builder.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
VisualStudioVersion = 15.0.26730.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{FAEFFF66-7944-48F4-972A-9A1C13A5C206}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -122,6 +122,10 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Bot.Connector.Sha
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Sample.TemplateBot", "Samples\TemplateBot\Microsoft.Bot.Sample.TemplateBot.csproj", "{BFAB8046-9ABD-44C2-8992-9F3A5D28E5C7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Connector.AspNetCore2", "Library\Microsoft.Bot.Connector.AspNetCore2\Microsoft.Bot.Connector.AspNetCore2.csproj", "{AA997154-61D1-49FA-A4C6-E6AC79A7508A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Sample.AspNetCore2.Echo", "Samples\Microsoft.Bot.Sample.AspNetCore2.Echo\Microsoft.Bot.Sample.AspNetCore2.Echo.csproj", "{E47F0592-A28B-41AA-A977-3114DBCF9C30}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
Library\Microsoft.Bot.Connector.Shared\Microsoft.Bot.Connector.Shared.projitems*{133e6973-cf90-4691-8685-d42d79c8884b}*SharedItemsImports = 13
Expand Down Expand Up @@ -273,6 +277,18 @@ Global
{BFAB8046-9ABD-44C2-8992-9F3A5D28E5C7}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{BFAB8046-9ABD-44C2-8992-9F3A5D28E5C7}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{BFAB8046-9ABD-44C2-8992-9F3A5D28E5C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA997154-61D1-49FA-A4C6-E6AC79A7508A}.Release|Any CPU.Build.0 = Release|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E47F0592-A28B-41AA-A977-3114DBCF9C30}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -305,6 +321,8 @@ Global
{A91F876C-BC71-49A8-BD24-444F80DF62BB} = {FAEFFF66-7944-48F4-972A-9A1C13A5C206}
{133E6973-CF90-4691-8685-D42D79C8884B} = {28BCEB2C-F4BE-416D-A4FD-A311E99A9EB5}
{BFAB8046-9ABD-44C2-8992-9F3A5D28E5C7} = {FAEFFF66-7944-48F4-972A-9A1C13A5C206}
{AA997154-61D1-49FA-A4C6-E6AC79A7508A} = {28BCEB2C-F4BE-416D-A4FD-A311E99A9EB5}
{E47F0592-A28B-41AA-A977-3114DBCF9C30} = {FAEFFF66-7944-48F4-972A-9A1C13A5C206}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F82D70DE-DC3B-4B04-97CD-3CFF66345798}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Connector;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Bot.Sample.AspNetCore2.Echo.Controllers
{
[Route("api/[controller]")]
public class MessagesController : Controller
{
private readonly IConfiguration configuration;

public MessagesController(IConfiguration configuration)
{
this.configuration = configuration;
}

[Authorize(Roles = "Bot")]
// POST api/values
[HttpPost]
public virtual async Task<OkResult> Post([FromBody]Activity activity)
{
var appCredentials = new MicrosoftAppCredentials(this.configuration);
var client = new ConnectorClient(new Uri(activity.ServiceUrl), appCredentials);
var reply = activity.CreateReply();
if (activity.Type == ActivityTypes.Message)
{
reply.Text = $"echo: {activity.Text}";
}
else
{
reply.Text = $"activity type: {activity.Type}";
}
await client.Conversations.ReplyToActivityAsync(reply);
return Ok();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Library\Microsoft.Bot.Connector.AspNetCore2\Microsoft.Bot.Connector.AspNetCore2.csproj" />
<ProjectReference Include="..\..\Library\Microsoft.Bot.Connector.NetCore\Microsoft.Bot.Connector.NetCore.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit ba60581

Please sign in to comment.