U4-4219 - Can't Preview protected pages

Created by Andrew Swerlick 12 Feb 2014, 15:03:10 Updated by YodasMyDad 08 Sep 2016, 06:10:38

Is duplicated by: U4-4817

Is duplicated by: U4-6531

Is duplicated by: U4-7358

Is duplicated by: U4-7528

Relates to: U4-6672

It appears that back office users are unable to preview protected pages. Hitting the Preview button on a protected page brings up the login page designated for the protected page. No credentials seem to be able to get me past this page.

Marked as a breaking change - please read if you have custom OWIN Authentication middleware

This is because if you are using custom OWIN startup code that specified custom Authentication providers, this may lead to Preview no longer working. In which case you need to apply a work-around.

In 7.4.2, here's the new UmbracoDefaultOwinStartup class: https://github.com/umbraco/Umbraco-CMS/blob/af1fe425a20d7e009b6fd0462b52297172da10c2/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs the startup logic has been split into 2 methods that can be overridden:

  • ConfigureServices - Configures services to be created in the OWIN context (CreatePerOwinContext)
  • ConfigureMiddleware - Configures middleware to be used (i.e. app.Use...)

By default Umbraco will call these 3 extension methods in ConfigureMiddleware: https://github.com/umbraco/Umbraco-CMS/blob/af1fe425a20d7e009b6fd0462b52297172da10c2/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs#L55

  • UseUmbracoBackOfficeCookieAuthentication
  • UseUmbracoBackOfficeExternalCookieAuthentication
  • UseUmbracoPreviewAuthentication

The important part is that UseUmbracoPreviewAuthentication must be used after all Authentication providers have been registered since it must execute on PipelineStage.PostAuthenticate at a minimum. If you register custom Authentication providers after UseUmbracoPreviewAuthentication and those providers execute on PipelineStage.Authenticate (which is the default), then it will force the PreviewAuthenticationMiddleware to execute on PipelineStage.Authenticate instead of PipelineStage.PostAuthenticate which will not work.

So if you have a custom OWIN startup class you can now override ConfigureMiddleware register Umbraco's Authentication middelwares like:

app.UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate)
     .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate);

Then register your own custom Authentication middlewares if you have any, and then finally register the preview middleware:

app.UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.PostAuthenticate);

Comments

Andrew Swerlick 18 Mar 2014, 13:24:24

Has anyone else seen this issue? It's potentially a big problem for the extranet project I'm working on since it basically completely breaks the preview functionality for a majority of our pages. I've tried to dig into the code to trace the root cause, but haven't been able to find much


Andrew Swerlick 25 Mar 2014, 03:43:04

Created a pull request with a fix. https://github.com/umbraco/Umbraco-CMS/pull/331


Andrew Swerlick 03 Apr 2014, 18:25:06

Pull request retracted. See https://github.com/umbraco/Umbraco-CMS/pull/331


Chad T 20 Jun 2014, 03:01:21

Hitting this issue here and it's a big problem! Realised I had created a dupe, will close that ticket: http://issues.umbraco.org/issue/U4-4817

Edit: I should note that if we're logged in to the front end as well, we get spat into a redirect loop.


Sunshine Lewis 03 Jul 2014, 21:41:57

I'm not getting the redirect loop but I also don't have a login page designated in the public access settings. Instead the back office preview gives a 404 (which is what the front end users see)


svdbrg 14 May 2015, 18:50:13

I'm still getting this error in 7.2.4. Any progress on this would be very appreciated.


Mirko Matytschak 15 May 2015, 07:39:27

After upgrading from 7.1.6 to 7.2.5 it worked for me. As far as I remember, I had to make sure, that the domain is allowed to show popups.


svdbrg 15 May 2015, 20:59:17

@mirkomaty Strange, I just upgraded to 7.2.5 (from 7.2.4) and still can't get it to work. How do you protect pages, role based or single user? Do you authenticate as a member when previewing, or can you preview as a user?


Mirko Matytschak 16 May 2015, 14:13:56

@svdbrg Protection is role based, I log in into the frontend before previewing. We have different contents for different roles, so our customer has to do a client login before previewing in order to see the right content.


Chris Evans 05 Aug 2015, 14:52:41

We're also encountering this bug, we were on 7.2.4 so upgraded to 7.2.8 but that hasn't fixed it. Issue seems to be that the membership cookies are not valid for the preview session, so even if the user tries to log in with a valid member login in the preview window, the login fails and they are stuck on the login screen. Because this particular site has nearly all content behind a membership login, it essentially means preview is entirely broken for the content admins.


