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

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() { ... }

Related

ASP.NET Core Web API POST No 'Access-Control-Allow-Origin' header is present

I have a GET Web API method in another controller working fine with UseCors allowing any origin enabled in Startup.cs
When I try to call a POST method I get a No 'Access-Control-Allow-Origin' header is present error returned along with a 500 error.
What am I doing wrong?
using System.Collections.Generic;
using CrossSell.Business.Exceptions;
using CrossSell.Business.Interfaces;
using CrossSell.Entities;
using Microsoft.AspNetCore.Mvc;
namespace CrossSell.API.Controllers
{
[Produces("application/json")]
[Route("api/Product")]
public class ProductController : Controller
{
private readonly IProductManager productManager;
public ProductController(IProductManager productManager)
{
this.productManager = productManager;
}
// POST: api/Product
[HttpPost]
public IEnumerable<Opportunity> Post([FromBody]ClientIdentifiable[] clients)
{
try
{
return productManager.GetCrossSellOpportunities(clients);
}
catch (NoInForceOrHistoricalPoliciesException)
{
return new[] { new Opportunity(true, "No In Force or historical policies") };
}
}
}
}
I'm calling the Post method from my React app (which runs on localhost:3000):
getDashboardData() {
var self = this;
axios.post( 'http://localhost:5000/product/api/', this.state.clientIdentifiables).then(function (response) {
console.log(response);
});
}
I forgot to add the dependency injection entry in ConfigureServices method of Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<IClientManager, ClientManager>();
services.AddTransient<IClientRepository, ClientRepository>();
// added the following and it hit the web api method correctly
services.AddTransient<IProductManager, ProductManager>();
services.AddTransient<IProductRepository, ProductRepository>();
}

Not able to access web api calls in a api/controller format

Hi I am developing one application using web api2 and accessing calls via angularjs. I created web api calls and hosted in iis server(public ip). I am accessing the web api2 methods in the below format.
$http.post('http://111.93.133.98:4500/api/NCT_Login/', credentials).success(function (response) { alert(response); });
This is my web api config.cs file.
routeTemplate: "api/{controller}/{id}"
This is my controller code.
public class NCT_LoginController : ApiController
{
public NCTEntities entityObject = new NCTEntities();
[EnableCors(origins: "*", headers: "*", methods: "GET, POST, PUT, DELETE")]
public IHttpActionResult Post(LoginClass obj)
{
try
{
if (ModelState.IsValid)
{
obj.User_Password = PasswordEncryption.sha256_hash(obj.User_Password);
bool result = (from c in entityObject.NCT_UserRegistration where obj.User_Name ==c.User_Name && obj.User_Password == c.User_Password select c).Any();
if(result==true)
{
obj.UserRole = (from c in entityObject.NCT_UserRegistration where obj.User_Name == c.User_Name && obj.User_Password == c.User_Password select c.User_Role).FirstOrDefault();
obj.Success = 0;
obj.User_Password = "";
var response = Request.CreateResponse(HttpStatusCode.OK, obj);
var newSessionId = new SessionIDManager().CreateSessionID(HttpContext.Current);
var cookie = new CookieHeaderValue("session-id", newSessionId);
cookie.Expires = DateTimeOffset.Now.AddDays(1);
cookie.Domain = Request.RequestUri.Host;
cookie.Path = "/";
response.Headers.AddCookies(new[] { cookie });
return ResponseMessage(response);
}
else
{
return Content(HttpStatusCode.BadRequest, 1);
}
}
else
{
return Content(HttpStatusCode.BadRequest, 1);
}
}
catch (DbEntityValidationException e)
{
foreach (var eve in e.EntityValidationErrors)
{
Console.WriteLine("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:",
eve.Entry.Entity.GetType().Name, eve.Entry.State);
foreach (var ve in eve.ValidationErrors)
{
Console.WriteLine("- Property: \"{0}\", Error: \"{1}\"",
ve.PropertyName, ve.ErrorMessage);
}
}
throw;
}
}
If i remove api from route template I am able to access api's and if i put api.NCT_Login then I am getting preflight error. I am not sure what i am missing here. Any help would be appreciated. Thank you.
I would decorate the post method with a attribute route like this
[RoutePrefix("api/NCT_Login")]
public class NCT_LoginController : ApiController
{
public NCTEntities entityObject = new NCTEntities();
[EnableCors(origins: "*", headers: "*", methods: "GET, POST, PUT, DELETE")]
[Route("Login")]
[HttpPost]
public IHttpActionResult Post(LoginClass obj)
{
This would set the route to api/NCT_Login/Login
IMO its a good practice to use attribute routing as it gets very clear what each method route is.
Also when controllers grow its easy to define new routes on the same http verb

How to avoid MVC resetting ExceptionContext.Result?

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;
}
}

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

Web API 405 Error with $http.post

I'm receiving a 405 error with a POST request using $http.post. What's weird is that I'm using $http.post in another area of my application and it works just fine.
I'm using AngularJS for client side, and Web API for server side. I've posted all relevant information (apart from my web.config) that I can think of. Is there something very obvious I'm missing here?
code below does not work (throws 405)
Here's the api controller method that I'm trying to hit:
public async Task<IHttpActionResult> LinkLogin(string provider)
{
Account user = await _repo.FindByNameAsync(User.Identity.Name);
if (user == null)
{
return BadRequest("User does not exist!");
}
return new ChallengeResult(provider, null, "auth/Manage/LinkLoginCallback", user.Id);
}
Here's how I'm trying to hit it on the client side:
var _linkLogin = function (provider) {
$http.post(serviceBase + 'auth/Manage/LinkLogin', provider).then(function (response) {
return response;
});
};
CODE BELOW IS CODE THAT WORKS
Api controller function that works:
// POST auth/Authorization/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(UserModel userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await _repo.RegisterUser(userModel);
IHttpActionResult errorResult = GetErrorResult(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
Calling it from the client side:
var _saveRegistration = function (registration) {
_logOut();
return $http.post(serviceBase + 'auth/Authorization/register', registration).then(function (response) {
return response;
});
};
Here is my web api configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "AuthenticationApi",
routeTemplate: "auth/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapODataServiceRoute("ODataRoute", "api", GenerateEdmModel());
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
private static IEdmModel GenerateEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
return builder.GetEdmModel();
}
}
Now I have tried a number of different solutions posted on the web to no avail, the following are links to things I have tried:
Web api not supporting POST method
Web API Put Request generates an Http 405 Method Not Allowed error
http://blog.dontpaniclabs.com/post/2013/01/23/That-Pesky-Requested-Resource-Does-Not-Support-HTTP-Method-POST-Error-When-Using-MVC-Web-API
I hate answering my own question. If anyone else runs into this issue it's because you're trying to send a simple string value to a web api controller.
I used this solution with success: http://jasonwatmore.com/post/2014/04/18/Post-a-simple-string-value-from-AngularJS-to-NET-Web-API.aspx
If the link is dead, you simple wrap the string value in double quotes in your POST request like so:
$http.post(Config.apiUrl + '/processfile', '"' + fileName + '"');

Resources