asp.net-coreblazorhttpcontext

Is it safe to access a request's HttpContext inside of a custom AuthenticationStateProvider in a Blazor Web App with Interactive rendering?


While there are a lot of questions related to Blazor and Authentication, I didnt see this particular question, so figured I would ask it here.

I am new to blazor and am working on implementing a custom cookie-based authentication scheme for an app that will be rendering in Interactive mode. I am using the new .Net 8 Blazor Web App template. In reading the docs I see that usage of the HttpContext is discouraged when rendering in interactive mode in this new template because there isn’t a proper HttpContext when you get down to the components.

In my custom scheme I have a cookie that has an id that would be used to obtain the claims principle in a custom AuthenticationStateProvider, and i was thinking that when the StateProvider’s GetAuthenticationStateAsync() method is invoked I could pull the cookie and get the value from it. I was hoping to be able to inject the HttpContext into my AuthenticationStateProvider and then I could grab the cookie that way. I think this should be ok, because the AuthenticationStateProvider is created well before the request gets to blazor (Authentication is now being handled by Middleware from AspNet.Core), so it would have gotten access to the actual HttpContext from AspNet.Core. So it would have the context, and when the Router in blazor invokes GetAuthenticationStateAsync() it should be able to pull the cookie from it’s internally stored context, sidestepping the whole Blazor-Has-No-Context issue.

Is my thinking and understanding of the situation correct here? The documentation for the Authentication for the new template is a bit confusing and not clear on this point.


Solution

  • You actually has 2 way to get cookie values. One is using HttpContext when SSR. Another is using Javascript Interop to read from the browser storage.
    In App.razor, add javascript to read and write cookies.

    ...
        <script src="_framework/blazor.web.js"></script>
        <script>
            function WriteCookie(name, value, days) {
                var expires;
                if (days) {
                    var date = new Date();
                    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                    expires = "; expires=" + date.toGMTString();
                }
                else {
                    expires = "";
                }
                document.cookie = name + "=" + value + expires + "; path=/";
            }
    
            function ReadCookie(name) {
                console.log("test");
                const value = `; ${document.cookie}`;
                const parts = value.split(`; ${name}=`);
                if (parts.length === 2) return parts.pop().split(';').shift();
            }
        </script>
    

    Then you Could read cookie from httpcontext or Jsinterop depending on the rendermode.

        public class CustomAuthenticationStateProvider : AuthenticationStateProvider
        {
            private readonly IHttpContextAccessor _httpContextAccessor;
            private readonly IJSRuntime js;
    
            public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor, IJSRuntime js)
            {
                this._httpContextAccessor = httpContextAccessor;
                this.js = js;
            }
    
            public async override Task<AuthenticationState> GetAuthenticationStateAsync()
            {
                string token = "";
                if (_httpContextAccessor.HttpContext != null)
                {
                    if (!_httpContextAccessor.HttpContext.Response.HasStarted)
                    {
    
                        token = _httpContextAccessor.HttpContext.Request.Cookies["token"];
                    }
                    else
                    {
                        //js.InvokeVoidAsync("test");
                        token = await js.InvokeAsync<string>("ReadCookie", "token");
                    }
                }
                else
                {
                    token = await js.InvokeAsync<string>("ReadCookie", "token");
                }
    
    
                if (string.IsNullOrEmpty(token))
                {
                    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
                }
    
                var tokenHandler = new JwtSecurityTokenHandler();
                var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(identity)));
    
            }
    
            //When user login, write a token which contains user identity to a cookie named "token"
            public async Task Update(string token)
            {
                if (_httpContextAccessor.HttpContext != null)
                {
                    if (!_httpContextAccessor.HttpContext.Response.HasStarted)
                    {
    
                        _httpContextAccessor.HttpContext.Response.Cookies.Append("token", token);
                    }
                    else
                    {
                        await js.InvokeVoidAsync("WriteCookie", "token", token, DateTime.Now.AddMinutes(1));
                    }
                }
                else
                {
                    await js.InvokeVoidAsync("WriteCookie", "token", token, DateTime.Now.AddMinutes(1));
    
                }
    
    
                var tokenHandler = new JwtSecurityTokenHandler();
                var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
                NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))));
            }
        }
    

    Just note that When interactiveServer and SSR, IhttpContext both not null. But only when SSR ,"Response.HasStarted" is true. Don't forget

    builder.Services.AddHttpContextAccessor();