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.












 


Saturday, February 10, 2018

Fix pod update issue after change the admin user of the mac

After changing the own of the mac, there may be errors when updating pod in the existing xcode project. The main reason is the brew, gem, ruby, cocoapod fail to upgrade due the permission issue.

The below are steps to update the mac

1.  give permission to install homebrew
sudo chmod -R 777 /usr/local/Homebrew

2. Install homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

3. Install ruby
brew install ruby

4. update gem
gem update --system

5. install cocoapods
sudo gem install cocoapods -n/usr/local/bin

6. go to your xcode project to update pod dependent libraries
pod update 

The above steps also applies to the refresh mac pod initialization.

Wednesday, January 31, 2018

Understanding SFSafariViewControllerDelegate and SFAuthenticationSession on IOS 11

SFSafariViewControllerDelegate 
SFSafariViewControllerDelegate has added the below new method in iOS 11
public func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL)
but Apple does not give much information in the API document.

One may attempt to use this method in oAuth library to handle redirect url after authorization is validated. However the testing shows this method is only called when a redirect (301) happens during loading the initial url in SFSafariViewController. After the page is loaded, any redirect (301) happened in following requests from client to server will not trigger this method. Probably that is why the method name is "initialLoadDidRedirectTo".

In addition, after initial load is done, if javascript uses a html form to post the page to a different host and port, or uses window.location to set to a different host and port, none of the operations will trigger this callback method.

The above behaviour pretty much makes the new method useless for oauth authentication and the traditional custom url scheme is still needed to handle the oauth authentication from mobile device.

By the way, the SFSafariViewController use the same cache mechanism as mobile Safari browser, so in order to delete the cached response, such as 301 (Permanent Moved), you will need to clear the cache from Mobile Safari settings page. Just delete and reinstall the application on your iOS device will not delete the cache for SFSafariViewController.

  
SFAuthenticationSession
For ios 11, SFAuthenticationSession is a good choice to handle oAuth. The important thing is set the callbackurlScheme parameter properly.

Although the parameter can be set to nil, which will rely the application open URL delegate method to handle the oauth result, but that really does not make sense, as the point of using this new method is avoiding defining custom url scheme and the related logic in application's openURL method.

The callbackurlScheme must be set to a custom schema, setting it to "http" or "https" will not work. In addition, the parameter must only contain the scheme, not a full url. For example, if the oauth redirect url is "sapbi://mypayload", then the parameter needs to be set as "sapbi" without any colon or slash.

It does not matter whether the custom url is loaded by redirect or by form submit, as long as webview tries to open the custom url, the callback method will be called. Once the callback method is called, then the webview will be automatically dismissed, so there is no need to explicitly call cancel in the completing block.

The biggest benefit of the SFAuthenticationSession for oauth is no longer required the app to define the custom URL scheme at compile time, the oauth redirect url can be changed at runtime when creating the SFAuthenticationSession object.

Allow asp.net core apps running on IIS express accessible from external computer

By default Asp.net core applications running on IIS Express can only be accessed from localhost, in order to access it from other computer or mobile device, the IIS Express needs to have some custom configuration.

The below steps are for Windows 10 box with Visual Studio 2017:

1. Open IIS express configuration file under the Visual Studio project folder, like
ProjectName\.vs\config\applicationhost.config
(The file will be generated after running the project at least once)

Change the below setting from
   <bindings>
          <binding protocol="http" bindingInformation="*:50532:localhost" />

  </bindings>

To
        <bindings>
          <binding protocol="http" bindingInformation="*:50532:localhost" />
          <binding protocol="http" bindingInformation="*:50532:*" />
        </bindings>


2.Stop and restart Visual Studio in Administrator mode, and run the asp.net core app on IIS Express

I saw few people posted the solution requires firewall disable as well as netsh, but those steps are not needed in my testing. Although I have the McAfee installed on my windows box.

Friday, January 19, 2018

Using FireFox SAML tracer to fix SAML identity authentication issue

When using SAML authentication, sometimes after changing the SAML Identity provider from one to another, the SAML authentication may stop work, one reason that may cause the error is due to the different SAML identity provider may have configured different SAML attribute assertion.

FireFox has a nice SAML Tracer that can be used to figure out this issue. First installing the SAML tracer on FireFox browser, and then testing with the SAML identity provider that works, notice what subject ID and attribute assertions are sent in the SAML response. Then replacing the SAML identity provider to the new one and repeat the same testing, and check whether any attributes are missing or whether the format or attribute names are different.

