AJAX Web API Request Error In Azure Hosted Web Applications

Error Description

“Response to the preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://<<yourazureWebApp.net>>” is therefore not allowed access “

Application Landscape

The application landscape where I have experienced this error is,

  • Azure Web App containing an ASP.net MVC application hosted with Azure AD Authentication (Using Open ID Connect)
  • The Application is registered within the Azure AD with permissions to “Read & Write to SharePoint Online Sites”
  • The application also has a custom Web API service layer specifically utilized to invoke SharePoint REST API’s utilizing the “Access Token”
  • Since the Web API Layer is within the MVC application, it is not configured with any specific authentication requirement but will utilize the Access token retrieved to invoke SharePoint list API’s (For operations like accessing lists and libraries in SharePoint)
  • The Web API as such being hosted within the same Web App doesn’t require additional authentication 
  • The Web Application Views primarily depend on AJAX-jQuery calls to invoke the internal Web API for SharePoint specific operations by calling specific API controllers.

    Azure

Issue

  • The access token issued from Azure AD as a result of the Open ID connect authentication has a life-span of only 1 hour by default. The specific token is also stored in the browser cookie for the span of an hour and once the token expires it needs to be re-issued again with additional one-hour validity. This continues until the user logs out explicitly or the session expires, in which case the user is prompted to log in again with Azure AD credentials.

  • It is not recommended to increase the access token lifetime, I believe there are some PowerShell commands that can be run to get this done but as a recommendation to retain this as default this was not changed.

  • In an ideal scenario, as an end user moves across multiple views/pages etc. and in-case the Access Token has expired (Assuming that the user session was active and the web page was still open for more than an hour) , as a part of the Open ID connect implementation the below method is invoked to re-generate the Access token silently without prompting user to log in again,
    1. string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;  
    2.             string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;  
    3.             AuthenticationContext authContext = new AuthenticationContext("Azure ad instance and tenant"new NaiveSessionCache(userObjectID));  
    4.             ClientCredential credential = new ClientCredential("your client id""your app key");  
    5.               
    6.                 var result = await authContext.AcquireTokenSilentAsync(resourceID, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));  
    7.                 if (result == null)  
    8.                 {  
    9.                     return null;  
    10.                 }  
    11.                 return result.AccessToken;  
    The specific methods are a part of the “AuthenticationContext” implementation as a part of the “Microsoft.IdentityModel.Clients.ActiveDirectory” namespace.
  • The above implementation specifically ensures that, if during a post back as a user moves across Views/Pages etc. and in-case the access token has expired the access token is silently renewed without asking for the end user to log in again with Azure AD credentials/prompt.

  • Irrespective of the fact that this method is invoked and it takes care of the access token renewal, in case of the jQuery-AJAX calls we observed that if the access token was expired the method calls from the jQuery were not going through.

  • As a result, if an end user was within the same Page and if the page was kept open for a duration of more than one hour (results in access token expiry), all the SharePoint requests failed with the above-highlighted error message if the end user tried to do any operations that involved SharePoint list API calls. Note that these were the calls which didn’t involve any page Post-Backs.

  • For the scenario where the access token was expired and the user moved across the pages and the post-backs were happening, there was no issue experienced since the AcquireTokenSilentAsync method got invoked in the backend.

How to fix the Error

The process to fix the error was a bit tricky but not that complicated. The process to fix this issue involved a JavaScript implementation within a Partial View within an Iframe with a sequence of Activities which was

Implementation of the Partial View’s Iframe

  1. Invoke a specific controller to get the current Token Expiry Time
  2. Convert the specific time to UTC
  3. Call another JavaScript function to pass the Token Expiry time to calculate the Refresh time (Refresh Time = Current Token Expiry Time – 15 minutes) considering that the token had to be renewed 15 minutes before the expiry
  4. Ensure to call another controller to force the sign in and issue a new access token
  5. Ensure that the Iframe is refreshed as per the Refresh Time
  6. Utilize the Partial View across various other generic views which require silent token refresh based on Iframe within them

