ASP.NET WebApi 2.0 HttpResponseMessage cookies being cleared - c#

I've got a project moving from MVC to WebApi 2. I have to occasionally set a cookie on the response from the webapi, so I wrote a class that extends OkNegotiatedContestResult,
public class OkWithCookiesResult<T> : OkNegotiatedContentResult<T>
{
IEnumerable<HttpCookie> cookies;
public OkWithCookiesResult(T content, IEnumerable<HttpCookie> cookies, ApiController controller)
: base(content, controller)
{
this.cookies = cookies;
foreach(var cookie in cookies)
{
cookie.Domain = Request.RequestUri.Host == "localhost" ? null : Request.RequestUri.Host;
cookie.HttpOnly = true;
cookie.Path = "/";
}
}
public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.ExecuteAsync(cancellationToken);
response.Headers.AddCookies(cookies.Select(c => c.ToCookieHeaderValue()));
return response;
}
}
This is the ToCookieHeaderValue method,
public static CookieHeaderValue ToCookieHeaderValue(this HttpCookie cookie) {
CookieHeaderValue val = new CookieHeaderValue(cookie.Name, cookie.Value);
val.Expires = cookie.Expires;
val.Domain = cookie.Domain;
val.HttpOnly = cookie.HttpOnly;
val.Path = cookie.Path;
val.Secure = cookie.Secure;
return val;
}
Some debug code is left over in there, but you get the gist. My problem is the cookies I add to the header are cleared when the request is finished. I figured this out because I subscribed to the Application_EndRequest event, and the final response there has only one cookie, .ASPXAUTH with a null value. Is this left over from some FormsAuthentication thing from MVC?

Related

Angular withCredentials is not sending cookies

I am using Angular 8, with old backend (ASP.NET MVC 5 Framework) NOT CORE
I am trying to send the cookies of the website so the request from the angular website considered authenticated
I created an interceptor for this
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest }
from '#angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from '#angular/core';
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor() { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const newRequest = request.clone({
withCredentials: true,
});
return next.handle(newRequest);
}
}
here is the code of the request
private getDefaultConfiguration(): configurationsType {
return {
observe: 'response',
responseType: 'json',
headers: new HttpHeaders().set('Content-Type', 'application/json')
};
}
public async get(endpoint: string, params: HttpParams = undefined) {
const options = this.getDefaultConfiguration();
if (params !== undefined)
options.params = params;
const response: HttpResponse<object> = await this.http.get(endpoint, options).toPromise();
return await this.errorCheck(response);
}
I can confirm that the interceptor is executing by a console.log statement
the problem is that I am not receiving the cookies in the request (by the way Http Get not Post) and my request is considered to be unauthenticated.
I am using the following Filter for CORS problem
public class AllowCrossSiteAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpResponseBase response = filterContext.RequestContext.HttpContext.Response;
response.AddHeader("Access-Control-Allow-Origin", "http://localhost:4200");
response.AddHeader("Access-Control-Allow-Headers", "*");
response.AddHeader("Access-Control-Allow-Methods", "*");
response.AddHeader("Access-Control-Allow-Credentials", "true");
base.OnActionExecuting(filterContext);
}
}
I register the filter globally,
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AllowCrossSiteAttribute());
}
}
here is the cookie that I expect to be sent in the header, this snippet is from the login method
var ticket = new FormsAuthenticationTicket(SessionHelper.DefaultSession().KullaniciAdi, model.RememberMe, timeout);
string encrypted = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encrypted)
{
Expires = DateTime.Now.AddMinutes(timeout),
HttpOnly = true // cookie not available in javascript.
};
Response.Cookies.Add(cookie);
return RedirectToAction("Index", "Home");
and here is the cookie in the chrome
if you need any more information please ask me in the comment and I will provide that.
Update
I check it out this article and I applied the same-site attribute of the set-cookie to none, but this still does not solve the problem.
I updated the [AllowCrossSiteAttribute] to be like this, because of completely another problem I was receiving in angular
public class AllowCrossSiteAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpResponseBase response = filterContext.RequestContext.HttpContext.Response;
response.AddHeader("Access-Control-Allow-Origin", "http://localhost:4200");
response.AddHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers");
response.AddHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
response.AddHeader("Access-Control-Allow-Credentials", "true");
base.OnActionExecuting(filterContext);
}
}
In the OnAuthorization method which exists in the BaseController and which is called on each request, I tested the header of the request.
if I requested something from the old MVC application I receive the cookies correctly
but when I make a request from the angular project I receive no Cookies at all
here is how the cookies appear in chrome inspector
for the angular project
and for the old MVC project

Handling session timeout with Ajax in .NET Core MVC

