Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP .NET Core webapi set cookie in middleware

I'm trying to set a cookie after the action is executed, struggling to get this working. I managed to see the cookie if I set it from a controller, but not from a middleware. I have played with the order of the configuration and nothing. The code sample is from a clean webapi created project, so if someone wants to play with it is simple, just create an empty webapi, add the CookieSet class and replace the Startup class with the one below (only added are the cookie policy options)

Here is my middleware

public class CookieSet
{
    private readonly RequestDelegate _next;

    public CookieSet(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await _next.Invoke(context);
        var cookieOptions = new CookieOptions()
        {
            Path = "/",
            Expires = DateTimeOffset.UtcNow.AddHours(1),
            IsEssential = true,
            HttpOnly = false,
            Secure = false,
        };
        context.Response.Cookies.Append("test", "cookie", cookieOptions);
    }
}

I have added the p assignment and checked that the execution never gets there, on the Cookies.Append line it stops the execution, so there is something going on I can't figure it out.

And here is my Startup class

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => false;
            options.MinimumSameSitePolicy = SameSiteMode.None;
            options.HttpOnly = HttpOnlyPolicy.None;
            options.Secure = CookieSecurePolicy.None;
            // you can add more options here and they will be applied to all cookies (middleware and manually created cookies)
        });

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseCookiePolicy(new CookiePolicyOptions
        {
            CheckConsentNeeded = c => false,
            HttpOnly = HttpOnlyPolicy.None,
            Secure = CookieSecurePolicy.None,
            MinimumSameSitePolicy = SameSiteMode.None,
        });

        app.UseMiddleware<CookieSet>();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseMvc();
    }
}

I have set all the options to the minimum requirements, tested with chrome and fiddler and nothing.

like image 253
dariogriffo Avatar asked Jun 06 '19 09:06

dariogriffo


Video Answer


1 Answers

Ok, I'm talking to myself, but this is for the community...

Got this working after digging into the AspNetCore code. Basically the cookie must be set on the callback OnStarting of the context response. Here is the code of the middleware that makes the trick

public class CookieSet
{
    private readonly RequestDelegate _next;
    private readonly ASessionOptions _options;
    private HttpContext _context;
    public CookieSet(RequestDelegate next, IOptions<ASessionOptions> options)
    {
        _next = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
        _context = context;
        context.Response.OnStarting(OnStartingCallBack);
        await _next.Invoke(context);
    }

    private Task OnStartingCallBack()
    {
        var cookieOptions = new CookieOptions()
        {
            Path = "/",
            Expires = DateTimeOffset.UtcNow.AddHours(1),
            IsEssential = true,
            HttpOnly = false,
            Secure = false,
        };
        _context.Response.Cookies.Append("MyCookie", "TheValue", cookieOptions);
        return Task.FromResult(0);
    }
}

The AspNetCore team uses an internal class for that.

Checking the SessionMiddleware class, part of the code is as follows (removed a lot of things just for the sake of the answer):

public class SessionMiddleware
{
    public async Task Invoke(HttpContext context)
    {
        // Removed code here

        if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
        {
                        // Removed code here
            var establisher = new SessionEstablisher(context, cookieValue, _options);
            tryEstablishSession = establisher.TryEstablishSession;
            isNewSessionKey = true;
        }

        // Removed code here

        try
        {
            await _next(context);
        }

        // Removed code here
    }

    //Now the inner class

    private class SessionEstablisher
    {
        private readonly HttpContext _context;
        private readonly string _cookieValue;
        private readonly SessionOptions _options;
        private bool _shouldEstablishSession;

        public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
        {
            _context = context;
            _cookieValue = cookieValue;
            _options = options;
            context.Response.OnStarting(OnStartingCallback, state: this);
        }

        private static Task OnStartingCallback(object state)
        {
            var establisher = (SessionEstablisher)state;
            if (establisher._shouldEstablishSession)
            {
                establisher.SetCookie();
            }
            return Task.FromResult(0);
        }

        private void SetCookie()
        {
            var cookieOptions = _options.Cookie.Build(_context);

            var response = _context.Response;
            response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions);

            var responseHeaders = response.Headers;
            responseHeaders[HeaderNames.CacheControl] = "no-cache";
            responseHeaders[HeaderNames.Pragma] = "no-cache";
            responseHeaders[HeaderNames.Expires] = "-1";
        }

        // Returns true if the session has already been established, or if it still can be because the response has not been sent.
        internal bool TryEstablishSession()
        {
            return (_shouldEstablishSession |= !_context.Response.HasStarted);
        }
    }
}
like image 90
dariogriffo Avatar answered Sep 21 '22 14:09

dariogriffo