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.
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.
Understanding the concepts behind asynchronous messaging
Understanding the concepts behind asynchronous messaging