Can I use Basic Authentication in Umbraco without a Login page - basic-authentication

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.

Related

SignalR encrypt QueryString parameters

I am using SignalR on a Asp.net Web Api project.
I am connecting to the hubs from separate Asp.net MVC projects.
Everything works fine until now.
However, i need to implement Authentication on the SignalR Hubs, in order to do this, i simply need a token to be send as QueryString parameter:
// Hub implementation on Asp.Net Web Api project
public class AppHub : Hub
{
public override async Task OnConnected()
{
string token = Context.QueryString["token"];
var validateResult = ValidateRequestService.ValidateToken(token);
Groups.Add(Context.ConnectionId, validateResult.UserName);
base.OnConnected();
}
}
// Javascript implementation on Asp.net MVC project
$.connection.hub.url = 'http://webApiProject.com/signalr';
$.connection.hub.qs = { 'token': '#(ViewBag.SessionToken)' };
This works.
The problem is that i store sensitive information (token) on the client (browser). If a hacker inspects the source code of the page, it can easily see the token key.
Is there any way to encrypt / decrypt the query string parameter so it will be encrypted on the client side?
I can easily encrypt it on the client, but the problem is that it will be sent encrypted to the Web Api server as well.
Would an HttpModule work in this case?
To implement a custom memebership provider implement System.Web.Security.MembershipProvider
Example from one of my projects
public class MembershipProvider : System.Web.Security.MembershipProvider
{
...
public override bool ValidateUser(string username, string password)
{
return DependencyResolver.Current.GetService<IUserManager>().ValidateUser(username, password);
}
}
If you need roles implement role provider System.Web.Security.RoleProvider
public class RoleProvider : System.Web.Security.RoleProvider
{
...
public override string[] GetRolesForUser(string username)
{
var user = dependencyResolver.Current.GetService<IUserManager>().GetUserBy(username);
return user.Roles.Select(r => r.Name).ToArray();
}
}
All other methods can be left unimplemented for basic functionality
In the web config do
<membership defaultProvider="MyProvider" userIsOnlineTimeWindow="20">
<providers>
<remove name="AspNetSqlProvider" />
<add name="MyProvider" type="MyApp.Web.Common.Membership.MembershipProvider" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" passwordFormat="Hashed" applicationName="/" />
</providers>
</membership>
<roleManager enabled="true" defaultProvider="MyRoleProvider">
<providers>
<clear />
<add name="MyRoleProvider" applicationName="/" type="MyApp.Web.Common.Membership.RoleProvider" />
</providers>
</roleManager>
You can then login like Forms auth was enabled for example
[HttpPost]
public bool Login([FromBody]CredentialsViewModel credentials)
{
if (Membership.ValidateUser(credentials.Username, credentials.Password))
{
FormsAuthentication.SetAuthCookie(credentials.Username, credentials.Remember);
return true;
}
return false;
}

Site redirecting to login page on every navigate action

I've got an MVC app that's an API documentation site, sitting behind forms auth There are really only two pages in the site... home, which lists (at a high level) all the endpoints on the site, as well as the data objects. You can click on any one of these data objects/endpoints, and be taken to a details page.
Each of my pages are decorated with the [Authorize(Roles="role,names,here")] attribute. As expected, when you enter the site, you're redirected to the login page, and any subsequent requests simply work.
However, yesterday the site started acting up and I'm not sure why. After initial login, the page you requested loads just fine. However, any time you click a link to navigate to any of the other pages, users are redirected to the login page, and credentials no longer work.
Any thoughts as to what's caused this, and how I can fix it?
[Edit]
I don't know why, but my sessionState config was commented out in my web.config (I didn't do that, and as I said, this was working 48 hrs ago), but it appears uncommenting this fixed it:
<sessionState mode="InProc" customProvider="DefaultSessionProvider">
<providers>
<add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="dbConn"/>
</providers>
</sessionState>
It was better to show us your class providing session.But use this:
you will have an account controller like this:
UserApplication userApp = new UserApplication();
SessionContext context = new SessionContext();
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User user)
{
var authenticatedUser = userApp.GetByUsernameAndPassword(user);//you get the user from your application and repository here
if (authenticatedUser != null)
{
context.SetAuthenticationToken(authenticatedUser.UserId.ToString(),false, authenticatedUser);
return RedirectToAction("Index", "Home");
}
return View();
}
public ActionResult Logout()
{
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
And your SessionContext will be like this:
public class SessionContext
{
public void SetAuthenticationToken(string name, bool isPersistant, User userData)
{
string data = null;
if (userData != null)
data = new JavaScriptSerializer().Serialize(userData);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, name, DateTime.Now, DateTime.Now.AddYears(1), isPersistant, userData.UserId.ToString());
string cookieData = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieData)
{
HttpOnly = true,
Expires = ticket.Expiration
};
HttpContext.Current.Response.Cookies.Add(cookie);
}
public User GetUserData()
{
User userData = null;
try
{
HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
userData = new JavaScriptSerializer().Deserialize(ticket.UserData, typeof(User)) as User;
}
}
catch (Exception ex)
{
}
return userData;
}
}

Unexpected results when using UPN vs legacyUsername vs Shortname validation against AD

