4

I have an issue with public access in Umbraco 7.

I use a custom membership provider to authenticate the users by my CRM database. I set a rule to let access to authenticated (front-end) users only and I used a custom role provider to define authenticated users have the visitors role. If they are not authenticated, they are redirected to the login page.

public access

When I debug the website the user has the role :

Role Test

I am authenticated and the role for the current user is the good one.

Role Test

But I am still redirected to the login page ! I don't understand.

My role provider:

public class CustomRoleProvider : Umbraco.Web.Security.Providers.MembersRoleProvider
{
    const int SITE_ID = 6;

    public override string ApplicationName
    {
        get
        {
            return "Site";
        }
    }

    public override string[] GetAllRoles()
    {
        return new[] { Const.VISITORS_LABEL };
    }

    public override string[] GetRolesForUser(string username)
    {
        return new[] { Const.VISITORS_LABEL };
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="username"></param>
    /// <param name="roleName"></param>
    /// <returns></returns>
    public override bool IsUserInRole(string username, string roleName)
    {
        //every user is a visitor
        if(roleName == Const.VISITORS_LABEL)
        {
            return true;
        }

        else
        {
            return base.IsUserInRole(username, roleName);
        }
    }

    public override string[] GetUsersInRole(string roleName)
    {
        if(roleName == Const.VISITORS_LABEL)
        {
            using (var db = new CRMEntities())
            {
                var usersEmails = db.Customer_View.Where(x => x.SiteID == SITE_ID).Select(x=>x.Email).ToArray();
                return usersEmails;
            }
        }
        else
        {
            return base.GetUsersInRole(roleName);
        }
    }
}

Here is the controller I use for authentication:

public class MemberLoginSurfaceController : Umbraco.Web.Mvc.SurfaceController
{
    // The MemberLogin Action returns the view, which we will create later. It also instantiates a new, empty model for our view:

    [HttpGet]
    [ActionName("MemberLogin")]
    public ActionResult MemberLoginGet()
    {
        return PartialView("MemberLogin", new MemberLoginModel());
    }

    // The MemberLogout Action signs out the user and redirects to the site home page:

    [HttpGet]
    public ActionResult MemberLogout()
    {
        Session.Clear();
        FormsAuthentication.SignOut();
        return Redirect("/");
    }

    // The MemberLoginPost Action checks the entered credentials using the standard Asp Net membership provider and redirects the user to the same page. Either as logged in, or with a message set in the TempData dictionary:
    [HttpPost]
    [ActionName("MemberLogin")]
    public ActionResult MemberLoginPost(MemberLoginModel model)
    {
        if (Membership.ValidateUser(model.Username, model.Password))
        {
            FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe);

            return RedirectToCurrentUmbracoPage();
        }

        else
        {
            TempData["Status"] = "Invalid username or password";
            return RedirectToCurrentUmbracoPage();
        }
    }
}

My role provider is in the web.config and the Visitors role is detected as role in the administration panel.

<roleManager enabled="true" defaultProvider="CustomRoleProvider">
  <providers>
    <clear />
    <add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider" />
    <add name="CustomRoleProvider" type="*.UI.Helpers.CustomRoleProvider" />
  </providers>
</roleManager>

EDIT: I forgot the membership provider :

 public class MyMembershipProvider : Umbraco.Web.Security.Providers.MembersMembershipProvider
{

const int SITE_ID = 6;
    //we dont let user change their password using RC website
    public override bool AllowManuallyChangingPassword
    {
        get
        {
            return false;
        }
    }

    public override bool EnablePasswordReset
    {
        get
        {
            return false;
        }
    }

    public override bool EnablePasswordRetrieval
    {
        get
        {
            return false;
        }
    }

    public override bool ValidateUser(string username, string password)
    {
        Customer_View user;

        //just to avoid errors with uppercase letters
        username = username.ToLowerInvariant();

        using (var db = new CRMEntities())
        {
            user = db.Customer_View.SingleOrDefault(x => x.Email == username && x.SiteID == SITE_ID);

            //no user with this email
            if (user == null)
                return false;

            //check if password is same
            return user.Password == password;
        }
    }

    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
        //just to avoid errors with uppercase letters
        username = username.ToLowerInvariant();
        MembershipUser toReturn;

