Wednesday, February 21, 2018

CORS issue when loading html from local file system in ios WKWebView

On iOS, UIWebView does not have any limitation for cross domain javascript requests (except for the this issue), so html file loaded from local host or a domain, can make xmlhttprequest to another remote domain and get response properly.

However, iOS WKWebView limits the cross domain js request, so when xmlhttpRequests are sent to a different domain, even if the server returns expected, the WKWebView will block the content and instead returning an error to xmlhttprequest.

To allow cross domain requests work on WKWebView, the server which handles the xmlhttpRequest issued from a different domain must be configured to tell the browser to allow the xmlhttpRequest to accept the response, which is how CORS specification tries to handle.

Note if the xmlhttprequest is sent from local html file, for uiwebview, the origin header in request is not set. For wkwebView, the origin header in request is "null";


For testing purpose, asp.net core is used to create the server web app, the client side html file is loaded from local using a cordova ios project.

1. In the asp.net core webapi project, adding the below line in startup.cs Configure method

app.UseCors(builder =>
    builder.WithOrigins("*"));

Then sending a cross domain xmlhttprequest from a local html file to remote server will work. Checking Fiddler trace, it will show in the server response, a new CORS header is added

Access-Control-Allow-Origin: *

This header tells browser that server trust the request to be sent from any other domain, and browser should pass the response to the xmlhttprequest object.


2. After the basic request works, now we try to make the request with credential and cookie information

First updating the client js script to set xmlhttprequest withCredentials to true
            xhr.open('GET', url, true);
            xhr.withCredentials = true;
            xhr.onreadystatechange = handler;

            xhr.send();

Now sending the same request as in section 1 will fail. The Fiddler trace shows the client request does not changed anything with the new withCredentials=true parameter, and the server's response also does not have any change. So it means the withCredentials flag is only applied to browser itself, and not involved in the http server request and response.

Actually when this error happens, the browser console logs an error of
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.
Failed to load resource: Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.

That indicates the exact problem. withCredential does not work when origin is set to "*" on server side.

The problem is if the html file is loaded from local file system using "file://" scheme, then the wkwebview will set the request origin as null, and on server side CORS does not to set allowed origin to "null". That means in order to make cross domain requests work for local html/js files loaded in wkwebview, the request must have a normal origin with scheme of http:// or https.

One option to work around this issue is using an inapp web server to server the local content as http://localhost:8080, iconic framework uses this approach as described at
https://ionicframework.com/docs/wkwebview/
https://blog.ionicframework.com/wkwebview-for-all-a-new-webview-for-ionic/
https://docs.google.com/document/d/19VQ-n7hGr9IDPPstQqU8_8WgqUh7R6sgQfL2neoT-Xw/edit

Another option is using unpublished api to use nsurlprotocol with wkwebview as mentioned at
https://github.com/yeatse/NSURLProtocol-WebKitSupport

Neither of them is ideal, hopefully the future ios release can allow CORS for html files loaded from local file system.

3. Since html file loaded from local cannot pass the CORS check on browser side, so the next testing is loading the html file from another http domain. So a regular origin can be set for Access-Control-Allow-Origin header.
For this purpose, another asp.net core app has been created to host the html page at the same host but a different port.
Now repeating the same testing in xhr request to set withCrdential = true
and the request header are
Origin: http://yyzn00596231a.amer.global.corp.sap:60208
Host: yyzn00596231a.amer.global.corp.sap:63132
and the response header has the expected header of
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208

However, the client still cannot received the response, the javascript console logs the error of
"Failed to load resource: Credentials flag is true, but access-control-allow-credential is not 'true'"

So in order to support credential, the server needs to specify access-control-allow-credential header in the response, for asp.net core project, this can be done with the below code
      public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(builder =>
                builder.WithOrigins("http://yyzn00596231a.amer.global.corp.sap:60208")
                .AllowCredentials());

            app.UseMvc();
        }

Repeat the testing again and the xhr request can get the response as expected. The response header are
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208

4. The next testing is enable basic authentication in the xhr request with credential enabled. To do so, add https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic into the asp.net core project as a dependent project, and then update the below source code to enable basic auth
   public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddMvc();
            services.AddAuthentication()
              .AddBasicAuthentication(credentials =>
                    Task.FromResult(
                        credentials.username == "user"
                        && credentials.password == "password"));

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(builder =>
                builder.WithOrigins("http://yyzn00596231a.amer.global.corp.sap:60208")
                .AllowCredentials());
            app.UseMvc();
            app.UseAuthentication();
        }



 [Route("api/[controller]")]
    [Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)]    public class ValuesController : Controller{

...
}

First testing on ipad mobile safari browser, when server side returns the below response
HTTP/1.1 401 Unauthorized
Server: Kestrel
WWW-Authenticate: Basic realm="MySite"
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208
X-SourceFiles: =?UTF-8?B?QzpcVGVtcFxjb3JzXGNvcnNcYXBpXHZhbHVlcw==?=
X-Powered-By: ASP.NET
Date: Fri, 23 Feb 2018 21:52:43 GMT
Content-Length: 0


The safari browser does not pop up a dialog to let user input user name and password, instead it just return the response to xhr response handler.

Repeating the same testing with cordova wkwebview and it gets the same result, the 401 response was directly returned to xml response handler with readystate of 4.

In addition, if directly loading the basic auth url
http://http://yyzn00596231a.amer.global.corp.sap:63132/api/values 
to Mobile safari or Cordova wkwebview, then it gets the challenge callback at didReceiveAuthenticationChallenge for wkwebwview, and also shows the 401 challenge dialog box for mobile safari, so it means the browser and wkwebview just do not allow the challenge to be handled by user for CORS requests.












 


1 comment:

  1. Thanks this saved me a few hours of trial and error. I wanted to download a pdf file and show it in webview. Went with downloading file locally and then displaying it.

    ReplyDelete