I'm building an ASP.Net MVC web application, which uses Umbraco7, to replace an old WebForms website.
The old WebForms site uses Basic Authentication
on some sections of the site (specified at the directory level in IIS), which specify a default Windows domain with its own Active Directory. The browser requests the user ID and password on the appropriate pages, and the code behind retrieves the user information using the System.Web.UI.Page.User.Identity
property.
I would like to provide a similar experience on the new Umbraco site.
Examples of MVC sites using Basic Authentication
specify the authentication and default domain as attributes on the Controller methods, http://www.asp.net/mvc/tutorials/older-versions/security/authenticating-users-with-windows-authentication-cs.
Umbraco doesn't appear to provide individual controller methods for its content pages, and I've not found any Umbraco authentication examples that use Basic Authentication
and rely on the browser to retrieve the credentials.
Is it possible to use Basic Authentication
on an Umbraco content page and retrieve the credentials using the browser?
Updated Answer:
I stumbled on this question several months after posting the original answer, Combining Forms Authentication and Basic Authentication. I have not tested this solution, since that ship has sailed, but it looks promising.
Original Answer:
From what I can tell, the answer to this question is "no". You cannot modify the header of a Umbraco content page, so you can't tell the browser to authenticate itself against a given LDAP server.
However, I was able to use forms authentication to behave in the same way (authenticate against Active Directory and authorize against my database). Below I've included all the code needed to get this authentication to work in Umbraco.
Login Page
The login page is just an Umbraco View with the following Partial View and Surface Control added to it using @Html.Action("MemberUmbLogin", "MemberUmbLoginSurface")
@model CustomUmbraco.Models.MemberUmbLoginModel
@if (User.Identity.IsAuthenticated)
{
<p>Logged in: @User.Identity.Name</p>
<p>@Html.ActionLink("Log out", "MemberUmbLogout", "MemberUmbLoginSurface")</p>
}
else
{
using (Html.BeginUmbracoForm("MemberUmbLogin", "MemberUmbLoginSurface"))
{
@Html.EditorFor(x => Model)
<input type="submit" />
}
<p>@TempData["Status"]</p>
}
Login Model
public class MemberUmbLoginModel
{
public string Username { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
public bool RememberMe { get; set; }
}
Surface Controller
public class MemberUmbLoginSurfaceController : SurfaceController
{
//
// GET: /MemberUmbLogin/
[HttpGet]
[ActionName("MemberUmbLogin")]
public ActionResult MemberUmbLoginGet()
{
return PartialView("MemberUmbLogin", new MemberUmbLoginModel());
}
[HttpGet]
public ActionResult MemberUmbLogout()
{
Session.Clear();
FormsAuthentication.SignOut();
return Redirect("/");
}
[HttpPost]
[ActionName("MemberUmbLogin")]
public ActionResult MemberUmbLoginPost(MemberUmbLoginModel model)
{
string returnUrl = GetValidReturnUrl(Request.UrlReferrer);
if (Membership.ValidateUser(model.Username, model.Password))
{
FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe);
if (Url.IsLocalUrl(returnUrl) && !String.IsNullOrWhiteSpace(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToCurrentUmbracoPage();
}
TempData["Status"] = "Invalid username or password";
return RedirectToCurrentUmbracoPage();
}
private static String GetValidReturnUrl(Uri uri)
{
string returnUrl = null;
if (uri != null && !String.IsNullOrWhiteSpace(uri.PathAndQuery) && uri.PathAndQuery.StartsWith("/") &&
!uri.PathAndQuery.StartsWith("//") && !uri.PathAndQuery.StartsWith("/\\"))
{
returnUrl = uri.PathAndQuery;
}
return returnUrl;
}
}
I'm using a custom MembershipProvider with the standard Umbraco Role Provider. I rely on the MembershipProvider to update the roles for a member based on my non-Umbraco database whenever they log in. The MembershipProvider then updates the member with the appropriate groups.
Note: Because I'm using Umbraco's role provider, I need to add the roles from my non-Umbraco database to Umbraco as "Member Groups".
Web.config
<!-- Membership Provider -->
<membership defaultProvider="CustomMembershipProvider" userIsOnlineTimeWindow="15">
<providers>
<clear />
<add name="CustomMembershipProvider" type="CustomUmbraco.MembershipProviders.CustomMembershipProvider" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="NetIDAlias" passwordFormat="Hashed" />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" passwordFormat="Hashed" />
</providers>
</membership>
<!-- Role Provider -->
<roleManager enabled="true" defaultProvider="UmbracoRoleProvider">
<providers>
<clear />
<add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider" />
</providers>
</roleManager>
MembershipProvider
public class CustomMembershipProvider : MembershipProvider
{
private String GetDomainFreeName(String fullName)
{
return fullName.Contains("\\") ? fullName.Substring(fullName.IndexOf("\\") + 1) : fullName;
}
public override bool ValidateUser(string username, string password)
{
DirectoryEntry directoryEntry = new DirectoryEntry(ConfigurationManager.ConnectionStrings["LDAPConnection"].ConnectionString, username, password);
DirectorySearcher searcher = new DirectorySearcher(directoryEntry);
String domainFreeName = GetDomainFreeName(username);
searcher.Filter = String.Format("(&(objectClass=user)(SAMAccountName={0})(!msExchUserAccountControl=2))", domainFreeName);
SearchResult result;
try
{
result = searcher.FindOne();
}
catch (COMException)
{
return false; // authentication failed
}
if (result != null)
{
NotReallyARoleProvider provider = new NotReallyARoleProvider();
provider.UpdateUserRoles(domainFreeName);
Member m = Member.GetMemberFromLoginName(domainFreeName);
Member.AddMemberToCache(m);
return true;
}
return false;
}
}
UpdateUserRoles methods
public void UpdateUserRoles(String username)
{
var groups = this.GetRolesForUser(username); // this is the method that gets the roles for your user.
this.RemoveUsersFromRoles(new[] { username }, this.GetAllRoles());
this.AddUsersToRoles(new[] { username }, groups);
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
foreach (String username in usernames)
{
Member m = Member.GetMemberFromLoginName(username);
if (m == null)
{
m = Member.MakeNew(username, MemberType.GetByAlias("Member"), new User(0));
m.LoginName = username;
}
roleNames.ForEach(group => m.AddGroup(MemberGroup.GetByName(group).Id));
m.Save(true);
}
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
foreach (String username in usernames)
{
Member m = Member.GetMemberFromLoginName(username);
if (m == null)
{
m = Member.MakeNew(username, MemberType.GetByAlias("Member"), new User(0));
m.LoginName = username;
}
roleNames.ForEach(group => m.RemoveGroup(MemberGroup.GetByName(group).Id));
m.Save(true);
}
}
Finally, the reason I don't use the ActiveDirectoryMembershipProvider is because I couldn't get sufficient permissions to modify accounts.
This solution is far from perfect, but it works for me. If you run into an issue where you log in to a page but it acts as though you're not in the correct group, OR if you remove a member from the Umbraco interface and they still show up when you call Member.GetMemberFromLoginName(username)
, then you may have to replace your Member saving code in your Provider with the following line once.
ApplicationContext.Current.Services.MemberService.DeleteMembersOfType(MemberType.GetByAlias("Member").Id);
That will clear out any members that are stored in the ethereal repository.
With all this code in place, users can select the groups they want to have access to content pages in the Umbraco backoffice, like normal.