        using (var db = new CRMEntities())
        {
            Customer_View user = db.Customer_View.SingleOrDefault(x => x.Email == username && x.SiteID == SITE_ID);

            toReturn = user != null ? new MembershipUser(
                //provider name
                "MyMembershipProvider", string.Format("{0} {1}", user.FirstName, user.LastName),
                username, username, string.Empty, string.Empty, true, true, user.CreateDate, new DateTime(), new DateTime(), new DateTime(), new DateTime()) :

                null;

        }

        return toReturn;
    }

    public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
    {
        return this.GetUser(providerUserKey as string, userIsOnline);
    }


}

Every time I try to access a page that has specific access I get redirected to the login page even if I am authenticated:

login page

How can I solve it ?

Giu
  • 1,832
  • 2
  • 16
  • 31
  • UmbracoContext.Security.CurrentUser returns null even if I am authenticated ! It's the right track. I will add the MemberLoginSurfaceController for more details. – Giu May 12 '15 at 14:18
  • My bad. I wasn't clear, I want to let access to front-end authenticated users. UmbracoContext.Security.CurrentUser is for back-end users, right ? – Giu May 12 '15 at 15:26
  • yes, its for backoffice users. Your controller seem to be alright. Take a look at what is returned from `Umbraco.MemberIsLoggedOn()` and `Members.GetCurrentMember()` - also at line 6 – anikiforov May 12 '15 at 15:47
  • basically I think that your member is not authenticated. Please check the result of the above methods, and also check if the call to `Membership.ValidateUser` in your controller returns `true` – anikiforov May 12 '15 at 15:55
  • My member is authenticated. Membership.ValidateUser returns true every time. I added another picture to show that the role for the current user is the good one. – Giu May 12 '15 at 20:23
  • Well, I have just tried your setup (only changed `GetUsersInRole(string roleName)` implementation), and it works fine for me. One more question - how member can authenticate if the **Login** page according to your screenshot **is protected**? If that's not it - please post your `` section from your web config, and also try to republish protected pages. – anikiforov May 12 '15 at 20:57
  • I tried to limit access to a page at the same level than the login page I still have the issue. Login page is no longer protected. – Giu May 13 '15 at 14:45
  • I pass on this one. Your code and configuration are fine - they work just fine for me. Maybe you are using some custom configuration / code elsewhere, not just for role provider. – anikiforov May 15 '15 at 08:23
  • @GiuDo as far as I can see you redirect an authenticated member to the same page as the login page "return RedirectToCurrentUmbracoPage();" Try to redirect to another page. – Morten OC May 17 '15 at 07:55

2 Answers2

2

Why do you want to add custom role provider. Just keep it simple. If you have a registration page for the site you can assign member type and member role programmatically

And if you add member from backend, you can add "Visitor" role easily.

So in both cases "Visitor" role can be applied easily to all members and you can keep your page behind login for visitor role(all authenticated) easily.

EDIT: I have removed the code of adding role to user programmatically as that is not needed by you and solution is as below:

As you know for custom role provider and custom membership provider go hand in hand. You have added the custom membership provider and overrided ValidateUser method, but for all this to work you need to override two more methods of GetUser See the code for custom provider as below and it would work

MemberShipProvider

public class MyMembershipProvider : MembersMembershipProvider
{
    public override bool ValidateUser(string username, string password)
    {
        if (base.ValidateUser(username,password))
        {
            //if this is umbraco user validate by base method
            return true;
        }
        else
        {
            var allow = //add your validation code for CRM, I have checked if username is "tester" and allowed for testing purpose.
            return allow;
        }
    }
    // These two methods below which you have not overridden and need to override for public access to work

