Search code examples
c#asp.net-core.net-coreasp.net-identity

Implementing Telegram LoginWidget with SignInManager


I want Telegram to do the user account management for my ASP.NET 5.0 Webapp

https://core.telegram.org/widgets/login

I got it working to the point that the Telegram servers redirect to my provided callback address, where I call _userManager.SignInAsync, but when later checking whether the user is logged in, SignInManager always returns false.

The callback is implemented like this:

public class AccountController : Controller
{
    private readonly IConfiguration _config;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public AccountController(IConfiguration config,
                             SignInManager<ApplicationUser> signInManager,
                             UserManager<ApplicationUser> userManager)
    {
        _config = config;
        _signInManager = signInManager;
        _userManager = userManager;
    }

    [HttpGet]
    public async Task<IActionResult> Logout()
    {
        await _signInManager.SignOutAsync();
        return RedirectToAction("index", "home");
    }

    [HttpGet]
    public async Task<IActionResult> Login()
    {
        await Task.CompletedTask;

        var botname = _config["BOT_NAME"];
        var fqdn = _config["FQDN"];

        ViewBag.WidgetEmbedCode = WidgetEmbedCodeGenerator.GenerateRedirectEmbedCode(
            _config["BOT_NAME"],
            $"https://{fqdn}{Url.Action("tgcallback", "account")}",
            ButtonStyle.Large,
            true,
            true);

        return View();
    }

    [HttpGet]
    public async Task<IActionResult> Tgcallback(
        string id,
        string first_name,
        string username,
        string photo_url,
        string auth_date,
        string hash)
    {
        // attempt to authenticate the login
        var token = _config["BOT_TOKEN"];
        var loginWidget = new LoginWidget(token);
        var auth = loginWidget.CheckAuthorization(new SortedDictionary<string, string>()
        {
            {"id",id},
            {"first_name", first_name},
            {"username", username},
            {"photo_url", photo_url},
            {"auth_date", auth_date},
            {"hash", hash}
        });

        // if the authorization was successful, create the user (if not exist) and sign in
        if (auth == Authorization.Valid)
        {
            var user = await _userManager.FindByNameAsync($"tg{id}");
            if (null == user)
            {
                user = new ApplicationUser()
                {
                    UserName = $"tg{id}",

                    TelegramNativeId = long.Parse(id),
                    TelegramUserName = username,
                    FirstName = first_name,
                    PhotoUrl = photo_url
                };

                var result = await _userManager.CreateAsync(user);
                if (!result.Succeeded)
                {
                    ViewBag.ErrorTitle = "Internal error";
                    ViewBag.ErrorMessage = $"Failed to create user tg{id}";
                    return View("Error");
                }

                user = await _userManager.FindByNameAsync($"tg{id}");
                if (null == user)
                {
                    ViewBag.ErrorTitle = "Internal error";
                    ViewBag.ErrorMessage = $"Failed to create user tg{id}";
                    return View("Error");
                }
            }

            await _signInManager.SignInAsync(user, true);
        }

        // return him back to home/index where he will be redirected to login,
        // if the login was unsuccessful
        return RedirectToAction("index", "home");
    }
}

After the callback is invoked, a user (without password hash) is created in the DB and I have a cookie from .AspNetCore.Identity.Application. I am then redirected to /home/index which is implemented like this:

public class HomeController : Controller
{
    private SignInManager<ApplicationUser> _signInManager;

    public HomeController(SignInManager<ApplicationUser> signInManager)
    {
        _signInManager = signInManager;
    }

    public async Task<IActionResult> Index()
    {
        if (_signInManager.IsSignedIn(User))
        {
            var user = await _signInManager.UserManager.GetUserAsync(User);

            var model = new HomeIndexViewmodel()
            {
                TgNativeId = user.TelegramNativeId.ToString(),
                FirstName = user.TelegramUserName,
                UserName = user.TelegramUserName,
                PhotoUrl = user.PhotoUrl
            };

            return View(model);
        }
        else
        {
            return RedirectToAction("login", "account");
        }
    }
}

which in turn redirects me back to the login page; hence thinking that I am not logged in.

What am I missing?

Edit: Maybe I have not configured Identity correctly. This is my ConfigureServices

 public void ConfigureServices(IServiceCollection services)
 {
    // Configuring Telegram Bot
    services.AddHostedService<TelegramBotHostedService>();
    services.AddHttpClient("tgwebhook")
        .AddTypedClient<ITelegramBotClient>(httpClient => new TelegramBotClient(_config["BOT_TOKEN"], httpClient));
    services.AddSingleton<ITelegramBotUpdateHandler, TelegramBotUpdateHandlerImpl>();

    // Configuring database connection
    services.AddDbContextPool<ApplicationDbContext>(
        options => options.UseNpgsql(_config["DB_CONNECTION_STRING"]));

    // Configuring for account management
    services.AddIdentity<ApplicationUser, IdentityRole>(options => {})
        .AddEntityFrameworkStores<ApplicationDbContext>();

    // Configuring MVC and NewtonsoftJson on which Telegram.Bot is highly dependent
    services.AddMvc()
        .AddNewtonsoftJson();
}

Solution

  • The problem was that I did not add authentication to the request processing pipeline.

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ----------------------------------------------------------------
        // run ef core database migration every time the app starts
        // feels hacky and this is properly not the right place to call it
        // but according to 
        // https://entityframeworkcore.com/knowledge-base/48617880/ef-core-migrations-in-docker-container
        // this is the way to do it
    
        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {    
            serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
        }
    
        // ----------------------------------------------------------------
        // from here onwards the request processing pipeline is created
    
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseStaticFiles();
    
        app.UseRouting();
        app.UseCors();
    
        app.UseAuthentication();
    
        app.UseEndpoints(endpoints =>
        {
            // setting up the route for the bot
            var token = _config["BOT_TOKEN"];
    
            endpoints.MapControllerRoute(
                "tgwebhook",
                $"bot/{token}",
                new { controller = "TelegramBotWebhook", Action = "Post" });
    
                // setting up routes for controllers
                endpoints.MapControllerRoute(
                    "default",
                    "{controller=Home}/{action=Index}/{id?}");
    
                /*
                // forward all other requests to here
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
                */
            });
        }
    }