Microsoft Bot Framework FAQ - 登入驗證處理(Authentication)

一般來說除了使用公用的服務外,在使用其他的系統之前都需要登入系統。
所以透過 BOT 來使用私有的服務時,也是需要登入!
我們可以參考「Build BOT with Authentication (Microsoft Bot Framework)」及「MicrosoftDX/AuthBot」。
來實作 BOT 登入系統,驗證使用者。

運作的過程如下圖,

程式處理說明如下,

1.判斷使用者沒有登入系統,就導到處理登入的 ZimbraAuthDialog ( RootDialog -> ZimbraAuthDialog )

[Serializable]
public class RootDialog : IDialog<object>
{
	public Task StartAsync(IDialogContext context)
	{
		context.Wait(MessageReceivedAsync);
		return Task.CompletedTask;
	}

	
	private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
	{
		var activity = await result as Activity;
		var messageText = activity?.Text;
		if (messageText == "logout")
		{
			//如果是登出的話,就將已登入的資料清掉
			context.UserData.RemoveValue(ContextConstants.AuthResultKey);
			context.UserData.SetValue(ContextConstants.MagicNumberValidated, "false");
			context.UserData.RemoveValue(ContextConstants.MagicNumberKey);
		}
		
	   
		string isMagicNumberValidated = "";
		context.UserData.TryGetValue<string>(ContextConstants.MagicNumberValidated, out isMagicNumberValidated);
		if (isMagicNumberValidated == "true")
		{
			//已登入過,echo 
			await context.PostAsync($"您已經登入,您輸入的是[{messageText}]");
			context.Wait(this.MessageReceivedAsync);
		}
		else
		{
			//沒登入,就要開啟 ZimbraAuthDialog 來處理登入動作
			await context.Forward(new ZimbraAuthDialog("請先登入 Zimbra 系統 "), this.ResumeAfterAuth, activity, CancellationToken.None);
		}
	}

	 
	private async Task ResumeAfterAuth(IDialogContext context, IAwaitable<string> result)
	{
		var message = await result;
		await context.PostAsync(message);
		await context.PostAsync("您已登入,如果您要登出的話,請輸入 \"logout\". ");
		context.Wait(MessageReceivedAsync);
	}
}

2.在 ZimbraAuthDialog 中如果沒有登入,就產生一個 SigninCard 來讓使用者點選開啟 Browser 

這裡我們呼叫 IMessageActivity.ToConversationReference() 來取得 Conversation 資訊,讓它可以串在 QueryString 之中。

private async Task LogIn(IDialogContext context, IMessageActivity msg)
{
	try
	{
		//記錄 userId 及 Conversation 的資訊
		var stateObject = new
		{
			UserId = context.Activity.From.Id,
			ConversationReference = msg.ToConversationReference()
		};
		//將資訊 Serialize 後,放在 QueryString 上面傳
		string state = Uri.EscapeDataString(JsonConvert.SerializeObject(stateObject));
		var hostUrl = WebConfigurationManager.AppSettings["hostUrl"];
		var loginPath = WebConfigurationManager.AppSettings["loginPath"];
		string authenticationUrl = $"{hostUrl}{loginPath}?state={state}";
		await PromptToLogin(context, msg, authenticationUrl);
		context.Wait(this.MessageReceivedAsync);
	}
	catch(Exception ex)
	{
		throw ex;
	}
}


protected virtual Task PromptToLogin(IDialogContext context, IMessageActivity msg, string authenticationUrl)
{
	Attachment plAttachment = null;
	switch (msg.ChannelId)
	{
		default:
		{
			SigninCard plCard = new SigninCard(this._prompt, GetCardActions(authenticationUrl, "signin"));
			plAttachment = plCard.ToAttachment();
			break;
		}
	}
	IMessageActivity response = context.MakeMessage();
	response.Recipient = msg.From;
	response.Type = "message";

	response.Attachments = new List<Attachment>();
	response.Attachments.Add(plAttachment);
	return context.PostAsync(response);
}

private List<CardAction> GetCardActions(string authenticationUrl, string actionType)
{
	List<CardAction> cardButtons = new List<CardAction>();
	CardAction plButton = new CardAction()
	{
		Value = authenticationUrl,
		Type = actionType,
		Title = "Authentication Required"
	};
	cardButtons.Add(plButton);
	return cardButtons;
}

 

3.在登入的頁面,成功登入後,產生驗證碼,並將相關資訊存到 BOT 的 UserData 之中,並將需要輸入驗證碼的訊息回給 Channel(請注意:如果用 direct line 是收不到的哦! 筆者還在努力中 ...)。 如下,

public partial class Login : System.Web.UI.Page
{
	private static RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
	private static readonly uint MaxWriteAttempts = 5;

	protected void Page_Load(object sender, EventArgs e)
	{
		phInfo.Visible = false;
		if (!Page.IsPostBack)
		{
			Session["state"] = Request.QueryString["state"];
		}
	}

	/// <summary>
	/// 產生亂數
	/// </summary>
	/// <returns></returns>
	private int GenerateRandomNumber()
	{
		int number = 0;
		byte[] randomNumber = new byte[1];
		do
		{
			rngCsp.GetBytes(randomNumber);
			var digit = randomNumber[0] % 10;
			number = number * 10 + digit;
		} while (number.ToString().Length < ContextConstants.MagicNumberLength);
		return number;

	}