    public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
    {
        if(base.GetUser(providerUserKey, false)!=null)
            //if this is umbraco user add it as is.
            return base.GetUser(providerUserKey,userIsOnline);
        else
            //Add your CRM user, I do not have database, so added test user
            return new MembershipUser("UmbracoMembershipProvider", "tester", 1233, "tester@test.com", null, null, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
    }
    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
        if (true)//check if this is CRM user here
        {
            return new MembershipUser("UmbracoMembershipProvider", "tester", 1233, "tester@test.com", null, null, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
            //I am adding test user, you should create user from your CRM database
        }
        else
            return base.GetUser(username,false);

    }

}

EDIT2

Okay, I have debugged every bit of code in roleprovider and membershipprovider, after the user is logged in when user tries to access protected page, first GetUser(string username, bool userIsOnline) gets called which, if failed, returns login page, after this is success GetUser(object providerUserKey, bool userIsOnline) gets a call. If this returns null, Insufficient access page is shown else GetRolesForUser(string username) gets called. and upon all success page is shown. I have set break point on each method of both files, so only these 3 methods are involved. as you see login page, my guess is GetUser(string username, bool userIsOnline) this first call is failing somewhere.

Below are my files if it helps

roles config part

<roleManager enabled="true" defaultProvider="UmbracoRoleProvider">
  <providers>
    <clear />
    <!--<add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider" />-->
    <add name="UmbracoRoleProvider" type="Assembly.Providers.MyRolesProvider" />
  </providers>
</roleManager>

membership config part

<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
  <providers>
    <clear />
    <add name="UmbracoMembershipProvider" type="Assembly.Providers.MyMemberShipProvider, Assembly" enablePasswordRetrieval="false" enablePasswordReset="false" 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" />-->
    <add name="UsersMembershipProvider" type="Assembly.Providers.MyUserMembershipProvider, Assembly" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" passwordFormat="Hashed" />
  </providers>
</membership>

AccountController

public class AccountSurfaceController : Umbraco.Web.Mvc.SurfaceController
{
    [HttpPost]
    public ActionResult LoginForm(LoginModel model)
    {
        //model not valid, do not save, but return current umbraco page
        if (!ModelState.IsValid)
        {
            //Perhaps you might want to add a custom message to the TempData or ViewBag
            //which will be available on the View when it renders (since we're not 
            //redirecting)          
            return CurrentUmbracoPage();
        }

        // Login
        if (Membership.ValidateUser(model.Username, model.Password))
        {
            FormsAuthentication.SetAuthCookie(model.Username, false);
            return RedirectToCurrentUmbracoUrl();
        }
        else
        {
            ModelState.AddModelError("Username", "Username is not valid");
            return CurrentUmbracoPage();
        }
    }
}

public class LoginModel
{
    [Required]
    public string Username { get; set; }

    [Required]
    [DataType(DataType.Password)] 
    public string Password { get; set; }
}

RolesProvider

public class MyRolesProvider : MembersRoleProvider
{
    const string VISITORS_LABEL = "Visitor";

