working with Microsoft Bot Framework V3 I started using Sign-In Cards.
I did a simple cut and paste from example code page into my code and let's say it works (compiles): https://docs.botframework.com/en-us/csharp/builder/sdkreference/attachments.html
What was expected is a behavior similar to oauth process so to be redirected to , do it's own stuffs and return the auth resul including all informations.
What I realized is that it simply open a new web page to the link I provided, that's all...
No other code founded elsewere...
So far it seems useless as I could provide the link simply with normal messages based on this behavior, also there is no communication with the bot.
Did I missed something?
Option 1) Custom Authentication using Windows Active Directory
I have made a custom authentication technique which queries Windows AD using Kerberos LDAP Protocol and using PrincipalContext class.
Firstly, in Root Dialog save the context of the chat in the ConversationReference and encode it using Base64 encoding.
using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.ConnectorEx;
using System.Threading;
namespace ADAuthBot.Dialogs
{
[Serializable]
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("Welcome to Auth Bot!");
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var message = await result as Activity;
ConversationReference conversationReference = message.ToConversationReference();
string username = string.Empty;
context.PrivateConversationData.SetValue<string>("usertext", message.Text);
if (!context.PrivateConversationData.TryGetValue<string>("Username", out username))
{
string encodedCookie = UrlToken.Encode(conversationReference);
await AuthDialog.createPromptForLogin(context, encodedCookie);
}
else
{
context.Call(this, ResumeAfter);
}
}
private async Task ResumeAfter(IDialogContext context, IAwaitable<object> result)
{
var item = await result;
context.Wait(MessageReceivedAsync);
}
}
}
Next, we come to the Auth Dialog in which we create a Sign-In Card and give the URL page that needs to be opened on the click of the authenticate button.
using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.ConnectorEx;
using System.Threading;
using System.Collections.Generic;
using System.Configuration;
namespace ADAuthBot.Dialogs
{
[Serializable]
public class AuthDialog: IDialog<object>
{
static string authenticationUrl = string.Empty; //Authentication URL is the MVC View URL, which will have the username and password window.
static string callbackurl = string.Empty;
static AuthDialog()
{
authenticationUrl = ConfigurationManager.AppSettings["AuthenticationUrl"];
callbackurl = ConfigurationManager.AppSettings["AuthCallbackUrl"];
}
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
}
public static async Task createPromptForLogin(IDialogContext context, string encodedCookie)
{
IMessageActivity response = context.MakeMessage();
response.Attachments = new List<Attachment>();
SigninCard signincard = new SigninCard()
{
Text = "Click here to sign in",
Buttons = new List<CardAction>() {
new CardAction()
{
Title = "Authentication Required",
Type = ActionTypes.OpenUrl,
Value = $"{authenticationUrl}?{encodedCookie}"
}
}
};
response.Attachments.Add(signincard.ToAttachment());
await context.PostAsync(response);
}
}
}
Next I made a MVC view which inputs your username and password and sends it to the ADAuthController to query it against the Windows Active Directory.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace ADAuthService.Controllers
{
public class LoginADController : Controller
{
// GET: LoginAD
[Route("Login")]
public ActionResult LoginUsingAD()
{
return View();
}
}
}
Next I created a simple Razor view which uses jQuery AJAX call to send username and password by encoding it in base64 encoding by using Javascript's btoa() function.
<script src="~/scripts/jquery-3.2.1.min.js"></script>
<script src="~/scripts/bootstrap.min.js"></script>
<link href="~/Content/bootstrap.min.css" rel="stylesheet" />
<script>
$(function () {
$("#txtUserName").html("");
$("#txtPassword").html("");
function make_base64_auth(username, password) {
var tok = username + ' ' + password;
var hash = btoa(tok);
return hash;
}
$("#btnSubmit").click(function () {
var userName = $("#txtUserName").val();
var passWord = $("#txtPassword").val();
var conversationReference = $(location).attr('search');
console.log(conversationReference);
var dataToBeSent = {
"ConversationReference": conversationReference,
"HashedUserCredentials": make_base64_auth(userName, passWord)
};
$.ajax({
url: "http://localhost:1070/api/Login",
method: "POST",
dataType: "json",
data: dataToBeSent,
contentType: "application/json",
crossDomain: true,
success: function (data) {
debugger;
console.log(data);
if(!$.isEmptyObject(data))
alert(data);
},
error: function (jqXHR, textStatus, errorThrown) {
debugger;
if (!$.isEmptyObject(jqXHR))
alert("Something happened wrong because: " + jqXHR.responseText);
}
});
});
});
</script>
<div class="panel-info">
<div class="panel panel-heading">
Enter your credentials
</div>
<div class="panel panel-body">
<div class="form-group">
<label for="username">Username: </label> <input id="txtUserName" type="text" placeholder="Enter username" required class="form-control" />
<label for="password">Password: </label> <input id="txtPassword" type="password" placeholder="Enter password" required class="form-control" />
<button id="btnSubmit" class="btn btn-info">Submit</button>
<button id="btnReset" class="btn btn-danger" type="reset">Reset</button>
</div>
</div>
</div>
I made a model class to store whether a user is identified or not.
namespace ADAuthService.Models
{
public class AuthenticatedUser
{
public string AuthenticatedUserName { get; set; } = string.Empty;
public bool IsAuthenticated { get; set; } = false;
}
}
and a model class to get details from MVC View.
namespace ADAuthService.Models
{
public class UserDetailsHashed
{
public string HashedUserCredentials { get; set; } = string.Empty;
public string ConversationReference { get; set; } = string.Empty;
}
}
Now the main content is to write a method which queries the Windows Active Directory by taking username, password and domain as input. After authenticating I am using the Service URL to send the authenticated user's name to the bot framework by resolving the scope using Autofac IoC Container.
using ADAuthService.Models;
using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Cors;
namespace ADAuthService.Controllers
{
public class ADAuthController : ApiController
{
[NonAction]
private void extractUserDetailsFromHash(UserDetailsHashed userDetails, out string username, out string password, out string conversationReference)
{
try
{
string[] userCredentials = userDetails.HashedUserCredentials.Split(' ');
byte[] userCredentialsBinary = Convert.FromBase64String(userCredentials.Last());
string decodedString = Encoding.UTF8.GetString(userCredentialsBinary);
string[] decodedStringArray = decodedString.Split(' ');
username = decodedStringArray[0];
password = decodedStringArray[1];
string[] userConversationReference = userDetails.ConversationReference.Split('?');
conversationReference = userConversationReference[1];
}
catch (Exception ex)
{
throw ex;
}
}
[NonAction]
private Task<AuthenticatedUser> ValidateUserAgainstAD(string username, string password)
{
AuthenticatedUser user = new AuthenticatedUser();
return Task.Run<AuthenticatedUser>(() => {
string ADDisplayName = string.Empty;
try
{
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, System.Environment.UserDomainName))
{
bool isValidCredentials = ctx.ValidateCredentials(username, password, ContextOptions.Negotiate);
// Additional check to search user in directory.
if (isValidCredentials)
{
UserPrincipal prUsr = new UserPrincipal(ctx);
prUsr.SamAccountName = username;
PrincipalSearcher srchUser = new PrincipalSearcher(prUsr);
UserPrincipal foundUsr = srchUser.FindOne() as UserPrincipal;
if (foundUsr != null)
{
user.AuthenticatedUserName = foundUsr.DisplayName;
user.IsAuthenticated = isValidCredentials;
}
}
else
throw new AuthenticationException($"Couldn't query no such credentials in Microsoft Active Directory such as Username: {username} and Password: {password}. Try entering a valid username and password combination.");
}
}
catch (Exception ex)
{
throw ex;
}
return user;
});
}
[NonAction]
public async Task ReplyToBot(string userName, string encodedConversationReference)
{
Activity reply = null;
ConversationReference decodedConversationReference = UrlToken.Decode<ConversationReference>(encodedConversationReference);
bool writeSuccessful = false;
IMessageActivity msgToBeSent = decodedConversationReference.GetPostToUserMessage();
using (ILifetimeScope scope = DialogModule.BeginLifetimeScope(Conversation.Container, msgToBeSent))
{
try
{
IConnectorClient client = scope.Resolve<IConnectorClient>();
IStateClient sc = scope.Resolve<IStateClient>();
BotData userData = sc.BotState.GetPrivateConversationData(msgToBeSent.ChannelId, msgToBeSent.From.Id, msgToBeSent.Id);
userData.SetProperty("Username", userName);
sc.BotState.SetPrivateConversationData(msgToBeSent.ChannelId, msgToBeSent.Conversation.Id, msgToBeSent.Id, userData);
writeSuccessful = true;
}
catch (Exception ex)
{
writeSuccessful = false;
throw ex;
}
if (!writeSuccessful)
{
msgToBeSent.Text = string.Empty;
await Conversation.ResumeAsync(decodedConversationReference, msgToBeSent);
}
if (writeSuccessful)
{
reply = msgToBeSent as Activity;
var connector = new ConnectorClient(new Uri(msgToBeSent.ServiceUrl));
reply.Text = $"Welcome {userName}!";
connector.Conversations.SendToConversation(reply);
}
}
}
[HttpPost]
[EnableCors("*", "*", "*")]
[Route("api/Login")]
public async Task<HttpResponseMessage> Login(UserDetailsHashed userDetails)
{
try
{
string username = string.Empty;
string password = string.Empty;
string conversationReference = string.Empty;
AuthenticatedUser userToBeAuthenticated = new AuthenticatedUser();
extractUserDetailsFromHash(userDetails, out username, out password, out conversationReference);
userToBeAuthenticated = await ValidateUserAgainstAD(username, password);
if (userToBeAuthenticated.IsAuthenticated)
{
await ReplyToBot(userName: userToBeAuthenticated.AuthenticatedUserName, encodedConversationReference: conversationReference);
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent($"Thanks, {userToBeAuthenticated.AuthenticatedUserName} you're now logged in!") };
}
else
{
return new HttpResponseMessage { StatusCode = HttpStatusCode.Forbidden, Content = new StringContent($"Couldn't query no such credentials in Microsoft Active Directory such as Username: {username} and Password: {password}. Try entering a valid username and password combination.") };
}
}
catch(Exception ex)
{
throw new HttpResponseException(new HttpResponseMessage() { StatusCode = HttpStatusCode.Forbidden, Content = new StringContent($"Couldn't query no such credentials in Microsoft Active Directory. Try entering a valid username and password combination.") });
}
}
}
}
Option 2) Use the patterns described in the following link: