Search code examples
ioscordovaoauthgoogle-oauthinappbrowser

InAppBrowser OAUTH didFailLoadWithError 1004 "Could not connect to the server." "<allow-navigation> not set for url"


I see that this question is similar to another question (webView:didFailLoadWithError -1004: Could not connect to the server while connecting google plus in Phonegap ios) , but somehow different because I've gone over the code line-by-line and it is doing the samething, but it is still not working for me. Perhaps also because I am on different versions: iOS 9.3.2 on an iPhone 5S), Cordova 6.1.1, and cordova-plugin-inappbrowser 1.3.0.

My code works well on my Android, but not on the iPhone. Code is as follows:

    var googleapi = {
    authorize: function(options) {
        var deferred = $.Deferred();
        var authUrl = GOOGLE_CLIENT_API_URL + $.param({
            client_id: options.client_id,
            redirect_uri: options.redirect_uri,
            response_type: 'code',
            scope: options.scope
        });
        console.log("authUrl: " + authUrl);
        var authWindow = window.open(authUrl, "_blank", "location=no,toolbar=no");  // for iOS add 'toolbar=no'

        //The recommendation is to use the redirect_uri "urn:ietf:wg:oauth:2.0:oob" 
        //which sets the authorization code in the browser's title. However, we can't 
        //access the title of the InAppBrowser. 
        // 
        //Instead, we pass a bogus redirect_uri of "http://localhost", which means the 
        //authorization code will get set in the url. We can access the url in the 
        //loadstart and loadstop events. So if we bind the loadstart event, we can 
        //find the authorization code and close the InAppBrowser after the user 
        //has granted us access to their data. 
        //
        // To clear the authorization, go to https://accounts.google.com/IssuedAuthSubTokens.
        $(authWindow).on('loadstart', function(e) {
            var url = e.originalEvent.url;
            var code = /\?code=(.+)$/.exec(url);
            var error = /\?error=(.+)$/.exec(url);

            if(code || error) {
                authWindow.close();
            }
            if (code) { 
                //Exchange the authorization code for an access token 
                $.post('https://accounts.google.com/o/oauth2/token', { 
                    code: code[1], 
                    client_id: options.client_id, 
                    client_secret: options.client_secret, 
                    redirect_uri: options.redirect_uri, 
                    grant_type: 'authorization_code' 
                }).done(function(data) {
                    // use the token we got back from oauth to setup the api.
                    gapi.auth.setToken(data);
                    // load the drive api.
                    loadDriveApi();
                    deferred.resolve(data); 
                }).fail(function(response) {
                    console.log("Posting code to Google failed.  No OAuth token will be returned.");
                    deferred.reject(response.responseJSON); 
                }); 
            } else if (error) { 
                //The user denied access to the app 
                console.log("Error retrieving code from Google.");
                deferred.reject({ 
                    error: error[1] 
                }); 
            } 
        });

        return deferred.promise();
    }
};

function checkAuth() {
    if(device.platform === 'browser') {
        console.log("calling gapi.auth.authorize()");
        gapi.auth.authorize(
        {
            'client_id' : CLIENT_ID,
            'scope' : SCOPES.join(' '),
            'immediate' : true
        }, handleAuthResult);
    } else {
        // because this is called only after deviceready(), InAppBrowser is initialized by now:
        console.log("using the InAppBrowser plugin to authenticate.");
        window.open = cordova.InAppBrowser.open;

        googleapi.authorize(
        {
            'client_id' : CLIENT_ID,
            'client_secret' : CLIENT_SECRET,
            'redirect_uri' : REDIRECT_URI,
            'scope' : SCOPES.join(' ')
        }, handleAuthResult);
    }
}

/**
 * Handle response from authorization server.
 *
 * @param {Object} authResult Authorization result.
 */
