0

I use this repo to implement authentication and authorization with cookie on the Blazor Server.

Suppose that I'd like to retrieve the current logged-in user in the DeleteHotelRoomAsync method in HotelRoomService.cs to log the information of the user who deleted a room.

public async Task<int> DeleteHotelRoomAsync(int roomId)
    {
        var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
        if (roomDetails == null)
        {
            return 0;
        }

        _dbContext.HotelRooms.Remove(roomDetails);
        //ToDo
        //_dbContext.DbLog.Add(userId,roomId);
        return await _dbContext.SaveChangesAsync();
    }

I can't use of AuthenticationStateProvider as it is there or there, becuase of cookie based system and so the AuthenticationStateProvider is null in below code.

I used HttpContextAccessor, and I could retrieve the authenticated userId as below, however, I couldn't use HttpContextAccessor because of Microsoft recommendations.

public class GetUserId:IGetUserId
{
    public IHttpContextAccessor _contextAccessor;
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    public GetUserId(IHttpContextAccessor contextAccessor,AuthenticationStateProvider authenticationStateProvider)
    {
        _contextAccessor = contextAccessor;
        _authenticationStateProvider = authenticationStateProvider;
    }
    public  string Get()
    {            
        var userId = _contextAccessor.HttpContext.User.Claims.First().Value;
        return userId;

    }
}

So is there any safe ways to retrieve authenticated user info (e.g. userId) in a .cs file to log it into database logs for user audit log?

Arani
  • 891
  • 7
  • 18

2 Answers2

1

First you should create a custom AuthenticationStateProvider

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;

namespace BlazorServerTestDynamicAccess.Services;

public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
        : base(loggerFactory) =>
        _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));

    protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        // Get the user from a new scope to ensure it fetches fresh data
        var scope = _scopeFactory.CreateScope();
        try
        {
            var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
            return await ValidateUserAsync(userManager, authenticationState?.User);
        }
        finally
        {
            if (scope is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync();
            }
            else
            {
                scope.Dispose();
            }
        }
    }

    private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
    {
        if (principal is null)
        {
            return false;
        }

        var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
        if (!int.TryParse(userIdString, out var userId))
        {
            return false;
        }

        var user = await userManager.FindUserAsync(userId);
        return user is not null;
    }
}

Then register it:

 services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();

Now you can use it in your services. Here is an example of it:

public class UserInfoService 
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;

    public UserInfoService(AuthenticationStateProvider authenticationStateProvider) =>
        _authenticationStateProvider = authenticationStateProvider ??
                                       throw new ArgumentNullException(nameof(authenticationStateProvider));

    public async Task<string?> GetUserIdAsync()
    {
        var authenticationState = await _authenticationStateProvider.GetAuthenticationStateAsync();
        return authenticationState.User.Identity?.Name;
    }
}
VahidN
  • 18,457
  • 8
  • 73
  • 117
  • Thank you very much. However, I get this error: System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState. I think that it's an issue. https://github.com/dotnet/aspnetcore/issues/28684. Any helps appreciated. – Arani Oct 29 '22 at 07:55
  • If someone would like to use this solution and encounters a reference error on `RevalidatingServerAuthenticationStateProvider`, he can solve the problem using this https://stackoverflow.com/a/72009596/4444757. – Arani Oct 29 '22 at 09:22
  • It works correctly when I use the `AddSingelton` instead of `AddScoped` in service register, However, by `AddSingelton` a single instance create for the lifetime of the application domain. Any Ideas? – Arani Oct 30 '22 at 10:00
  • 1
    It's answered here: https://stackoverflow.com/a/74285512/298573 – VahidN Nov 02 '22 at 07:16
-1

Well, if You don't want to use AuthenticationStateProvider, and the cookie is not working you need any other method to Authenticate and Authorize the User to Delete the reservation.

I would go to handle this by some email+password account managed during reservation, and then verification in this way, or just any "token".

Even if somebody doesn't want to register, when You make a reservation for Your client, you get his phone or e-mail. This way You can generate some random password and send it to the client with information that he needs to log in by phone/email and this password to manage the reservation.

Even more simple way is to generate some token as a parameter, that the user can use with deleteURL to be authenticated with it. Just store it in the database room reservation with that token. In example @page "/deleteRoom/{token}

Then You can use it this way

public async Task<int> DeleteHotelRoomAsync(string token)
{
    var roomDetails = await _dbContext.HotelRooms.Where(n=>n.deleteToken == token).FirstOrDefaultAsync();
    if (roomDetails == null)
    {
        return 0;
    }

    _dbContext.HotelRooms.Remove(roomDetails);
    //ToDo
    //_dbContext.DbLog.Add(userId,roomId);
    return await _dbContext.SaveChangesAsync();
}

Just override OnInitialized method to get that token string as component parameter.

Endi
  • 17
  • 4
  • I already have authorization and authentication. You can see in the repository link that I put it on the question. I just need user audit log. – Arani Oct 23 '22 at 04:58