I am making a Blazor Server app, which is tied to my Telegram bot. I want to add the ability for the user to login using Telegram Login Widget. I have no plans to add login/password authentication and I therefore don't see any reason to use the database to store anything login-related other than the Telegram User ID.
All of the samples imply using the login-password model along with the database, somewhat like this:
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>();
Inevitable, this line appears in all of the samples: services.AddEntityFrameworkStores<AppDbContext>();
Here's my question: how do I just put the user's data (after checking the info from Telegram) into app's context, without storing anything in the database? Or if I'm forced to, where do I change the database scheme? Maybe I don't even need to use the Identity framework for this? All I want is for all the pages to have the info about the user, and the authentication happens on Telegram's side, I just get all the info in response and check the hash with my private key. All I want to do after that is put that model into app's context, I'm not even sure I plan on storing the cookie for the user.
To be clear: I already know how to get info from Telegram and check the hash, let's assume after executing some code on a page I already have some User
model with some filled out fields
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.
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.
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);
}
CustomAuthenticationStateProvider
like this:@inject AuthenticationStateProvider _authenticationStateProvider
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.