0

I would like to add a second authentication factor to a Blazor application created with RadZen. Despite the known uncertainties, it was decided that the second factor should be sent by e-mail.

So I created a new page to enter the second factor and then extended the RadZen login function to handle the second factor:

public async Task<IActionResult> Login(string userName, string password, string redirectUrl)
{
    if (env.EnvironmentName == "Development" && userName == "admin" && password == "admin")
    {
        var claims = new List<Claim>()
        {
                new Claim(ClaimTypes.Name, "admin"),
                new Claim(ClaimTypes.Email, "admin")
        };

        roleManager.Roles.ToList().ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r.Name)));
        await signInManager.SignInWithClaimsAsync(new ApplicationUser { UserName = userName, Email = userName }, isPersistent: false, claims);

        return Redirect($"~/{redirectUrl}");
    }

    if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
    {
        var user = await userManager.FindByNameAsync(userName);

        if (user == null)
        {
            return RedirectWithError("Invalid user or password", redirectUrl);
        }

        if (!user.EmailConfirmed)
        {
            return RedirectWithError("User email not confirmed", redirectUrl);
        }

        var result = await signInManager.PasswordSignInAsync(userName, password, false, true);

        if (result.Succeeded)
        {
            return Redirect($"~/{redirectUrl}");
        }
        else if (result.RequiresTwoFactor)
        {
            return await SendSecondFactorRequest(user, redirectUrl);
        }
    }

    return RedirectWithError("Invalid user or password", redirectUrl);
}

private async Task<IActionResult> SendSecondFactorRequest(ApplicationUser user, string redirectUrl)
{
    if (!user.TwoFactorEnabled)
    {
        return Redirect($"~/{redirectUrl}");
    }

    var token = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
    this.mailService.SendToken(user.Email, token);
    return Redirect($"~/login-confirmation?Redirecturl={redirectUrl};User={user.Name}");
}

This generates the token and sends it by mail before calling the input page for the token.

After the user enters the token, the following function is executed and fails.

public async Task<IActionResult> LoginVerification(string user, string key, string redirectUrl)
{
    var result = await signInManager.TwoFactorSignInAsync(TokenOptions.DefaultEmailProvider, key, false, false)

        if (result.Succeeded)
    {
        var autentic = this.securetyService.IsAuthenticated();
        return Ok(redirectUrl);
    }

    return StatusCode(420, "Ungültiger Benutzername oder Verifikationsschlüssel.");
}

If I work with the userManager's VerifyTwoFactorTokenAsync() function instead, it confirms that the correct token has been entered but the RadZen login status check via SecurityService.IsAuthenticated() then returns false, so the app doesn't realize that the user is logged in.

public async Task<IActionResult> LoginVerification(string user, string key, string redirectUrl)
{
    if ((!string.IsNullOrEmpty(user)) && (!string.IsNullOrEmpty(key)))
    {
        var userInfo = await userManager.FindByNameAsync(user);

        var result = await userManager.VerifyTwoFactorTokenAsync(userInfo, TokenOptions.DefaultEmailProvider, key);
        
        if (result)
        {
            var autentic = this.securetyService.IsAuthenticated();
            return Ok(redirectUrl);
        }

    }
    return StatusCode(420, "Ungültiger Benutzername oder Verifikationsschlüssel.");
}
    

RadZen Login check:

protected override async System.Threading.Tasks.Task OnInitializedAsync()
{
    Globals.PropertyChanged += OnPropertyChanged;
    await Security.InitializeAsync(AuthenticationStateProvider);
    if (!Security.IsAuthenticated())
    {
        UriHelper.NavigateTo("Login", true);
    }
    else
    {
        await Load();
    }
}

Obviously I'm doing something wrong, but unfortunately I don't see what. Can someone tell me what I need to change to accept the second factor?

ringhat

ringhat
  • 23
  • 4
  • In "Blazor Server app, each browser screen requires a separate circuit and separate instances of server-managed component state" - from documentation. So if your client is clicking a link from email and opens a new window ... is another "circuit" and the previous one will not be aware of it. – D A Jul 19 '23 at 09:21
  • Thank you for the advice. The email does not contain a link. The user is redirected to the input page after entering the password by the above code. Do I have to navigate in another way to not start a new "circuit"? – ringhat Jul 19 '23 at 13:44

0 Answers0