Provider Hosted App Times Out

Disclaimer: The supplied solution is convoluted, a better solution would be to completely recreate the SharePointContext class.

I came across a problem with a SharePoint Provider Hosted App I created for a client while working on the Gold Coast. The app was a Single Page Application created in MVC C# utilising the standard SharePointContext and TokenHelper class. A problem occurred when the end user was using the application for longer than an hour.

The ajax calls which accessed the MVC Actions decorated with [SharePointContextFilter] were being redirected to appredirect.aspx, and therefore failing.

The problem is due to the SharePoint’s OAuth2 implementation. The SPContextToken was expiring, which would cause the ActionFilter (SharePointContextFilterAttribute) to redirect to the login page (via appredirect).

I found surprisingly very little on this subject online, I also found nothing from Microsoft.

I had to dig through the SharePoint’s classes to create a fix.

The First thing we have to do is store the Refresh token which is supplied in the post when SharePoint redirects the user to your app. I added this to the HttpContext Session in SharePointContext.cs I edited the SaveSharePointContext method:

 

protected override void SaveSharePointContext(SharePointContext spContext, HttpContextBase httpContext)
{
  SharePointAcsContext spAcsContext = spContext as SharePointAcsContext;
            if (spAcsContext != null)
            {
                HttpCookie spCacheKeyCookie = new HttpCookie(SPCacheKeyKey)
                {
                    Value = spAcsContext.CacheKey,
                    Secure = true,
                    HttpOnly = true
                };
                httpContext.Response.AppendCookie(spCacheKeyCookie);
            }
            httpContext.Session["SPRefreshKey"] = spAcsContext.RefreshToken;
            httpContext.Session[SPContextKey] = spAcsContext;
        }

}

Now when the contextkey is saved, the refresh key will also be saved. At the time of posting this the refresh token was good for 6 months.

The next thing we need to do is to stop the page from redirecting. The SharePointContextFilter “checks the redirect status” via a method in SharePointContext CheckRedirectStatus, If it finds the stored SharePointContext to be expired it will redirect the user. I added a method in this function to check if there is a valid sphost url and a valid refresh token, if there is, do not redirect.

public static RedirectionStatus CheckRedirectionStatus(HttpContextBase httpContext, out Uri redirectUrl)
 {
 if (httpContext == null)
 {
 throw new ArgumentNullException("httpContext");
 }
 redirectUrl = null;
 if (SharePointContextProvider.Current.GetSharePointContext(httpContext) != null)
 {
 return RedirectionStatus.Ok;
 }
 //If refresh token and SP Host Exist don't redirect ADDED CODE
 if (SharePointContextProvider.Current.CheckIfRefreshtokenValid(httpContext))
 {
 return RedirectionStatus.Ok;
 }
 …
 // Standard CheckRedirectionStatus functions below

Now that we have prevented it redirecting and stored the refresh token, we just have to check if the spContext is null, if it is, use the refreshtoken to create the clientcontext, if its not use the standard CreateUserClientContextForSPHost method to create it.

We also do not want it getting a refresh token for every call after the sharepointcontext expires, so I store the access token in a session, and only refresh every 30 minutes.

This maybe a convoluted, and I think I’ll end up recreating SharePointContext from scratch, but it works for now.

I put this in a base controller class (I named GenericController) which all my controllers inherit from.

#region Constants
 public int EXPIRYMINUTES = 30;
 #endregion
 public DateTime? AccessTokenExpiry
 {
 get
 {
 var expiry = Session["AccessTokenExpiry"];
 if (expiry == null)
 {
 return null;
 }
 return expiry as DateTime?;
 }
 set
 {
 Session["AccessTokenExpiry"] = value;
 }
 }
 public ClientContext GetSpClientContext(SharePointContext spContext)
 {
 string spHostUrl = Request.QueryString["SPHostUrl"];
 Uri spHostUri = new Uri(spHostUrl);
 if (spContext != null)
 {
 return spContext.CreateUserClientContextForSPHost();
 }
 else if (Session["AccessToken"] != null
 && AccessTokenExpiry != null
 && AccessTokenExpiry.Value.AddMinutes(EXPIRYMINUTES) > DateTime.Now)
 {
 var accessToken = Session["AccessToken"].ToString();
 return TokenHelper.GetClientContextWithAccessToken(spHostUri.ToString(), accessToken);
 }
 else
 {
 //Refresh token valid for 6 months
 var refreshToken = HttpContext.Session["SPRefreshKey"].ToString();
 var accessToken = TokenHelper.GetAccessToken(refreshToken,
 "00000003-0000-0ff1-ce00-000000000000",
 spHostUri.Authority,
 TokenHelper.GetRealmFromTargetUrl(spHostUri)).AccessToken;
 Session["AccessToken"] = accessToken;
 AccessTokenExpiry = DateTime.Now;
 return TokenHelper.GetClientContextWithAccessToken(spHostUri.ToString(), accessToken);
 }

If you are wondering where “00000003-0000-0ff1-ce00-000000000000” comes from, You can find more information here https://blogs.msdn.microsoft.com/kaevans/2013/04/05/inside-sharepoint-2013-oauth-context-tokens/

But suffice to say it’s just the ID value we use for ACS.

Now an action in your controller can look like this:

[SharePointContextFilter]
public ActionResult Index()
{
User spUser = null;
var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
using (var clientContext = GetSpClientContext(spContext))
{
if (clientContext != null)
{
spUser = clientContext.Web.CurrentUser;
clientContext.Load(spUser, user => user.Title);
clientContext.ExecuteQuery();
ViewBag.UserName = spUser.Title;
}
}
return View();
}

If everything is setup properly, you won’t be redirected.

 

Related Posts

Upsert function for Rest SharePoint API
Web deployment task failed. http://go.microsoft.com/fwlink/?LinkId=221672#ERROR_CONNECTION_TERMINATED.

Leave a Comment

Top