    public override string[] GetAllRoles()
    {
        var roles = base.GetAllRoles().ToList();
        roles.Add(VISITORS_LABEL);
        return roles.ToArray();
    }
    public override string[] FindUsersInRole(string roleName, string usernameToMatch)
    {
        if(roleName== VISITORS_LABEL)
        {
            var users = ApplicationContext.Current.Services.MemberService
                .GetAllMembers().Select(m => m.Email).ToList();
            users.Add("tester@test.com");
            return users.ToArray();
        }
        return base.FindUsersInRole(roleName, usernameToMatch);
    }
    public override bool RoleExists(string roleName)
    {
        if(roleName == VISITORS_LABEL)
        {
            return true;
        }
        return base.RoleExists(roleName);
    }
    public override string[] GetRolesForUser(string username)
    {
        var roles = base.GetRolesForUser(username).ToList();
        roles.Add(VISITORS_LABEL);
        return roles.ToArray();
    }
    public override bool IsUserInRole(string username, string roleName)
    {
        if(roleName == VISITORS_LABEL)
        {
            return true;
        }
        return base.IsUserInRole(username, roleName);
    }
    public override string[] GetUsersInRole(string roleName)
    {
        if(roleName == VISITORS_LABEL)
        {
            var list = ApplicationContext.Current.Services.MemberService
                .GetAllMembers().Select(m => m.Email).ToList();
                list.Add("tester@test.com");
            return list.ToArray();
        }
        return base.GetUsersInRole(roleName);
    }
}

EDIT3:

I reproduced your scenario, it is related to web.config configuration

when I kept web config for membership as below it did not hit my provider unless i called

<membership defaultProvider="MyMembershipProvider" userIsOnlineTimeWindow="15">
  <providers>
    <clear />
    <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="MyMembershipProvider" type="Umbraco724.Providers.MyMembersMembershipProvider, Umbraco724" />
    <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>

But when changed it to as given below it worked. please check the difference in config carefully.

<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
  <providers>
    <clear />
    <add name="UmbracoMembershipProvider" type="Umbraco724.Providers.MyMembersMembershipProvider, Umbraco724"  minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Visitor" 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>

I think umbraco only takes one provider for members as well as users. Also when its name should be UmbracoMembershipProvider only. when I kept that different then also there was error.

Chaitanya Gadkari
  • 2,669
  • 4
  • 30
  • 54
  • 1
    I have to use a custom membership provider to access our CRM database and I don't want to duplicate data and save them in the umbraco database. The custom role provider is mandatory with the custom membership provider. – Giu May 19 '15 at 13:55
  • 1
    @GiuDo: you are right, I created a role provider and reproduced the issue. If that custom role is applied to the member from umbraco backend, it works fine, the the user is also from other source, it redirects to login. I'll update you if I find out the missing link. – Chaitanya Gadkari May 20 '15 at 06:12
  • updated my answer and tested, with this all should work – Chaitanya Gadkari May 20 '15 at 08:00
  • FYI, `GetUser(string username, bool userIsOnline)` gets called first and then `GetUser(object providerUserKey, bool userIsOnline)` just to clarify and I dont know why this is needed, but thats what is needed to be done. – Chaitanya Gadkari May 20 '15 at 08:04
  • Thank you for your comment. I noticed that my provider inherited from UsersMembershipProvider instead of MembersMembershipProvider, I changed that. I thought it would solve it but finally it didn't. I added my membership provider. – Giu May 20 '15 at 14:35
  • `UsersMembershipProvider` is for backend users.. you need to inherit from `MembersMembershipProvider`.... also did you override `GetUser` as suggested. It worked out for me... I found it on one of umbraco forum..It will work if `GetUser` returns proper logged in member. – Chaitanya Gadkari May 20 '15 at 17:35
  • It is done. I added my membership provider so you can have a look. – Giu May 20 '15 at 18:41
  • there are two boolean values which you have set as `true,true` and I have set as `true,false` can you change that and check, I think 2nd value is for IsUserLocked or something like that which you are setting as true. Also can you debug and check these methods are actually returning something and not null? – Chaitanya Gadkari May 20 '15 at 19:15
  • I tried with this value, it has no effect, sorry. Membership.GetUser() returns an user, as it is in the 3rd picture – Giu May 20 '15 at 20:41
  • Okay.. I'll compare in the morning what differences are there in our code... Because mine is working. – Chaitanya Gadkari May 20 '15 at 20:46
  • 1
    From my debugging `GetUser(string username, bool userIsOnline)` is where you have problem.. see my all files if it helps, updated answer – Chaitanya Gadkari May 21 '15 at 06:37
  • You are right, I am redirected to the login page after the call to GetUser(string ... I will investigate – Giu May 21 '15 at 14:34
  • I commented the lines //var user = Membership.GetUser(); //var roles = Roles.GetRolesForUser(); //var test = roles; in master.cshtml and now I can't debug my membership provider or my role provider, do I have to clear some cache to enable it again ? – Giu May 21 '15 at 15:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/78456/discussion-between-chaitanya-gadkari-and-giu-do). – Chaitanya Gadkari May 21 '15 at 16:28
  • You were right. The name of my provider was the reason it did not hit the provider when I was on that page. My previous tests with master page were the reason why I didn't see it before. Thank you very much, does it look like a bug I can report to the umbraco team ? – Giu May 22 '15 at 14:07
  • 1
    Yes, i was also thinking same – Chaitanya Gadkari May 22 '15 at 14:16
0

With Umbraco's public access, when a user is logged out and they try to access a protected page they'll be presented with the login page. However, the URL in their address bar will be that of the page that they tried to access.

Upon validating the user RedirectToCurrentUmbracoPage() will actually perform a full redirect to the login page and the URL in the address bar will update accordingly. What you actually want to do is to redirect them to the current URL. You can do this by replacing the first RedirectToCurrentUmbracoPage() in your MemberLoginPost method with RedirectToCurrentUmbracoUrl().

You're also using RedirectToCurrentUmbracoPage() for when the user's credentials are incorrect, which will also cause a full redirect to the login page. If you simply return the CurrentUmbracoPage() to the user then everything should work properly. See the updated method below:

[HttpPost]
[ActionName("MemberLogin")]
public ActionResult MemberLoginPost(MemberLoginModel model)
{
    if (Membership.ValidateUser(model.Username, model.Password))
    {
        FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe);

        return RedirectToCurrentUmbracoUrl();
    }

    else
    {
        TempData["Status"] = "Invalid username or password";

        return CurrentUmbracoPage();
    }
}
Rob Purcell
  • 1,285
  • 1
  • 9
  • 19
  • I tried with these functions but it has no effect, I added a screenshot of the login page for more details. I don't think the login page redirection is related to this issue, even when I manually type the url (as authenticated user) the content of my page is the login page. – Giu May 19 '15 at 14:08