Thoughts on preview and protected pages

247 views
Skip to first unread message

Andrew Swerlick

unread,
Apr 8, 2014, 1:45:03 AM4/8/14
to umbra...@googlegroups.com
A few months ago I started experimenting with how preview works with protected pages, and realized umbraco really doesn't support this scenario right now. I naively thought this was a simple bug that I could add a quick fix for, and submitted the pull request found here https://github.com/umbraco/Umbraco-CMS/pull/331. However as Sebastian noted, my approach really didn't consider the complexity of the issue, and probably stood to make things worse rather than better. Since then I've been thinking more on the issue and I have a couple of ideas I wanted to throw out to the community for feedback on.

As Sebastian points out, once you're in the business of handling pages which are restricted to certain roles, you have to account for the fact that developers may have put additional roles based logic into those pages using the built in ASP.Net membership and roles framework. This adds a whole host of issues that simply aren't a problem when you're previewing as an anonymous user.

To solve this issue, I started kicking around the idea of "Preview Profiles." These would be actual entries in the umbraco members table, that could be edited using the standard back office member UI. For all intents and purposes they would be members, though we'd probably want to take some steps to prevent them from logging on and showing up in the members tree.

When previewing, instead of just inserting the preview badge into the page, we would inserting a dropdown listing all the preview profiles. The previewer could select which one they wanted to use to view the page. Selecting a new profile would cause the system to actually log that user in, so all aspects of the ASP.Net membership framework would behave appropriately.

Since these profiles are actually umbraco members from the membership providers perspective, administrators could edit their membership groups to ensure that all important preview scenarios are covered.

The other thing I like about this approach is that they key components can be implemented without actually touching the umbraco code base. I can put together an extension that creates a new member type, or adds properties to the existing member types, includes a partial view and appropriate surface controller for the switcher, and create a http module that handles ensuring the login. I already created a rudimentary proof of concept of the http module, copied below. 

public class ProtectedPreview : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += context_AuthenticateRequest;
        }

        void context_AuthenticateRequest(object sender, EventArgs e)
        {
            if (UmbracoContext.Current != null && UmbracoContext.Current.InPreviewMode)
            {
                //Hardcoding user for testing, in reality would want to extract from a cookie set by the switcher
                var user = Membership.GetUser("te...@test.com");
                var ticket = new FormsAuthenticationTicket(1, user.UserName, DateTime.Now, DateTime.Now.AddMinutes(30),
                    true, "", FormsAuthentication.FormsCookiePath);


                var identity = new FormsIdentity(ticket);
                //set the principal object
                var principal = new GenericPrincipal(identity, Roles.GetRolesForUser(user.UserName));
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = principal;
                }
            }
        }

        public void Dispose()
        {
            
        }
    }


The only concern I have with this approach is that the module above overwrites the HttpContext.Current.User property.  In the Umbraco.Web.UmbracoModule class the ShouldAuthenticateRequest specifically checks if we're in preview mode. If ShouldAuthenticateRqeust returns true the code later explicitly sets that property to the current back office user. My approach would be overriding that. I did some initial testing, and didn't see any issues, but maybe someone more familiar with the codebase can way in on this specific issue as well as the general approach.

Stephen Gay

unread,
Apr 9, 2014, 7:36:07 AM4/9/14
to umbra...@googlegroups.com
I'm assuming this is the post you mention in the other post, right?

Then... if I understand correctly, you would want to preview the site "as seen by a logged-in member within a given role". However you certainly don't want to change the "user currently logged into the back-office" as that is what is used in order to retrieve the preview XML.

I must admit I don't know a lot about members. At least not enough to figure out whether it is possible to log both a (back-office) user and a (front-end) member at the same time? Looks like we're relying on HttpContext.Current.User.Identity, which would imply that only one can be logged-in at a time.

So, unless I miss something... I'm afraid what you're trying to do won't work.

That being said... we seem to rely on HttpContext.Current.User.Identity in MembershipProviderExtensions.GetCurrentUserName() and it would be rather easy to override that code in order to return any username. So I'd rather investigate how, instead of actually logging a member in, it would be possible for the membership provider to pretend that a member is there.

Making sense?
Stephan

Andrew Swerlick

unread,
Apr 13, 2014, 8:21:37 PM4/13/14
to umbra...@googlegroups.com
Sorry to take so long to reply. I took a closer look at this over the weekend, and surprisingly it does work. But it looks like that's only because of some fortunate implementation details that I probably shouldn't be relying on. Basically, early on in the request, Umbraco stores the current user in UmbracoContext.UmbracoUser. At the start of the request, that's the backoffice user. The PublishedContentCache class pulls from the stored value, even after HttpContext.Current.User.Identity has been swapped out. 

I have a project up in github where you can test this. https://github.com/AndrewSwerlick/UmbracoPreviewSpike/tree/master.

I think the approach you outline does make the most sense. I'm thinking having a membership provider that checks if we're in preview mode, and if so reads a separate cookie to determine the current member. Thanks for the guidance.


On Tuesday, April 8, 2014 1:45:03 AM UTC-4, Andrew Swerlick wrote:
A few months ago I started experimenting with how preview works with protected pages, and realized umbraco really doesn't support this scenario right now. I naively thought this was a simple bug that I could add a quick fix for, and submitted the pull request found here https://github.com/umbraco/Umbraco-CMS/pull/331. However as Sebastian noted, my approach really didn't consider the complexity of the issue, and probably stood to make things worse rather than better. Since then I've been thinking more on the issue and I have a couple of ideas I wanted to throw out to the community for feedback on.