For example, in a recent testing with custom SAP cloud identity provider, the user authentication always fails. After comparing the saml response, it indicates one attribute "display_name" is set by default SAP cloud identity, but is not configured by custom SAP cloud identity as shown below, after
adding the display_name attribute in the custom identity provider attribute then the function works as expected.


        <Subject>
            <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">I826633</NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <SubjectConfirmationData InResponseTo="Sa71696cb-67b6-42c8-b667-e91abbf02f2b-HPjGyP7iI5mFsWJ2rfj_YOFtZs_6FfDhQhvVAtK5MYQ"
                                         NotOnOrAfter="2018-01-18T21:54:13.288Z"
                                         Recipient="https://mobile-i826633sapdev.int.sap.hana.ondemand.com/odata/applications/latest/com.sap.fiori.client.debug/Connections(&apos;cfd5cea6-d078-4ace-9125-c452ebb061d6')"
                                         />
            </SubjectConfirmation>
        </Subject>
        <Conditions NotBefore="2018-01-18T21:39:13.288Z"
                    NotOnOrAfter="2018-01-18T21:54:13.288Z"
                    >
            <AudienceRestriction>
                <Audience>https://sap.hana.ondemand.com</Audience>
            </AudienceRestriction>
        </Conditions>
        <AuthnStatement AuthnInstant="2018-01-18T21:44:13.288Z"
                        SessionIndex="S-SP-8698d43a-d131-4bc6-b623-26401f791236"
                        SessionNotOnOrAfter="2018-01-19T09:44:13.288Z"
                        >
            <AuthnContext>
                <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
        <AttributeStatement>
            <Attribute Name="mail">
                <AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >jonathan.li@sap.com</AttributeValue>
            </Attribute>
            <Attribute Name="display_name">
                <AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Jonathan Li I826633</AttributeValue>
            </Attribute>
            <Attribute Name="first_name">
                <AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Jonathan</AttributeValue>
            </Attribute>
            <Attribute Name="last_name">
                <AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Li</AttributeValue>
            </Attribute>
        </AttributeStatement>
    </Assertion>
</Response>

 AML response.

Monday, January 8, 2018

Using mobile Authenticator app for time-based two factor authentication TOTP

1. What authenticator mobile app does
Many major identity service (Google, Microsoft, SAP etc) providers have published their Authenticator mobile application for time-based two factor authentication. Essentially, the Authenticator app uses a shared secret between identity service provider and mobile Authenticator app, and current time to generate a passcode, and if the key matches with each other within 30 seconds period, then the identity will accept the passcode generated by Authenticator app and pass the authentication.

A sample authenticator configuration for google accout looks like below
otpauth://totp/Google%3Ajonathanli2000%40gmail.com?secret=rrrhpj3zrgzgwq4d53qrmyb6fsttuwee&issuer=Google

In addition, all authenticator apps use the same logic, so as long as they use the same configuration, then no matter the passcode is generated by with company's authenticator app, it can also pass the check by other company's identity service provider.

2. Where to implement TOTP two factor authentication
If the web application handles the user account by itself, like maintaining the user account in a user table, then the application must implement the TOTP logic, which keeps the secret key for each user account and then verify the client side Authenticator generated passcode based on the secret key and current time.
However, preferably, the user account management is handled by third party identity service provider. In that case, enabling and handling the TOTP authentication should only need to be configured and handled by the identity service provider, which is transparent to the application. For example, if the google social login service is used, once the two factor authentication using mobile device is enabled, then the user will be automatically prompted to input the Authenticator generated passcode during user authentication, there is nothing in the web app that needs to be changed for this purpose.


3. Note
The mobile authenticator app does not need to communicate with identity service provider when it generates the passcode, so it does not to be online when doing so.
In addition, user changes the account password does affect the mobile authenticator app, as long as the shared secret is not changed.

Thursday, January 4, 2018

View ipa file and its bundle content as regular folder on MAC

It is easy to view the content of an IPA file or bundle file in mac Finder. Instead of always needing to use the context menu of "Show Content" to open the bundle file.

For ipa file, just change the file extension from "ipa" to "zip" and then double click the zip file will extract the zip file to a regular folder.

Then in the payload subfolder, you will see a bundle file. In order to change the bundle file to a normal folder, right click the file and select context menu "get info", then from there, changing the Name and Extension section to remove ".app" from the file name. The bundle file now should show as a regular mac folder.