How to avoid MVC resetting ExceptionContext.Result? - angularjs

I am using the following code to handle exceptions in my MVC controllers:
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new { Error = true, Msj = filterContext.Exception.Message }
};
}
}
In the front-end I am using angularjs ($http) to execute the requests.
If I set filterContext.ExceptionHandled to true, $http will execute the successCallback function but if I don't set it, .Net will reset the filterContext.Result to the tipycal yellow page.
I want to manage the error with the errorCallback function($http) but avoid .Net resetting the Data property of the JsonResult
Does anyone have any idea how to achieve it?
Any help will be appreciated =D

Angular $http service does not send the X-Requested-With headers by default. You need to explicitly enable it.
var app = angular.module('yourApp', []);
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
}]);
EDIT : As per the comment
The problem is that .Net replaces the 'Data' property
Because your are missing 2 things in your OnException method.
You need to set the ExceptionHandled property to true so that the framework will not do the normal exception handling procedure and return the default yellow screen of death page.
You need to specify the Response status code as 5xx ( Error response). The client library will determine whether to execute the success callback or error callback based on the status code on the response coming back.
The below code should totally work.
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new { Error = true, Msj = filterContext.Exception.Message }
};
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.ExceptionHandled = true;
}
}

Related

Adding Angular XSRF to Slim app - Is this sound?

I created an app using Slim 2 a while ago and I'm trying to add Angular. It's been going well so far, but I can no longer use the CSRF protection that I was using since Angular is handling all my post requests. Below is the Before Middleware I had working.
<?php
namespace Cache\Middleware;
use Exception;
use Slim\Middleware;
class CsrfMiddleware extends Middleware {
protected $key;
public function call() {
$this->key = $this->app->config->get('csrf.key');
$this->app->hook('slim.before', [$this, 'check']);
$this->next->call();
}
public function check() {
if (!isset($_SESSION[$this->key])) {
$_SESSION[$this->key] = $this->app->hash->hash($this->app->randomlib->generateString(128));
}
$token = $_SESSION[$this->key];
if (in_array($this->app->request()->getMethod(), ['POST', 'PUT', 'DELETE'])) {
$submittedToken = $this->app->request()->post($this->key) ?: '';
if (!$this->app->hash->hashCheck($token, $submittedToken)) {
throw new Exception('CSRF token mismatch');
}
}
$this->app->view()->appendData([
'csrf_key' => $this->key,
'csrf_token' => $token
]);
}
}
I know that angular automatically looks for a token named XSRF-TOKEN and adds it to the header as X-XSRF-TOKEN. How can I modify the middleware below to write, read, and compare the correct values.
EDIT:
After looking at this again and checking the slim documentation, I changed the line:
$submittedToken = $this->app->request()->post($this->key) ?: '';
to this:
$submittedToken = $this->app->request->headers->get('X-XSRF-TOKEN') ?: '';
If I'm right, this assigns the $submittedToken the value passed as X-XSRF-TOKEN in the header. It's throwing the exception with the message from the middleware "CSRF token mismatch". This feels like progress. Below is the relevant Angular:
app.controller('itemsCtrl', ['$scope', '$http', function($scope, $http) {
// Initailize object when the page first loads
$scope.getAll = function() {
$http.post('/domain.com/admin/getNames').success(function(data) {
$scope.names = data;
});
}
EDIT
Below is where the php code stands now. I think this is working. I've received the expected CSRF error when I remove the cookie or alter the value of the $token before submitting a form. I'm a little concerned about what will happen when I have multiple users on. I haven't tested it yet. Based on this revision, does the protection appear sound?
<?php
namespace Cache\Middleware;
use Exception;
use Slim\Middleware;
class CsrfMiddleware extends Middleware {
protected $key;
public function call() {
$this->key = $this->app->config->get('csrf.key');
$this->app->hook('slim.before', [$this, 'check']);
$this->next->call();
}
public function check() {
// if (!isset($_SESSION[$this->key])) {
if (!isset($_SESSION[$this->key])) {
// $_SESSION[$this->key] = $this->app->hash->hash($this->app->randomlib->generateString(128));
$this->app->setcookie($this->key, $this->app->hash->hash($this->app->randomlib->generateString(128)));
}
// $token = $_SESSION[$this->key];
if(isset($_COOKIE[$this->key])) {
$token = $_COOKIE[$this->key];
}
if (in_array($this->app->request()->getMethod(), ['POST', 'PUT', 'DELETE'])) {
// $submittedToken = $this->app->request()->post($this->key) ?: '';
$submittedToken = $this->app->request->headers->get('X-XSRF-TOKEN') ?: '';
if (!$this->app->hash->hashCheck($token, $submittedToken)) {
throw new Exception('CSRF token mismatch');
}
}
}
}
From the Angular docs for $http Cross Site Request Forgery (XSRF) Protection:
The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, or the per-request config object.
So to change them to use different cookie name / header name, change those values.

Get a custom header in angular $http from web api controller

I am trying to pass a piece of information along with my content in my HttpResponseMessage like:
string jsonFiles = JsonConvert.SerializeObject(listFiles);
HttpResponseMessage response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(jsonFiles)
};
response.Headers.Add("Key", "Value");
return response;
However in my angular call and response I cannot see the "Key" header in response.config.headers or response.headers. Any idea why?
$http.get("/api/Locker").then(function (response) {
console.log(response);
console.log(response.headers);
console.log(response.config.headers);
});
In my Startup.cs I do have:
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(config);
As per the documentation response.config returns the config used while sending the request. response.headers is a function. Try using response.headers("Key") and see if it helps.
You have to explicitly add the custom header to your CORS policy, or AngularJS will be unable to read it. Change this:
app.UseCors(CorsOptions.AllowAll);
To this:
var policy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true,
AllowAnyOrigin = true,
SupportsCredentials = true
};
policy.ExposedHeaders.Add("MyHeader");
app.UseCors(new CorsOptions
{
PolicyProvider= new CorsPolicyProvider
{
PolicyResolver = c => Task.FromResult(policy)
}
});
I am using WebApi version 5.2.3. To export a response header, you can try creating a custom attribute, for example
public class CustomHeadersAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
actionExecutedContext.Response.Headers.Add("Access-Control-Expose-Headers", "<Your Custom Key>");
base.OnActionExecuted(actionExecutedContext);
}
}
Then on your controller or wherever you need it, just add
[CustomHeaders]
public HttpResponseMessage GetMethod() { ... }

