Bot Framework Unauthorized when creating a conversation - c#

I'm trying to resume a conversation between a bot and a user from a Web Job, and I'm getting an Unauthorized Exception.
I can successfully reply to a conversation in my MessagesController class, but when I try to run the following code in my Web Job, I get the following exception:
private static async Task SendAlertToUser(ChannelAccount botAccount, LipSenseUser user, AlertFlags alert, string extra)
{
Console.WriteLine($"Sending alert to user: {user.Name}");
var sb = new StringBuilder(GetTextFromAlert(alert));
if (!string.IsNullOrEmpty(extra))
{
sb.AppendLine();
sb.Append(extra);
}
var userAccount = new ChannelAccount(user.ChannelId, user.Name);
var connector = new ConnectorClient(new Uri(user.ChannelUri));
var message = Activity.CreateMessageActivity();
message.From = botAccount;
message.Recipient = userAccount;
var conversation = await connector.Conversations.CreateDirectConversationAsync(botAccount, userAccount);
message.Conversation = new ConversationAccount(id: conversation.Id);
message.Locale = "en-Us";
message.Text = sb.ToString();
await connector.Conversations.SendToConversationAsync((Activity)message);
}
And the exception is:
Exception:System.UnauthorizedAccessException
Authorization for Microsoft App ID 58c04dd1-1234-5678-9123-456789012345 failed with status code Unauthorized and reason phrase 'Unauthorized'
When I inspect the connector's Credentials, I see that everything is correct.  I've set a breakpoint in my MessagesController and inspected the connector's Credentials from there, and everything is identical.
Also, when I look at the IntelliTrace, I see the following messages:
My user.ChannelUri is "https://facebook.botframework.com", which I pulled off of the user when they initialized the conversation.  Is that not the correct Uri?
Is there anything else I need to do to send a message?  My App.config appSettings looks like this:
 
 
<appSettings>
    <add key="BotId" value="MyBotName" />
    <add key="MicrosoftAppId" value="58c04dd1-1234-5678-9123-456789012345" />
    <add key="MicrosoftAppPassword" value="5xyadfasdfaweraeFAKEasdfad" />
    <add key="AzureWebJobsStorage" value="DefaultEndpointsProtocol=https;AccountName=BLAH;AccountKey=THIS IS A KEY" />
</appSettings>

Answer from Bot Framework Team on a different channel:
You need to add a call to:
MicrosoftAppCredentials.TrustServiceUrl(serviceUrl);
This is done automatically when you are replying to a message, but for
proactive messages from another process you need to do this.

Related

Azure AD B2C Tenant ID Null