I'm using the AD Membership provider to validate user names and am having issues getting anything other than user#upnDomain.com to work.
Is it possible to get the other username formats to work?
Code
MembershipProvider domainProvider;
domainProvider = Membership.Providers["MyADMembershipProvider"];
if (domainProvider.ValidateUser("zzTest123", "pass"))
{
}
if (domainProvider.ValidateUser(#"PARTNERSGROUP\zzTest123", "pass"))
{
}
if (domainProvider.ValidateUser("zzTest123#company.com", "pass"))
{
}
if (domainProvider.ValidateUser("zzTest123#testfirm.com", "pass"))
{
// this is the UPN and the only one that works.
}
Web.config
<authentication mode="Forms">
<forms loginUrl="~/Account/Login.aspx" name=".ADAuthCookie" timeout="10" />
</authentication>
<membership>
<providers>
<add name="MyADMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="TestDomain1ConnectionString" />
</providers>
</membership>
Based on my testing the Membership provider only works with the UPN. To implement support for other types, override ActiveDirectoryMembershipProvider's ValidateUser function and add some variation of the following:
//
// Will validate UPN, shortname only, or domain prefixed (domain\user)
public bool IsAuthenticated( string usr, string pwd)
{
bool authenticated = false;
DirectorySearcher dseSearcher=null;
DirectoryEntry entry = null;
try
{
dseSearcher = new DirectorySearcher();
string rootDSE = dseSearcher.SearchRoot.Path;
entry = new DirectoryEntry(rootDSE, usr, pwd);
object nativeObject = entry.NativeObject;
authenticated = true;
}
catch (DirectoryServicesCOMException cex)
{
//not authenticated; reason why is in cex
}
catch (Exception ex)
{
//not authenticated due to some other exception [this is optional]
}
finally
{
dseSearcher.Dispose();
entry.Dispose();
}
return authenticated;
}
Be aware that the System.DirectoryServices.AccountManagement namespace will only validate the shortname, the UPN, but doesn't appear to validate DOMAIN\Username accounts.
The following code will throw an exception if a username is passed in DOMAIN\Username format
"LdapException: A local error occurred."
var ctx = new PrincipalContext(ContextType.Domain);
if (ctx.ValidateCredentials(username,password , ContextOptions.Negotiate))
{
}

How to Override createUser() ASP .NET Membership method to display custom error message?

How to Override createUser() Membership method to display custom error message when password check fails??
I Used the Web Site Administration Tool, which provides a wizard-like interface for creating new users. (To start this tool, click ASP.NET Configuration on the Website menu in the Microsoft Visual Studio)
Web.Config file:
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear />
<add name="MyMembershipProvider" type="BlueDDApp.Controllers.MyMembershipProvider" connectionStringName="ApplicationServices" enablePasswordRetrieval="false" minRequiredPasswordLength="8" minRequiredNonalphanumericCharacters="0" passwordStrengthRegularExpression="^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])|(?=.*[a-z])(?=.*[A-Z])(?=.*[!%,.;:])|(?=.*[a-z])(?=.*[0-9])(?=.*[!%,.;:])|(?=.*[A-Z])(?=.*[0-9])(?=.*[!%,.;:])$" passwordAttemptWindow="10" applicationName="/" />
</providers>
</membership>
Custom Membership class::
public class MyMembershipProvider : SqlMembershipProvider
{
public MyMembershipProvider()
{
//Membership.ValidatingPassword += new MembershipValidatePasswordEventHandler(OnValidatePassword);
ValidatingPassword += ValidatePassword;
}
/* public override MembershipUser CreateUser( string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
ValidatingPassword += ValidatePassword;
return base.CreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status);
}*/
void ValidatePassword(object sender, ValidatePasswordEventArgs e)
{
Regex check = new Regex("^(?i)(?!.*" + e.UserName + ").*$");
if (!check.IsMatch(e.Password))
{
e.FailureInformation = new HttpException("blah blah");
e.Cancel = true;
}
}
}
If you are using asp:CreateUserWizard control, which I presume you are ( it will connect to a membership provider from your web.config ), then :
In Design mode, if you click on this control, in the top right corner you have an icon, sort of an arrow, and there you can choose "Customize Create User Step" option. This will transform the control, expanding it into a separate controls that are used inside. Now you can access error message ( Literal control ) and change it to display static message, or display dynamically changing messages from code.
You can also add events to the CreateUserWizard like CreatingUser, CreateUserError and CreatedUser which will let you customize the behavior and how the creation is being used even more.
Here is a great sample about custom MembershipUser:
Sample Membership Provider Implementation

Integration between our Asp.NET MVC form authentication and Office 365

I have the following:-
1) Intranet Asp.net MVC-4 web application, which currently authenticate the users using form authentication with our on-premises active directory through ldap string.
2) So users enter their username and password >> the application will authenticate their user names and passwords from AD and login them accordingly.
3) Now our organization will turn off the on-pressies AD, and we will migrate to office 365.
so i am not sure if i can modify my asp.net mvc-4 intranet (access from our internal network only) to authenticate the entered username and password from office 365 instead of using ldap string?
currently the login is been performed as follow:-
the login post action method:-
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Login(LoginModel model, string returnUrl)
{
MembershipProvider domainProvider;
domainProvider = Membership.Providers["Domain1ADMembershipProvider"];
if (ModelState.IsValid)
{
// Validate the user with the membership system.
if (domainProvider.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
List<String> domains2 = new List<String>();
domains2.Add("AD-*****GROUP");
ViewBag.Domains = domains2;
return View(model);
}
}
List<String> domains = new List<String>();
domains.Add("AD-*********GROUP");
ViewBag.Domains = domains;
return View(model);
}
web.config contain the ldap to the on-premises AD and username/password settings:-
<membership>
<providers>
<add name="Domain1ADMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="Domain1ConnectionString" connectionUsername="********8" connectionPassword="********" attributeMapUsername="sAMAccountName" />
</providers>
</membership>
<connectionStrings>
<add name="Domain1ConnectionString" connectionString="LDAP://ad-*****roup.intra/OU=TDM,DC=ad-***group,DC=intra" />
i do not have any idea from where to start and if i can achive what i am looking for,, and if this is supported or not?

Resources