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.