I'm creating an AngularJS (Typescript) SPA with a WebAPI2 backend, requiring authentication and authorization from the API. The API is hosted on a different server, so I'm using CORS, mainly following the guidance found at http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En as I'm a newcomer in this field.
All works fine, I can register and login, and then make requests to restricted-access controller actions (here the dummy "values" controller from the default VS WebAPI 2 template) by passing the received access token, in a client-side service with this relevant code:
private buildHeaders() {
if (this.settings.token) {
return { "Authorization": "Bearer " + this.settings.token };
}
return undefined;
}
public getValues(): ng.IPromise<string[]> {
var deferred = this.$q.defer();
this.$http({
url: this.config.rootUrl + "api/values",
method: "GET",
headers: this.buildHeaders(),
}).success((data: string[]) => {
deferred.resolve(data);
}).error((data: any, status: any) => {
deferred.reject(status.toString() + " " +
data.Message + ": " +
data.ExceptionMessage);
});
return deferred.promise;
}
Now, I'd like to retrieve the user's roles once logged in so that the AngularJS app can behave accordingly. Thus I added this method in my account API (which at the class level has attributes [Authorize], [RoutePrefix("api/Account")], [EnableCors(origins: "*", headers: "*", methods: "*")] (* are for testing purposes):
[Route("UserRoles")]
public string[] GetUserRoles()
{
return UserManager.GetRoles(User.Identity.GetUserId()).ToArray();
}
I then added this code to my login controller:
private loadUserRoles() {
this.accountService.getUserRoles()
.then((data: string[]) => {
// store roles in an app-settings service
this.settings.roles = data;
}, (reason) => {
this.settings.roles = [];
});
}
public login() {
if ((!this.$scope.name) || (!this.$scope.password)) return;
this.accountService.loginUser(this.$scope.name,
this.$scope.password)
.then((data: ILoginResponseModel) => {
this.settings.token = data.access_token;
// LOAD ROLES HERE
this.loadUserRoles();
}, (reason) => {
this.settings.token = null;
this.settings.roles = [];
});
}
where the account controller's method is:
public getUserRoles() : ng.IPromise<string[]> {
var deferred = this.$q.defer();
this.$http({
url: this.config.rootUrl + "api/account/userroles",
method: "GET",
headers: this.buildHeaders()
}).success((data: string[]) => {
deferred.resolve(data);
}).error((data: any, status: any) => {
deferred.reject(status.toString() + ": " +
data.error + ": " +
data.error_description);
});
return deferred.promise;
}
Anyway this triggers an OPTIONS preflight request, which in turn causes a 500 error. If I inspect the response, I can see that the GetOwinContext method gets a null request. Here is the beginning of the error stack trace:
{"message":"An error has occurred.","exceptionMessage":"Value cannot be null.\r\nParameter name: request","exceptionType":"System.ArgumentNullException","stackTrace":" at System.Net.Http.OwinHttpRequestMessageExtensions.GetOwinContext(HttpRequestMessage request)\r\n at Accounts.Web.Controllers.AccountController.get_UserManager() ...}
Yet, the code I'm using for GETting the roles is no different from that I use for GETting the dummy "values" from the WebAPI test controller. I can't exactly see the reason why a preflight should be required here, but in any case I'm getting this nasty exception in OWIN code.
My request header is (the API being at port 49592):
OPTIONS /api/account/userroles HTTP/1.1
Host: localhost:49592
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: http://localhost:64036
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Access-Control-Request-Headers: accept, authorization
Accept: */*
Referer: http://localhost:64036/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,it;q=-5.4
Could anyone explain?
I think I found some sort of working solution even if it looks somewhat dirty, but at least it works. I'm posting it here so other can eventually take advantage of it, but I'm open to suggestions. (Sorry for the bad formatting, but I tried several times and the editor does not allow me to correctly mark the code).
Essentially, the solution was suggested by the answer to this post: Handling CORS Preflight requests to ASP.NET MVC actions, but I changed the code which did not work for me (WebAPI 2 and .NET 4.5.1). Here is it:
in Global.asax, method Application_Start, add BeginRequest += Application_BeginRequest;.
add the override, which simply responds to OPTIONS requests by allowing everything (this is OK in my testing environment):
protected void Application_BeginRequest(object sender, EventArgs e)
{
if ((Request.Headers.AllKeys.Contains("Origin")) &&
(Request.HttpMethod == "OPTIONS"))
{
Response.StatusCode = 200;
Response.Headers.Add("Access-Control-Allow-Origin", "*");
Response.Headers.Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
string sRequestedHeaders = String.Join(", ",
Request.Headers.GetValues("Access-Control-Request-Headers") ?? new string[0]);
if (!String.IsNullOrEmpty(sRequestedHeaders))
Response.Headers.Add("Access-Control-Allow-Headers", sRequestedHeaders);
Response.End();
}
}
the attribute decorating the accounts controller method is just the RouteAttribute:
[Route("UserRoles")]
public string[] GetUserRoles()
{
string id = User.Identity.GetUserId();
Debug.Assert(id != null);
string[] aRoles = UserManager.GetRoles(id).ToArray();
return aRoles;
}
This way the OPTIONS request gets a proper response and the successive GET succeeds.
ADDITION
I must also add that the EnableCors attribute is not enough as we must not only handle the OPTIONS verb, but also ensure that any CORS request gets the Access-Control-Allow-Origin header. Otherwise, you might observe an apparently correct response (code 200 etc) but see the $http call failing. In my case I add to global.asax this line:
GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsAllowOriginHandler());
My CorsAllowOriginHandler is a DelegatingHandler which simply ensures that this header with value * is present in each response where the request included a Origin header:
public sealed class CorsAllowOriginHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
// all CORS-related headers must contain the Access-Control-Allow-Origin header,
// or the request will fail. The value may echo the Origin request header,
// or just be `*`.
if ((request.Headers.Any(h => h.Key == "Origin")) &&
(response.Headers.All(h => h.Key != "Access-Control-Allow-Origin")))
{
response.Headers.Add("Access-Control-Allow-Origin", "*");
}
return response;
}
}
Related
I create a rest react front end to talk to a Jersey servlet on tomcat on the back end for RH 8.6. When react tried to do on REST GET or POST commands I got the "‘access-control-allow-origin’ is not allowed according to header" error. So I then added the CORS filter which was suppose to fix the origin problem, but the react client is still failing. I have tried different filters but there is no change. I assume the problem is in the react GET fetch but it looks ok with me and gets a header back when mode: 'no-cors' is set. In the debugger the CORSFilter class gets the GET, but it does not reach the resource class endpoint so its getting rejected.
Using postman I have verified the CORSFilter is inserting the values in the response as you can see here.
POST http://localhost:8080/rtc-servlet/mcd/location
Headers from postman tool:
Status Code: 200
access-control-allow-credentials: true
access-control-allow-headers: X-Requested-With, CSRF-Token, X-Requested-By, Authorization, Content-Type
access-control-allow-methods: API, GET, POST, PUT, DELETE, OPTIONS, HEAD
access-control-allow-origin: *
access-control-max-age: 151200
connection: keep-alive
content-length: 701
content-type: application/json
date: Sat, 10 Dec 2022 02:52:19 GMT
keep-alive: timeout=20
servlet code:
#Provider
public class CORSFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
// *(allow from all servers) OR https://crunchify.com/
responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
// As part of the response to a request, which HTTP headers can be used during the actual request.
responseContext.getHeaders().add("Access-Control-Allow-Headers",
"X-Requested-With, CSRF-Token, X-Requested-By, Authorization, Content-Type");
Also tried these options:
"Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
responseContext.getHeaders().add("Access-Control-Allow-Methods",
"API, GET, POST, PUT, DELETE, OPTIONS, HEAD");
// How long the results of a request can be cached in a result cache.
responseContext.getHeaders().add("Access-Control-Max-Age", "151200");
}
}
#GET // read in updated/original files
#Produces(MediaType.APPLICATION_JSON) // what format we send back
public JsonObject getLocationValues() {
System.out.println("Called location getLocationValues ");
return locationRepository.readConfigFile(false);
}
React Rest GET fetch:
const urll1 = "http://localhost:8080/rtc-servlet/mcd/location";
useEffect(() => {
const fetchPost = async () => {
await fetch(urll1, {
// mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
"Accept": "application/json",
"Access-Control-Allow-Origin": "*",
},
})
.then((response) => {
if (response.ok) {
response.json().then(data => {
console.log("response fetchPost :" + JSON.stringify(data));
setPosts1(data);
});
} else {
console.log("response was not ok");
}
})
.catch((err) => {
console.log(err.message);
});
};
fetchPost();
}, []);
The console error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8080/rtc-servlet/mcd/location. (Reason: header ‘access-control-allow-origin’ is not allowed according to header ‘Access-Control-Allow-Headers’ from CORS preflight response).
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8080/rtc-servlet/mcd/location. (Reason: CORS request did not succeed). Status code: (null).
NetworkError when attempting to fetch resource.
So does anyone see that I am doing wrong?
After read the CORS not working posts in stackoverflow again I came across a commit about getting the origin from the header and then setting Access-Control-Allow-Origin to it vs. "*" and react on port localhost:3000 started to get responses back from the localhost:8080 servlet (origin is being set to "localhost:3000"). This was the forum string if you want to read up on it:
How to enable Cross domain requests on JAX-RS web services?.
So the change in the filter class is as follows:
String origin = requestContext.getHeaderString("origin");
if ((origin != null) && (!origin.isEmpty())) {
headers.add("Access-Control-Allow-Origin", origin);
} else {
// *(allow from all servers) OR https://crunchify.com/
headers.add("Access-Control-Allow-Origin", "*");
}
headers.add("Access-Control-Allow-Credentials", "true");
and in the js script "Access-Control-Allow-Origin": "*" was deleted:
await fetch(urll1, {
headers: {
'Content-Type': 'application/json',
"Accept": "application/json"
},
})
I am not sure if I now need the else since "*" didn't work for me, but I left it in. If its not needed or I am just doing something that sort of works because I am using firefox please let me know.
I'm building an app that has a backend in Java Spring boot, and a frontend in React. To pass a token from client to server I'm using a filter class that looks like this:
#Component
#Order(1)
public class ApplicationFilter implements Filter {
#Autowired
private UserDetails userDetails;
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader("Authorization").substring(7);
Jws<Claims> jwsParsed = null;
try {
jwsParsed = Jwts.parserBuilder()
.setSigningKey("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes())
.build()
.parseClaimsJws(token);
} catch (Exception e) {
throw new AuthenticationException("The token is not correct.");
}
Long id = jwsParsed.getBody().get("id", Long.class);
String name = jwsParsed.getBody().get("name", String.class);
userDetails.setUserId(id);
userDetails.setUserName(name);
chain.doFilter(httpRequest, response);
}
In addition to this I'm configuring web config:
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer{
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("PUT", "DELETE","POST","GET")
.allowedOrigins("*")
.exposedHeaders("*");
}
}
to let the requests pass the CORS policy. And also I've configured the context listener to use request context in the app:
#Configuration
public class RequestContextListenerConfig {
#Bean
#Order(0)
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
Blockquote
On the client-side I'm defining an authentication service:
class AuthApiService {
api = null;
constructor() {
this.api = axios.create();
this.api
.interceptors
.request
.use(config => {
config.baseURL = "http://localhost:8080/";
config.headers = {
Authorization: `Bearer ${loginService.getUserToken()}`
};
return config;
});
}
getApi = () => this.api;
}
const authApiService = new AuthApiService();
export const authApi = authApiService.getApi();
export default authApiService;
and use it in my requests like this:
addItem(item) {
authApi.post("image", item)
.then((response) => {
item.id = response.data.id;
this.galleryItems.push(item);
this.dataWasChanged.next("ADD");
}).catch((err) => {
console.error(err.response);
});
}
The problem is when I'm running this add function it gives me an error in the console:
Access to XMLHttpRequest at 'http://localhost:8080/' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
and also in java:
ERROR 46280 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
Cannot invoke "String.substring(int)" because the return value of "javax.servlet.http.HttpServletRequest.getHeader(String)" is null
Never the less I'm sure that the token was sent because before an error I see it printed in the console. Besides, when I'm getting to the app through Postman, everything works with the token.
I printed header from Postman, and they are:
Header 'authorization' = Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbGdhIiwic3ViIjoiZ2FsbGVyeUFwcCIsImlkIjoyLCJuYW1lIjoiTWF4IiwiaWF0IjoxNjM1MTkwMDA5LCJleHAiOjE2Mzc4NzIwMDl9.KV7PNXDIITMQcmzsLOF0WMF34-t4PP47URMoiGJYvvs
Header 'content-type' = application/json
Header 'user-agent' = PostmanRuntime/7.28.4
Header 'accept' = */*
Header 'postman-token' = 21eee286-c554-442f-a169-ab82926165f7
Header 'host' = localhost:8080
Header 'accept-encoding' = gzip, deflate, br
Header 'connection' = keep-alive
Header 'content-length' = 85
but from the browser request I see these headers:
host: localhost:8080
connection: keep-alive
accept: */*
access-control-request-method: POST
access-control-request-headers: authorization,content-type
origin: http://localhost:3000
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
sec-fetch-mode: cors
sec-fetch-site: same-site
sec-fetch-dest: empty
referer: http://localhost:3000/
accept-encoding: gzip, deflate, br
accept-language: ru-RU,ru;q=0.9,en;q=0.8,he;q=0.7
No authorization header at all.
So the question is why the authorization token doesn't come to the java code from the react client? What am I doing wrong, where can be a bug? Or is there any other way to receive the token from the headers that I'm sending to the server?
It seems that preflight request is blocked by Spring Security filter.
You will need additional CORS configuration for Spring Security.
#Configuration
// unnecessary in most cases
// https://docs.spring.io/spring-boot/docs/2.5.6/reference/htmlsingle/#features.developing-web-applications.spring-mvc.auto-configuration
// #EnableWebMvc
public class WebConfig implements WebMvcConfigurer{
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("PUT", "DELETE","POST","GET")
.allowedOrigins("*")
.exposedHeaders("*");
}
}
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// if disabling CSRF protection
// https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#csrf-when
http.csrf().disable();
// if Spring MVC is on classpath and no CorsConfigurationSource is provided,
// Spring Security will use CORS configuration provided to Spring MVC
http.cors(Customizer.withDefaults());
}
}
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
I am writing a Web API method to export a PDF file from querying data from database.
I am able to get this worked by calling directly and get the result as expected. But calling this method from angularjs is not working. I see that the method is called but download is not happenning. I saw in someposts that ajax calls dont work on downloading the content....if so what are the other options to call this method and get the file downloaded ? Can somebody give some hint on how to get this worked ?
This is the code I wrote on WEB API and angularjs.
[HttpGet]
[Route("ExportAppsData")]
public HttpResponseMessage ExportAppsData(string ids, bool skip)
{
Int64[] convertedIds = new Int64[] { };
if (ids != null)
convertedIds = ids.Split(',').Select(n => Convert.ToInt64(n)).ToArray();
List<App> apps = new List<App>();
if (skip)
apps = _appRepository.GetAllApps().SkipWhile(a => convertedIds.Contains(a.Id)).ToList();
else
apps = _appRepository.GetAllApps().Where(a => convertedIds.Contains(a.Id)).ToList();
List<AppDto> mappedResult = _mapppingEngine.Map<List<App>, List<AppDto>>(apps);
return Request.CreateResponse(HttpStatusCode.OK, mappedResult,new PdfMediaTypeFormatter());
}
function exportAppsData(appIds, skipFlag) {
return $http(
{
url: ngSettings.apiServiceUri + 'api/Apps/ExportAppsData',
responseType : ArrayBuffer,
params: { ids: appIds.join(","), skip: skipFlag },
contentType: 'application/json; charset=utf-8',
headers: {
Accept: "text/csv; charset=utf-8",
},
method: "GET",
}
);
}
You need to set your Accept header to application/pdf instead of text/csv; charset=utf-8.
Hi after some struggle I am finally got past the angular js hurdle to pass the proper parameters to my server, but the web api 2 service fails to accept it.
below is the sample code
[RoutePrefix("api/v2/bids")]
public class BidsController : ApiController
{
[Route("{quoteId:long}/accept")]
public HttpResponseMessage AcceptQuote(long quoteId,[FromBody] string remarks)
{
HttpResponseMessage response;
response = Request.CreateResponse(HttpStatusCode.Accepted, quoteId);
return response;
}
}
if you notice i have both the route parameter and also a post parameter of type sting. When I post using fiddler with the following:
POST http://127.0.0.1:81/api/v2/Bids/101/accept? HTTP/1.1
Authorization: Basic a2lyYW5AYWJjc2hpcHBlci5jb206a2lyYW5AYWJjc2hpcHBlci5jb20=
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=utf-8
Referer: http://127.0.0.1:81/shipper/
Accept-Language: en-US
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0; EIE10;ENUSWOL)
Host: 127.0.0.1:81
Content-Length: 40
DNT: 1
Connection: Keep-Alive
Pragma: no-cache
{"remarks":"Accepting this as the best"}
or use angularjs function:
function acceptQuote(quoteId, accept_remarks, fnSuccess, fnError) {
return $resource("/api/v2/Bids/:id/accept", { quoteId: "#id"},
{ "AcceptQuote": { method: "POST", isArray: false } })
.AcceptQuote({ id: quoteId }, { remarks: accept_remarks }, fnSuccess, fnError);
}
returns the following error:
{"Message":"The request is invalid.","ModelState":{"remarks":["Error reading string. Unexpected token: StartObject. Path '', line 1, position 1."]}}
i expected that using [FromBody] was sufficient to pass the simple types as post parameters, any ideas to what else I am missing here.
The [FromBody] is working a bit differently. Please, check this Parameter Binding in ASP.NET Web API. If you'd like to get the string [FromBody] string remarks, then your body must look like:
"Accepting this as the best"
Not a JSON. On the other hand, if the body contains the JSON, the most natural way how to consume that with ASP.NET Web API, is via the Entity/Object. So, we can create this
public class MyObject
{
public string remarks { get; set; }
}
And the Controller action should look like this:
[Route("{quoteId:long}/accept")]
public HttpResponseMessage AcceptQuote(long quoteId, MyObject myObject)
{
var remarks = myObject.remarks;