I'm developing a MS Bot with the v4 framework and I'm trying to set up authentication. The problem is that I can authenticate but if I want to recapture the user authentication token by reprompting the dialog (found here) then I do get a new loginprompt instead of just retrieving the token.
This is what tried:
using Azure AD v1 and v2
searching for documentation (but is always deprecated or the same as found at the link)
My code (if you need something else feel free to ask):
// Register the Promt
AddDialog(Prompt(ConnectionName));
// Prompts the user to log in using the OAuth provider specified by the connection name.
// Prompt definition
public static OAuthPrompt Prompt(string connectionName) => new OAuthPrompt(
LoginPromptName,
new OAuthPromptSettings
{
ConnectionName = connectionName,
Text = "Please login",
Title = "Login",
Timeout = 300000, // User has 5 minutes to login
});
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext context, CancellationToken cancellationToken)
{
var state = await UserProfileAccessor.GetAsync(context.Context);
return await context.BeginDialogAsync(LoginPromptName, cancellationToken: cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext context, CancellationToken cancellationToken)
{
var loginState = await UserProfileAccessor.GetAsync(context.Context);
// Get the token from the previous step. Note that we could also have gotten the
// token directly from the prompt itself. There is an example of this in the next method.
var tokenResponse = (TokenResponse)context.Result;
if (tokenResponse != null)
{
* DOES SOMETHING WHEN LOGGED IN*
}
else
{
* DOES SOMETHING WHEN LOGIN FAILED *
}
/* !!
HERE IS THE PROBLEM IN STEAD OF CHECKING IF THE USER
IS ALREADY LOGGED IN AND JUST RETRIEVING THE TOKEN I
GET A NEW LOGIN SCREEN.
!!
*/
var token2Response = await context.BeginDialogAsync(LoginPromptName, null, cancellationToken)
}
So basically I want to retrieve the user token so I can check in different methods and dialogs if the user is logged in.
This is due to this bug: OAuthPrompt requires user credentials when message contains guid or 6-digit number, which currently has a Pull Request to fix.
The workaround for now would be to make sure that the bot gets any kind of other user response before ending the dialog. The sample does it here with:
return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Would you like to view your token?") }, cancellationToken);
I tested adding this to your code in * DOES SOMETHING WHEN LOGGED IN* and it worked:
Related
I have developed chat bot with C#
Need to show different greeting message, on User using the chatbot for second time with in a day.
If user is using the chatbot for first time in a day, need to show "Hi, how can I help You" greeting message. second time on using chatbot need to show "Welcome back 'Username', how can I help you" message.
How to achieve this?
By default Bots are stateless. The state and storage features of the Bot Framework SDK allow you to manage the state of your bot. There is documentation available in this context - Save user and conversation data
Here is some code that meets your requirement.
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Get the state properties from the turn context.
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());
if (string.IsNullOrEmpty(userProfile.Name))
{
// First time around this is set to false, so we will prompt user for name.
if (conversationData.PromptedUserForName)
{
// Set the name to what the user provided.
userProfile.Name = turnContext.Activity.Text?.Trim();
// Acknowledge that we got their name.
await turnContext.SendActivityAsync($"Thanks {userProfile.Name}. To see conversation data, type anything.");
// Reset the flag to allow the bot to go through the cycle again.
conversationData.PromptedUserForName = false;
}
else
{
// Prompt the user for their name.
await turnContext.SendActivityAsync($"What is your name?");
// Set the flag to true, so we don't prompt in the next turn.
conversationData.PromptedUserForName = true;
}
}
else
{
// Add message details to the conversation data.
// Convert saved Timestamp to local DateTimeOffset, then to string for display.
var messageTimeOffset = (DateTimeOffset) turnContext.Activity.Timestamp;
var localMessageTime = messageTimeOffset.ToLocalTime();
conversationData.Timestamp = localMessageTime.ToString();
conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();
// Display state data.
await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");
await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");
await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");
}
}
I am using GenerateEmailConfirmationTokenAsync to generate the token Email Confirmation Link and using ConfirmEmailAsync to Validate the link. It is working fine.
Problem - Link should work only once. If user using same link 2nd time link should be invalid. I debug and found each time ConfirmEmailAsync IdentityResult.IsSucceded true. I was expecting VerifyUserTokenAsync should return false second time but it is always returning true.
Please suggest solution. Thanks
Using .Net Core 3.1, Identity Server 4
// To Generate Token
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
// To Confirm Token
var result = await _userManager.ConfirmEmailAsync(user, code);
// Customised Token Provider
public class EmailConfirmationTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
{
public EmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
IOptions<EmailConfirmationTokenProviderOptions> options, ILogger<EmailConfirmationTokenProvider<TUser>> logger)
: base(dataProtectionProvider, options, logger)
{
}
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{ }
// Starup.cs Code
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Tokens.EmailConfirmationTokenProvider = "email_confirmation_provider";
options.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultTokenProviders()
.AddTokenProvider<EmailConfirmationTokenProvider<ApplicationUser>>("email_confirmation_provider");
services.Configure<EmailConfirmationTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromSeconds(3600);
});
I think you need to override the behavior of ConfirmEmailAsync to something like this,
If the token matches a known user that indicates that it was a validly issued token.
Will then attempt to confirm token with User manager.
If confirmation fails then token has expired and an appropriate action is taken.
Else if the token confirmed, it is removed from associated user and thus invalidating the reuse of that token.
public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
var user = await FindByIdAsync(userId);
if (user == null) {
return IdentityResult.Failed("User Id Not Found");
}
var result = await base.ConfirmEmailAsync(userId, token);
if (result.Succeeded) {
user.EmailConfirmationToken = null;
return await UpdateAsync(user);
} else if (user.EmailConfirmationToken == token) {
//Previously Issued Token expired
result = IdentityResult.Failed("Expired Token");
}
return result;
}
Similar can be done for password resets.
Approach 2, not tried yet but give a try,
Try to modify the Security timestamps of token to invalidate those once confirmation is done,
UserManager.UpdateSecurityStampAsync(userId);
I am running an OAuth Dialog that allows user to sign in. I am looking to get this Auth token from DialogsClass.cs to my Bot.Cs class file and use it to make Graph calls.
I have tried to save token as string in local file within my dialog class and then read it back in main bot class but this solution does not seems as a right way of doing it.
AuthDialog.cs in Waterfall step:
var tokenResponse = (TokenResponse)stepContext.Result;
Expected result. Transfer this token from Dialog class to MainBot.cs class and use as string to make Graph calls.
Are you using one waterfall step to get token with OAuthPrompt and then another step to call a different class (in which you do graph api calls)?
Why can't you just pass the token to the down stream class?
If there are other steps in the middle, there are multiple ways to resolve it:
Use WaterfallStepContext Values
Save to your own UserState
Microsoft suggests not to store token in the system but make a call to oAuth prompt
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
and get latest token whenever you have to call Graph API. Once you receive the token in var tokenResponse = (TokenResponse)stepContext.Result;
you can make a call to GraphClient class which will create the Graph API client using the token in Authorization attribute.
var client = new GraphClientHelper(tokenResponse.Token);
Graph Client implementation:
public GraphClientHelper(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentNullException(nameof(token));
}
_token = token;
}
private GraphServiceClient GetAuthenticatedClient()
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
requestMessage =>
{
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
return Task.CompletedTask;
}));
return graphClient;
}
Once graph client is created you can make a call to the intended graph api:
await client.CreateMeeting(meetingDetails).ConfigureAwait(false);
Please refer this sample code:
Graph Sample
I've created a Bot application in Visual Studio 2017, which I want to use in MS Teams. This application is part of a solution, which contains 2 components, the bot application itself and a windows application, which I have created that is used by the bot application to retrieve an authentication token from Microsoft (using similar code to what is on this website https://learn.microsoft.com/en-us/azure/active-directory/develop/guidedsetups/active-directory-uwp-v2).
When debugging the bot after hosting it locally, I'am able to use the bot successfully in Teams. There is no error. However, now that I have registered the bot with the Microsoft Bot Framework in Azure, I'm now having issues as Teams returns back the message "Sorry, my bot code is having an issue." In Azure I have a Bots Channels Registration entity, which in its settings points to a messaging endpoint that is https://.azurewebsites.net/api/messages. I also have a Apps Service. Now I have transferred the application id that I received when registering the bot with the Microsoft Bot Framework and have put this into the bot application in Visual Studio in the web.config file along with the app password.
After testing this in the Bot Framework Emulator I get "POST 401 directline.postActivity" and in the "Inspector-JSON" I get "BotAuthenticator failed to authenticate incoming request!". This is my first bot application so I'm lost as to what I have potentially missed out so does anyone have any idea what I could try?
So here's what I have in my RootDialog.cs file, which where the endpoint will hit when the bot is used.
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
string userInfo = "";
AuthTokenDeploy tokenDeploy = new AuthTokenDeploy();
userInfo = await tokenDeploy.MsGraphUserInfo();
if(!userInfo.Equals(""))
{
// send webhook to end user system
await SendToEndpoint(context, activity, activity.Text,
userInfo);
}
}
AuthTokenDeploy is an instance of another class, which is where the function to obtain the access token along with the user information from Microsoft is held. So I created a string "userInfo", which then takes the value given by MsGraphUserInfo().
public async Task<string> MsGraphUserInfo()
{
AuthenticationResult authResult = null;
string Text = null;
try
{
authResult = await App.PublicClientApp.AcquireTokenSilentAsync(_scopes, App.PublicClientApp.Users.FirstOrDefault());
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await App.PublicClientApp.AcquireTokenAsync(_scopes);
}
catch (MsalException msalex)
{
}
}
catch (Exception ex)
{
}
if (authResult != null)
{
Text = await GetHttpContentWithToken(_graphAPIEndpoint, authResult.AccessToken);
}
return Text;
}
Calling MsGraphUserInfo() will open the "AuthToken.exe" as a popup Window and ask the user to log in with their credentials. You can see from the code above that it acquires the access token first, which is then passed into GetHttpContentWithToken(), which is where a HTTP GET request is run against "https://graph.microsoft.com/v1.0/me" and a JSON string is returned with the user information in it.
public async Task<string> GetHttpContentWithToken(string url, string token)
{
var httpClient = new System.Net.Http.HttpClient();
System.Net.Http.HttpResponseMessage response;
try
{
var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
//Add the token in Authorization header
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
response = await httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
return content;
}
catch (Exception ex)
{
return ex.ToString();
}
}
Now I feel as if this method of using a Windows application to allow the user to log in might not be the best way forward, hence why I've been reading and following this guide https://learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-auth-aad. I would like to know whether it is possible to use what is on this page to allow my bot to retrieve an access token?
I'm using MS Bot Framework and C# to build a bot that can handle 3 dialogs. Each dialog is built using the FormDialog and FormBuilder, like this:
internal static IDialog<OrderDialogForm> BuildDialog()
{
return Chain.From(() => FormDialog.FromForm(BuildForm));
}
When you first talk to the bot, it offers you to select one of the three dialogs, e.g. "fill in the order", "enter your user profile", "get support",
Once the users selects, for example, "fill in the order", the bot launches the appropriate dialog.
Obviously, the user should just continue answering the questions inside the dialog until the dialog is over.
But every time the user sends a message, it is passed to this method in the API controller:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
From here, the bot needs to decide which of the three dialogs is currently in progress, and continue that dialog.
How do I do that, remember which dialog is currently in progress and with each new message from the user, continue that dialog instead of returning the user to the main screen?
My idea is to create some kind of global variable or a record that is stored somewhere else, maybe in the database. The record would contain the type of the current dialog that this user is having with the bot right now. Every time the bot receives a message, it would query the database to find out that the last interaction of the user was with the OrderDialog, and so the program code can decide to continue with the OrderDialog. But it seems slow and maybe there is some kind of built-in function in Bot Framework to store data about the user, such as which dialog type it last interacted with.
Use the Bot State Service
https://docs.botframework.com/en-us/csharp/builder/sdkreference/stateapi.html
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
// Detect if this is a Message activity
if (activity.Type == ActivityTypes.Message)
{
// Get any saved values
StateClient sc = activity.GetStateClient();
BotData userData = sc.BotState.GetPrivateConversationData(
activity.ChannelId, activity.Conversation.Id, activity.From.Id);
var boolProfileComplete = userData.GetProperty<bool>("ProfileComplete");
if (!boolProfileComplete)
{
// Call our FormFlow by calling MakeRootDialog
await Conversation.SendAsync(activity, MakeRootDialog);
}
else
{
// Get the saved profile values
var FirstName = userData.GetProperty<string>("FirstName");
var LastName = userData.GetProperty<string>("LastName");
var Gender = userData.GetProperty<string>("Gender");
// Tell the user their profile is complete
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append("Your profile is complete.\n\n");
sb.Append(String.Format("FirstName = {0}\n\n", FirstName));
sb.Append(String.Format("LastName = {0}\n\n", LastName));
sb.Append(String.Format("Gender = {0}", Gender));
// Create final reply
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
Activity replyMessage = activity.CreateReply(sb.ToString());
await connector.Conversations.ReplyToActivityAsync(replyMessage);
}
}
else
{
// This was not a Message activity
HandleSystemMessage(activity);
}
// Send response
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
Sample From:
Introduction To FormFlow With The Microsoft Bot Framework
http://aihelpwebsite.com/Blog/EntryId/8/Introduction-To-FormFlow-With-The-Microsoft-Bot-Framework