I don't even know where to begin. I am a Windows Forms C# developer. I am trying to learn Azure. I am following a tutorial here: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
I have checked and rechecked my steps. I can successfully log in to the app. I can see the claims displayed when I click the claims button. As soon I click the To-Do List button, my app crashes in TaskController.cs Index method. It crashes on trying to obtain an AuthenticationResult.
// GET: Makes a call to the API and retrieves the list of tasks
public async Task<ActionResult> Index()
{
try
{
// Retrieve the token with the specified scopes
var scope = new string[] { Globals.ReadTasksScope };
//string[] scopes = new string[] { "user.read" };
var app = publicClientApplicationBuilder
.Create(Globals.ClientId)
.WithB2CAuthority(Globals.B2CAuthority)
.Build();
// IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
// var accounts = await cca.GetAccountsAsync();
//AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault()).ExecuteAsync();
//var accounts = await app.GetAccountsAsync(Globals.SignUpSignInPolicyId);
//var accounts = await app.GetAccountsAsync(Globals.SignUpSignInPolicyId);
//AuthenticationResult ar = await app.AcquireTokenInteractive(scope)
// .WithAccount(accounts.FirstOrDefault())
// .ExecuteAsync();
AuthenticationResult result;
try
{
IEnumerable<IAccount> accounts = await app.GetAccountsAsync(Globals.SignUpSignInPolicyId);
// Try to acquire an access token from the cache. If an interaction is required, MsalUiRequiredException will be thrown.
result = await app.AcquireTokenSilent(scope, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring an access token interactively. MSAL will cache it so you can use AcquireTokenSilent on future calls.
result = await app.AcquireTokenInteractive(scope)
.ExecuteAsync();
}
//var accounts = await cca.GetAccountsAsync(Globals.SignUpSignInPolicyId);
// var accounts = await app.GetAccountsAsync();
//AuthenticationResult ar;
//try
//{
// ar = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
// .ExecuteAsync();
//}
//catch (MsalUiRequiredException)
//{
// ar = await app.AcquireTokenInteractive(scopes)
// .ExecuteAsync();
//}
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);
// Add token to the Authorization header and make the request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Handle the response
switch (response.StatusCode)
{
case HttpStatusCode.OK:
string responseString = await response.Content.ReadAsStringAsync();
JArray tasks = JArray.Parse(responseString);
ViewBag.Tasks = tasks;
return View();
case HttpStatusCode.Unauthorized:
return ErrorAction("Please sign in again. " + response.ReasonPhrase);
default:
return ErrorAction("Error. Status code = " + response.StatusCode + ": " + response.ReasonPhrase);
}
}
catch (MsalUiRequiredException ex)
{
/*
If the tokens have expired or become invalid for any reason, ask the user to sign in again.
Another cause of this exception is when you restart the app using InMemory cache.
It will get wiped out while the user will be authenticated still because of their cookies, requiring the TokenCache to be initialized again
through the sign in flow.
*/
return new RedirectResult("/Account/SignUpSignIn?redirectUrl=/Tasks");
}
catch (Exception ex)
{
return ErrorAction("Error reading to do list: " + ex.Message);
}
}
As you see, I have commented out several attempts to get this to work. At the end of the tutorial, it says:
Known Issues
MSAL cache needs a TenantId along with the user's ObjectId to function. It retrieves these two from the claims returned in the id_token. As TenantId is not guaranteed to be present in id_tokens issued by B2C unless the steps listed in this document, if you are following the workarounds listed in the doc and tenantId claim (tid) is available in the user's token, then please change the code in ClaimsPrincipalsExtension.cs GetB2CMsalAccountId() to let MSAL pick this from the claims instead.
I have noticed that in the app variable in my code, the TenantID property is null. I followed the work-arounds listed in the document, and still no TenantID.
Here are the steps listed in the document: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/AAD-B2C-specifics#acquiring-a-token-to-apply-a-policy
I followed all steps, not just from the bookmarked location and below.
Here is what I am seeing when I debug:
Here is what I am seeing from app:
Notice the UserTokenCache.NullPreferredUsernameDisplayLabel says:
Missing from the token response
It talks about that in the known issues mentioned above.
Now the accounts variable has the following:
The result variable stays null and execution goes to the catch block where it tries again to get a result using AcquireTokenInteractive(scope). This also fails and execution moves to the final catch block at the bottom of the method.
The error message states:
ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.
That doesn't make sense to me. I think the problem starts with the fact that the app variable is not acquiring the TenantID. But I do not know what to do about it.
I noticed a couple of things missing from the tutorial. It did not say to grant Admin privileges to the scope and API permissions, but I did that.
Here is my web.config from the TaskWebApp project:
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="ida:Tenant" value="ShoppingCartB2C.onmicrosoft.com" />
<!--MSAL cache needs a tenantId along with the user's objectId to function. It retrieves these two from the claims returned in the id_token.
As tenantId is not guaranteed to be present in id_tokens issued by B2C unless the steps listed in this
document (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/AAD-B2C-specifics#caching-with-b2c-in-msalnet).
If you are following the workarounds listed in the doc and tenantId claim (tid) is available in the user's token, then please change the
code in <ClaimsPrincipalsExtension.cs GetB2CMsalAccountId()> to let MSAL pick this from the claims instead -->
<add key="ida:TenantId" value="db1b052a-415c-4604-887c-e27b59860001" />
<add key="ida:ClientId" value="975f1457-e3e2-4cb8-b069-6b0b6b46611d" />
<add key="ida:ClientSecret" value="Gw4.3o-DRDr.j_828H-JMfsk_Jd1d-jQ5p" />
<add key="ida:AadInstance" value="https://ShoppingCartB2C.b2clogin.com/tfp/{0}/{1}" />
<add key="ida:RedirectUri" value="https://localhost:44316/" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_signupsignin1" />
<add key="ida:EditProfilePolicyId" value="B2C_1_edit_profile" />
<add key="ida:ResetPasswordPolicyId" value="B2C_1_reset" />
<add key="api:TaskServiceUrl" value="https://localhost:44332/" />
<!-- The following settings is used for requesting access tokens -->
<add key="api:ApiIdentifier" value="https://ShoppingCartB2C.onmicrosoft.com/demoapi/" />
<add key="api:ReadScope" value="read" />
<add key="api:WriteScope" value="write" />
</appSettings>
And my TaskService web.config:
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="ida:AadInstance" value="https://ShoppingCartB2C.b2clogin.com/{0}/{1}/v2.0/.well-known/openid-configuration" />
<add key="ida:Tenant" value="ShoppingCartB2C.onmicrosoft.com" />
<add key="ida:ClientId" value="1c8e9aee-d04a-4fb1-aa32-8ba808122e76" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_signupsignin1" />
<!-- The following settings is used for requesting access tokens -->
<add key="api:ReadScope" value="read" />
<add key="api:WriteScope" value="write" />
</appSettings>
Please let me know how I can go about troubleshooting this. Out of everything I have tried, app always ends up with a null TenantID.
Have also tried this that ends up with null TenantID:
https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token?tabs=dotnet#acquire-a-token-interactively
See below:
string[] scopes = new string[] {"user.read"};
var app = PublicClientApplicationBuilder.Create(clientId).Build();
var accounts = await app.GetAccountsAsync();
AuthenticationResult result;
try
{
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch(MsalUiRequiredException)
{
result = await app.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}
It seems to be mapping web app to AAD. Please check these steps once, hope it helps you:
Null Tenant Id will come during the mapping the web app to Azure active directory (B2B/B2C/AD).
Follow these steps present in these Microsoft documentations:
Integrate web app to Azure AD B2C
As you said, the application crashes and trying to obtain authenticationResult, make sure these settings configured correctly in order to get rid of that error.

Sending email via Gmail SMTP hangs indefinitely without error

In my ASP.NET MVC 5 application, I use emails (System.Net.Mail) primarily for account authentication. It's worked perfectly until recently, and I have no idea what happened. I didn't change anything even slightly related to emails, as far as I know.
When I try to step into the SendAsync call in the controller, it transfers control back to the browser where it hangs indefinitely. Eventually I have to stop and restart the application pool just to access any page, which takes a couple minutes (usually it can be turned back on almost instantly).
I have it set up to use a Google app password, which is a requirement (you get an error about security otherwise). It doesn't seem to even get as far as Google, since the new app password hasn't been used.
I've tried the TLS port as well as the SSL port. Last time I got it working was using TLS.
Web.config:
<configuration>
<appSettings>
<add key="SmtpUsername" value="email#gmail.com" />
<add key="SmtpPassword" value="AppPassword" />
<add key="SmtpSender" value="email#gmail.com" />
<add key="SmtpHost" value="smtp.gmail.com" />
<add key="SmtpPort" value="587" /> <!-- SSL: 465, TLS: 587 -->
<add key="SmtpEnableSsl" value="true" />
</appSettings>
</configuration>
Email code:
public class EmailClient : SmtpClient
{
public EmailClient()
{
UseDefaultCredentials = false;
Host = WebConfigurationManager.AppSettings.Get("SmtpHost");
Port = int.Parse(WebConfigurationManager.AppSettings.Get("SmtpPort"));
EnableSsl = bool.Parse(WebConfigurationManager.AppSettings.Get("SmtpEnableSsl"));
Credentials = new NetworkCredential(WebConfigurationManager.AppSettings.Get("SmtpUsername"),
WebConfigurationManager.AppSettings.Get("SmtpPassword"));
DeliveryMethod = SmtpDeliveryMethod.Network;
Timeout = 30000; // Waiting 30 seconds doesn't even end the "loading" status
}
}
public class EmailMessage : MailMessage
{
private bool isBodyHtml = true;
public EmailMessage(string subject, string body, string recipients)
: base(WebConfigurationManager.AppSettings.Get("SmtpSender"), recipients, subject, body)
{
IsBodyHtml = isBodyHtml;
}
public EmailMessage(string subject, string body, IEnumerable<string> recipients)
: base(WebConfigurationManager.AppSettings.Get("SmtpSender"), string.Join(",", recipients), subject, body)
{
IsBodyHtml = isBodyHtml;
}
}
public static class Email
{
/// <param name="recipients">Comma-delimited list of email addresses</param>
public static async Task SendAsync(string subject, string body, string recipients)
{
using (EmailMessage msg = new EmailMessage(subject, body, recipients))
using (EmailClient client = new EmailClient())
{
await client.SendMailAsync(msg);
}
}
/// <param name="recipients">Collection of email addresses</param>
public static async Task SendAsync(string subject, string body, IEnumerable<string> recipients)
{
using (EmailMessage msg = new EmailMessage(subject, body, recipients))
using (EmailClient client = new EmailClient())
{
await client.SendMailAsync(msg);
}
}
}
Usage:
public class TestController : BaseController
{
public async Task<ActionResult> Test()
{
await Email.SendAsync("TEST", "test", "anaddress#gmail.com");
return View(); // Never reaches this point
}
}
OP here. As some answers allude, there was nothing wrong with my code. I'm not sure which of the below I had changed without retesting, but this is what you must have to use Gmail SMTP:
Use TLS port 587
Set SmtpClient.EnableSsl to true
Enable MFA for the Google account and use an app password for the SmtpClient.Credentials. I needed to enable MFA to create an app password.
Please note the Documentation and see the Gmail sending limits. under Gmail SMTP server section.
Your code looks fine, the only thing I see is that you are enabling SSL, but using the port distained for 'TLS' so users who will use the SSL method, will engage in an issue.
Beside from that, nothing appears to the eye.
There is standard way to send emails from ASP.NET.
web.config
<system.net>
<mailSettings>
<smtp deliveryMethod="Network">
<network defaultCredentials="false" enableSsl="true" host="smtp.gmail.com" password="password" port="587" userName="user#gmail.com"/>
</smtp>
</mailSettings>
</system.net>
.cs
var smtp = new SmtpClient(); // no other code.
var msg = CreateEmailMessage();
//I use try... catch
try{
smtp.Send(msg);
//return true; //if it is a separate function
}
catch(Exception ex){
//return false;
//use ex.Message (or deeper) to send extra information
}
Note that Gmail doesn't except a sender other than username. If you want your addressee to answer to another address then use
msg.ReplyToList.Add(new MailAddress(email, publicName));

SendGrid Tutorial resulting in Bad Request

I apologize if this is a dupe question, but I have not found any solid information about this issue either on this site or on others.
With that being said, I am working on an MVC 5 web application. I am following this tutorial over on ASP.net.
public async Task SendAsync(IdentityMessage message)
{
await configSendGridasync(message);
}
private async Task configSendGridasync(IdentityMessage message)
{
var myMessage = new SendGridMessage();
myMessage.AddTo(message.Destination);
myMessage.From = new System.Net.Mail.MailAddress(
"info#ycc.com", "Your Contractor Connection");
myMessage.Subject = message.Subject;
myMessage.Text = message.Body;
myMessage.Html = message.Body;
var credentials = new NetworkCredential(
Properties.Resources.SendGridUser,
Properties.Resources.SendGridPassword,
Properties.Resources.SendGridURL // necessary?
);
// Create a Web transport for sending email.
var transportWeb = new Web(credentials);
// Send the email.
if (transportWeb != null)
{
await transportWeb.DeliverAsync(myMessage);
}
else
{
Trace.TraceError("Failed to create Web transport.");
await Task.FromResult(0);
}
}
Each time it gets to the await transportWeb.SendAsync(myMessage) line in the above method, this error shows up in the browser:
Server Error in '/' Application.
Bad Request
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Exception: Bad Request
Line 54: if (transportWeb != null)
Line 55: {
Line 56: await transportWeb.DeliverAsync(myMessage);
Line 57: }
Line 58: else
Line 59: {
Line 60: Trace.TraceError("Failed to create Web transport.");
Line 61: await Task.FromResult(0);
Line 62: }
I signed up for a free account over at https://sendgrid.com/, using the "Free Package Google", giving me 25,000 monthly credits. The account has been provisioned.
I have tried a bunch of things so far including: disabling SSL, putting username/password directly in the code instead of pulling them from the Resources.resx file, specifying the SMTP server inside the NetworkCredential object, and also tried changing DeliverAsync(...) to Deliver().
I tried explicitly setting the subject instead of using message.Subject, as this post suggested. I also tried HttpUtility.UrlEncode on the callbackUrl generated in the Account/Register method as suggested here. Same results, unfortunately.
Does anyone happen to have some insight as to what might be causing this to not function correctly?
I ended up using the built-in SmtpClient to get this working. Here is the code that I am using:
private async Task configSendGridasync(IdentityMessage message)
{
var smtp = new SmtpClient(Properties.Resources.SendGridURL,587);
var creds = new NetworkCredential(Properties.Resources.SendGridUser, Properties.Resources.SendGridPassword);
smtp.UseDefaultCredentials = false;
smtp.Credentials = creds;
smtp.EnableSsl = false;
var to = new MailAddress(message.Destination);
var from = new MailAddress("info#ycc.com", "Your Contractor Connection");
var msg = new MailMessage();
msg.To.Add(to);
msg.From = from;
msg.IsBodyHtml = true;
msg.Subject = message.Subject;
msg.Body = message.Body;
await smtp.SendMailAsync(msg);
}
Even though it doesn't use SendGrid's C# API, the messages still show up on my SendGrid dashboard.
It might be a problem with your credentials.
If you signed up with SendGrid through Windows Azure, then you need to do the following:
Log in to your Azure Portal
Navigate to the Marketplace
Locate and click on the SendGrid application
Down at the bottom, click on Connection Info
Use the Username and Password listed.
I was initially under the impression that I was to use my Azure account password until I found this. Hope this corrects your problem like it did for me.
Check that you are using the correct "username" as the "mailAccount" setting.
This should be your sendgrid username, NOT the email address of the account you are trying to send from.
I had created an SendGrid account via Azure,
I fixed this by setting these values in my Web.Config file:
<add key="mailAccount" value="azure_************#azure.com" />
<add key="mailPassword" value="[My Azure Password]" />
to my azure username and password. the username I found from the Azure Dashboard, I navigated to SendGrid Accounts >> [Clicked the Resource I had Created] >> Configurations. The password was the same one I set up the Azure account with.
I also faced this issue. solved by adding textcontent and htmlcontent. Before i was sending empty string.now its working
code below
var client = new SendGridClient(_apiKey);
var from = new EmailAddress(_fromEmailAddress, _fromName);
var to = new EmailAddress("devanathan.s#somedomain.com", "dev");
var textcontent = "This is to test the mail functionality";
var htmlcontent = "<div>Devanathan Testing mail</div>";
var subject = "testing by sending mail";
var msg = MailHelper.CreateSingleEmail(from, to, subject, textcontent, htmlcontent);
var response = await client.SendEmailAsync(msg);
I got the same error. All I had to do was to copy the appsettings in webconfig(see below) and paste it into the OTHER webconfig file (there are 2 of them in asp.net project).
<add key="webpages:Version" value="3.0.0.0" />
<add key="mailAccount" value="xxUsernamexx" />
<add key="mailPassword" value="Password" />
I had the same problem and the problem was that I had the same email on TO and BCC field. Hope it helps others..
I had the same problem, happened to have misspelled the name of the config value for the mailAccount (put mainAccount instead of the mailAccount).
NetworkCredential credential = new NetworkCredential(ConfigurationManager.AppSettings["mailAccount"], ConfigurationManager.AppSettings["mailPassword"]);
Web transportWeb = new Web(credential);
The config value was coming back as null but the exception wasnt raised and the empty username was assigned instead. Basically, put the breakpoint on the line "Web transportWeb = new Web(credential);" and see what username/password you are actually passing in credential, and see also the nevada_scout's answer.
The company domain you have registered in SendGrid should be used to call the MailAddress API. Thus, if your company web site you are registering in SendGrid is www.###.com you should use:
var from = MailAddress("info####.com", "Your Contractor Connection")

How can I debug PayPalCoreSDK IPN validation failures?

I'm using PayPal's own .NET library to validate IPNs, specifically IPNMessage.cs in PayPalCoreSDK 1.5.0 available via NuGet (released 2014-09-09).
Here's how I'm using it:
[HttpPost]
public ActionResult Ipn()
{
try
{
byte[] bytes = Request.BinaryRead(Request.ContentLength);
IPNMessage message = new IPNMessage(bytes);
string description = string.Format("PayPal IPN {0} {1} {2}",
message.IpnValue("txn_id"),
message.IpnValue("payment_type"),
message.IpnValue("payment_status"));
Trace.TraceInformation(description + " Received");
if (message.Validate())
{
Trace.TraceInformation(description + " Valid");
// do work here
}
else
{
Trace.TraceError(description + " Invalid");
}
}
catch (Exception e)
{
Trace.TraceError("PayPal IPN Exception: " + e.Message);
}
return null;
}
And my logs are telling me (txnId changed):
Application: 2014-09-11T19:52:40 PID[11584] Information PayPal IPN ABC536DEFP96XYZ3U instant Completed Received
Application: 2014-09-11T19:52:40 PID[11584] Error PayPal IPN ABC536DEFP96XYZ3U instant Completed Invalid
The IPN itself is full of all the keys/values I'm expecting. When I log into PayPal and look at the Instant Payment Notification (IPN) history section, I see the IPN with a matching transaction ID and its status is "Sent". I'm just getting back false from Validate(), so PayPal must not be replying with "VERIFIED". No exception is thrown, how do I debug this?
Turns out my app wasn't even communicating with PayPal.
PayPalCoreSDK requires the following in app.config/web.config:
<configuration>
<configSections>
<section name="paypal" type="PayPal.Manager.SDKConfigHandler, PayPalCoreSDK" />
</configSections>
<paypal>
<settings>
<add name="mode" value="sandbox" /> <!-- "live" or "sandbox" -->
</settings>
</paypal>
</configuration>
Credit to James Dibble
To expand on your own answer - yes they are complete idiots in the API.
The IPN message actually contains a flag whether or not the message is sandbox - so it should be able to figure this out for itself without needing explicit config set. Or at the very least it should give an error.
Alternatively you can provide the config like this (I have a config object which is just a simple struct)
var message = new PayPal.IPNMessage(new Dictionary<string, string>()
{
{ "account1.apiUsername", config.APIUsername },
{ "account1.apiPassword", config.APIPassword },
{ "account1.apiSignature", config.APISignature },
{ "mode", config.IsLiveMode ? "live" : "sandbox" }
}, bytes);
(I'm pretty sure the account user info is not actually required here)
Wondering why don't you get an error?...
I actually went into the source code for IPNMessage to see what was going on. The following call retrieves the URL inside the validate() method. It looks in your configuration to find it and throws an exception if it fails (which is what we want).
Unfortunately it is wrapped inside a try block which really should only be catching runtime errors - but instead it masks the configuration error and just returns a useless false for validate().
string ipnEndpoint = GetIPNEndpoint();

Does the WebAuthenticationBroker work in Windows 8 Metro App post Release Candidate

SOLUTION
My working solution can be found in the answer or in my update two.
1) Now make sure, for testing on localhost, that you have setup windows firewalls for inbound on the localhost port. Port forwarding on the router if you have one.
2) Then you need to tell IIS Express that its okay that the request comes from outsite the localhost:
Find Documents\IISExpress\config and edit applicationhost.config. Find your site in the list and remove the localhost from the binding.
<site name="S-Innovations.TrafficTheory.Web2" id="1591449597">
<application path="/" applicationPool="Clr4IntegratedAppPool">
<virtualDirectory path="/" physicalPath="G:\Documents\Visual Studio 2012\Projects\S-Innovations.TrafficTheory\S-Innovations.TrafficTheory.Web2" />
</application>
<bindings>
<binding protocol="http" bindingInformation="*:909090:localhost" />
</bindings>
</site>
2a) ISS need to run as administrator, running visual studio as administrator also starts iss as admin...
3) Locate your ip, www.myip.com and change the ACS return uri to : http://90.90.90.90:909090/api/federation/
4) change the webbroker to use your ip also:
WebAuthenticationResult webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
new Uri("https://traffictheory.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=http%3a%2f%2flocalhost%3a48451%2f"),
new Uri("http://99.99.99.99:909090/api/federation/end"));
Everything worked for me like this. I got a hello world passed on to my metro app as the token.
Problem
I have set up a WCF Service and a Metro App.
The WCF service is set up to authenticate using Azure ACS.
I made a Console Application that works with the WebService and ACS:
static void Main(string[] args)
{
try
{
// First start the web project, then the client
WebClient client = new WebClient();
var token = RetrieveACSToken();
client.Headers.Add("Authorization", token);
client.Headers.Add("Content-type", "text/xml");
var url = new Uri("http://traffictheory.azurewebsites.net/UserService.svc/Users");
//var url = new Uri("http://localhost:4000/UserService.svc/Users");//
Stream stream = client.OpenRead(url);
StreamReader reader = new StreamReader(stream);
String response = reader.ReadToEnd();
Console.Write(response);
}
catch (Exception ex)
{
Console.Write(ex.Message);
}
Console.ReadLine();
}
private static string RetrieveACSToken()
{
var acsHostName = ConfigurationManager.AppSettings.Get("ACSHostName");
var acsNamespace = ConfigurationManager.AppSettings.Get("ACSNamespace");
var username = ConfigurationManager.AppSettings.Get("ServiceIdentityUserName");
var password = ConfigurationManager.AppSettings.Get("ServiceIdentityCredentialPassword");
var scope = "http://traffictheory.azurewebsites.net/";
//var scope = "http://localhost:4000/";//
// request a token from ACS
WebClient client = new WebClient();
client.BaseAddress = string.Format("https://{0}.{1}", acsNamespace, acsHostName);
NameValueCollection values = new
NameValueCollection();
values.Add("wrap_name", username);
values.Add("wrap_password", password);
values.Add("wrap_scope", scope);
byte[] responseBytes =
client.UploadValues("WRAPv0.9", "POST", values);
string response =
Encoding.UTF8.GetString(responseBytes);
string token = response
.Split('&')
.Single(value =>
value.StartsWith("wrap_access_token=",
StringComparison.OrdinalIgnoreCase))
.Split('=')[1];
var decodedToken = string.Format("WRAP access_token=\"{0}\"", HttpUtility.UrlDecode(token));
return decodedToken;
}
I face two problems now when i want to use it from my Metro App.
First one is unrelated to the service and is about the WebAuthenticationBroker.
1)
When i use
WebAuthenticationResult webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
new Uri("https://s-innovations.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=http%3a%2f%2ftraffictheory.azurewebsites.net%2f"),
new Uri("https://s-innovations.accesscontrol.windows.net")
);
I am able to log in using, LiveID, Facebook ect. Not google because ACS dont include the ID correctly. But I dont get any kind of token back or Claims. I only get:
https://s-innovations.accesscontrol.windows.net/v2/wsfederation?wa=wsignin1.0
https://s-innovations.accesscontrol.windows.net/v2/facebook?cx=cHI9d3NmZWRlcmF0aW9uJn...cmFmZmljdGhlb3J5LmF6dXJld2Vic2l0ZXMubmV0JTJmJmlwPUZhY2Vib29rLTM1NTk5MjQ2NzgxNzc5OQ2&code=AQDagvqoXQ.......
How do I get the claims like in the end of this movie:
http://channel9.msdn.com/Events/BUILD/BUILD2011/SAC-858T
His app works!
2)
The console app shown above get authenticated and get the token to pass to the service when calling the API, how do i get this token from within the metro app.
UPDATE
I created the controller as suggested:
[HttpPost]
public ActionResult End()
{
return Json("Hello World");
}
I have put in a break point to see if it get it. No hit yet.
WebAuthenticationResult webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
new Uri("https://traffictheory.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=http%3a%2f%2flocalhost%3a48451%2f"),
new Uri("http://localhost:909090/Federation/End"));
On my Relying Party Application i ahave
Realm http://localhost:909090/
Return Url: Nothing (have tried http://localhost:909090/Federation/End )
The response data contains : http://localhost:909090/Federation/End right now.
UPDATE 2
I also tried with an api controller as you shown in another post:
public class FederationController : ApiController
{
public HttpResponseMessage Post()
{
var response = this.Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Add("Location", "/api/federation/end?acsToken=" + ExtractBootstrapToken());
return response;
}
public string Get()
{
return "hello world";
}
protected virtual string ExtractBootstrapToken()
{
return "Hello World";
}
}
Now the login screen just hang and ends with a service you looking for is not ready right now (or something like that).
acs return url http://localhost:48451/api/Federation
WebAuthenticationResult webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
new Uri("https://traffictheory.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=http%3a%2f%2flocalhost%3a909090%2f"),
new Uri("http://localhost:909090/api/federation/end"));
The WebAuthenticationBroker simply keeps browsing until the next requested page is the one specified by the callbackUri parameter. At that point it returns the final URL to you so if you want to get anything back it needs to be encoded in that URL.
In the ACS control panel for the relying party you need to specify a return url that is somewhere on your site. For example https://traffictheory.azurewebsites.net/federationcallback. Then create a controller to handle accept a post to that URL. The post will have a form field wresult which is some xml that will contain the token returned from ACS.
You can then send the token back to the WebAuthenticationBroker by redirecting to https://traffictheory.azurewebsites.net/federationcallback/end?token={whatever you want to return}
You would then need to change the usage of the authentication broker to the following:
var webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
new Uri("https://s-innovations.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=http%3a%2f%2ftraffictheory.azurewebsites.net%2f"),
new Uri("https://traffictheory.azurewebsites.net/federationcallback/end")
);
// The data you returned
var token = authenticateResult.ResponseData.Substring(authenticateResult.ResponseData.IndexOf("token=", StringComparison.Ordinal) + 6);
My controller for handling the authentication callback post looks like this.
public class FederationcallbackController : ApiController
{
public HttpResponseMessage Post()
{
var response = this.Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Add("Location", "/api/federationcallback/end?acsToken=" + ExtractBootstrapToken());
return response;
}
protected virtual string ExtractBootstrapToken()
{
return HttpContext.Current.User.BootstrapToken();
}
}
The BootstrapToken() extenion method is part of the wif.swt NuGet package. By default WIF doesn't save anything to the bootstrap token property you need to enable it by including the saveBootstrapTokens="true" attribute on the <service> element under <microsoft.identityModel> in your web.config. Mine looks like this:
<microsoft.identityModel>
<service saveBootstrapTokens="true">
<audienceUris>
<add value="http://localhost:3949/" />
</audienceUris>
<federatedAuthentication>
<wsFederation passiveRedirectEnabled="true" issuer="https://xyz.accesscontrol.windows.net/v2/wsfederation" realm="http://localhost:3949/" reply="http://localhost:3949/" requireHttps="false" />
<cookieHandler requireSsl="false" path="/" />
</federatedAuthentication>
<issuerNameRegistry type="Microsoft.IdentityModel.Swt.SwtIssuerNameRegistry, Wif.Swt">
<trustedIssuers>
<add name="https://readify.accesscontrol.windows.net/" thumbprint="{thumbprint}" />
</trustedIssuers>
</issuerNameRegistry>
<securityTokenHandlers>
<add type="Microsoft.IdentityModel.Swt.SwtSecurityTokenHandler, Wif.Swt" />
</securityTokenHandlers>
<issuerTokenResolver type="Microsoft.IdentityModel.Swt.SwtIssuerTokenResolver, Wif.Swt" />
</service>
</microsoft.identityModel>

Categories