In the end, this is how I did it. While not ideal, this works for me. However, I'd love to get some clarifications from someone, specifically on IUserStore stuff.
- I've added Blazored SessionStorage as a dependency to the project
- I've registered my own implementations of
AuthenticationStateProvider, IUserStore and IRoleStore in Startup.cs like this:
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
services.AddTransient<IUserStore<User>, CustomUserStore>();
services.AddTransient<IRoleStore<Role>, CustomRoleStore>();
The first line is the most important one. Implementations of IUserStore and IRoleStore don't really matter, but it seems like I have to register them for Identity framework to work, even though I won't use them. All of the methods in my "implementation" are literally just throw new NotImplementedException(); and it still works, it just needs them to exist for the UserManager somewhere deep down, I guess? I'm still a little unclear on that.
- My
CustomAuthenticationStateProvider looks like this:
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly ISessionStorageService _sessionStorage;
private readonly ILogger _logger;
private readonly AuthenticationState _anonymous = new(new ClaimsPrincipal(new ClaimsIdentity()));
public CustomAuthenticationStateProvider(
ILoggerFactory loggerFactory,
ISessionStorageService sessionStorage,
IConfiguration configuration) : base(loggerFactory)
{
_logger = loggerFactory.CreateLogger<CustomAuthenticationStateProvider>();
_sessionStorage = sessionStorage;
// setting up HMACSHA256 for checking user data from Telegram widget
...
}
private bool IsAuthDataValid(User user)
{
// validating user data with bot token as the secret key
...
}
public AuthenticationState AuthenticateUser(User user)
{
if (!IsAuthDataValid(user))
{
return _anonymous;
}
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.FirstName),
new Claim("Username", user.Username),
new Claim("Avatar", user.PhotoUrl),
new Claim("AuthDate", user.AuthDate.ToString()),
}, "Telegram");
var principal = new ClaimsPrincipal(identity);
var authState = new AuthenticationState(principal);
base.SetAuthenticationState(Task.FromResult(authState));
_sessionStorage.SetItemAsync("user", user);
return authState;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var state = await base.GetAuthenticationStateAsync();
if (state.User.Identity.IsAuthenticated)
{
return state;
}
try
{
var user = await _sessionStorage.GetItemAsync<User>("user");
return AuthenticateUser(user);
}
// this happens on pre-render
catch (InvalidOperationException)
{
return _anonymous;
}
}
public void Logout()
{
_sessionStorage.RemoveItemAsync("user");
base.SetAuthenticationState(Task.FromResult(_anonymous));
}
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState,
CancellationToken cancellationToken)
{
try
{
var user = await _sessionStorage.GetItemAsync<User>("user");
return user != null && IsAuthDataValid(user);
}
// this shouldn't happen, but just in case
catch (InvalidOperationException)
{
return false;
}
}
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromHours(1);
}
- In my Login Blazor page I inject the
CustomAuthenticationStateProvider like this:
@inject AuthenticationStateProvider _authenticationStateProvider
- And finally, after getting data from the Telegram widget, I call the
AuthenticateUser method:
((CustomAuthenticationStateProvider)_authenticationStateProvider).AuthenticateUser(user);
Note, that I have to cast AuthenticationStateProvider to CustomAuthenticationStateProvider to get exactly the same instance as AuthorizedView would.
Another important point is that AuthenticateUser method contains call to SessionStorage, which is available later in the lifecycle of the page, when OnAfterRender has completed, so it will throw an exception, if called earlier.