The implementation within the Partial View is as below,

  1. <iframe id="renewSessionIFrameImplementation" hidden></iframe>  
  2. <script>  
  3.   
  4.     GetTokenExpiryFunction();  
  5.     //Function that will retreive the current token expiry time  
  6.     function GetTokenExpiryFunction () {  
  7.         $.ajax({  
  8.             type: "POST",  
  9.             traditional: true,  
  10.             async: true,  
  11.             //Invoke the specific controller to retrieve the current issued tokens expiry time  
  12.             url: "/<<Controller>>/GetCurrentTokenExpirySchedule",  
  13.             context: document.body,  
  14.             success: function (result) {  
  15.                 if (result) {  
  16.                     //convert the result from controller to UTC format  
  17.                     var tokenExpiresOnSchedule = ConvertUTCtoLocalTime(result);  
  18.                     //Retrieve the actual refresh interval by passing the current expiry time  
  19.                     RefresIframeImplementation(tokenExpiresOnSchedule);  
  20.                 }  
  21.             },  
  22.             error: function (xhr) {  
  23.                 console.log("Error :" + xhr);  
  24.             }  
  25.         });  
  26.     }  
  27.   
  28.       
  29.     function ConvertUTCtoLocalTime(UTCString) {  
  30.         var newDate = new Date(UTCString);  
  31.         newDate.setMinutes(newDate.getMinutes() - newDate.getTimezoneOffset());  
  32.         return newDate;  
  33.     }  
  34.   
  35.     //Function which will renew the token based on refresh interval  
  36.     function RefresIframeImplementation(CurrentTokenExpiryTime) {  
  37.         //calculate the refresh interval, in this case will be -15 minutes before token expiry  
  38.         var refreshTimeIntervel = moment(CurrentTokenExpiryTime).subtract(15, 'minutes').toDate();  
  39.         //arrive at the time in milliseconds to refresh the Iframe  
  40.         var milliSecondstoRefresh = refreshTimeIntervel - new Date();  
  41.         //Invoke the SetInterval function to refresh the IFrame within this view to refresh the token by   
  42.         //Forecefully signing in  
  43.         var refreshIframeInterval = setInterval(function () {  
  44.             @if (Request.IsAuthenticated) {  
  45.   
  46.                 <text>  
  47.                //Clear existing refresh interval  
  48.                 clearInterval(refreshIframeInterval);  
  49.                //Get teh Iframe ID to refresh the current Iframe  
  50.                 var element = window.parent.document.getElementById("renewSessionIFrameImplementation");  
  51.                 //Invoke the controller to renew access token  
  52.                 var renewUrl = "/<<Controller>>/EnsureForcedSignIn";  
  53.                 console.log("sending request to: " + renewUrl);  
  54.                 element.src = renewUrl;  
  55.                 </text>  
  56.             }  
  57.             else {  
  58.                 <text>  
  59.             console.log("No renewal attempt without a valid session");  
  60.   
  61.                 </text>  
  62.             }  
  63.         }, milliSecondstoRefresh);  
  64.   
  65.     }  
  66.   
  67. </script>  

 

The Controller (GetCurrentTokenExpirySchedule) Implementation to Get the Current Access Token’s Refresh Time is as below,

  1. public static async Task<string> GetAppUserTokenExpiryDate(string resourceID)  
  2.         {  
  3.   
  4.             string currentTenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;  
  5.             string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;  
  6.             AuthenticationContext authContext = new AuthenticationContext("Azure AD instance & Tenant"new NaiveSessionCache(userObjectID));  
  7.             ClientCredential credential = new ClientCredential("Your Client ID""<<Your App Key");  
  8.               
  9.                 var result = await authContext.AcquireTokenSilentAsync(resourceID, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));  
  10.                 if (result == null)  
  11.                 {  
  12.                     return null;  
  13.                 }  
  14.                 return result.ExpiresOn.UtcDateTime.ToString();  
  15.         }  

 

Note that the resourceID is the Microsoft Graph API URL - https://graph.microsoft.com

The Controller Implementation (EnsureForcedSignIn) to Force issue of new Access Tokens is as below,

  1. public void EnsureForcedSignIn()  
  2.         {  
  3.             // Send an OpenID Connect sign-in request.   
  4.             HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },OpenIdConnectAuthenticationDefaults.AuthenticationType);  
  5.         }