Member Login With Umbraco 9

Last week, I wrote about Member Registration in Umbraco 9. Let us now look at how we can achieve a member login form in Umbraco 9.

Member Login With Umbraco 9

As said in my previous post, Umbraco 9 makes use of ASP.NET Core Identity for both backoffice users and website members. While member login was available as a method on MembershipHelper class, it's not the same with Umbraco 9. In Umbraco 9, we have the interfaces IMemberManager and IMemberSignInManager to help us out.

Umbraco ships with a LoginModel in the Umbraco.Cms.Web.Common.Models namespace which I am using as my view model. Now we need a ViewComponent for the login form. The ViewComponent passes an instance of the LoginModel to my view.

public class LoginViewComponent : ViewComponent
{
	public IViewComponentResult Invoke()
	{
		return View(new LoginModel());
	}
}

I am using the default convention for ViewComponents here and following that, I have the view(Default.cshtml) for my ViewComponent in the folder ~/Views/Components/Login and the code is as shown below.

@using Umbraco.Cms.Web.Common.Models
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<LoginModel>

@using (Html.BeginUmbracoForm("Login", "AccountSurface", FormMethod.Post, new { @class = "text-left" }))
{
    <div class="form-floating">
        <input asp-for="@Model.Username" class="form-control" id="email" placeholder="Enter your email..."  autocomplete="off" data-sb-validations="required,email"/>
        <label for="Email">Email</label>
        <span asp-validation-for="@Model.Username" class="text-danger"></span>
    </div>
    <div class="form-floating">
        <input asp-for="@Model.Password" class="form-control" type="password" id="password" placeholder="Enter your email..." autocomplete="off" data-sb-validations="required"/>
        <label for="Email">Password</label>
        <span asp-validation-for="@Model.Password" class="text-danger"></span>
    </div>
   
    <div class="form-floating">
        <input type="checkbox" asp-for="@Model.RememberMe" >
        <label for="Message">Remember Me</label>
    </div>
    <br />
    <div class="form-floating">
        <button class="btn btn-primary text-uppercase float-end" id="register" type="submit">Login</button>

 @Html.ValidationSummary()
    </div>
}

I have a Login document type that has a default template by the same name. And I invoke my Login ViewComponent in the template. 

@await Component.InvokeAsync("Login")

My ViewComponent posts back to an AccountSurfaceController which has a Login action. Below is the code for the action. I am using the IMemberManager to validate the credentials without actually logging in. This interface helps you retrieve a member as an IPublishedContent and also has a bunch of other methods to act upon the member. I am also using the IMemberSignInManager which is again a new concept in Umbraco 9. This interface helps with signing in and signing out primarily. Both these interfaces are injected to my surface controller.

[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
	if (!ModelState.IsValid)
	{
		ModelState.AddModelError(string.Empty, "Please provide username and password");
		return CurrentUmbracoPage();
	}

	//validate credentials without logging in
	var validCredentials = await _memberManager.ValidateCredentialsAsync(model.Username, model.Password);

	if (validCredentials)
	{
		//sign in
		var loginResult = await _memberSignInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe,true);
		if (loginResult.Succeeded)
		{
			return Redirect("/password-protected-page/");
		}
		else
		{
			ModelState.AddModelError(string.Empty, "Unable to log in.");
		}

	}
	else
	{
		ModelState.AddModelError(string.Empty, "Wrong credentials");
	}

	return CurrentUmbracoPage();
}

Now we have the login working, lets think about login status and a log out link as well. For the login status I am making use of the IMemberManager again. The interface has a IsLoggedIn() method and a GetCurrentMemberAsync method which can help with this. So you can have a partial with this below code and use the partial anywhere on your site.

@inject IMemberManager memberManager

@if (memberManager.IsLoggedIn())
{
    var currentMember = await memberManager.GetCurrentMemberAsync();
    <li class="nav-item">
       <span class="nav-link px-lg-3 py-3 py-lg-4">Welcome, @currentMember.Name</span>
        <a class="nav-link px-lg-3 py-3 py-lg-4" href="@Url.SurfaceAction("Logout","AccountSurface")">Logout</a>
    </li>
}

For the logout link, I am using the @Url.SurfaceAction helper which helps generate a url to the surface controller specified. And my Logout action makes use of the IMemberManager to sign out he logged in member.

public async Task<IActionResult> Logout()
{
	await _memberSignInManager.SignOutAsync();
	return Redirect(_publishedContentQuery.ContentAtRoot().FirstOrDefault().Url());
}

Update

I posted about my article on Twitter and @partapruder asked me about the ability to add custom claims on login. I was intrigued about it and started working out to see whether this can be achieved. The IMemberSignInManager does not have any methods to help you sign in with claims. I tried a few other routes as well but all resulted in a NotImplementedException for the SignInWithClaimsAsync method as well. But here is how I got it working in the end. I am not sure whether this is right or it has any negative after-effects.

I added additional claims to ASP.NET Core Identity using the IUserClaimsPrincipalFactory and registered it in my DI container.

public class AdditionalUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<MemberIdentityUser, UmbracoIdentityRole>
{
	public AdditionalUserClaimsPrincipalFactory(
		UserManager<MemberIdentityUser> memberManager,
		RoleManager<UmbracoIdentityRole> roleManager,
		IOptions<IdentityOptions> optionsAccessor)
		: base(memberManager, roleManager, optionsAccessor)
	{ }

	public async override Task<ClaimsPrincipal> CreateAsync(MemberIdentityUser user)
	{
		var principal = await base.CreateAsync(user);
		var identity = (ClaimsIdentity)principal.Identity;

		var claims = new List<Claim>()
		{
			new Claim("customtype", "customvalue")
		};   

		identity.AddClaims(claims);

		return principal;
	}
}


//register in DI container
services.AddScoped<IUserClaimsPrincipalFactory<MemberIdentityUser>,AdditionalUserClaimsPrincipalFactory>();

To access this claim value I can use the below code.

var user = httpContextAccessor.HttpContext.User;
var customClaim = user.FindFirst("customtype");

 As I said, there might be other ways to do it, but this worked as the first proof of concept for me.