ASP .NET Core webapi set cookie in middleware
Asked Answered
B

3

8

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.

Bria answered 6/6, 2019 at 9:30 Comment(0)
B
19

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);
        }
    }
}
Bria answered 7/6, 2019 at 8:57 Comment(3)
This was mega helpful. Spend hours trying to understand why the cookies weren't being added. THANKS!Cornered
Yeah, I spent lots of hours too, glad I could help ;)Bria
Thank you. I think it might be better using an anonymous function instead of storing http context in a field. Storing context might result in ObjectDisposedExceptionLevesque
P
3

.NET 5

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ........
    app.Use(async (context, next) =>
    {
        var cookieOptions = new CookieOptions()
        {
            Path = "/",
            Expires = DateTimeOffset.UtcNow.AddHours(1),
            IsEssential = true,
            HttpOnly = false,
            Secure = false,
        };
        context.Response.Cookies.Append("MyCookie", "TheValue", cookieOptions);

        await next();
    });
    // ........
}
Prudhoe answered 6/1, 2021 at 14:18 Comment(1)
This works if you can set the cookie before the controller execution. Same works in .net core 3.1. I needed a way to set the cookie right after the execution of the action also injecting something to get some dataBria
H
0

Mine worked in controller by adding these lines to program.cs

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();


app.UseAuthentication();
app.UseAuthorization();

then in controller used the following code

CookieOptions options = new()
{
    Domain = HttpContext.Request.Host.ToString().Contains("localhost") ? null : HttpContext.Request.Host.ToString(),
    Expires = DateTime.UtcNow.AddDays(7),
    IsEssential = true,
    SameSite = SameSiteMode.None,
    HttpOnly = true,
    Secure = true
    
};
            
HttpContext.Response.Cookies.Append("RefreshToken", "Hello", options);

refrence:

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-8.0

Hanhhank answered 22/11, 2023 at 10:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.