I am looking for some feedback or guidance. I have built a grpc server and client in dotnet core 3.1. Only this client will be talking to the server, but still need to protect the server endpoints. I am trying to implement authentication security on it and the jwt bearer token seems like a good path. I am trying to follow the example here(which came from this MS Doc) which seems like a pretty simple implementation, however seems insecure for production use since anyone can hit the generate token endpoint. I figured it could modify and implement a shared secret that is passed on the get token request. Does this seem like a good approach or other thoughts?
Besides this part, I have a question about the client side code that I don't quite understand. At the start of the client, it establishes a grpc channel by calling the CreateAuthenticatedChannel() which appears to only add the bearer token if the _token is set. You don't get the token until the client starts up and hit number three, but I don't see where the channel or client is rebuilt to include the metadata token value. The sample appears to work to but don't understand it.
class Program
{
// The port number(5001) must match the port of the gRPC server.
private const string Address = "localhost:5001";
private static string _token;
static async Task Main(string[] args)
{
var channel = CreateAuthenticatedChannel($"https://{Address}");
var client = new Ticketer.TicketerClient(channel);
Console.WriteLine("gRPC Ticketer");
Console.WriteLine();
Console.WriteLine("Press a key:");
Console.WriteLine("1: Get available tickets");
Console.WriteLine("2: Purchase ticket");
Console.WriteLine("3: Authenticate");
Console.WriteLine("4: Exit");
Console.WriteLine();
var exiting = false;
while (!exiting)
{
var consoleKeyInfo = Console.ReadKey(intercept: true);
switch (consoleKeyInfo.KeyChar)
{
case '1':
await GetAvailableTickets(client);
break;
case '2':
await PurchaseTicket(client);
break;
case '3':
_token = await Authenticate();
break;
case '4':
exiting = true;
break;
}
}
Console.WriteLine("Exiting");
}
private static GrpcChannel CreateAuthenticatedChannel(string address)
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
// SslCredentials is used here because this channel is using TLS.
// Channels that aren't using TLS should use ChannelCredentials.Insecure instead.
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
return channel;
}
private static async Task<string> Authenticate()
{
Console.WriteLine($"Authenticating as {Environment.UserName}...");
var httpClient = new HttpClient();
var request = new HttpRequestMessage
{
RequestUri = new Uri($"https://{Address}/generateJwtToken?name={HttpUtility.UrlEncode(Environment.UserName)}"),
Method = HttpMethod.Get,
Version = new Version(2, 0)
};
var tokenResponse = await httpClient.SendAsync(request);
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadAsStringAsync();
Console.WriteLine(token);
Console.WriteLine("Successfully authenticated.");
return token;
}
private static async Task PurchaseTicket(Ticketer.TicketerClient client)
{
Console.WriteLine("Purchasing ticket...");
try
{
var response = await client.BuyTicketsAsync(new BuyTicketsRequest { Count = 1 });
if (response.Success)
{
Console.WriteLine("Purchase successful.");
}
else
{
Console.WriteLine("Purchase failed. No tickets available.");
}
}
catch (Exception ex)
{
Console.WriteLine("Error purchasing ticket." + Environment.NewLine + ex.ToString());
}
}
private static async Task GetAvailableTickets(Ticketer.TicketerClient client)
{
Console.WriteLine("Getting available ticket count...");
var response = await client.GetAvailableTicketsAsync(new Empty());
Console.WriteLine("Available ticket count: " + response.Count);
}
}
}
Related
WRONG APPROACH
I have a problem, I can log in through my program, however I am always sent to another website. (deviceauth)
Every time I start the program, I am asked to enter a code on this website. I want to be logged in directly without having to go through the previous step.
Is there a way to log in directly with the AccessToken? If yes, how?
My Goal is to log in once and everytime I start the program, I should be directly logged in and be able to look at my Mails.
My Code:
using Microsoft.Graph;
Console.WriteLine(".NET Graph Tutorial\n");
//var settings = Settings.LoadSettings("appsettings.json");
var settings = Settings.LoadSettings("appsettingsSLSTracer.json");
// Initialize Graph
InitializeGraph(settings);
// Greet the user by name
await GreetUserAsync();
int choice = -1;
while (choice != 0)
{
Console.WriteLine("Please choose one of the following options:");
Console.WriteLine("0. Exit");
Console.WriteLine("1. Display access token");
Console.WriteLine("2. List my inbox");
Console.WriteLine("3. Send mail");
Console.WriteLine("4. Make a Graph call");
try
{
choice = int.Parse(Console.ReadLine() ?? string.Empty);
}
catch (System.FormatException)
{
// Set to invalid value
choice = -1;
}
switch (choice)
{
case 0:
// Exit the program
Console.WriteLine("Goodbye...");
break;
case 1:
// Display access token
await DisplayAccessTokenAsync();
break;
case 2:
// List emails from user's inbox
await ListInboxAsync();
break;
case 3:
// Send an email message
await SendMailAsync();
break;
case 4:
// Run any Graph code
await MakeGraphCallAsync();
break;
default:
Console.WriteLine("Invalid choice! Please try again.");
break;
}
}
// </ProgramSnippet>
// <InitializeGraphSnippet>
void InitializeGraph(Settings settings)
{
//if (System.IO.File.Exists(#"C:\windows\temp\msaccesstokenSLSTracer.txt"))
//{
//_userClient = System.IO.File.ReadAllText("C:\\temp\\msaccesstoken.txt");
//return;
//}
//else
{
GraphHelper.InitializeGraphForUserAuth(settings,
(info, cancel) =>
{
// Display the device code message to
// the user. This tells them
// where to go to sign in and provides the
// code to use.
Console.WriteLine(info.Message);
return Task.FromResult(0);
});
}
}
// </InitializeGraphSnippet>
// <GreetUserSnippet>
async Task GreetUserAsync()
{
try
{
var user = await GraphHelper.GetUserAsync();
Console.WriteLine($"Hello, {user?.DisplayName}!");
// For Work/school accounts, email is in Mail property
// Personal accounts, email is in UserPrincipalName
Console.WriteLine($"Email: {user?.Mail ?? user?.UserPrincipalName ?? ""}");
}
catch (Exception ex)
{
Console.WriteLine($"Error getting user: {ex.Message}");
}
}
// </GreetUserSnippet>
// <DisplayAccessTokenSnippet>
async Task DisplayAccessTokenAsync()
{
try
{
var userToken = await GraphHelper.GetUserTokenAsync();
Console.WriteLine($"User token: {userToken}");
}
catch (Exception ex)
{
Console.WriteLine($"Error getting user access token: {ex.Message}");
}
}
// </DisplayAccessTokenSnippet>
// <ListInboxSnippet>
async Task ListInboxAsync()
{
try
{
var messagePage = await GraphHelper.GetInboxAsync();
// Output each message's details
foreach (var message in messagePage.CurrentPage)
{
Console.WriteLine($"Message: {message.Subject ?? "NO SUBJECT"}");
Console.WriteLine($" From: {message.From?.EmailAddress?.Name}");
Console.WriteLine($" Status: {(message.IsRead!.Value ? "Read" : "Unread")}");
//Console.WriteLine($" BodyPreview: {message.Body}");
Console.WriteLine($" Attachments: {message.Attachments?.Count ?? 0}");
Console.WriteLine($" Received: {message.ReceivedDateTime?.ToLocalTime().ToString()}\n");
}
// If NextPageRequest is not null, there are more messages
// available on the server
// Access the next page like:
// messagePage.NextPageRequest.GetAsync();
var moreAvailable = messagePage.NextPageRequest != null;
Console.WriteLine($"\nMore messages available? {moreAvailable}");
}
catch (Exception ex)
{
Console.WriteLine($"Error getting user's inbox: {ex.Message}");
}
}
// </ListInboxSnippet>
// <SendMailSnippet>
async Task SendMailAsync()
{
try
{
// Send mail to the signed-in user
// Get the user for their email address
var user = await GraphHelper.GetUserAsync();
var userEmail = user?.Mail ?? user?.UserPrincipalName;
if (string.IsNullOrEmpty(userEmail))
{
Console.WriteLine("Couldn't get your email address, canceling...");
return;
}
await GraphHelper.SendMailAsync("Test des Programmes",
"Es hat geklappt", "Testobjekt1919#outlook.com"); //userEmail
Console.WriteLine("Mail sent.");
}
catch (Exception ex)
{
Console.WriteLine($"Error sending mail: {ex.Message}");
}
}
// </SendMailSnippet>
// <MakeGraphCallSnippet>
async Task MakeGraphCallAsync()
{
await GraphHelper.MakeGraphCallAsync();
}
// </MakeGraphCallSnippet>
Other Code:
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using Azure.Core;
using Azure.Identity;
using Microsoft.Graph;
class GraphHelper
{
#region User-auth
// <UserAuthConfigSnippet>
// Settings object
private static Settings? _settings;
// User auth token credential
private static DeviceCodeCredential? _deviceCodeCredential;
// Client configured with user authentication
private static GraphServiceClient? _userClient;
public static void InitializeGraphForUserAuth(Settings settings,
Func<DeviceCodeInfo, CancellationToken, Task> deviceCodePrompt)
{
_settings = settings;
_deviceCodeCredential = new DeviceCodeCredential(deviceCodePrompt,
settings.AuthTenant, settings.ClientId);
_userClient = new GraphServiceClient(_deviceCodeCredential, settings.GraphUserScopes);
/// Todo Idee, hier prüfen ob wir nicht den Token mitsenden können
//if (System.IO.File.Exists(#"C:\windows\temp\msaccesstokenSLSTracer.txt"))
//{ }
//_userClient.Token = System.IO.File.ReadAllText("C:\\temp\\msaccesstoken.txt");
}
// </UserAuthConfigSnippet>
// <GetUserTokenSnippet>
public static async Task<string> GetUserTokenAsync()
{
// Ensure credential isn't null
_ = _deviceCodeCredential ??
throw new System.NullReferenceException("Graph has not been initialized for user auth");
// Ensure scopes isn't null
_ = _settings?.GraphUserScopes ?? throw new System.ArgumentNullException("Argument 'scopes' cannot be null");
// Request token with given scopes
var context = new TokenRequestContext(_settings.GraphUserScopes);
var response = await _deviceCodeCredential.GetTokenAsync(context);
System.IO.File.WriteAllText( #"C:\windows\temp\msaccesstokenSLSTracer.txt", response.Token);
return response.Token;
}
// </GetUserTokenSnippet>
// <GetUserSnippet>
public static Task<User> GetUserAsync()
{
// Ensure client isn't null
_ = _userClient ??
throw new System.NullReferenceException("Graph has not been initialized for user auth");
return _userClient.Me
.Request()
.Select(u => new
{
// Only request specific properties
u.DisplayName,
u.Mail,
u.UserPrincipalName
})
.GetAsync();
}
// </GetUserSnippet>
// <GetInboxSnippet>
public static Task<IMailFolderMessagesCollectionPage> GetInboxAsync()
{
// Ensure client isn't null
_ = _userClient ??
throw new System.NullReferenceException("Graph has not been initialized for user auth");
return _userClient.Me
// Only messages from Inbox folder
.MailFolders["Inbox"]
.Messages
.Request()
.Select(m => new
{
// Only request specific properties
m.From,
m.IsRead,
m.ReceivedDateTime,
m.Subject,
m.Attachments
//m.BodyPreview,
//m.Body
})
// Get at most 25 results
.Top(15)
// Sort by received time, newest at the top
.OrderBy("ReceivedDateTime DESC")
.GetAsync();
}
// </GetInboxSnippet>
// <SendMailSnippet>
public static async Task SendMailAsync(string subject, string body, string recipient)
{
// Ensure client isn't null
_ = _userClient ??
throw new System.NullReferenceException("Graph has not been initialized for user auth");
// Create a new message
var message = new Message
{
Subject = subject,
Body = new ItemBody
{
Content = body,
ContentType = BodyType.Text
},
ToRecipients = new Recipient[]
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = recipient
}
}
}
};
// Send the message
await _userClient.Me
.SendMail(message)
.Request()
.PostAsync();
}
// </SendMailSnippet>
#endregion
#pragma warning disable CS1998
// <MakeGraphCallSnippet>
// This function serves as a playground for testing Graph snippets
// or other code
public async static Task MakeGraphCallAsync()
{
// INSERT YOUR CODE HERE
// Note: if using _appClient, be sure to call EnsureGraphForAppOnlyAuth
// before using it.
// EnsureGraphForAppOnlyAuth();
}
// </MakeGraphCallSnippet>
}
At the moment your code is using the device code oAuth flow https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code so you will always be redirected/prompt that way. For unattended apps you generally want to use the client credentials flow https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow and application permissions.
If you still want to use delegate access (eg user creds) then you can look at using a persistent cache https://pnp.github.io/pnpcore/demos/Demo.PersistentTokenCache/README.html in that case you should be using the authorization code flow https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow eg https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-interactive?tabs=dotnet a very simplified implementation would look like
GraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
var authResult = publicClientApplication.AcquireTokenInteractive(new[] { "https://graph.microsoft.com/Mail.Read" })
.ExecuteAsync().GetAwaiter().GetResult();
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}), httpProvider
);
but you need a more complex implementation to use caching
I am reading different mailboxes in windows service using MS graph api using threads. Service runs well for few days, but after it goes in hung state.
By observing logs found that stuck point is while calling GetAsync() method for users, after that it is not updating log file also but service is showing in running state.
After restarting the service it runs normally for few days.
public static async Task MainAsync()
{
Task t1 = ParallelThreadOne.MainAsync();
Task t2 = ParallelThreadSecond.MainAsync();
Task t3 = ParallelThreadThird.MainAsync();
await Task.WhenAll(t1, t2, t3);
}
public static async Task MainAsync()
{
try
{
EmailMaster objEmailMaster = new EmailMaster();
List<MailBoxConfiguration> lstMailBoxConfiguration = objEmailMaster.GetMailBoxInformation(1,logger);
if (lstMailBoxConfiguration != null)
{
if (lstMailBoxConfiguration.Count != 0)
{
GraphServiceClient client = GetAuthenticatedClient();
if (client != null)
{
for (int j = 0; j < lstMailBoxConfiguration.Count; j++)
{
var users = await graphClient
.Users
.Request()
.Filter("startswith(Mail,'" + lstMailBoxConfiguration[j].EmailId + "')")
.GetAsync();
if (users.Count > 0)
{
var msgs = await graphClient
.Users[users[0].Id]
.MailFolders["Inbox"].Messages
.Request().Top(500)
.GetAsync();
if (msgs.Count > 0)
{
foreach (var item in msgs)
{
//business logic goes here
}
}
else
{
logger.Info("msg.Count is zero");
}
}
else
{
logger.Info("users.Count is zero");
}
}
}
else
{
logger.Info("client is null");
}
}
else
{
logger.Info("lstMailBoxConfiguration.Count is zero from the database");
}
}
else
{
logger.Info("lstMailBoxConfiguration is null from the database");
}
logger.Info("MainAsync(1) : End of MainAsync(1)");
}
catch (Exception ex)
{
logger.Error("MainAsync(1) : Exception : " + ex.Message);
}
}
public static GraphServiceClient GetAuthenticatedClient()
{
string clientId = ConfigurationManager.AppSettings["AzureClientId"];
string password = ConfigurationManager.AppSettings["password"];
string tenantId = ConfigurationManager.AppSettings["tenantId"];
string getTokenUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
const string grantType = "client_credentials";
const string myScopes = "https://graph.microsoft.com/.default";
string postBody = $"client_id={clientId}&scope={myScopes}&client_secret={password}&grant_type={grantType}";
try
{
if (graphClient == null)
{
graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, getTokenUrl);
httpRequestMessage.Content = new StringContent(postBody, Encoding.UTF8, "application/x-www-form-urlencoded");
HttpResponseMessage httpResponseMessage = await client.SendAsync(httpRequestMessage);
string responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
userToken = JObject.Parse(responseBody).GetValue("access_token").ToString();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", userToken);
}));
}
return graphClient;
}
catch (Exception ex)
{
logger.Error("Could not create a graph client: " + ex.Message);
}
finally
{
logger.Info("GetAuthenticatedClient() :inside finally!");
}
return graphClient;
}
From looking through the source code of the GraphServiceClient, it is using HTTPClient as its underlying communication provider. The HTTPClient has an issue because windows keeps TCP/IP connections open for a certain amount of time after the HTTPClient is disposed. If you call new and then dispose on the HTTPClient class fast enough for long enough it can lead to socket starvation. (note, using(var client = new HTTPClient()) calls dispose under the covers when the instance goes out of scope)
Take a look at this blog post on the issue.
https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
You should be able to use a single instance of the GraphServiceClient as long as you are talking to the same GraphQL endpoint and fix your issues with the service hanging. If you add logging you will probably notice that the service hangs after a flurry of activity causing lots of new instances of the GraphServiceClient to be created then disposed in a short time frame and your open network connections on the server exploding causing an error that crashes one of your threads.
I have two projects. One is an Identity server 4 who handle users and authentication. The second need to use the first to login and ask for a token to access an API.
When I need to refresh the token I don't now how to handle the new access token. How I can set to the authentification asp dot net core the new token. All the refresh process are made on a AuthorizationHandler.
I tried to modify the claims on identity but doesnt work. I tried to stock access token and refresh token inside my own cookie but I have trouble because when I refresh the token I can only use them at the next request (I didn't achieve to modify the request.cookies only the response.cookies.
public static async Task SetToken(TokenKind token,string value, HttpContext context)
{
context.Response.Cookies.Append(GetTokenCookieName(token), value);
}
public static async Task<string> GetRefreshTokenAsync(HttpContext context)
{
return await SearchToken(TokenKind.Refresh,context);
}
private static async Task<string> SearchToken(TokenKind token, HttpContext context)
{
var tokenName = GetTokenName(token);
var test = context.Request.Cookies;
var apiToken = context.Request.Cookies.FirstOrDefault(x => x.Key == GetTokenCookieName(token)).Value;
if (apiToken == null)
{
// Save token into cookie
var tokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Access));
await SetToken(TokenKind.Access, tokenValue, context);
var refreshTokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Refresh));
await SetToken(TokenKind.Refresh, refreshTokenValue, context);
switch (token)
{
case TokenKind.Access:
return tokenValue;
case TokenKind.Refresh:
return refreshTokenValue;
default:
return null;
break;
}
}
else
{
return apiToken;
}
}
private async Task<bool> RefreshToken(AuthorizationFilterContext mvcContext, HttpClient client)
{
var refreshToken = await TokenUtils.GetRefreshTokenAsync(mvcContext.HttpContext);
//await mvcContext.HttpContext.GetTokenAsync("refresh_token");
var variables = new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", _configuration["ApplicationOptions:ClientId"] },
{ "client_secret", _configuration["ApplicationOptions:ClientSecret"] },
{ "refresh_token", refreshToken }
};
var content = new FormUrlEncodedContent(variables);
var url = _configuration["ApplicationOptions:AuthorizeServer"] + "/connect/token";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode == false)
{
var errorString = await response.Content.ReadAsStringAsync();
var errorData = JsonConvert.DeserializeObject<dynamic>(errorString);
return false;
}
var contentAsString = await response.Content.ReadAsStringAsync();
var responseData = JsonConvert.DeserializeObject<dynamic>(contentAsString);
var newAccessToken = (string)responseData.access_token;
var newRefreshToken = (string)responseData.refresh_token;
await TokenUtils.SetAccessToken(newAccessToken, mvcContext.HttpContext);
await TokenUtils.SetRefreshToken(newRefreshToken, mvcContext.HttpContext);
var result = await mvcContext.HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
result.Properties.StoreTokens(new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = newAccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = newRefreshToken
}
});
return true;
}
else
{
return false;
}
}
I would like to know what is the good pratice to store (or replace the actual access token) with the .net core authentification. I'm sure I'm not doing it the right way. At the end I want to handle correctly my token and my refresh token for not ask the user to login again. Now with my solution my project will denied the access when it need to refresh the token and next request will be granted (because the cookie need to be resend from the user).
Thank to Ruard van Elburg I found the solution (here's the complete answer)
And that's what I used to replace my tokens:
// Save the information in the cookie
var info = await mvcContext.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", newRefreshToken);
info.Properties.UpdateTokenValue("access_token", newAccessToken);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await mvcContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
I want to retrieve ticks from Poloniex in real time. They use wamp for that. I installed via nugget WampSharp and found this code :
static async void MainAsync(string[] args)
{
var channelFactory = new DefaultWampChannelFactory();
var channel = channelFactory.CreateMsgpackChannel("wss://api.poloniex.com", "realm1");
await channel.Open();
var realmProxy = channel.RealmProxy;
Console.WriteLine("Connection established");
int received = 0;
IDisposable subscription = null;
subscription =
realmProxy.Services.GetSubject("ticker")
.Subscribe(x =>
{
Console.WriteLine("Got Event: " + x);
received++;
if (received > 5)
{
Console.WriteLine("Closing ..");
subscription.Dispose();
}
});
Console.ReadLine();
}
but no matter at the await channel.open() I have the following error : HHTP 502 bad gateway
Do you have an idea where is the problem
thank you in advance
The Poloniex service seems not to be able to handle so many connections. That's why you get the HTTP 502 bad gateway error. You can try to use the reconnector mechanism in order to try connecting periodically.
static void Main(string[] args)
{
var channelFactory = new DefaultWampChannelFactory();
var channel = channelFactory.CreateJsonChannel("wss://api.poloniex.com", "realm1");
Func<Task> connect = async () =>
{
await Task.Delay(30000);
await channel.Open();
var tickerSubject = channel.RealmProxy.Services.GetSubject("ticker");
var subscription = tickerSubject.Subscribe(evt =>
{
var currencyPair = evt.Arguments[0].Deserialize<string>();
var last = evt.Arguments[1].Deserialize<decimal>();
Console.WriteLine($"Currencypair: {currencyPair}, Last: {last}");
},
ex => {
Console.WriteLine($"Oh no! {ex}");
});
};
WampChannelReconnector reconnector =
new WampChannelReconnector(channel, connect);
reconnector.Start();
Console.WriteLine("Press a key to exit");
Console.ReadKey();
}
This is based on this code sample.
Get rid of the Console.WriteLine it is interfering with your code.
My bot keeps failing on the following line of code with the error in Skype, WebChat, and FB chat. I've tried entering the MS App ID and password via the app settings and the web.config. This works fine in the emulator without any errors. When I ran remote debugging I found that this fails on Webchat, Skype, and FB messenger at this line of code:
await connector.Conversations.ReplyToActivityAsync(reply1);
My bot is integrated with SmartThings so the smart accessories are turning on and off as expected but the response back that should be returned in the chat seems to be failing. I've tried creating a new bot service app but it fails as well.
Update including all code:
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using System.Net.Http.Headers;
using System.Threading;
using Microsoft.IdentityModel.Protocols;
using System.Configuration;
namespace FirstBotApp
{
[BotAuthentication]
public class Switches
{
public string name { get; set; }
public string value { get; set; }
}
public class MessagesController : ApiController
{
static HttpClient client = new HttpClient();
static HttpResponseMessage responsemain = null;
static int j = 0;
static void ShowSwitches(Switches[] switches)
{
for (int i = switches.Length - 1; i >= 0; i--)
{
Console.WriteLine($"Name: {switches[i].name}\tPrice: {switches[i].value}\t");
}
return;
}
static async Task<Switches[]> GetSwitchesAsync(string path)
{
Switches[] switches = null;
responsemain = await client.GetAsync(path);
//Console.WriteLine(client.GetAsync(path));
//Console.WriteLine(response);
if (responsemain.IsSuccessStatusCode)
{
//Console.WriteLine($"Successful response: {response.StatusCode}");
var response1 = await client.GetStringAsync("");
Console.WriteLine($"{response1}");
switches = await responsemain.Content.ReadAsAsync<Switches[]>();
//JObject j = JObject.Parse(response1);
}
else Console.WriteLine($"Error in response: {responsemain.StatusCode}");
return switches;
}
static async Task<Switches> UpdateSwitchesAsync(Switches switches)
{
Console.WriteLine("Turning off the light");
HttpResponseMessage response = await client.PutAsJsonAsync($"switches/off?room=LivingRoom", switches);
response.EnsureSuccessStatusCode();
Console.WriteLine($"The response from the server was: {response.StatusCode}");
// Deserialize the updated switches from the response body.
switches = await response.Content.ReadAsAsync<Switches>();
Thread.Sleep(10000);
Console.WriteLine($"Turning on the light");
response = await client.PutAsJsonAsync($"switches/on?room=LivingRoom", switches);
Console.WriteLine($"The response from the server was: {response.StatusCode}");
Thread.Sleep(10000);
return switches;
}
static async Task<HttpStatusCode> DeleteSwitchesAsync(string id)
{
HttpResponseMessage response = await client.DeleteAsync($"api/switchess/{id}");
return response.StatusCode;
}
/// <summary>
/// POST: api/Messages
/// Receive a message from a user and reply to it
/// </summary>
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
Switches[] switches = null;
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
//string appdId = AppSettings.Settings["MicrosoftAppId"];
//string appPassword = ConfigurationManager.AppSettings["MicrosoftAppPassword"];
if (j == 0)
{
//client.Timeout = TimeSpan.FromSeconds(4);
//Declaration of client and Switches variable
string accessToken = "xxxxx";
client.BaseAddress = new Uri("xxxxx");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
j++;
//client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Authorization", "Bearer " + accessToken);
}
if (activity.Type == ActivityTypes.Message)
{
//, appdId, appPassword);
//Switches switches = new Switches;
Activity reply1;// = activity.CreateReply($"breakpoint1");
//await connector.Conversations.ReplyToActivityAsync(reply1);
switch (activity.Text.ToLower())
{
case"turn on livingroom":
try
{
Console.WriteLine("Turning on the light");
responsemain = await client.PutAsJsonAsync($"switches/on?room=LivingRoom", switches);
responsemain.EnsureSuccessStatusCode();
//Console.WriteLine($"The response from the server was: {responsemain.StatusCode}");
// Deserialize the updated product from the response body.
switches = await responsemain.Content.ReadAsAsync<Switches[]>();
reply1 = activity.CreateReply($"Successfully turned on LivingRoom Light");
await connector.Conversations.ReplyToActivityAsync(reply1);
}
catch
{
reply1 = activity.CreateReply($"Error");
await connector.Conversations.ReplyToActivityAsync(reply1);
}
break;
case "turn off livingroom":
try
{
Console.WriteLine("Turning off the light");
responsemain = await client.PutAsJsonAsync($"switches/off?room=LivingRoom", switches);
responsemain.EnsureSuccessStatusCode();
Console.WriteLine($"The response from the server was: {responsemain.StatusCode}");
// Deserialize the updated product from the response body.
switches = await responsemain.Content.ReadAsAsync<Switches[]>();
reply1 = activity.CreateReply($"Successfully turned off LivingRoom Light");
await connector.Conversations.ReplyToActivityAsync(reply1);
}
catch
{
reply1 = activity.CreateReply($"Error");
await connector.Conversations.ReplyToActivityAsync(reply1);
}
break;
default: //"What lights are on?":
try
{
switches = await GetSwitchesAsync("");
//Console.WriteLine($"About to show the product");
ShowSwitches(switches);
//await connector.Conversations.ReplyToActivityAsync(switches[].ToString);
for (int i = switches.Length - 1; i >= 0; i--)
{
reply1 = activity.CreateReply($"Room: ");//{switches[i].name}\tStatus: {switches[i].value}\t");
responsemain.EnsureSuccessStatusCode();
await connector.Conversations.ReplyToActivityAsync(reply1);
}
break;
}
catch
{
}
break;
}
// calculate something for us to return
//int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
//Activity reply = activity.CreateReply($"You sent {activity.Text} which was {length} characters");
//await connector.Conversations.ReplyToActivityAsync(reply);
}
else
{
HandleSystemMessage(activity);
}
// var response = Request.CreateResponse(HttpStatusCode.OK);
return responsemain;
}
static Activity HandleSystemMessage(Activity message)
{
if (message.Type == ActivityTypes.DeleteUserData)
{
// Implement user deletion here
// If we handle user deletion, return a real message
}
else if (message.Type == ActivityTypes.ConversationUpdate)
{
// Handle conversation state changes, like members being added and removed
// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
// Not available in all channels
}
else if (message.Type == ActivityTypes.ContactRelationUpdate)
{
// Handle add/remove from contact lists
// Activity.From + Activity.Action represent what happened
}
else if (message.Type == ActivityTypes.Typing)
{
// Handle knowing tha the user is typing
}
else if (message.Type == ActivityTypes.Ping)
{
}
return null;
}``
//client.dispose();
}
}
I found the error after some additional troubleshooting with remote debugging. Based on #JimLewallen's response I looked closer at the "Credentials" portion of the connector object. The OAuthScope for version 3.0 of the Botframework (connector-> Credentials->OAuthScope in the locals of the remote debugger) was pointing to api.botframework.com/.default when it should have been pointing at "graph.microsoft.com/.default". When I created a new bot service application in Visual Studios using the bot template the OAuthScope showed the correct endpoint.
Incorrect Value:
OAuthScope "api.botframework.com/.default"
Correct Value: Image from the Remote Debugger showing the correct OAuthScope