I have a regular application using cookie based authentication. This is how it's configured:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.AccessDeniedPath = new PathString("/Home/Denied");
});
}
This works for my regular actions:
[Authorize]
public IActionResult Users()
{
return View();
}
But doesn't work well for my ajax requests:
[Authorize, HttpPost("Api/UpdateUserInfo"), ValidateAntiForgeryToken, Produces("application/json")]
public IActionResult UpdateUserInfo([FromBody] Request<User> request)
{
Response<User> response = request.DoWhatYouNeed();
return Json(response);
}
The problem is that when the session expires, the MVC engine will redirect the action to the login page, and my ajax call will receive that.
I'd like it to return the status code of 401 so I can redirect the user back to the login page when it's an ajax request.
I tried writing a policy, but I can't figure how to unset or make it ignore the default redirect to login page from the authentication service.
public class AuthorizeAjax : AuthorizationHandler<AuthorizeAjax>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeAjax requirement)
{
if (context.User.Identity.IsAuthenticated)
{
context.Succeed(requirement);
}
else
{
context.Fail();
if (context.Resource is AuthorizationFilterContext redirectContext)
{
// - This is null already, and the redirect to login will still happen after this.
redirectContext.Result = null;
}
}
return Task.CompletedTask;
}
}
How can I do this?
Edit: After a lot of googling, I found this new way of handling it in version 2.0:
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.Events.OnRedirectToLogin = (context) =>
{
// - Or a better way to detect it's an ajax request
if (context.Request.Headers["Content-Type"] == "application/json")
{
context.HttpContext.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
});
And it works for now!
What you need can be achieved by extending AuthorizeAttribute class.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.Result = new JsonResult
{
Data = new { Success = false, Data = "Unauthorized" },
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = "application/json",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
You can then specify this attribute on Ajax methods.
Hope this helps.
Reference: http://benedict-chan.github.io/blog/2014/02/11/asp-dot-net-mvc-how-to-handle-unauthorized-response-in-json-for-your-api/

Authenticate users in Asp .net Web API

I'm writing API which will be consumed by mobile devices and I want to secure this API end points.
User authentication details is provide by another application called User Manger API (another project which contains user details).
How to make use of ASP.NET Identity framework Authorization and other features to secure my API endpoints while getting the user data from the User manager API ?
The question is a bit broad; basically you are looking for a strategy to authenticate and authorise a client for a web api (dotnet core or normal framework?) using a different existing API (is that API in your control, can you modify it if needed?)
If you can modify both, id say look through StackOverflow and Google for JWT tokens, OAuth and identity server.
1- you can implement an attribute and decorate your api controller.
2- you can implement and register a global filter inside your asp.net's app_start (and make sure you are registering filters in your global.asax).
3- you can do what #Roel-Abspoel mentions implement Identity Server in your User Manager API and have your client talk to it and get the token, then your API talk to it to validate the token.
There are other ways, but i will keep this short and sweet.
Here is an example using an attribute:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Filters;
namespace myExample
{
public class ExternalAuthenticationAttribute : IAuthenticationFilter
{
public virtual bool AllowMultiple
{
get { return false; }
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// get request + authorization headers
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// check for username and password (regardless if it was validated on the client, server should check)
// this will only accept Basic Authorization
if (String.IsNullOrEmpty(authorization.Parameter) || authorization.Scheme != "Basic")
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return null;
}
var userNameAndPasword = GetCredentials(authorization.Parameter);
if (userNameAndPasword == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Could not get credentials", request);
return null;
}
// now that we have the username + password call User manager API
var client = new HttpClient();
// POST USERNAME + PASSWORD INSIDE BODY, not header, not query string. ALSO USE HTTPS to make sure it is sent encrypted
var response = AuthenticateAgainstUserMapagerApi1(userNameAndPasword, client);
// THIS WILL WORK IN .NET CORE 1.1. ALSO USE HTTPS to make sure it is sent encrypted
//var response = AuthenticateAgainstUserMapagerApi2(client, userNameAndPasword);
// parse response
if (!response.IsSuccessStatusCode)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
var readTask = response.Content.ReadAsStringAsync();
var content = readTask.Result;
context.Principal = GetPrincipal(content); // if User manager API returns a user principal as JSON we would
}
return null;
}
//private static HttpResponseMessage AuthenticateAgainstUserMapagerApi2(HttpClient client, Tuple<string, string> userNameAndPasword)
//{
// client.SetBasicAuthentication(userNameAndPasword.Item1, userNameAndPasword.Item2);
// var responseTask = client.GetAsync("https://your_user_manager_api_URL/api/authenticate");
// return responseTask.Result;
//}
private static HttpResponseMessage AuthenticateAgainstUserMapagerApi1(Tuple<string, string> userNameAndPasword, HttpClient client)
{
var credentials = new
{
Username = userNameAndPasword.Item1,
Password = userNameAndPasword.Item2
};
var responseTask = client.PostAsJsonAsync("https://your_user_manager_api_URL/api/authenticate", credentials);
var response = responseTask.Result;
return response;
}
public IPrincipal GetPrincipal(string principalStr)
{
// deserialize principalStr and return a proper Principal instead of ClaimsPrincipal below
return new ClaimsPrincipal();
}
private static Tuple<string, string> GetCredentials(string authorizationParameter)
{
byte[] credentialBytes;
try
{
credentialBytes = Convert.FromBase64String(authorizationParameter);
}
catch (FormatException)
{
return null;
}
try
{
// make sure you use the proper encoding which match client
var encoding = Encoding.ASCII;
string decodedCredentials;
decodedCredentials = encoding.GetString(credentialBytes);
int colonIndex = decodedCredentials.IndexOf(':');
string userName = decodedCredentials.Substring(0, colonIndex);
string password = decodedCredentials.Substring(colonIndex + 1);
return new Tuple<string, string>(userName, password);
}
catch (Exception ex)
{
return null;
}
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
}
use the attribute on your API class like this, which will call User Manager API each time PurchaseController is accessed:
[ExternalAuthenticationAttribute]
public class PurchaseController : ApiController

GetCookies() returns null in WebApi Controller

I have two projects, one is client other is API provider. When I send request to api provider 'request.Headers.GetCookies()' have values on the client part but in
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var test1 = context.Request.Headers.ToList().FirstOrDefault(h => h.Key == "Cookie");
test1 is null and in
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var test2 = actionContext.Request.Headers.GetCookies();
test2 is null, this is how I send request:
[HttpPost]
public ActionResult PostToApiProvider(RequestViewModel requestModel)
{
var request = new HttpResponseMessage();
// Here I add cookie header in request for authentification
if (HttpContext.Request.Cookies.Count != 0)
{
var cookieList = new List<string>();
for (var i = 0; i < Request.Cookies.Count; i++)
{
cookieList.Add(Request.Cookies[i].Value);
}
request.Headers.Add("Cookie", cookieList);
}
var response = client.SendAsync(request).Result;
var testRequestCookies = request.Headers.GetCookies();
return View("SomeView", model)
request and testRequestCookies (before and after call to api provider) will have cookie values , please see the screenshoot :
CookieObject
If I can help with more code, please ask. Thanks :)
Here's how to get a cookie value
private async Task<string> GetCookieValue(string url, string cookieName)
{
var cookieContainer = new CookieContainer();
var uri = new Uri(url);
using (var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
})
{
using (var httpClient = new HttpClient(httpClientHandler))
{
await httpClient.GetAsync(uri);
var cookie = cookieContainer.GetCookies(uri).Cast<Cookie>().FirstOrDefault(x => x.Name == cookieName);
return cookie?.Value;
}
}
}