	protected async void btnLogin_OnClick(object sender, EventArgs e)
	{
		try
		{
			phInfo.Visible = true;
			var state = JsonConvert.DeserializeObject<State>((string)Session["state"]);
			var token = "fakeToken"; 
			if (!string.IsNullOrWhiteSpace(token))
			{
				int magicNumber = GenerateRandomNumber();
				// Create the message that is send to conversation to resume the login flow
				var message = state.ConversationReference.GetPostToUserMessage();
				using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
				{
					var authResult = new AuthResult();
					authResult.AccessToken = token;
					authResult.UserName = txtUserId.Text;
					IStateClient sc = scope.Resolve<IStateClient>();
					bool writeSuccessful = false;
					uint writeAttempts = 0;
					//將資料存回 botState 之中
					while (!writeSuccessful && writeAttempts++ < MaxWriteAttempts)
					{
						try
						{
							var userData = sc.BotState.GetUserData(message.ChannelId, message.Recipient.Id);
							userData.SetProperty(ContextConstants.AuthResultKey, authResult);
							userData.SetProperty(ContextConstants.MagicNumberKey, magicNumber);
							userData.SetProperty(ContextConstants.MagicNumberValidated, "false");
							sc.BotState.SetUserData(message.ChannelId, message.Recipient.Id, userData);
							writeSuccessful = true;
						}
						catch (HttpOperationException)
						{
							writeSuccessful = false;
						}
					}

					if (!writeSuccessful)
					{
						message.Text = String.Empty; // fail the login process if we can't write UserData
						await Conversation.ResumeAsync(state.ConversationReference, message);
						txtAlertMsg.Text = "無法登入,請再試一次,謝謝您!";
						 
					}
					else
					{
						await Conversation.ResumeAsync(state.ConversationReference, message);
						txtAlertMsg.Text = $"請將以下的{ContextConstants.MagicNumberLength}個數字驗證碼輸入到IM之中,以完成登入程序,謝謝您!<br/> <h1>{magicNumber}</h1>";
					}
				}
			}
			else
			{
				// login fail
				txtAlertMsg.Text = "登入失敗! 請重新輸入! ";
			}
		}
		catch
		{
			txtAlertMsg.Text = "不是從 IM 進來的!";
		}
	}
}

4.使用者將登面頁面中的驗證碼輸入到 Channel 之中,處理登入的 ZimbraAuthDialog 就會接手判斷驗證碼是否正確,如果不正確就請使用者重新輸入,如果正確就結束 ZimbraAuthDialog ,回到 RootDialog 。

public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
	var msg = await argument;
	AuthResult authResult;
	//先判斷個人資料中是否包含 登入驗證結果
	if (context.UserData.TryGetValue(ContextConstants.AuthResultKey, out authResult))
	{
		try
		{
			string validated;
			context.UserData.TryGetValue<string>(ContextConstants.MagicNumberValidated, out validated);
			if (validated == "true")
			{
				//已輸入登入頁面的驗證碼,可以結束本 Dialog
				context.Done($"謝謝 {authResult.UserName}. 您已登入系統. ");
			}
			else
			{
				//尚未登入頁面的驗證碼
				var magicNumber = 0;
				if (context.UserData.TryGetValue<int>(ContextConstants.MagicNumberKey, out magicNumber))
				{
					if (msg.Text == null)
					{
						await context.PostAsync(
							$"請輸入登入完成後的{ContextConstants.MagicNumberLength}位數字驗證碼");
						context.Wait(this.MessageReceivedAsync);
					}
					else
					{
						if (msg.Text.Length >= ContextConstants.MagicNumberLength && magicNumber.ToString() == msg.Text.Substring(0, ContextConstants.MagicNumberLength))
						{
							//驗證成功,將資訊寫到 UserData 之中,並結束本 Dialog
							context.UserData.SetValue<string>(ContextConstants.MagicNumberValidated, "true");
							context.Done($"謝謝 {authResult.UserName}. 您已登入系統. ");
						}
						else
						{
							//驗證失敗,重新輸入一次
							await context.PostAsync($"驗證碼錯誤,請重新輸入登入完成後的{ContextConstants.MagicNumberLength}位數字驗證碼!");
							context.Wait(this.MessageReceivedAsync);

							//也可以讓user重新登入
							//context.UserData.RemoveValue(ContextConstants.AuthResultKey);
							//context.UserData.SetValue<string>(ContextConstants.MagicNumberValidated, "false");
							//context.UserData.RemoveValue(ContextConstants.MagicNumberKey);
							//await this.LogIn(context, msg);
						}
					}
				}
			}
		}
		catch(Exception ex)
		{
			Debug.WriteLine(ex.ToString());
			context.UserData.RemoveValue(ContextConstants.AuthResultKey);
			context.UserData.SetValue(ContextConstants.MagicNumberValidated, "false");
			context.UserData.RemoveValue(ContextConstants.MagicNumberKey);
			context.Done($"I'm sorry but something went wrong while authenticating.");
		}
	}
	else
	{
		await this.LogIn(context, msg);
	}

}

完整的程式碼,請參考 Github「rainmakerho/BotAuth

參考資料

Integrate your bot with a web browser

Build BOT with Authentication (Microsoft Bot Framework)

MicrosoftDX/AuthBot

rainmakerho/BotAuth

How to use ConversationReference to send a delayed or proactive message with Microsoft Bot Framework

Hi, 

亂馬客Blog已移到了 「亂馬客​ : Re:從零開始的軟體開發生活

請大家繼續支持 ^_^