SharePoint 2013 App fails with custom FBA login page

I recently worked a support ticket that resulted in the discovery of a rather acute problem but one that might go unnoticed except in the case when SharePoint hosted apps are used in an FBA enabled site where a custom login page is used by FBA.

I don’t wish to cover the well documented and easily searchable part of how to create a custom login page for SharePoint 2013 in this post (I know that link is for SharePoint 2010, but it’s pretty much the same for SharePoint 2013 as well).  Instead I’ll delve into the problem in hand and the solution.

Assume that we have our custom login page created and deployed to C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\TEMPLATE\LAYOUTS\CustFormsLogin as shown in the below picture.

image

The way we tell the SharePoint web application that it needs to use our custom login page instead of the default, is by setting the Custom Sign In Page of the appropriate zone of the web application to point to the relative path of the custom login page.

Below are the quick steps along with screenshots for quicker understanding.

1. Click Manage web applications.

image

2. Click Authentication Providers after selecting the web application.

image

3. Select the zone where FBA is configured.  In my case, I’ve configured FBA in my Default zone.

image

4. Scroll to the bottom of the dialog to find Sign In Page URL and set the relative path to where our custom login page is deployed to on the server.

image

After this, if all is well, we should see our custom login page once we browse to a site collection on the web application and should be able to login without issues.

image

But once we click on a SharePoint Hosted App that’s installed in this site collection.

image

We’ll get to see this error.  Had we used the default login page, we wouldn’t have hit this issue in the first place.

image

Oh yes, the above (IMO, more descriptive) error is because I had the option Show friendly HTTP error messages turned off (shown below) in IE browser.

image

If we have this option turned on, we’ll see a 403: Forbidden error instead.

image

A fiddler trace will pretty much show the same 403 error code and if we probe the ULS, we’ll see entries talking about access denied (notably, EventIDs: ftd0 and ai1wu the stack is provided below).

ftd0:

 Access Denied. Exception: Attempted to perform an unauthorized operation., StackTrace:  
  at Microsoft.SharePoint.Utilities.SPUtility.HandleAccessDenied(Exception ex)    
  at Microsoft.SharePoint.SPWeb.GetWebPartPageContent(Uri pageUrl, Int32 pageVersion, PageView requestedView, HttpContext context, Boolean forRender, Boolean includeHidden, Boolean mainFileRequest, Boolean fetchDependencyInformation, Boolean& ghostedPage, String& siteRoot, Guid& siteId, Int64& bytes, Guid& docId, UInt32& docVersion, String& timeLastModified, Byte& level, Object& buildDependencySetData, UInt32& dependencyCount, Object& buildDependencies, SPWebPartCollectionInitialState& initialState, Object& oMultipleMeetingDoclibRootFolders, String& redirectUrl, Boolean& ObjectIsList, Guid& listId)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.FetchWebPartPageInformationForInit(HttpContext context, SPWeb spweb, Boolean mainFileRequest, String path, Boolean impersonate, Boolean& isAppWeb, Boolean& fGhostedPage, Guid& docId, UInt32& docVersion, String& timeLastModified, SPFileLevel& spLevel, String& masterPageUrl, String& customMasterPageUrl, String& webUrl, String& siteUrl, Guid& siteId, Object& buildDependencySetData, SPWebPartCollectionInitialState& initialState, String& siteRoot, String& redirectUrl, Object& oMultipleMeetingDoclibRootFolders, Boolean& objectIsList, Guid& listId, Int64& bytes)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.GetFileForRequest(HttpContext context, SPWeb web, Boolean exclusion, String virtualPath)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.InitContextWeb(HttpContext context, SPWeb web)    
  at Microsoft.SharePoint.WebControls.SPControl.SPWebEnsureSPControl(HttpContext context)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.GetContextWeb(HttpContext context)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PostResolveRequestCacheHandler(Object oSender, EventArgs ea)    
  at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()    
  at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    
  at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)    
  at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)    
  at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
  at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)  .

