3
votes

I would like to have login form as PartialView or ViewComponent. User types username and password I want to login using ajax and show possible validation errors, either using jQuery or rerendering the login form on the server. I don't care.

This seems trivial, but are there any existing samples or templates for login using AJAX? I don't want to reinvent the wheel.

Im starting with default Visual Studio Template for ASP.NET Core Web Application with local accounts where login is separate page. It uses bootstrap. Ideally I would like to stick as close as possible to this.

The login post action looks like this:

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginFormViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        if (ModelState.IsValid)
        {
            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, set lockoutOnFailure: true
            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                _logger.LogInformation(1, "User logged in.");
                return RedirectToLocal(returnUrl);
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning(2, "User account locked out.");
                return View("Lockout");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }
1

1 Answers

4
votes

Here is a snippet my code in the account controller for AJAX based login to get you started:

// GET: /Account/LoginAjax
[HttpGet]
[AllowAnonymous]
[RequireHttps]
public IActionResult LoginAjax(string returnUrl = null)
{
    if (!_signInManager.IsSignedIn(User))
    {
        if (Request.Cookies["Identity.External"] != null)
        {
            // TODO: this is a temp solution, see https://github.com/aspnet/Security/pull/980
            // http://stackoverflow.com/questions/38751641/app-redirects-to-account-accessdenied-on-adding-oauth
            // when fixed in Microsoft.AspNetCore.Authentication, remove the whole block
            Response.Cookies.Delete("Identity.External");
        }
    }
    return PartialView("_LoginPartial", new LoginViewModel { RememberMe = true, ReturnUrl = returnUrl });
}

// POST: /Account/LoginAjax
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RequireHttps]
public async Task<IActionResult> LoginAjax(LoginViewModel model, string returnUrl = null)
{
    returnObject ret = new returnObject();
    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            ret.success = true;
            ret.message = "logged-in";
        }
        else if (result.IsLockedOut)
        {
            ModelState.AddModelError(string.Empty, "This account has been locked out, please try again later.");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "The email address or password supplied are incorrect. Please check your spelling and try again.");
        }
    }
    if (!ret.success)   //login was unsuccessful, return model errors
        ret.message = ModelErorrs(ModelState);

    return Json(ret);
}

public static string ModelErorrs(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState)
{
    return string.Join("; ", modelState.Values
        .SelectMany(x => x.Errors)
        .Select(x => x.ErrorMessage));
}

public class returnObject
{
    public bool success { get; set; } = false;
    public string message { get; set; } = "";
    public string action { get; set; } = "";
}

_LoginPartial.chtml:

<form id="formLoginAJAX" asp-controller="Account" asp-action="LoginAjax" asp-route-returnurl="@Model.ReturnUrl" method="post">
......
</form>

Prototype fot client JS:

// dialog form submit handler
$form.submit(function (event) {
    event.preventDefault();                
    if (!$form.valid())
        return; // form is not valid

    // submit validated form via Ajax
    var res = { success: false, message: '' };
    $.ajax({
        type: 'POST',
        dataType: 'json',
        url: $form.attr('action'),
        xhrFields: {
            withCredentials: true
        },
        data: $form.serializeObject(),
        beforeSend: function () {
            //disable submit button to prevent double-click                        
        },
        success: function (data) {
            res = data;
        },
        error: function (jqXHR, textStatus, errorThrown) {
            res.message = 'Request failed: ' + errorThrown;
        },
        complete: function () {
            if (res.success) {
                // all good, user is logged-in 
                // do callbacks if needed
                // close dialog
            } else {
                // no luck, show returned errors (res.message) in the summary (".validation-summary")                            
            }                        
        }
    });
});