Nathan Skidmore 04 Sep 2015, 08:50:25

I am experiencing the same issue in Umbraco 7.2.0. I am using the form:

@using (Html.BeginUmbracoForm("HandleLogin"))

When using the Public Access feature in Umbraco, the secure page will always display the login page unless the authentication cookie exists (yourAuthCookie). In preview mode, the authentication cookie doesn't get created by the form submission.

I have looked into this further by running ILSpy on the HandleLogin action. This is the method that runs:

public ActionResult HandleLogin([Bind(Prefix = "loginModel")] LoginModel model) { if (!base.ModelState.IsValid) { return base.CurrentUmbracoPage(); } if (!base.Members.Login(model.Username, model.Password)) { base.ModelState.AddModelError("loginModel", "Invalid username or password"); return base.CurrentUmbracoPage(); } if (!model.RedirectUrl.IsNullOrWhiteSpace()) { return this.Redirect(model.RedirectUrl); } base.TempData["LoginSuccess"] = true; return base.RedirectToCurrentUmbracoPage(); } When debugging this the following method returns true.

base.Members.Login(model.Username, model.Password) Within that method the Forms Authentication cookie should be created.

public bool Login(string username, string password) { MembershipProvider membersMembershipProvider = MembershipProviderExtensions.GetMembersMembershipProvider(); if (!membersMembershipProvider.ValidateUser(username, password)) { return false; } MembershipUser user = membersMembershipProvider.GetUser(username, true); if (user == null) { LogHelper.Warn("The member validated but then no member was returned with the username " + username, new Func[0]); return false; } FormsAuthentication.SetAuthCookie(user.UserName, true); return true; }

The fact that it returns true suggests the cookie WAS created. However something else must be going on after this process because the cookie doesn't make it to the browser.


Sebastiaan Janssen 15 Oct 2015, 17:47:43

As a workaround for now you could try to get a logged in member like so:

IPublishedContent member = null; var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; if (authCookie != null) { var cookieValue = authCookie.Value; var decrypted = FormsAuthentication.Decrypt(cookieValue);

    if (decrypted != null)
    {
        member = Members.GetByUsername(decrypted.Name);
    }
}


Sebastiaan Janssen 15 Oct 2015, 17:52:18

This, however, depends on how your login page works, logging in worked fine for me, and the login logic is only:

if (Members.Login(model.Username, model.Password)) return Redirect(model.RedirectUrl);

The code in the previous comment to get a member is to make sure you get the logged in member in the frontend and not the backoffice user.


Barry Fogarty 13 Nov 2015, 12:25:05

This workaround does not work for me in v7.3. I have overridden MembershipHelper to provide my own Login method and updated it with the workaround code as follows:

public class MyMemberHelper : MembershipHelper { public MyMemberHelper(UmbracoContext umbracoContext) : base(umbracoContext)

    public bool SiteLogin(string username, string password)
    {
        if (base.Login(username, password))
        {

            var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];

            if (authCookie != null)
            {
                try
                {
                    var cookieValue = authCookie.Value;
                    var decrypted = FormsAuthentication.Decrypt(cookieValue);

                    // Find the umb member by username and retrieve the isActivated property
                    var member = GetByUsername(decrypted.Name);
                    var isActivated = member.GetPropertyValue<bool>("isActivated");

                    return isActivated;
                }
                catch
                {
                    return false;
                }
            }

            // authCookie is null
            return false;
        }

        // base Login failed
        return false;
    }
}{code}

The method does indeed resolve the correct member account on login, but in preview mode I am still bounced back to the login page. Stepping through the code looks like it should work, but at the point after successful login where it should redirect to the NodeId.aspx the login form displays. Moreover, authorization on the front end also fails - only once I close preview mode via the badge does authorisation succeed. Something must be happening with the setting of the cookie in preview mode where it does not apply the correct role to the logged in user.


Shannon Deminick 13 Nov 2015, 12:29:13

It's a strange one to fix because to preview you need to be auth'd by the back office user, but when you are previewing a member's page you also need auth on the member. The reason why this is tricky is that that 'User' object on the thread/httpcontext can only be one thing, it's also tricky because of the way we detect that a request needs to be auth'd by a back office user. Anyways, I have ideas on how to fix this but i won't have time until 7.4


Barry Fogarty 13 Nov 2015, 15:23:00