ai1wu:

 System.UnauthorizedAccessException: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)), StackTrace:   
  at Microsoft.SharePoint.SPWeb.InitWeb()    
  at Microsoft.SharePoint.SPWeb.get_EnableMinimalDownload()    
  at Microsoft.SharePoint.Utilities.SPUtility.Redirect(String url, SPRedirectFlags flags, HttpContext context, String queryString)    
  at Microsoft.SharePoint.Utilities.SPUtility.HandleAccessDenied(HttpContext context)    
  at Microsoft.SharePoint.Utilities.SPUtility.HandleAccessDenied(Exception ex)    
  at Microsoft.SharePoint.SPWeb.GetWebPartPageContent(Uri pageUrl, Int32 pageVersion, PageView requestedView, HttpContext context, Boolean forRender, Boolean includeHidden, Boolean mainFileRequest, Boolean fetchDependencyInformation, Boolean& ghostedPage, String& siteRoot, Guid& siteId, Int64& bytes, Guid& docId, UInt32& docVersion, String& timeLastModified, Byte& level, Object& buildDependencySetData, UInt32& dependencyCount, Object& buildDependencies, SPWebPartCollectionInitialState& initialState, Object& oMultipleMeetingDoclibRootFolders, String& redirectUrl, Boolean& ObjectIsList, Guid& listId)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.FetchWebPartPageInformationForInit(HttpContext context, SPWeb spweb, Boolean mainFileRequest, String path, Boolean impersonate, Boolean& isAppWeb, Boolean& fGhostedPage, Guid& docId, UInt32& docVersion, String& timeLastModified, SPFileLevel& spLevel, String& masterPageUrl, String& customMasterPageUrl, String& webUrl, String& siteUrl, Guid& siteId, Object& buildDependencySetData, SPWebPartCollectionInitialState& initialState, String& siteRoot, String& redirectUrl, Object& oMultipleMeetingDoclibRootFolders, Boolean& objectIsList, Guid& listId, Int64& bytes)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.GetFileForRequest(HttpContext context, SPWeb web, Boolean exclusion, String virtualPath)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.InitContextWeb(HttpContext context, SPWeb web)    
  at Microsoft.SharePoint.WebControls.SPControl.SPWebEnsureSPControl(HttpContext context)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.GetContextWeb(HttpContext context)    
  at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PostResolveRequestCacheHandler(Object oSender, EventArgs ea)    
  at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()    
  at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    
  at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)    
  at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)    
  at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
  at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
  at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

Looking at the stack we can guess that the app redirection process is not able to get hold of a valid SPWeb object to move on with its operation and hence failed.  We have successfully reproduced the problem now!

Solution:

The solution to this (at least the path I took) was to figure out how out of the box forms login page works.  Switching the web application in question to use the default login page, I realized that the default forms login page was served from a different location other than the 15 hive (which happens to be the default location most developers deploy their custom login page to).  The default forms login page was indeed served from a folder (configured as a virtual directory) right under the root of the IIS web site that’s running SharePoint.

The default forms login page default.aspx is served from a virtual directory named _forms.

image

Looking this virtual directory up from IIS Manager, I was able to determine the above fact.

image

Having learnt this, the solution appeared to be very simple.  I copied over my CustFormsLogin folder I had deployed under the 15 hive directly under the root of my SharePoint’s IIS web site and configured it as a virtual directory.

image

I then configured the Custom Sign In Page to point to the default.aspx located in the newly created virtual directory.

image

Now, when I browse to the site collection, I get to see the custom login page deployed to the root of the SharePoint IIS web site.

image

And once I login and click on the SharePoint hosted app’s hyperlink, I am correctly redirected to my custom login page again.

image

And needless to say, I can login and see the sample app too.

image

Reasoning for this solution:

Sounds like this has to do with the way app redirection is performed that it constructs the relative path to custom login page based off of the root IIS web site path that SharePoint is running on.  So, while it could figure out how to get to https://servername/custformslogin/default.aspx after the change, it couldn’t figure out how to get to https://servername/_layouts/15/custformslogin/default.aspx because the _layouts\15\custformslogin folder structure isn’t present directly under the root IIS web site that SharePoint is running on.

Hope this is helpful if you happen to hit this error at some point in time!