aogilvie.github.io

GitHub website innit.

Twitter CV GitHub

Ally's github site.

Ally leads the work on iOS, Android and Windows Mobile PhoneGap plugin development to enhance HTML5 games at Wizcorp. Ally is an early adopter to PhoneGap and loves solving demanding HTML5 challenges with PhoneGap plugins. Wizcorp is a Tokyo based mobile games & middleware company that has partnered with publishers such as GREE, Capcom and Take2.

Posted by: Ally

22 Aug 2013

Basic Authentication with Android Webkit

A common simple solution to keeping HTML5 games private while in development, but allowing access for testing is using basic authentication. Publish your HTML5 app on Testflight, your client can download and check out your app securely over a public network.

Cool.

Until, you want to have application assets loaded from multiple domains or sub-domains into your application with basic authentication access.

There is a bug in android.webkit.WebViewClient…

Example

  • My game is hosted at http://game.domain.com/folder/pages/
  • My images are hosted at http://game-images.domain.com/images/

The are many reasons why you might host assets in another location (a dedicated server - services like Akamai, are very popular right now for this) to spread the load off your main game server.

So let’s say you load a JavaScript file with src="http://user:password@game.domain.com/folder/pages/", Webkit will grab your user and password from the source and use it to load your JavaScript - so far so good!

Next, we preload an image using src=http://user:password@game-images.domain.com/images/image.png, Webkit catches the request to load a web resource but completely ignores the user credentials in the URL! - Whut?

Prove it!

So there are two interesting methods to observe this behaviour;

public android.webkit.WebResourceResponse shouldInterceptRequest(android.webkit.WebView view, 
	java.lang.String url) { }

and

public void onReceivedHttpAuthRequest(android.webkit.WebView view, 
	android.webkit.HttpAuthHandler handler, java.lang.String host, java.lang.String realm) { } 

Using shouldInterceptRequest() we can check what the URLs look like when webkit tries to load them.

Here is an example of shouldInterceptRequest and some logs from an application I made earlier;

@Override
public android.webkit.WebResourceResponse shouldInterceptRequest(android.webkit.WebView view, 
	java.lang.String url) {
    // Check for authentication in url and set if necessary
    try {
            URL url1 = new URL(url);
            Log.d("DEBUG", "Check host: " + url1.getHost() + 
            	" has user info: " + 
            	url1.getUserInfo() + 
            	" for url: " + url);
            String userInfo = url1.getUserInfo();
            if (userInfo != null) {
                String[] credentials = userInfo.split(":");
                Log.d("DEBUG", "Setting credentials for host: " + getBaseDomain(url));
                view.setHttpAuthUsernamePassword(getBaseDomain(url), 
                	"Restricted", credentials[0], credentials[1]);
            }
    } catch (MalformedURLException e) {
        e.printStackTrace();
    }
    return super.shouldInterceptRequest(view, url);
}

All URL host names have been redacted.

D/DEBUG: Check host: <subdomain>.<domain>.jp has user info: user:password for url: http://user:password@<subdomain>.<domain>.jp/app/game/

D/DEBUG: Setting credentials for host: <domain>.jp

D/DEBUG: Check host: <subdomain>.<domain>.wizcorp.jp has user info: null for url: http://<subdomain>.<domain>.jp/assets/img/default/ui/landing.jpg

D/DEBUG: Check host: <subdomain>.<domain>.wizcorp.jp has user info: null for url: http://<subdomain>.<domain>.jp/assets/img/default/ui/btn_red.png

D/DEBUG: Check host: <subdomain>.<domain>.wizcorp.jp has user info: null for url: http://<subdomain>.<domain>.jp/assets/img/default/ui/motif_6.png

D/DEBUG: Check host: <subdomain>.<domain>.wizcorp.jp has user info: null for url: http://<subdomain>.<domain>.jp/app/game/landing?language=en
...

As you can see only the first URL to be caught by shouldInterceptRequest() contains the credentials in the URL. Every subseqent URL however, has had its credentials extracted!

The next method to look at is onReceivedHttpAuthRequest() using this we can force Webkit to achnoledge our basic authentication credentials when it receives an authentication challenge.

@Override
public void onReceivedHttpAuthRequest(android.webkit.WebView view, 
	android.webkit.HttpAuthHandler handler, java.lang.String host, java.lang.String realm) {
    Log.d"DEBUG", "onReceivedHttpAuthRequest host : " + host);
    String[] credentials = view.getHttpAuthUsernamePassword(host, realm);
    if (credentials != null) {
        Log.d("DEBUG", "Setting credentials for user : " + credentials[0]);
        handler.proceed(credentials[0], credentials[1]);
    } else {
        Log.d("DEBUG", "No credentials found");
    }
}

Here we can see that when a challenge is made, we can use view.getHttpAuthUsernamePassword() to get the user and password that we set earlier in the previous method and pass that to the HttpAuthHander .

The devil is in the detail, we actually use a private method getBaseDomain() in shouldInterceptRequest() before, to set the credentials for all the things on <domain>.jp. Unfortunately, if you have different user and password authentication on separate sub domains this technique will not work :(