Excellent. For protected pages it would be great to have some sort of UI in the backoffice, e.g. a dropdown panel that says 'Preview As [Role]'. FYI I have detailed a workaround for my specific scenario here https://our.umbraco.org/forum/umbraco-7/using-umbraco-7/72885-preview-mode-breaks-when-using-a-default-controller-with-a-custom-authorize-attribute


Jason Prothero 22 Dec 2015, 04:45:52

Barry, that idea to "Preview as [Role]" is excellent. If we could not have to login in Preview mode, that would really be the most convenient. And testing as specific roles is even better (instead of having to login as each role).


Craig Noble 08 Jan 2016, 10:05:49

For now, I have fixed this on our sites by inheriting from the MembersMembershipProvider and overriding the GetUser method.

Code: public class MembershipProvider : MembersMembershipProvider { protected static string AUTHCOOKIE = "yourAuthCookie";

    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
        HttpCookie authCookie = HttpContext.Current.Request.Cookies[AUTHCOOKIE];

        if (authCookie != null)
        {
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
            username = ticket.Name;
        }

        
        var member = base.GetUser(username, userIsOnline);

        return member;
    }
}

This can obviously be improved but it's a temp workaround until the problem is fixed.


Olivier Albertini 08 Jan 2016, 17:54:09

@Shandem Do you have a workaround when we use UmbracoIdentity (https://github.com/Shazwazza/UmbracoIdentity) ? This feature is really important for content-editor.

Any help will be appreciate


Shannon Deminick 09 Mar 2016, 16:52:30

I've just pushed a PR to fix this: https://github.com/umbraco/Umbraco-CMS/pull/1161 I'll now go and test with UmbracoIdentity as well to see if there are any issues with that. Here's what these changes do:

Changes BackOfficeCookieManager so that it will not return a cookie when a preview request is requested. Before it was returning the cookie which would force back office authentication to occur. When this happened it would mean that a back office user was assigned as the current Identity for the request. This was sort of required in order to be able to Authorize the current back office User to be able to preview pages, however this is sort of backwards to what we need. Since a preview request is a front-end request, the primary Identity should be assigned based on how a normal front-end request would operate including the ability to authorize a front-end member. Once this is changed, the request will auth the member but not auth the user and therefore the preview request will just show the non-preview content... So we need to do more:

There are 2 ways that the Principal (User) gets assigned to the request:

  • If we are using Membership/FormsAuth, then the FormsAuthenticationModule will assign the IPrincipal - this happens during the Authentication stage in the request pipeline, and the the RoleManagerModule will overwrite that IPrincipal with a role version - this happens in the PostAuthentication stage in the request pipeline.
  • If UmbracoIdentity is used, then the IPrincipal is just set with that middleware - this will happen in the Authenticate stage in the request pipeline.

In all of these cases the underlying IPrincipal object will be of type ClaimsPrincipal. If for some odd reason this is not the case (i.e. some very odd custom authn taking place), then unfortunately preview will just not work but I don't suspect this will ever be the case. When the IPrincipal is ClaimsPrincipal, it can support attaching multiple Identities. So what we will do is create a new custom middleware: PreviewAuthenticationMiddleware which will execute on PostAuthenticate. This middleware will check for a ClaimsPrincipal and if the request is a front-end request with a preview cookie, if this criteria is matched then it will attempt to read the back office cookie and convert the ticket to a UmbracoBackOfficeIdentity and then append this Identity to the ClaimsPrincipal. So now we have 2x Identities assigned to the ClaimsPrincipal, the primary one being the member and the secondary one being the back office user.

Next we need to change how the BackOfficeUser is returned from the request so the request can be authorized. This was normally done by just reading the primary Identity from the IPrincipal but now it will also check for an instance of UmbracoBackOfficeIdentity in the secondary identities too.

The end result is:

  • The preview request authn/authz will be treated normally like your normal front-end request
  • The preview request will also be authn/authz for the back office user so that the preview content is used instead of the currently published content


Shannon Deminick 09 Mar 2016, 18:33:51

I've tested with UmbracoIdentity and it will require a new major version release for preview to work with it since the functionality of how it authn's will change a little bit and will be pinned to a min version of umb 7.4.2.


Shannon Deminick 09 Mar 2016, 18:36:54

In the meantime, for reviewers to test:

  • Create a content page and in the template you can include 3 partial view macros - easiest is to create these partial view macro's and use the templates in the drop down: Login, Login Status, Register
  • This content page should also render some text from a textbox property type - so we can test preview
  • Now you are able to register a new member in your front-end ... do this
  • Then login as this member
  • Now update the textbox content for this content item and press Preview - this should save the content item and put the page in preview mode. You should still see that the member is logged in but the page is being rendered in preview mode so you'll see the updated text

The next thing to test is to create a protected page in the back office and ensure that it can be previewed with the member logged in


Shannon Deminick 09 Mar 2016, 18:37:55

Once, reviewed let me know and i'll release a new UmbracoIdentity.


Shannon Deminick 10 Mar 2016, 08:39:28

I've realized there is one potential breaking change here - If a developer has some custom Identity/OWIN code executing (that is not UmbracoIdentity ... since that will need to be fixed to work with this anyways) and depending on the order in which they've registered OWIN components, then preview may not work and it will just show the non-preview content. There is definitely a work-around which would be to re-arrange some of the OWIN registered components (the same way that UmbracoIdentity will be fixed to support this).

Based on this, we have 2 options:

  • Leave the code as-is, mark this as a breaking change for this scenario and document the work-around/fix
  • Change the code to be opt-in only until version 8 which means if people want to have protected pages able to be previewed, they'd have to implement a custom OWIN startup class with some code (IMO this is not ideal)


Warren Buckley 15 Mar 2016, 16:51:10

Hey @Shandem I have tested the functionality and it works great and with your call earlier today explaining how the OWIN stuff works & that two Identities can be assigned in the code review. I am happy to set this to fixed.

However with your notes. I won't merge & set to fixed as you seem you want to write docs & do stuff for Identity Extensions too.


Shannon Deminick 17 Mar 2016, 17:09:09

I've just tested UmbracoIdentity with these changes and there's a few more things that I'll need to change.


Murray Roke 14 Jul 2016, 05:29:27

Hi, I'm a little confused.... In my scenario, we're using the out of the box Auth (running on IIS7) umbraco is v7.2.4 and we are seeing this problem where you can't preview protected pages.

How do I fix this problem for me? will an upgrade fix it? or do I need to write a custom owin something? (or is this ticket not related to my issue?)


Sebastiaan Janssen 14 Jul 2016, 08:16:11

@Murray.Roke Yes, this was fixed in 7.4.2, so you'd need to upgrade.


Jeroen Breuer 26 Jul 2016, 14:15:14

Just upgraded a large project from 7.3.8 to 7.4.3 and now preview works for all protected pages. Thanks for fixing this. Customer will be very happy with this!


YodasMyDad 08 Sep 2016, 05:37:51

I have just upgraded a site to 7.5.2 and now I am getting this error on preview?

Cannot create a Umbraco.Core.Security.UmbracoBackOfficeIdentity from System.Security.Claims.ClaimsIdentity since the required claim http://umbraco.org/2015/02/identity/claims/backoffice/sessionid is missing

[InvalidOperationException: Cannot create a Umbraco.Core.Security.UmbracoBackOfficeIdentity from System.Security.Claims.ClaimsIdentity since the required claim http://umbraco.org/2015/02/identity/claims/backoffice/sessionid is missing] Umbraco.Core.Security.UmbracoBackOfficeIdentity.FromClaimsIdentity(ClaimsIdentity identity) +586 Umbraco.Web.Security.Identity.d__0.MoveNext() +465 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.d__5.MoveNext() +204 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +777 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Umbraco.Web.Security.Identity.d__0.MoveNext() +3007 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Umbraco.Web.Security.Identity.d__6.MoveNext() +922 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +777 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +777 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.AspNet.Identity.Owin.d__0.MoveNext() +451 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.AspNet.Identity.Owin.d__0.MoveNext() +451 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.d__5.MoveNext() +204 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +14139120 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62 Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.d__2.MoveNext() +194 Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.StageAsyncResult.End(IAsyncResult ar) +96 System.Web.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +363 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +157

Has this issue been introduced?


Shannon Deminick 08 Sep 2016, 06:01:48

Tried clearing your cookies?


YodasMyDad 08 Sep 2016, 06:10:38

I'm going to go and flush my head down the toilet now.... Thank you, sorry for wasting your time.


Priority: Normal

Type: Bug

State: Fixed

Assignee:

Difficulty: Normal

Category:

Backwards Compatible: False

Fix Submitted: None

Affected versions: 7.1.0, 7.0.3, 7.0.4, 7.2.0, 7.1.4, 7.3.0, 7.2.4

Due in version: 7.4.2

Sprint: Sprint 11

Story Points:

Cycle: