Search code examples
authenticationhttpcontrollerblazorauthorization

Blazor WASM Authentication and Authorization on Components and Controllers


I am developing a Blazor WASM with authentication and authorization. The idea is that the user need to login in order to be able to view the Components of the Client Project but also to be able to consume data of Controllers from Server Project which are behind the /api.

Currently I have implemented the restriction on Client components:

<AuthorizeView>
    <NotAuthorized>
        <div class="row">
            <div class="col-md-4">
                <p>Please sign in to use the Platform...</p>
            </div>
        </div>
    </NotAuthorized>
    <Authorized>
        @Body
    </Authorized>
</AuthorizeView>

I have also a Login and a Logout Page which are storing a Cookie for later use and perform a custom AuthenticationStateProvider

await LocalStorage.SetItemAsync<int>($"{Parameters.application}_{Parameters.enviroment}_userid", authentication.user_id);
await LocalStorage.SetItemAsync<string>($"{Parameters.application}_{Parameters.enviroment}_username", authentication.user_name);
await AuthStateProvider.GetAuthenticationStateAsync();

The AuthenticationStateProvider code is the following:

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var state = new AuthenticationState(new ClaimsPrincipal());

    string authcookie_name = $"{Parameters.application}_{Parameters.enviroment}_username";
    string authcookie_value = await _localStorage.GetItemAsStringAsync(authcookie_name);
    if (!string.IsNullOrEmpty(authcookie_value))
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Authentication, authcookie_value)
        }, "Login");

        state = new AuthenticationState(new ClaimsPrincipal(identity));
    }

    NotifyAuthenticationStateChanged(Task.FromResult(state));

    return state;
}

The authentication controller is the following:

[HttpPost, Route("/api/auth/login")]
public IActionResult AuthLogin(Authentication authentication)
{
    try
    {
        int auth = _IAuth.AuthLogin(authentication);
        if (auth != -1)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Authentication, authentication.user_name)
            };
            var claimsIdentity = new ClaimsIdentity(claims, "Login");

            var properties = new AuthenticationProperties()
            {
                IsPersistent = true
            };

            HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), properties);
        }

        return Ok(auth);
    }
    catch { throw; }
}

Everything is working as excepted and the user need to login in order to see the content of the pages, but he is able to see the data of each page if he perform an http call http://domain.ext/api/model/view

In order to resolve this problem I added the Authorize attribute on each controller of Server project like this:

[Authorize]
[Route("/api/model")]
[ApiController]
public class Controller_Model : ControllerBase
{
}

And also added this code on the Program.cs of Server project in order to be able to make controller to work

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
    options.SlidingExpiration = true;
    options.LoginPath = new PathString("/auth/login");
    options.LogoutPath = new PathString("/auth/logout");
    options.Cookie = new CookieBuilder();
    options.Cookie.MaxAge = options.ExpireTimeSpan;
    options.AccessDeniedPath = "/";
    options.EventsType = typeof(CustomCookieAuthenticationEvents);
});

Now the user is not able to see the content of a page even he is making a request to the /api.

The problem is that after some time, even I see the User is still logged in the Authorize attribute of controllers is consider the user not authorized and it returns an error because controller is not returning the supposed object list. enter image description here

I have no clue why and when this is happening. Then if user Logout and Login again it works for a while again.

===============UPDATE 1===============

After lot of investigation, seems that the client side is authenticated and then every time it sees the localstorage item it continues to be in authenticated state. On the other side the server state is based on a cookie which expires after 30mins.

enter image description here

So the Client and the Server states are operated differently and that's why the Client seems authenticated while Server is not while denying access on controllers.

I think the solution is to change the CustomAuthenticationStateProvider in order to check if the cookie exists and if it's valid. So the event order be as follow:

User SingIn via Client Page -> Server Controller creates the cookie -> Client Page is authenticated via Authentication State Provider which reads the cookie.

Any ideas?


Solution

  • Seems that is possible to read and write cookies from Client Project only via Javascript. What needs to be done is the following:

    A custom javascript file "cookie.js", under wwwroot/js:

    export function get() {
        return document.cookie;
    }
    
    export function set(key, value) {
        document.cookie = `${key}=${value}`;
    }
    

    A C# class file "CookieStorageAccessor.cs", under /Classes:

    public class CookieStorageAccessor
    {
        private Lazy<IJSObjectReference> _accessorJsRef = new();
        private readonly IJSRuntime _jsRuntime;
    
        public CookieStorageAccessor(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }
    
        private async Task WaitForReference()
        {
            if (_accessorJsRef.IsValueCreated is false)
            {
                _accessorJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/cookie.js"));
            }
        }
    
        public async ValueTask DisposeAsync()
        {
            if (_accessorJsRef.IsValueCreated)
            {
                await _accessorJsRef.Value.DisposeAsync();
            }
        }
    
        public async Task<T> GetValueAsync<T>(string key)
        {
            await WaitForReference();
            var result = await _accessorJsRef.Value.InvokeAsync<T>("get", key);
    
            return result;
        }
    
        public async Task SetValueAsync<T>(string key, T value)
        {
            await WaitForReference();
            await _accessorJsRef.Value.InvokeVoidAsync("set", key, value);
        }
    }
    

    The C# class can be used injecting javascript and reading the cookie on

    CustomAuthStateProvider:
    //CREATE INSTANCE OF COOKIE ACCESSOR
    CookieStorageAccessor cookieStorageAccessor = new CookieStorageAccessor(_jSRuntime);
    
    //CHECK IF COOKIE IS EXISTS FROM COOKIE ACCESSOR
    string auth_cookie = await cookieStorageAccessor.GetValueAsync<string>("authentication");
    if (!string.IsNullOrEmpty(auth_cookie))
    { }
    else
    { }