I am using NLog to log errors into a file when they occur and I also log the payload in the body request. But I don't want to log the payload if, for example, an error occurred while authenticating, because I have a username and password there.
What are your solutions for this issue?
I use a generic class to handle my HttpClient requests.
private static async Task<ApiMethodResult<string>> SendGenericRequestAsync(this HttpClient client,
HttpMethod method,
string requestString, object payload = null)
{
HttpRequestMessage requestMessage = new HttpRequestMessage
{
RequestUri = new Uri(ConnectionUrl.ExternalUrl + requestString),
Method = method
};
if (payload != null && method != HttpMethod.Get)
{
HttpContent requestContent = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8,
"application/json");
requestMessage.Content = requestContent;
}
ApiMethodResult<string> result = new ApiMethodResult<string>();
HttpResponseMessage responseMessage;
try
{
responseMessage = await client.SendAsync(requestMessage);
}
catch (Exception)
{
string errorMessage = $"Cannot connect to external data API. Requested url: {requestString}";
result.SetErrorMessage(errorMessage);
StaticLogger.LogError(errorMessage);
return result;
}
string httpContent = await responseMessage.Content.ReadAsStringAsync();
result.ApiData = httpContent;
return result;
}
This is my nlog.config
<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true" internalLogLevel="Trace"
internalLogFile="C:\Users\albug\source\repos\orgill-vendor-portal-v2\InnerLog.txt">
<extensions>
<add assembly="NLog.Web.AspNetCore" />
</extensions>
<targets>
<target name="default" xsi:type="File"
fileName="LogFolderPath"
layout="Logger: ${logger}${newline}
Date: ${shortdate}${newline}
Time: ${time}${newline}
LogType: ${level}${newline}
URL: ${aspnet-request-url:IncludeQueryString=true}${newline}
Payload: ${aspnet-request-posted-body}${newline}
Controller: ${aspnet-mvc-controller}${newline}
Endpoint: ${aspnet-mvc-action}${newline}
Message: ${message}${newline}"/>
</targets>
<rules>
<logger name="*" minlevel="Warn" writeTo="default" />
</rules>
</nlog>
Ok, so after a random search I actually found out that you can use a conditional operator-ish in the NLog layout.
What I did is this:
Payload: ${when:when='${aspnet-mvc-controller}'=='Account':inner=restricted:else=${aspnet-request-posted-body}}${newline}
So basically if the controller inside which the log action was triggered is "Account", it will put "restricted", else it will put the actual request body.
Here is the actual documentation.
${when:when=[condition to be verified]:inner=[what to log if true]:else=[what to print if not]}
Related
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.
In my ASP.NET MVC project I have an action with [LogErrors] attribute as below:
[LogErrors]
public ActionResult Index()
{
var i = 0;
var c = 10 / i;
return View();
}
I made an aunhandled exception without trycatch(devide 10 by 0) in this action and I must log this exception error text and else log in which action this exception happened in a text file with NLog. I made the [LogErrors] as below:
public class LogErrorsAttribute : FilterAttribute, IExceptionFilter
{
void IExceptionFilter.OnException(ExceptionContext filterContext)
{
if (filterContext != null && filterContext.Exception != null)
{
string controller = filterContext.RouteData.Values["controller"].ToString();
string action = filterContext.RouteData.Values["action"].ToString();
string loggerName = string.Format("{0}Controller.{1}", controller, action);
NLog.LogManager.GetLogger(loggerName).Error(string.Empty, filterContext.Exception);
}
}
}
and my NLog.config is as below:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd" >
<targets>
<target name="file" xsi:type="File" fileName="C:\Logs\${shortdate}.log"
layout="
--------------------- ${level} (${longdate}) ----------------------${newline}
IP: ${aspnet-request-ip}${newline}
Call Site: ${callsite}${newline}
${level} message: ${message}${newline}
Id: ${activityid}${newline}
aspnet-sessionid: ${aspnet-sessionid}${newline}
aspnet-request-method: ${aspnet-request-method}${newline}
aspnet-request-host: ${aspnet-request-host}${newline}
aspnet-request-form: ${aspnet-request-form}${newline}
aspnet-request-cookie: ${aspnet-request-cookie}${newline}
aspnet-request: ${aspnet-request:serverVariable=HTTP_URL}${aspnet-request:queryString}${newline}
aspnet-mvc-controller: ${aspnet-mvc-controller}${newline}
aspnet-mvc-action: ${aspnet-mvc-action}${newline}
aspnet-appbasepath: ${aspnet-appbasepath}${newline}
" encoding="UTF8"/>
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="file" />
</rules>
</nlog>
How fix my configs to log this exception error text and else log that in which action this exception happened in a text file? Any help will be appriciated!
You need to send a message to .Error and send the Exception as first parameter.
Otherwise it roughly translated to string.Format("", filterContext.Exception).
So something like this:
NLog.LogManager.GetLogger(loggerName)
.Error(filterContext.Exception, "Unhandled exception in controller");
If that isn't working, then please check the NLog troubleshooting guide
I've written some code (MetadataProcessor) that locates an xml file and sends it to an API (http://localhost:5000/api/Events/UploadAzureEncodedMetadata) to be parsed. However, when the API is called, the IFormFile parameter is always null. I've examined the resulting http request with fiddler to determine what is causing the model-binding problem (if that's what it is), but I see nothing wrong.
Metadata processor:
// Build a path to the directory containing the metadata file
string folderName = string.Format("OUTPUT_ENCODER_{0}_{1}", eventId, id);
string folderPath = Path.Combine(
this._config.MediaBasePath,
clientId.ToString(),
eventId.ToString(),
"results",
folderName);
// Find the metadata file
string fileName = null;
byte[] data = null;
bool exists = false;
string[] files = Directory.GetFiles(folderPath);
foreach (var file in files)
{
if (file.EndsWith("_metadata.xml"))
{
data = File.ReadAllBytes(file);
exists = true;
fileName = file;
break;
}
}
// Generate token for access to Events controller
TokenProvider tokens = new TokenProvider(this._config, this._logger);
var authValue = new AuthenticationHeaderValue("Bearer", tokens.GetTokenObject(clientId));
// Build the HttpClient
HttpClient client = new HttpClient()
{
DefaultRequestHeaders =
{
Authorization = authValue
}
};
// Bundle the xml data into the request body
ByteArrayContent formFile = new ByteArrayContent(data, 0, data.Length);
MultipartFormDataContent multiContent = new MultipartFormDataContent();
multiContent.Add(formFile, "formFile", fileName);
// Post to EventsController
var baseUri = new Uri(this._config.BaseUrl);
var url = new Uri(baseUri, string.Format("{0}api/Events/UploadAzureEncodedMetadata/{1}", baseUri, videoId));
var result = await client.PostAsync(url.ToString(), multiContent);
The MVC controller in the API:
[HttpPost("UploadAzureEncodedMetadata/{id}")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
public async Task<IActionResult> UpdateDuration([FromRoute]Guid id, [FromForm]IFormFile formFile)
{
// Find the video to update
var video = await this.context.Videos.Where(v => v.Id == id).SingleOrDefaultAsync();
if(formFile == null)
{
return this.BadRequest();
}
// Ensure that the service account has authorization
var eventId = video.EventId;
if (!await this.securityProvider.HasEventEditAccessAsync(eventId))
{
return this.Unauthorized();
}
// Attempt to deserialize the xml
int videoDuration = 0;
try
{
var serializer = new XmlSerializer(typeof(AssetFiles));
Stream stream = formFile.OpenReadStream();
var assetFiles = (AssetFiles)serializer.Deserialize(stream);
// Find duration and convert to seconds
string durationString = assetFiles.AssetFile.Duration;
TimeSpan durationTimeSpan = XmlConvert.ToTimeSpan(durationString);
videoDuration = (int)durationTimeSpan.TotalSeconds;
}
catch(Exception ex)
{
return this.BadRequest();
}
// Update the video entry
video.Duration = videoDuration;
await this.context.SaveChangesAsync();
return this.NoContent();
}
Lastly, here's a raw readout of the http request produced by the MetadataProcessor, that resulted in a null value for formFile in the controller:
POST http://localhost:5000/api/Events/UploadAzureEncodedMetadata/dc14829c-3d1b-4379-615b-08d5f8d07e16 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1OWNiODg5YS01YjRjLTRlMzUtOWMyOS0zYWQ3MGU4NTJhYTgiLCJqdGkiOiI5OTU3MWIyMy0xYTQwLTQ0OTUtOTE2Zi03NDY5YjYwYzI1N2MiLCJpYXQiOjE1MzM4MzM4NzAsIkV2ZW50UmVnVXNlciI6ZmFsc2UspOgHjFVudElkIjoiMTdhMjc4NDYtOTc1My00OGEzLTRlYzEtMDhkNGUxMjgwZjVhIiwibmJmIjoxNTMzODMzODcwLCJleHAiOjE1MzM4MzM5MzAsImlzcyI6IjYzYmVmM2NkYTM1OTRmZjBhOTdiYWFiYWJjYTQzODhmIiwiYXVkIjoiSW5mZXJub0NvcmUifQ.rhIjYRUtFjHWrrgd9XnmW4kMXaZ5UFyr2ApNK1EBJRI
Content-Type: multipart/form-data; boundary="1b6c2d6e-53d6-414c-bb1b-681ff9c766f0"
Content-Length: 2951
Host: localhost:5000
--1b6c2d6e-53d6-414c-bb1b-681ff9c766f0
Content-Disposition: form-data; name=formFile; filename="C:\tmp\17a27846-9753-48a3-4ec1-08d4e1280f5a\213e65ee-fff9-4668-b123-7d0746bb4b05\results\OUTPUT_ENCODER_213e65ee-fff9-4668-b123-7d0746bb4b05_00000000-0000-0000-0000-000000000000\a35f0612-7a07-4bec-a9fd-a61d2a2f71bb_metadata.xml"; filename*=utf-8''C%3A%5Ctmp%5C17a27846-9753-48a3-4ec1-08d4e1280f5a%5C213e65ee-fff9-4668-b123-7d0746bb4b05%5Cresults%5COUTPUT_ENCODER_213e65ee-fff9-4668-b123-7d0746bb4b05_00000000-0000-0000-0000-000000000000%5Ca35f0612-7a07-4bec-a9fd-a61d2a2f71bb_metadata.xml
<?xml version="1.0" encoding="utf-8"?>
<AssetFiles xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/windowsazure/mediaservices/2014/07/mediaencoder/inputmetadata">
<AssetFile Name="bcff8e9a-7cb7-4d09-abd2-f04a83df1be1.mp4" Size="383631" Duration="PT5.568S" NumberOfStreams="2" FormatNames="mov,mp4,m4a,3gp,3g2,mj2" FormatVerboseName="QuickTime / MOV" StartTime="PT0S" OverallBitRate="551">
<VideoTracks>
<VideoTrack Id="1" Codec="h264" CodecLongName="H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10" TimeBase="1/90000" NumberOfFrames="166" StartTime="PT0S" Duration="PT5.533S" FourCC="avc1" Profile="Constrained Baseline" Level="3.0" PixelFormat="yuv420p" Width="560" Height="320" DisplayAspectRatioNumerator="0" DisplayAspectRatioDenominator="1" SampleAspectRatioNumerator="0" SampleAspectRatioDenominator="1" FrameRate="30" Bitrate="465" HasBFrames="0">
<Disposition Default="1" Dub="0" Original="0" Comment="0" Lyrics="0" Karaoke="0" Forced="0" HearingImpaired="0" VisualImpaired="0" CleanEffects="0" AttachedPic="0" />
<Metadata key="creation_time" value="2010-03-20T21:29:11.000000Z" />
<Metadata key="language" value="und" />
<Metadata key="encoder" value="JVT/AVC Coding" />
</VideoTrack>
</VideoTracks>
<AudioTracks>
<AudioTrack Id="2" Codec="aac" CodecLongName="AAC (Advanced Audio Coding)" TimeBase="1/48000" NumberOfFrames="261" StartTime="PT0S" Duration="PT5.568S" SampleFormat="fltp" ChannelLayout="mono" Channels="1" SamplingRate="48000" Bitrate="83" BitsPerSample="0">
<Disposition Default="1" Dub="0" Original="0" Comment="0" Lyrics="0" Karaoke="0" Forced="0" HearingImpaired="0" VisualImpaired="0" CleanEffects="0" AttachedPic="0" />
<Metadata key="creation_time" value="2010-03-20T21:29:11.000000Z" />
<Metadata key="language" value="eng" />
</AudioTrack>
</AudioTracks>
<Metadata key="major_brand" value="mp42" />
<Metadata key="minor_version" value="0" />
<Metadata key="compatible_brands" value="mp42isomavc1" />
<Metadata key="creation_time" value="2010-03-20T21:29:11.000000Z" />
<Metadata key="encoder" value="HandBrake 0.9.4 2009112300" />
</AssetFile>
</AssetFiles>
--1b6c2d6e-53d6-414c-bb1b-681ff9c766f0--
I'm aware that there are other ways to upload xml data, but I'd prefer to keep the xml parsing on the api side of things, if possible. Thanks!
Problem was solved when I removed the [FromRoute] attribute from the IFormFile parameter and the [ApiController] attribute (not shown in code) from the MVC controller. So as not to disrupt functionality on the controller, I'll be creating a new controller specifically for the UpdateDuration() action.
I need to enable CORS for my Web API and I can't upgrade to Framework 4.5 at the moment. (I know about System.Web.Http.Cors.EnableCorsAttribute.)
I've tried to add the following to my Web.config to see if it worked, but it didn't:
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*"/>
</customHeaders>
</httpProtocol>
I've also tried to set the Access-Control-Allow-Origin header to "*" manually by use of System.Web.Http.Filters.ActionFilterAttribute (based on this post: Add custom header to all responses in Web API) - but that didn't work out either as the request is rejected before it gets to the action filtering.
So I'm kinda stuck now.. Any help is appreciated.
Edit: Turns out
<add name="Access-Control-Allow-Origin" value="*"/>
was the answer all along, I must've done something wrong previously when I tested it. But this solution means that all actions are CORS enabled (which will do for now).
POST, PUT, DELETE, etc use pre-flighted CORS. The browser sends an OPTIONS request. This is because browser first, checks if serverside can handle CORS or not using OPTIONS request, if succeeds, then sends actual request PUT or POST or Delete. Since you do not have an action method that handles OPTIONS, you are getting a 405. In its most simplest form, you must implement an action method like this in your controller.
More explanation - http://www.w3.org/TR/cors/#resource-preflight-requests
http://www.html5rocks.com/en/tutorials/cors/
public HttpResponseMessage Options()
{
var response = new HttpResponseMessage();
response.StatusCode = HttpStatusCode.OK;
return response;
}
Note: This this action just responds to OPTION request, so along with this you need to add necessary config to web.config, such as Access-Control-Allow-Origin = * and Access-Control-Allow-Methods = POST,PUT,DELETE.
Web API 2 has CORS support, but with Web API 1, you have to follow this path.
try to add also:
<add name="Access-Control-Allow-Headers" value="*" />
I had faced the lot of issue with webAPI 1 Cross domain access finally able to fix it have a look at my blog http://keerthirb.blogspot.in/2017/08/making-cross-enable-for-webapi1.html
Cross code is
public class CorsHandler : DelegatingHandler
{
const string Origin = "Origin";
const string AccessControlRequestMethod = "Access-Control-Request-Method";
const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
bool isCorsRequest = request.Headers.Contains(Origin);
bool isPreflightRequest = request.Method == HttpMethod.Options;
if (isCorsRequest)
{
if (isPreflightRequest)
{
return Task.Factory.StartNew<HttpResponseMessage>(() =>
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
string accessControlRequestMethod = request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
if (accessControlRequestMethod != null)
{
response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);
}
string requestedHeaders = string.Join(", ", request.Headers.GetValues(AccessControlRequestHeaders));
if (!string.IsNullOrEmpty(requestedHeaders))
{
response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
}
return response;
}, cancellationToken);
}
else
{
return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(t =>
{
HttpResponseMessage resp = t.Result;
resp.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
return resp;
});
}
}
else
{
return base.SendAsync(request, cancellationToken);
}
}
}
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>