Crossdomain form authentication (using WebClient and cookies)

I have 2 sites, the first site is basic forms authentication system and some method, that needs to authenticate user. In the second I use modified WebClient (it can use cookies) and send request for authenticate and after request for secure operation. At first step all are OK, first server return AUTH cookie in response and remember it, and on the second it send request for secure operation with AUTH cookie, but at the first site, our request is not authorized! Request.isauthenticated = false.
Why is it? The AUTH cookie at this request are valid.
It is code for site №1 WebClient requests.
//Create an instance of your new CookieAware Web Client
var client = new CookieAwareWebClient();
//Authenticate (username and password can be either hard-coded or pulled from a settings area)
var values = new NameValueCollection { { "Name", "name" }, { "Password", "1234" } };
//Perform authentication - after this has been performed the cookie will be stored within the Web Client
client.UploadValues(new Uri("http://localhost:15536/Plugins/ProductListGetter/login"), "POST", values);
var _cookies = client.ResponseHeaders["Set-Cookie"];
client.UploadString(new Uri("http://localhost:15536/Plugins/ProductListGetter/ChangeNameForCurrentUser"), "POST", "Example Message");
client.UploadString(new Uri("http://localhost:15536/Plugins/ProductListGetter/ChangeNameForCurrentUser"), "POST", "Example Message");
client.Dispose();
And code for modified WebClient
public class CookieAwareWebClient : WebClient
{
//Properties to handle implementing a timeout
private int? _timeout = null;
public int? Timeout
{
get
{
return _timeout;
}
set
{
_timeout = value;
}
}
//A CookieContainer class to house the Cookie once it is contained within one of the Requests
public CookieContainer CookieContainer { get; private set; }
//Constructor
public CookieAwareWebClient()
{
CookieContainer = new CookieContainer();
}
//Method to handle setting the optional timeout (in milliseconds)
public void SetTimeout(int timeout)
{
_timeout = timeout;
}
//This handles using and storing the Cookie information as well as managing the Request timeout
protected override WebRequest GetWebRequest(Uri address)
{
//Handles the CookieContainer
var request = (HttpWebRequest)base.GetWebRequest(address);
request.CookieContainer = CookieContainer;
//Sets the Timeout if it exists
if (_timeout.HasValue)
{
request.Timeout = _timeout.Value;
}
return request;
}

Categories