Redirect to Identity Server Login page from AngularJs http web api request

I am trying to redirect to Identity Server's default login page when calling an API controller method from Angular's $http service.
My web project and Identity Server are in different projects and have different Startup.cs files.
The web project Statup.cs is as follows
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
var openIdConfig = new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.GivenName,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
n.Request.Headers.SetValues("Authorization ", new string[] { "Bearer ", n.ProtocolMessage.AccessToken });
}
}
};
app.UseOpenIdConnectAuthentication(openIdConfig);
app.UseResourceAuthorization(new AuthorizationManager());
app.Map("/api", inner =>
{
var bearerTokenOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi" }
};
inner.UseIdentityServerBearerTokenAuthentication(bearerTokenOptions);
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
inner.UseWebApi(config);
});
}
}
You will notice that the API is secured with bearer token authentication, whereas the rest of the app uses OpenIdConnect.
The Identity Server Startup.cs class is
public class Startup
{
public void Configuration(IAppBuilder app)
{
var policy = new System.Web.Cors.CorsPolicy
{
AllowAnyOrigin = true,
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true
};
policy.ExposedHeaders.Add("Location");
app.UseCors(new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(policy)
}
});
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
Notice that I have added a CorsPolicy entry in order to allow the Web App to hopefully redirect to the Login page. In addition, the Cors policy exposes the Location request header, since it contains the url that I would like to redirect to.
The Web Api controller method is secured using the Authorize Attribute, like so
[HttpPost]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[Authorize]
public PlayerData GetFilteredPlayers(PlayerInformationParameters parameters)
{
var playerInformation = composer.Compose<PlayerInformation>().UsingParameters(parameters);
var players = playerInformation.Players
.Select(p => new {
p.NameLast,
p.NameFirst,
p.Nickname,
p.BirthCity,
p.BirthState,
p.BirthCountry,
p.BirthDay,
p.BirthMonth,
p.BirthYear,
p.Weight,
p.Height,
p.College,
p.Bats,
p.Throws,
p.Debut,
p.FinalGame
});
var playerData = new PlayerData { Players = players, Count = playerInformation.Count, Headers = GetHeaders(players) };
return playerData;
}
The angular factory makes a call to $http, as shown below
baseballApp.factory('playerService', function ($http, $q) {
return {
getPlayerList: function (queryParameters) {
var deferred = $q.defer();
$http.post('api/pitchingstats/GetFilteredPlayers', {
skip: queryParameters.skip,
take: queryParameters.take,
orderby: queryParameters.orderby,
sortdirection: queryParameters.sortdirection,
filter: queryParameters.filter
}).success(function (data, status) {
deferred.resolve(data);
}).error(function (data, status) {
deferred.reject(status);
});
return deferred.promise;
}
}});
When this call occurs, the response status is 200, and in the data, the html for the login page is returned.
Moreover, I can see on Chrome's Network tab that the response has a Location header with the url of the Login page. However, if I set up an http interceptor, I only see the Accept header has been passed to the javascript.
Here are the http headers displayed in Chrome's network tab:
The response does not have the Access-Control-Allow-Origin header for some reason.
So I have the following questions:
Is there a way I could get access to the Location header of the response in the angular client code to redirect to it?
How might I be able to get the server to send me a 401 instead of 200 in order to know that there was an authentication error?
Is there a better way to do this, and if so, how?
Thanks for your help!
EDIT:
I have added a custom AuthorizeAttribute to determine what http status code is returned from the filter.
The custom filter code
public class BearerTokenAutorizeAttribute : AuthorizeAttribute
{
private const string AjaxHeaderKey = "X-Requested-With";
private const string AjaxHeaderValue = "XMLHttpRequest";
protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var headers = actionContext.Request.Headers;
if(IsAjaxRequest(headers))
{
if (actionContext.RequestContext.Principal.Identity.IsAuthenticated)
actionContext.Response.StatusCode = System.Net.HttpStatusCode.Forbidden;
else
actionContext.Response.StatusCode = System.Net.HttpStatusCode.Unauthorized;
}
base.HandleUnauthorizedRequest(actionContext);
var finalStatus = actionContext.Response.StatusCode;
}
private bool IsAjaxRequest(HttpRequestHeaders requestHeaders)
{
return requestHeaders.Contains(AjaxHeaderKey) && requestHeaders.GetValues(AjaxHeaderKey).FirstOrDefault() == AjaxHeaderValue;
}
I have observed two things from this: first, the X-Requested-With header is not included in the request generated by the $http service on the client side. Moreover, the final http status returned by the base method is 401 - Unauthorized. This implies that the status code is changed somewhere up the chain.
Please don't feel like you have to respond to all the questions. Any help would be greatly appreciated!
You have probably configured the server correctly since you are getting
the login page html as a response to the angular $http call -> it is
supposed to work this way:
angularjs $http
Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning that the outcome (success or error) will be determined by the final response status code.
You are getting a 200 OK response since that is the final response as the redirect is instantly followed and it's result resolved as the $http service outcome, also the response headers are of the final response
One way to achieve the desired result - browser redirect to login page:
Instead of redirecting the request server side (from the web project to the Identity Server) the web api controller api/pitchingstats/GetFilteredPlayer could return an error response (401) with a json payload that contains a {redirectUrl: 'login page'} field or a header that could be read as response.headers('x-redirect-url')
then navigate to the specified address using window.location.href = url
Similar logic can often be observed configured in an $httpInterceptors that handles unauthorized access responses and redirects them to the login page - the redirect is managed on the client side

