couchbaselabs / Couchbase-Lite-PhoneGap-Plugin

Plugin to install Couchbase Lite in your PhoneGap app on iOS or Android
183 stars 67 forks source link

Can't connect to CBL on Android if not using coax library #74

Open snej opened 8 years ago

snej commented 8 years ago

Developers who don't use the coax library to send their REST requests won't be able to connect to CBL at all on Android — every request will fail with a 401 Unauthorized status. See this forum thread, and this one too.

The reason is that Android's CBL listener is configured with a random username/password (for security reasons), which is encoded in the URL given to the app; but those credentials have to be extracted from the URL and turned into an Authorization: header in the actual XHR. Coax knows how to do this, but other libraries apparently don't — in the thread above, the developer was using the fetch library.

This problem doesn't occur on iOS (or Mac) because on those platforms we don't need to set a password — we're able to use NSURLProtocol instead of opening a real TCP socket, so there's no possibility of untrusted apps being able to connect. But this can blindside developers because if they develop on those platforms first, their app will work great on iOS but mysteriously fail completely on Android.

We should probably bundle an API for REST requests, like coax, with the plugin itself, and encourage developers to use it. But we also need to document that, if they don't use it, they need to set the Authorization header themselves. (Ideally we can provide a JS function that takes a URL with credentials and returns the username and password.)

jfspencer commented 8 years ago

I have been using cbl on android successfully without coax using a typescript wrapper over the REST API. https://github.com/happieio/cordova-plugin-couchbase-lite/blob/master/APIs/typescript/cbl.ts

Here is a reference cordova app using the typescript wrapper. https://github.com/happieio/typeapp

snej commented 8 years ago

Well, that's weird. Your wrapper doesn't do anything with Authorization headers; that would imply that the XHR implementation generates one from the credentials in the URL. Then why isn't that happening to the people running into this problem? Maybe it has something to do with the version of Cordova?

jfspencer commented 8 years ago

It looks like the native side of the Cordova API is handling the generation of the user name and password. https://github.com/couchbaselabs/Couchbase-Lite-PhoneGap-Plugin/blob/master/src/android/CBLite.java#L77-L92

perhaps people that are running into the issue haven't updated this file? I do remember this being an issue a while ago.

snej commented 8 years ago

No, the people having this problem definitely have the username and password in the URL. The problem is that those aren't being sent back in the XHR — they don't get translated into an Authorization header.

snej commented 8 years ago

See also this forum thread, where a document attachment can't be used as the src in an HTML IMG tag because of 401 errors.

HanzhiDou commented 8 years ago

My app uses Sencha Touch, the Ext.Ajax.request can access cbl without additional authorization handling.

Ext.Ajax.request({ url: url, method: 'GET', useDefaultXhrHeader: false, success: function (response) { }, failure: function (response) { } });

However, when I use Ext.Ajax.request to request replication with sync gateway, the web authorization popup window will show.

hideki commented 8 years ago

Note:

TJWS might not accept credential with url (http://<username>:<password>@hostname/path).

https://github.com/couchbase/couchbase-lite-java-listener/blob/master/src/main/java/com/couchbase/lite/listener/LiteServer.java http://tjws.sourceforge.net/

snej commented 8 years ago

A web server never sees the credentials in this form -- it only receives the path and the host name. It's up to the client to detect the credentials and convert them into an Authorization header that the server will receive.

hideki commented 8 years ago

Implementation of couchbase-lite-java-listener only accepts credential from Authorization HTTP request header. The client needs to set Authorization header.

https://github.com/couchbase/couchbase-lite-java-listener/blob/master/src/main/java/com/couchbase/lite/listener/LiteServlet.java#L56-L68

        Credentials requestCredentials = credentialsWithBasicAuthentication(request);

        if (allowedCredentials != null && !allowedCredentials.empty()) {
            if (requestCredentials == null || !requestCredentials.equals(allowedCredentials)) {
                Log.d(Log.TAG_LISTENER, "Unauthorized -- requestCredentials not given or do not match allowed credentials");
                response.setHeader("WWW-Authenticate", "Basic realm=\"Couchbase Lite\"");
                response.setStatus(401);
                return;
            }
            Log.v(Log.TAG_LISTENER, "Authorized via basic auth");
        } else {
            Log.v(Log.TAG_LISTENER, "Not enforcing basic auth -- allowedCredentials null or empty");
        }

https://github.com/couchbase/couchbase-lite-java-listener/blob/master/src/main/java/com/couchbase/lite/listener/LiteServlet.java#L166-L198

    public Credentials credentialsWithBasicAuthentication(HttpServletRequest req) {
        try {
            String authHeader = req.getHeader("Authorization");
            if (authHeader != null) {
                StringTokenizer st = new StringTokenizer(authHeader);
                if (st.hasMoreTokens()) {
                    String basic = st.nextToken();
                    if (basic.equalsIgnoreCase("Basic")) {
                        try {
                            String credentials = new String(Base64.decode(st.nextToken()), "UTF-8");
                            Log.v(Log.TAG_LISTENER, "Credentials: ", credentials);
                            int p = credentials.indexOf(":");
                            if (p != -1) {
                                String login = credentials.substring(0, p).trim();
                                String password = credentials.substring(p + 1).trim();

                                return new Credentials(login, password);
                            } else {
                                Log.e(Log.TAG_LISTENER, "Invalid authentication token");
                            }
                        } catch (Exception e) {
                            Log.w(Log.TAG_LISTENER, "Couldn't retrieve authentication", e);
                        }
                    }
                }
            } else {
                Log.d(Log.TAG_LISTENER, "authHeader is null");
            }
        } catch (Exception e) {
            Log.e(Log.TAG_LISTENER, "Exception getting basic auth credentials", e);
        }
        return null;
    }
snej commented 8 years ago

I know; that's what I said yesterday. The problem here isn't in the CBL listener. The problem is that we're expecting something to convert the credentials in the URL into an Authorization header, and for some reason it isn't happening. That "something" seems like part of Cordova itself, or part of the web view the app is running in.