the problem I am having is when I send withCredentials: true to my Web API and return Unauthorized, the function is called again which then returns with the browser login form. I just want the function to run once with simple 401 returned.
For example, in my controller:
[Route("search")]
[HttpGet]
public async Task<IHttpActionResult> GetDeposit([FromUri] DepositSearchFilter filter)
{
return Unauthorized();
}
If I set a break-point then return Unauthorized() is executed twice and after the second execution the browser display the login form. However, if I replace Unauthorized with Forbidden(), then the function only runs once and returns.
Additionally, I create my own AuthorizeAttribute to handle the Authentication Token being sent in a cookie. When I add the header [ValidateToken] the same thing happens inside of the custom Attribute. If I return Unauthorized() then OnAuthorization is called again and the browser displays the login form. But If I return Forbidden inside the custom Attribute it runs once.
public class ValidateToken : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
CookieHeaderValue headers = actionContext.Request.Headers.GetCookies().FirstOrDefault();
CookieState authCookie = headers.Cookies.Where(p => p.Name == "AUTH-TOKEN").FirstOrDefault();
actionContext.Request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authCookie.Value);
base.OnAuthorization(actionContext);
//if (!base.IsAuthorized(actionContext))
//{
// HandleUnauthorizedRequest(actionContext);
//}
}
//protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
//{
//actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Unauthorized");
//}
}
So, when I send withCredentials then any code that returns Unauthorized runs twice and then the login form in the browser is displayed. I need the code to run only once and return just a 401.
For the browser login form showing, I am returning a 401 along with
WWW-Authenticate:Bearer
WWW-Authenticate:NTLM
WWW-Authenticate:Negotiate
The Web API does not return Basic or Digest. I have other Web API calls in which the response returns the same thing but the Unauthorized code does not execute twice and the login form is not shown. The difference is withCredentials is not sent.
Make sure Anonymous Authentication is enabled and Windows Authentication is disabled. That should remove NTLM and Negotiate
<IISExpressAnonymousAuthentication>enabled</IISExpressAnonymousAuthentication>
<IISExpressWindowsAuthentication>disabled</IISExpressWindowsAuthentication>
Related
I have a web application that uses asp.net core (3.1) backend and angular front end (8.2.11). It uses asp.net Identity framework for user authentication. It store authentication token in local storage to be used as authentication header in requests. Everything is working in the sense controller endpoints are only accessible when a user is logged in, if logged out, typing an endpoint directly into browser would be rejected.
I am still not certain if such a setup prevent the Cross-Site Request Forgery (XSRF/CSRF) attacks. I know using cookie to store authentication token is susceptible to CSRF and I tried a little bit with the [ValidateAntiForgeryToken] attribute on some endpoint, it broke those endpoints of course. I know in Razor page, a form is automatically injected with anti-forgery token. So, do I need to set it up in my angular front-end? and if yes, how? (I've searched a bit on the web and the instructions are all over the place, quite messy with no clear consensus).
Step 1
Add a middleware to your middleware pipeline that generates an AntiforgeryToken, and embeds the token in a non-http-only cookie that's attached to the response:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAntiforgery(options => {
options.HeaderName = "X-XSRF-TOKEN";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
...;
app.Use((context, next) => {
var tokens = antiforgery.GetAndStoreTokens(httpContext);
httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
});
}
}
I created a little package for this that contains this middleware.
Step 2
Configure your angular app to read the value of the non-http-only cookie (XSRF-TOKEN) through javascript, and pass this value as a X-XSRF-TOKEN header for requests sent by the HttpClient:
#NgModule({
declarations: [...],
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
...
],
providers: [...],
bootstrap: [AppComponent]
})
export class AppModule { }
Step 3
Now you can decorate your controller methods with the [ValidateAntiforgeryToken] attribute:
[ApiController]
[Route("web/v1/[controller]")]
public class PersonController : Controller
{
private IPersonService personService;
public PersonController(IPersonService personService)
{
this.personService = personService;
}
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<ActionResult<Person>> Post([FromBody] Person person)
{
var new_person = await personService.InsertPerson(person);
return Ok(new_person);
}
}
Step 4
Make sure the requests you're sending have the following type of url as stated here:
/my/url
//example.com/my/url
Wrong url:
https://example.com/my/url
Note
I use Identity Cookie authentication:
services.AddAuthentication(/* No default authentication scheme here*/)
Since the ASP.NET Core Authentication middleware only looks after the XSRF-TOKEN header, and not the X-XSRF-TOKEN cookie, you're no longer susceptible to Cross-site request forgery.
Spoiler
You will notice that right after signing in/out, the first webrequest that's being sent will still be blocked by XSRF protection. This is because the Identity does not change during the lifetime of the webrequest. So when sending the Login webrequest, the response will attach a cookie with a csrf-token. But this token is still generated with the identity from when you weren't signed in yet.
The same counts for sending the Logout webrequest, the response will contain a cookie with a csrf-token as if you're still signed in.
To solve this, you have to simply send another webrequest that does literally nothing, every time you've signed in/out. During this request you'll once again have the correct Identity in order to generate the csrf-token.
logoutClicked() {
this.accountService.logout().then(() => {
this.accountService.csrfRefresh().then(() => {
this.activeUser = null;
});
}).catch((error) => {
console.error('Could not logout', error);
});
}
Same for login
this.accountService.login(this.email, this.password).then((loginResult) => {
this.accountService.csrfRefresh().then(() => {
switch (loginResult.status) {
case LoginStatus.success:
this.router.navigateByUrl(this.returnUrl);
this.loginComplete.next(loginResult.user);
break;
default:
this.loginResult = loginResult;
break;
}
});
});
The contents of the csrfRefresh method
public csrfRefresh() {
return this.httpClient.post(`${this.baseUrl}/web/Account/csrf-refresh`, {}).toPromise();
}
Server-side
[HttpPost("csrf-refresh")]
public async Task<ActionResult> RefreshCsrfToken()
{
// Just an empty method that returns a new cookie with a new CSRF token.
// Call this method when the user has signed in/out.
await Task.Delay(5);
return Ok();
}
This is where I login the user in my own app
Angular provides built-in enabled by default anti CSRF/XSRF protection.
Angular's HttpClient has built-in support for the client-side half of this technique. Read about it more in the HttpClient guide
Note that the CSRF/XSRF protection is enabled by default on the HttpClient but only works if the backend sets a cookie named XSRF-TOKEN with a random value when the user authenticates.
I have a question about verifying an Auth Token. Currently, I'm generating my Auth Token at /token and sending it back to the client side where I store it in session storage. To make the process as secure as possible I'm looking to store the token in a HttpOnly and Secure cookie to protect against XSS.
I'm sending the cookie along with a XSRF token where on my client side (Angular) I set the X-XSRF-TOKEN header to the XSRF token value. The browser then automatically send the Auth Cookie back to me where I can read the token.
My main question is how do I extract the token and validate it? Before I was using Authentication Bear auth_token with the [Authorize] attribute but that doesn't work now since the token is in a cookie. I made my own Attribute (see below, but I'm stuck on the line where I actually verify the token. I'm not sure what function to call and pass the token to, to validate it.
public class ValidateToken : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var headers = actionContext.Request.Headers;
CookieHeaderValue authToken = headers.GetCookies().FirstOrDefault();
CookieState authCookie = authToken.Cookies.Where(p => p.Name == "AUTH-TOKEN").FirstOrDefault();
if (authCookie.Value is valid -> need help here) {
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
}
}
}
Edit: I've tried the following
actionContext.Request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authCookie.Value);
bool isAuth = base.IsAuthorized(actionContext);
But the RequestContext is already set once OnAuthorization is called and it doesn't update to use the new Header I added.
I have the following asp.net web api method to delete a file:
[Authorize]
[HttpPost]
public IHttpActionResult Delete(int id)
{
var uploadedFile = unitOfWork.FileRepository.Get(id);
if (uploadedFile == null)
return NotFound();
if (uploadedFile.CreatedBy != User.Identity.GetUserId())
return Unauthorized();
unitOfWork.FileRepository.Remove(uploadedFile);
unitOfWork.Complete();
return Ok();
}
I want to return an unauthorized result if the user attempting to delete the file did not create the file. I have the following ajax to handle the call but upon testing I always get response 200 and so the fail function never get called in my ajax function?
I have debugged the web api method with break points and it clearly fires the Unauthorized method - return Unauthorized();
So why is it returning status 200 when i view in firefox console:
POST http://localhost:65179/api/file/2 200 OK 29ms
When i check the response header in console it shows the following:
X-Responded-JSON {"status":401,"headers":{"location":"http:\/\/localhost:65179\/Account\/Login?ReturnUrl=%2Fapi%2Ffile%2F94"}}
So im at a loss as to why the fail function is not being called? I thinks it's doing a redirect to the login page hence the status is being returned as 200. How do i suppress the redirect then?
function () {
$.ajax({
url: "api/file/2",
method: "POST"
})
.done(function () {
alert("File has been deleted.");
})
.fail(function ( jqXHR) {
alert("Unable to delete file");
});
});
*** UPDATE ****
I've found the following blog post as a potential solution but it will only work if your project is only web api.
https://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/
My project is a combination of MVC 5 and web api 2 so I've amended it and added the code to startup.auth.cs as follows but I've had to comment out the OnValidateIdentity bit inorder to add the OnApplyRedirect to the cookieoptions.
public void ConfigureAuth(IAppBuilder app)
{
var cookieOptions = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
//OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
// validateInterval: TimeSpan.FromMinutes(30),
// regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
OnApplyRedirect = ctx =>
{
if (!IsApiRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
};
app.UseCookieAuthentication(cookieOptions);
}
private static bool IsApiRequest(IOwinRequest request)
{
string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
return request.Uri.LocalPath.StartsWith(apiPath);
}
The correct way would be to use the error callback
$.ajax({
url: "api/file/2",
method: "POST"
})
.success(function () {
alert("File has been deleted.");
})
.error(function (xhr, textStatus, errorThrown) {
alert("Unable to delete file");
});
See the docs here
I came across the issue where I am using cookie authentication in .NET Core 5, yet once the user is authenticated, everything BUT any initial AJAX request in the application works.
Every AJAX request would result in a 401. Even using the jQuery load feature would result in a 401, which was just a GET request to a controller with the [Authorize(Role = "My Role")]
However, I found that I could retrieve the data if I grabbed the URL directly and pasted it in the browser. Then suddenly, all my AJAX worked for the life of the cookie. I noticed the difference in some of the AJAX posts. The ones that didn't work used AspNetCore.AntiForgery in the headers, whereas the ones that did use AspNetCore.Cookies that authenticated.
My fix was to add a redirect in the OnRedirectToLogin event under cookie authentication. It works for all synchronous and asynchronous calls ensuring that AJAX redirects to the login page and authenticates as the current user. I don't know if this is the proper way to handle my issue, but here is the code.
I should mention that all of the AJAX code worked perfectly in my .NET 4 web application. When I changed to 5, I experienced new issues.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => {
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.AccessDeniedPath = "/Error/AccessDenied";
o.SlidingExpiration = true;
//add this to force and request to redirect (my purpose AJAX not going to login page on request and authenticating)
o.Events.OnRedirectToLogin = (context) => {
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});
Shouldn't the status code returned in this case be Forbidden (403) rather than Unauthorized (401) which is more meaningful - return new HttpResponseMessage(HttpStatusCode.Forbidden)? 401 vs 403
Status codes 401 (Unauthorized) and 403 (Forbidden) have distinct
meanings.
A 401 response indicates that access to the resource is restricted,
and the request did not provide any HTTP authentication. It is
possible that a new request for the same resource will succeed if
authentication is provided. The response must include an HTTP
WWW-Authenticate header to prompt the user-agent to provide
credentials. If valid credentials are not provided via HTTP
Authorization, then 401 should not be used.[2]
A 403 response generally indicates one of two conditions:
Authentication was provided, but the authenticated user is not
permitted to perform the requested operation.
The operation is
forbidden to all users. For example, requests for a directory listing
return code 403 when directory listing has been disabled.
You can extract the information from the XMLHttpRequest object supplied as first parameter of .fail(function(jqXHR) {}). The object has status property which returns the numerical status code. The status codes returned are the standard HTTP status codes.
$.ajax({
url: "api/file/2",
method: "POST"
})
.fail(function (jXHR) {
if (jXHR.status == 401) {
alert('Unauthorised')
}
});
I'm implementing Angular Project the Service Side I'm using .NET Web API. The Web API is a Stateless, so I'm using Cookie to maintain the Authentication. AuthLogin Controller Checks the Credential is correct, If it is TRUE then it create the Cookie and pass the cookie via HTTP Response. Additionally I added ActionFilterAttribute to validate the User. If any request the Web API received then it triggers the ActionFilterAttribute before entering into the Controller. Their I'm checking, the Request is containing any Cookie and it checks it with the database. Once the Cookie is validated I need to extend the cookie expiry to 30 mins.
My Web API Controller Methods
[HttpPost]
public HttpResponseMessage AuthLogin()
{
serverCookie = new CookieHeaderValue
("Test", "Super");
serverCookie.Expires = DateTimeOffset.Now.AddMinutes(15);
serverCookie.Domain = Request.RequestUri.Host;
serverCookie.Path = "/";
HttpResponseMessage response = Request.CreateResponse(_credential.Item2 ? HttpStatusCode.OK : HttpStatusCode.Unauthorized);
response.Headers.AddCookies(new CookieHeaderValue[] { serverCookie });
return response;
}
[HttpPost]
[Authenticate]
public string SecurePost()
{
return "Success";
}
The ActionFilterAttribute C# Code:
public class AuthenticateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
Debug.WriteLine("Cookie Life Time Extended Successfully to 30 Mins.");
}
}
My Requirement is to extended the life span of Cookie to 30 Mins and send the information along with the return value. Kindly assist me in this regards.
I was able to extent cookie expiry time in MVC application
HttpCookie cookie = Request.Cookies["MyCookie"];
if (cookie!+null && !cookie.Value.IsEmpty())
{
// Update the cookie expiration
cookie.Expires = DateTime.Now.AddMinutes(Convert.ToInt32(1));
Response.Cookies.Set(cookie);//Request.Cookies.Set(cookie);
}
else
{
}
The HTTP protocol never sends cookie expiration time to the server. So you can not extend cookie expiration time.
Browser will send only cookie name and value to the server. All the other properties like expires, domain, path, httponly can not be accessed once the cookie has been set.
When you assign a Cookie in the response a SetCookie header gets added to the output and that will contain not only the value but also the path and the expires value.
But when the client browser sends the cookie back to the server, it simply includes the cookie name and value only and It does not send path or expires info.
I'm following along Chapter 8 in Pro ASP .NET Web API Security by Badri L., trying to implement basic authentication for a web application that will be consumed by HTTP/JS clients.
I've added the following Authentication Handler to my WebAPI project:
public class AuthenticationHandler : DelegatingHandler
{
private const string SCHEME = "Basic";
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken
cancellationToken)
{
try
{
// Request Processing
var headers = request.Headers;
if (headers.Authorization != null && SCHEME.Equals(headers.Authorization.Scheme))
{
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
// etc
When I decorate methods in my API with [Authorize] and set a breakpoint at the if statement above, headers.Authorization is null upon the first request. If I continue on this break, the if statement gets hit again, this time with headers.Authorization.Scheme as "Negotiate", instead of "Basic":
I have registered my Handler in WebApiConfig:
config.MessageHandlers.Add(new AuthenticationHandler());
But I'm at a loss as to why the Authorize attribute is not respecting basic authentication, or why - since the scheme is not "basic" and the if() in my handler returns false - I'm getting data from my API controller when I should be getting 401 Unauthorized.
I have not specified any authenticationType in my web.config.
Any idea what I'm doing wrong?
Edit: Full Handler:
public class AuthenticationHandler : DelegatingHandler
{
private const string SCHEME = "Basic";
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken
cancellationToken)
{
try
{
// Request Processing
var headers = request.Headers;
if (headers.Authorization != null && SCHEME.Equals(headers.Authorization.Scheme))
{
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string credentials = encoding.GetString(Convert.FromBase64String(headers.Authorization.Parameter));
string[] parts = credentials.Split(':');
string userId = parts[0].Trim();
string password = parts[1].Trim();
// TODO: Authentication of userId and Pasword against credentials store here
if (true)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userId),
new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)
};
var principal = new ClaimsPrincipal(new[] {new ClaimsIdentity(claims, SCHEME)});
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
HttpContext.Current.User = principal;
}
}
var response = await base.SendAsync(request, cancellationToken);
// Response processing
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(SCHEME));
}
return response;
}
catch (Exception)
{
// Error processing
var response = request.CreateResponse(HttpStatusCode.Unauthorized);
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(SCHEME));
return response;
}
}
}
When I decorate methods in my API with [Authorize] and set a breakpoint at the if statement above, headers.Authorization is null upon the first request.
This is expected. This is how it is supposed to work. Browser will show the popup to get credentials from the user only when it receives a 401. Subsequent request will have the authorization header with credentials in the basic scheme.
If I continue on this break, the if statement gets hit again, this time with headers.Authorization.Scheme as "Negotiate", instead of "Basic":
Yes, as answered by Dominick (is it Dominick?), you have Windows Authentication enabled and that is the reason for you getting the Negotiate scheme back from the browser. You must disable all authentication methods either in the config or using IIS manager.
But I'm at a loss as to why the Authorize attribute is not respecting basic authentication, or why - since the scheme is not "basic" and the if() in my handler returns false - I'm getting data from my API controller when I should be getting 401 Unauthorized.
Authorize attribute knows nothing about basic authentication. All it cares is if the identity is authenticated or not. Since you have anonymous authentication enabled (I guess that is the case), Authorize attribute is happy and there is no 401 for the message handler response handling part to add the WWW-Authenticate response header indicating that web API expects credentials in the Basic scheme.
Looks like you have Windows authentication enabled for your app in IIS. Disable all authentication methods in config (system.web and system.webServer) and allow anonymous since you do your own authentication in the message handler.
i think you need to register the handler on global.asa
This article looks good: http://byterot.blogspot.com.br/2012/05/aspnet-web-api-series-messagehandler.html
your global.asa.cs would have something like this:
public static void Application_Start(GlobalFilterCollection filters) {
//...
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler());
}