Process promise returned data using decorator or interceptor in AngularJS

I have a service that does an http request to save some data. When the data comes from the backend I am doing some manipulation on the data and then return it so that controllers can use them. Something like:
public savePerson = (person: Model.IPerson): ng.IPromise<Model.IMiniPerson> => {
return this.api.persons.save({}, person).then((savedPerson) => {
this.enrichWithLookups(savedPerson);
var miniPerson = new Model.MiniPerson();
angular.extend(miniPerson, savedPerson);
miniPerson.afterLoad();
this.persons.unshift(miniPerson);
this.notifyOfChanges();
return miniPerson;
});
}
In order to clean up the code a bit and make it more testable I wanted to remove the private manipulation functions into decorating/intercepting services. Problem is I do not know how to hook on the promise data before the success function is executed and after it is returned.
For example enrichWithLookups must be applied first just after the data arrives and not after the miniPerson is returned.
you can create a local promise and call the "resolve" method when you have completed your operations on the http response. Look at the code down here:
public savePerson = (person: Model.IPerson): ng.IPromise<Model.IMiniPerson> => {
var waiter = $q.defer();
this.api.persons.save({}, person).then((savedPerson) => {
this.enrichWithLookups(savedPerson);
var miniPerson = new Model.MiniPerson();
angular.extend(miniPerson, savedPerson);
miniPerson.afterLoad();
this.persons.unshift(miniPerson);
this.notifyOfChanges();
waiter.resolve(miniPerson);
});
return waiter.promise;
}
I've wrote the code directly with angularjs, but I think that you can easily adapt it to fit your needs.
Bye.