As Sebastian points out, once you're in the business of handling pages which are restricted to certain roles, you have to account for the fact that developers may have put additional roles based logic into those pages using the built in ASP.Net membership and roles framework. This adds a whole host of issues that simply aren't a problem when you're previewing as an anonymous user.

To solve this issue, I started kicking around the idea of "Preview Profiles." These would be actual entries in the umbraco members table, that could be edited using the standard back office member UI. For all intents and purposes they would be members, though we'd probably want to take some steps to prevent them from logging on and showing up in the members tree.

When previewing, instead of just inserting the preview badge into the page, we would inserting a dropdown listing all the preview profiles. The previewer could select which one they wanted to use to view the page. Selecting a new profile would cause the system to actually log that user in, so all aspects of the ASP.Net membership framework would behave appropriately.

Since these profiles are actually umbraco members from the membership providers perspective, administrators could edit their membership groups to ensure that all important preview scenarios are covered.

The other thing I like about this approach is that they key components can be implemented without actually touching the umbraco code base. I can put together an extension that creates a new member type, or adds properties to the existing member types, includes a partial view and appropriate surface controller for the switcher, and create a http module that handles ensuring the login. I already created a rudimentary proof of concept of the http module, copied below. 

public class ProtectedPreview : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += context_AuthenticateRequest;
        }

        void context_AuthenticateRequest(object sender, EventArgs e)
        {
            if (UmbracoContext.Current != null && UmbracoContext.Current.InPreviewMode)
            {
                //Hardcoding user for testing, in reality would want to extract from a cookie set by the switcher
                var user = Membership.GetUser("test2@test.com");
                var ticket = new FormsAuthenticationTicket(1, user.UserName, DateTime.Now, DateTime.Now.AddMinutes(30),
                    true, "", FormsAuthentication.FormsCookiePath);


                var identity = new FormsIdentity(ticket);
                //set the principal object
                var principal = new GenericPrincipal(identity, Roles.GetRolesForUser(user.UserName));
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = principal;
                }
            }
        }

        public void Dispose()
        {
            
        }
    }

Andrew Swerlick

unread,
Apr 13, 2014, 10:12:40 PM4/13/14
to umbra...@googlegroups.com
Looking closer, overriding GetCurrentUserName isn't going to be so straightfoward. Both it and GetCurrentUser are extension methods. From what I'm seeing, it looks like the module might be the best approach for now, even though it relies on implementation details of the PublishedContentCache. Any thoughts on whether the main code base can change to allow for more control over how the current user is retrieved? My initial thought is that the bulk of GetCurrentUser could be brought into the UmbracoMembershipProvider class as a virtual method that can be overridden. The extension method could then check and see if the current membership provider can be cast to  UmbracoMembershipProvider. If it can, then it uses the method on the provider, and if not it uses the current approach. Thoughts?

Shannon Deminick

unread,
Apr 13, 2014, 10:54:57 PM4/13/14
to umbra...@googlegroups.com
I'm assuming we are talking about v7 here right?

Just some background - you can def log in to the back office and front-end at the same time. The front-end uses regular forms-auth, the way that was intended by .Net. The back-office uses custom forms-auth with a custom cookie. As noted above, we only set the current thread's user and culture properties if we detect a request is for the back office (installer, etc... which is determined by UmbracoModule.ShouldAuthenticateRequest). For a front-end request we don't set the User object, this is done automatically by .Net. 

So I think your solution will work:
  • In an http module (or our current UmbracoModule) on authenticate request, check if we are previewing and if ShouldAuthenticateRequest == false (meaning it's def a front-end request)
  • Read from the preview cookie to see if a specific member username has been set, if so then manually create the forms auth ticket, set the User object and Culture (if necessary)
    • Some installations will have custom authorization and might have their own identity format, so this should be extensible (i.e. events)
    • Our current preview cookie is just a flag but can easily be extended to have more information in it such as a member username to preview with

Andrew Swerlick

unread,
Apr 14, 2014, 9:25:49 AM4/14/14
to umbra...@googlegroups.com
Shannon,

That's more our less the approach I'm taking. I think my major concern was that Stephen pointed out that the preview content is loaded by checking the currently logged in back office user. I was afraid that by manually setting HttpContext.Current.User I would interfere with the code that loads the preview content. 

In practice this doesn't happen because the preview content is ready from UmbracoContext.User. This loads the back office user from HttpContext.Current.User the first time it's read, and then caches the result in a private variable for subsequent requests. Since I don't set HttpContext.Current.User until later in the pipeline, the back office user has already been cached, ensuring that the back office user is used to load the preview content.

However, all this seems like an implementation detail I probably shouldn't be relying on, particularly since UmbracoContext.User is marked as obsolete. If something changes, and preview stops using this cached value, I could see my approach breaking. 

Shannon Deminick

unread,
Apr 14, 2014, 10:10:28 PM4/14/14
to umbra...@googlegroups.com
Hi Andrew,

I agree that potentially how this is working for you currently may stop working in the future. Definitely something to consider when implementing more of the "new cache" and how user objects are read. I'll have to review with Stephen about how the preview content is loaded and where it checks the user so that we can streamline this and ensure it's not just working by fluke :)
Reply all
Reply to author
Forward
0 new messages