Search code examples
asp.net-coreasp.net-identitytelegramblazor-server-side

.NET Core Identity Framework and Telegram Login Widget with no database


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


Solution

  • 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.

    1. I've added Blazored SessionStorage as a dependency to the project
    2. 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.

    1. 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);
    }
    
    1. In my Login Blazor page I inject the CustomAuthenticationStateProvider like this:
    @inject AuthenticationStateProvider _authenticationStateProvider
    
    1. 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.