function handleAuthResult(authResult) {
    var authMenuItem = document.getElementById("menuitemenablegoogledrivebackup");
    if (authResult && !authResult.error) {
        // If already authorized, change menu option to allow user to deny Authorization
        authMenuItem.innerHTML = l("Disable Google Drive Backup");
        loadDriveApi();
    } else {
        alert("Authorization Error: " + authResult.error);
        console.log("inside handleAuthResult, authResult.error: " + authResult.error);

        // Show auth menu item, allowing the user to initiate authorization
        authMenuItem.innerHTML = l("Enable Google Drive Backup");
        // use the InAppBrowser to display the authorization window:
        // var authWindow = window.open(authUrl, '_blank', 'location=no,toolbar=no');
        // or?
        // gapi.auth.authorize(
        //  {
        //      client_id: CLIENT_ID,
        //      scope: SCOPES.join(' '),
        //      immediate: false
        //  }, handleAuthResult)
    }
}

/**
 * Load Drive API client library.
 */
function loadDriveApi() {
    try {
    gapi.client.load('drive', 'v2', null).then(function(resp) {
        console.log("Google Drive API v2 loaded successfully.");
    }, function(reason) {
        alert('Google Drive API v2 FAILED to load: ' + reason.result.error.message);
        console.log('Google Drive aPI v2 FAILED to load: ' + reason.result.error.message);
    });
    } catch(err) {
        alert(err.message);
        console.log("Google Drive API v2 FAILED to load.  Exception: " + err.message);
    }
}

From debugging, I see that the Android version calls the window.open() call, which goes through the loadstart handler first once, with the original URL, but it contains no code, and no error, so it just passes through. Then the redirect_url comes up, on a second call to the loadstart handler (is this by the InAppBrowser?) but this time it has the shorter redirect_url with the code appended, so the code is then successfully used to get the token on the "$.post" call. However, on iOS, there is no second call to the loadstart handler.

When I run it in the Chrome debugger, I get no errors, just silent failure. In the XCode debugger, I get errors as follows:

2016-06-09 20:47:27.014 APass2[675:398271] Setting the WebView's frame to {{0, 0}, {320, 524}} 2016-06-09 20:47:27.015 APass2[675:398271] Setting the WebView's frame to {{0, 0}, {320, 568}} 2016-06-09 20:47:27.026 APass2[675:398271] THREAD WARNING: ['InAppBrowser'] took '39.259033' ms. Plugin should use a background thread. 2016-06-09 20:47:27.749 APass2[675:398271] webView:didFailLoadWithError - -1004: Could not connect to the server. 2016-06-09 20:47:28.955 APass2[675:398271] ERROR Internal navigation rejected - not set for url='https://content.googleapis.com/static/proxy.html?jsh=m%3B%2F_%2Fscs%2Fapps-static%2F_%2Fjs%2Fk%3Doz.gapi.en.joG9nQvYxYQ.O%2Fm%3D__features__%2Fam%3DAQ%2Frt%3Dj%2Fd%3D1%2Frs%3DAGLTcCPyXDgCg_S7GlvvvMpztuAZ6V0pEA#parent=file%3A%2F%2F&rpctoken=1268129019'

None of my success or fail callbacks is called.

Please help!!! I'm totally at a loss now.

Thanks, Edward


Solution

  • First of all, by looking at the InAppBrowser documentation, I learned that there is also a "loaderror" event. Only on iOS, the call to the inAppBrowser.open() was resulting in "loaderror" handler being called. Inside the "loaderror" handler, I was also able to grab the url, just as the original code did on "loadstart". Debugging simultaneously in Chrome and Safari I was able to see that the url was exactly the same in the "loaderror" as in the "loadstart" handler, and the parsing for code and error worked exactly the same way. So, in the first cut, I hacked it that way and got to the next phase (success - sort of). Then I hit another error related to <access-navigation>. Googling that much more, I found that there is a configuration setting available in config.xml in the root of your project.

    Lots more Googling pointed me at someone who said to use <allow-navigation href="*" />

    Clearly, I was unhappy with that broad a security hole.

    So, the bottom line is that I needed to add the urls that the Google api needs to access to the config.xml file as follows:

    <allow-navigation href="https://accounts.google.com/*" />
    <allow-navigation href="https://content.googleapis.com/*" />
    

    I still need to clean up the code, and probably simplify the error handling in the "loaderror" handler, but I have got it working now!

    Most frustrating of all is that this setting is not necessary at all on Android, so I had no reason to suspect this was the problem.

    Thank you to those of you who took the time to look at this!

    Edward