WPF and MVC4 Web API Internal Server Error 500 on POST

So I'm attempting to attach to a web api method via a WPF service, but get only a 500 error on anything other than a GET.
WPF call:
using (var client = new HttpClient())
{
var user = new MyUser
{
EntityID = Guid.NewGuid(),
FirstName = "WPF",
LastName = "test"
};
var formatter = new JsonMediaTypeFormatter();
HttpContent content = new ObjectContent<MyUser>(user, formatter);
client.BaseAddress = new Uri("http://localhost:19527/api/");
var response = await client.PostAsJsonAsync("MyUser", content);
//.ContinueWith((postTask) => result = (postTask.Result.Content == null) ? "Could not create user" : "User created successully!");
var r = response.StatusCode;
}'
...and the receiving controller:
public HttpResponseMessage Get(string badgeId)
{
return Request.CreateResponse<bool>(HttpStatusCode.OK, (service.UserByBadge(badgeId) != null));
}
public HttpResponseMessage Put(MyUser user)
{
return Request.CreateResponse<bool>(HttpStatusCode.OK, service.UpsertUser(user));
}
public HttpResponseMessage Post(MyUser user)
{
if (service.UpsertUser(user)) return Request.CreateResponse<MyUser>(HttpStatusCode.OK, service.Get<MyUser>(u => u.BadgeID == user.BadgeID));
return Request.CreateResponse<MyUser>(HttpStatusCode.NoContent, null);
}'
The service on the WebApi controller is a GenericRepository, which is working fine, since the Get method returns as expected. It's only when I use Post that I get the error. Debugging the methods throws the break point in the Get, but not in the Post, so I don't think it's ever being called.
Here's the route config:
routes.MapRoute(
name: "Default",
url: "api/{controller}/{action}/{id}",
defaults: new { controller = "{controller}", action = "{action}", id = UrlParameter.Optional }
);
I've tried different examples from other SO posts, but none appear to address this issue specifically. I'm guessing there's something wrong with how I've constructed the Post() method?
================================================================
RESOLUTION: Model being passed was failing property validations. Why this was causing a 500, not certain. But once I solved for this, API method began working.
If anybody has a "why" explanation, would love to know for future reference.

Resources