Closed mkonkolowicz closed 5 years ago
Spent some time on this today unfortunately plan A and plan B were not possible. Working on plan C but hit a roadblock. Will continue to look into this and report my findings.
So I have figured out a workaround for this that is fairly involved. I do not think it is too bad as you will be generally able to copy and paste my code. The general idea is that instead of sending the link via the normal means, we call and get the link. We also generate a Guid for the user so we know to whom that link belongs if there are multiple concurrent users. We store these values in a static dictionary as a Key(guid)/Value(url) pair. So now we have that stored and we can use the guid to look up that url. We create another endpoint in out bot for the sole purpose of redirecting to the obscenely long url so the user never sees it.
First we create our controller to handle the redirecting: NOTE: we have to use a 307 redirect, a 302 will not work
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace Microsoft.Bot.Sample.AadV2Bot.Controllers
{
public class RedirectController : ApiController
{
[System.Web.Http.AcceptVerbs("GET", "POST")]
[System.Web.Http.HttpGet]
public HttpResponseMessage Redirection()
{
//get the dictionary key and entry
var guid = Request.RequestUri.PathAndQuery;
var url = CustomCode.RedirectDictionary[guid];
//make our redirect reponse as a 307 redirect
//NOTE: 302 redirects will NOT work for twilio
var response = Request.CreateResponse(HttpStatusCode.TemporaryRedirect);
response.Headers.Location = new Uri(url);
return response;
}
}
}
Great now we have our endpoint taken care of. In my project, I happen to have a lot of custom code for handling the quirkiness of the other channels. So naturally I put my static dictionary there:
NOTE: you may want to use another means of storing data than something just kept in memory, also you could make this Dictionary<Guid,Uri>():
if you really wanted to
public static class CustomCode
{
public static Dictionary<string, string> RedirectDictionary = new Dictionary<string, string>();
}
So right now we have our endpoint and our data store. We need a way to call and get the token so we can execute the graph calls late on. Right now in the RootDialog
, there is a method called GetTokenDialog
we basically need to copy that Dialog and change it for our purposes. There was nothing changed in the WaitForToken
method except we are deleting the dictionary entry for this user. CustomCode.RedirectDictionary.Remove(_guid.ToString());
NOTE: I could not get ngrok endpoints to work in this code, I have to do my testing with a deployed bot because of it. However, localhost endpoints did work for testing in the emulator before deploying. The emulator and Twilio have different behaviors though. Just because something worked on the emulator does not mean it will work in Twilio.
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Newtonsoft.Json.Linq;
using System;
using System.Threading.Tasks;
namespace Microsoft.Bot.Sample.AadV2Bot.Dialogs
{
[Serializable]
public class SignInDialog : IDialog<GetTokenResponse>
{
private string _connectionName;
private string _buttonLabel;
private string _signInMessage;
private int _retries;
private string _retryMessage;
//store our guid here
private Guid _guid;
public SignInDialog(string connectionName, string signInMessage, string buttonLabel, int retries = 0,
string retryMessage = null)
{
_connectionName = connectionName;
_signInMessage = signInMessage;
_buttonLabel = buttonLabel;
_retries = retries;
_retryMessage = retryMessage;
}
public async Task StartAsync(IDialogContext context)
{
// First ask Bot Service if it already has a token for this user
var token = await context.GetUserTokenAsync(_connectionName);
if (token != null)
{
context.Done(new GetTokenResponse() {Token = token.Token});
}
else
{
// If Bot Service does not have a token, send an OAuth card to sign in
await SendOAuthLinkAsync(context, (Activity) context.Activity);
}
}
private async Task SendOAuthLinkAsync(IDialogContext context, Activity activity)
{
var reply = activity.CreateReply();
_guid = Guid.NewGuid();
var client = activity.GetOAuthClient();
var link = await client.OAuthApi.GetSignInLinkAsync(activity, _connectionName);
CustomCode.RedirectDictionary.Add("/api/redirect?" +_guid, link);
var url = $"https://jasonaadv2test.azurewebsites.net/api/redirect?{_guid}";
if (activity.ChannelId == ChannelIds.Sms)
{
reply.Text ="Use this link to get your magic code\r\n " +url;
}
else
{
reply.Attachments= new List<Attachment>()
{
new HeroCard(
title: "Sign in",
text: "Click this button to get your magic code.\r\n Enter the magic code to continue",
buttons: new List<CardAction>()
{
new CardAction(text:"Get Code",type: ActionTypes.OpenUrl, title:"Get Code", value: url, displayText:"Get Code")
}
).ToAttachment()
};
}
await context.PostAsync(reply);
context.Wait(WaitForToken);
}
private async Task WaitForToken(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
CustomCode.RedirectDictionary.Remove(_guid.ToString());
var tokenResponse = activity.ReadTokenResponseContent();
string verificationCode = null;
if (tokenResponse != null)
{
context.Done(new GetTokenResponse() {Token = tokenResponse.Token});
return;
}
else if (activity.IsTeamsVerificationInvoke())
{
JObject value = activity.Value as JObject;
if (value != null)
{
verificationCode = (string) (value["state"]);
}
}
else if (!string.IsNullOrEmpty(activity.Text))
{
verificationCode = activity.Text;
}
tokenResponse = await context.GetUserTokenAsync(_connectionName, verificationCode);
if (tokenResponse != null)
{
context.Done(new GetTokenResponse() {Token = tokenResponse.Token});
return;
}
// decide whether to retry or not
if (_retries > 0)
{
_retries--;
await context.PostAsync(_retryMessage);
await SendOAuthLinkAsync(context, activity);
}
else
{
context.Done(new GetTokenResponse() {NonTokenResponse = activity.Text});
return;
}
}
}
}
Now we are in the home stretch, the final thing we need to do is call that dialog. So, for example, we call the ListMe
method like this in the RootDialog
context.Call(GetTokenDialog(), ListMe);
which right now calls the GetTokenDialog
and not the SignInDialog
we just made. So lets make a method to do that by basically copying the GetTokenDialog Method and making it call our new Dialog.
private SignInDialog CreateSignInDialog()
{
return new SignInDialog(
ConnectionName,
$"Please sign in to {ConnectionName} to proceed.",
"Sign In",
0,
"Hmm. Something went wrong, let's try again.");
}
And wherever we are currently calling the GetTokenDialog we need to add an if statement like this example where we are using the ListMe method
else if (message.ToLowerInvariant().Equals("me"))
{
if (activity.ChannelId == ChannelIds.Sms)
{
context.Call(CreateSignInDialog(), ListMe);
}
else
{
context.Call(CreateGetTokenDialog(), ListMe);
}
}
Now the link sent from the bot should show up as a link preview in iOS, and a much shorter link in android. All functionality should be retained. Please let me know if you have any issues
A colleague just told me I should have used a ConcurrentDictionary
over a Dictionary
, so If you would like to go that route you would have to make those changes.
@mkonkolowicz please let me know your results with this when you have the chance to implement it.
@JasonSowers the workaround def. worked. Thanks so much for your help! When can we expect this baked into the bot framework?
Right now I do not have a timeline.
Thank you for opening an issue against the Bot Framework SDK v3. As part of the Bot Framework v4 release, we’ve moved all v3 work to a new repo located at https://github.com/microsoft/botbuilder-v3. We will continue to support and offer maintenance updates to v3 via this new repo.
From now on, https://github.com/microsoft/botbuilder repo will be used as hub, with pointers to all the different SDK languages, tools and samples repos.
As part of this restructuring, we are closing all tickets in this repo.
For defects or feature requests, please create a new issue in the new Bot Framework v3 repo found here: https://github.com/microsoft/botbuilder-v3/issues
For Azure Bot Service Channel specific defects or feature requests (e.g. Facebook, Twilio, Teams, Slack, etc.), please create a new issue in the new Bot Framework Channel repo found here: https://github.com/microsoft/botframework-services/issues
For product behavior, how-to, or general understanding questions, please use Stackoverflow. https://stackoverflow.com/search?q=bot+framework
Thank you.
The Bot Framework Team
Bot Info
Issue Description
When using generic oauth mechanisms for authentication, the link returned by the authentication mechanism is not encapsulated, so it is not easily clicked on, on a mobile device. In order to retrieve the magic string for authentication from bot framework, the user has to copy paste the string into a note, remove some verbiage and then repaste the link into a mobile browser.
Code Example
var tokenResponse = Context.GetUserTokenAsync(string connectionName);
Reproduction Steps
Expected Behavior
The full provided link is clickable in SMS (iOS) app. The user should not have to cut and paste the link, but be able to click to launch it in a mobile browser.
Actual Results
The link is cut and the user has to copy paste and "clean" (remove verbiage) from the provided link before pasting into browser.