// // Copyright (c) 2016 Couchbase, Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distributed under the // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing permissions // and limitations under the License. // package com.couchbase.lite.syncgateway; import com.couchbase.lite.CouchbaseLiteException; import com.couchbase.lite.Database; import com.couchbase.lite.Document; import com.couchbase.lite.LiteTestCaseWithDB; import com.couchbase.lite.TransactionalTask; import com.couchbase.lite.UnsavedRevision; import com.couchbase.lite.auth.Authenticator; import com.couchbase.lite.auth.AuthenticatorFactory; import com.couchbase.lite.auth.Authorizer; import com.couchbase.lite.auth.MemTokenStore; import com.couchbase.lite.auth.OpenIDConnectAuthorizer; import com.couchbase.lite.auth.OIDCLoginCallback; import com.couchbase.lite.auth.OIDCLoginContinuation; import com.couchbase.lite.auth.TokenStore; import com.couchbase.lite.auth.TokenStoreFactory; import com.couchbase.lite.replicator.RemoteFormRequest; import com.couchbase.lite.replicator.RemoteRequest; import com.couchbase.lite.replicator.RemoteRequestCompletion; import com.couchbase.lite.replicator.RemoteRequestResponseException; import com.couchbase.lite.replicator.Replication; import com.couchbase.lite.support.CouchbaseLiteHttpClientFactory; import com.couchbase.lite.support.PersistentCookieJar; import com.couchbase.lite.util.Log; import junit.framework.Assert; import java.net.URL; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.Response; /** * Replicaton_Tests.m * <p/> * Steps to run this test * 1. Run sync gateway with assets/configs/cbl_unit_tests.json * 2. In assets/test.properties * set "true" for syncgatewayTestsEnabled * set "Sync Gateway's IP address" for replicationServer */ public class ReplicationWithSGTest extends LiteTestCaseWithDB { public static final String TAG = "AuthFailureTest"; @Override protected void setUp() throws Exception { if (!syncgatewayTestsEnabled()) return; super.setUp(); } public void test19_Auth_Failure() throws Exception { if (!syncgatewayTestsEnabled()) return; URL remoteDbURL = getRemoteTestDBURL("cbl_auth_test"); assertNotNull(remoteDbURL); Replication repl = database.createPullReplication(remoteDbURL); repl.setAuthenticator(AuthenticatorFactory.createBasicAuthenticator("wrong", "wrong")); runReplication(repl); Throwable error = repl.getLastError(); assertNotNull(error); assertTrue(error instanceof RemoteRequestResponseException); RemoteRequestResponseException rrre = (RemoteRequestResponseException) error; assertEquals(401, rrre.getCode()); Map challenge = (Map) rrre.getUserInfo().get("AuthChallenge"); assertEquals("Basic", challenge.get("Scheme")); assertEquals("Couchbase Sync Gateway", challenge.get("realm")); // TODO: // OAuthAuthenticator is not implemented. } // Creates a doc with a very deep revision history and pushes the entire history to the server. // Then pulls it into another database. public void test25_DeepRevTree() throws Exception { if (!syncgatewayTestsEnabled()) return; final int kNumRevisions = 2000; URL remoteDbURL = getRemoteTestDBURL("db"); assertNotNull(remoteDbURL); Replication push = database.createPushReplication(remoteDbURL); final Document doc = database.getDocument("deep"); final AtomicInteger numRevisions = new AtomicInteger(0); for (; numRevisions.get() < kNumRevisions; ) { database.runInTransaction(new TransactionalTask() { @Override public boolean run() { // Have to push the doc periodically, to make sure the server gets the whole // history, since CBL will only remember the latest 20 revisions. int batchSize = Math.min(database.getMaxRevTreeDepth() - 1, kNumRevisions - numRevisions.get()); Log.i(TAG, String.format(Locale.ENGLISH, "Adding revisions %d -- %d ...", numRevisions.get() + 1, numRevisions.get() + batchSize)); try { for (int i = 0; i < batchSize; ++i) { doc.update(new Document.DocumentUpdater() { @Override public boolean update(UnsavedRevision newRevision) { Map<String, Object> properties = newRevision.getUserProperties(); properties.put("counter", numRevisions.addAndGet(1)); newRevision.setUserProperties(properties); return true; } }); } } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in Document.update()", e); Assert.fail("Error in Document.update()"); } return true; } }); Log.i(TAG, "Pushing ..."); runReplication(push); } Log.i(TAG, "\n\n$$$$$$$$$$ PULLING TO DB2 $$$$$$$$$$"); // Now create a second database and pull the remote db into it: Database db2 = manager.getDatabase("prepopdb"); assertNotNull(db2); Replication pull = db2.createPullReplication(remoteDbURL); runReplication(pull); Document doc2 = db2.getDocument("deep"); assertEquals(db2.getMaxRevTreeDepth(), doc2.getRevisionHistory().size()); assertEquals(1, doc2.getConflictingRevisions().size()); Log.i(TAG, "\n\n$$$$$$$$$$ PUSHING 1 DOC FROM DB $$$$$$$$$$"); // Now add a revision to the doc, push, and pull into db2: doc.update(new Document.DocumentUpdater() { @Override public boolean update(UnsavedRevision newRevision) { Map<String, Object> properties = newRevision.getUserProperties(); properties.put("counter", numRevisions.addAndGet(1)); newRevision.setUserProperties(properties); return true; } }); runReplication(push); Log.i(TAG, "\n\n$$$$$$$$$$ PULLING 1 DOC INTO DB2 $$$$$$$$$$"); runReplication(pull); assertEquals(db2.getMaxRevTreeDepth(), doc2.getRevisionHistory().size()); assertEquals(1, doc2.getConflictingRevisions().size()); } // #pragma mark - OPENID CONNECT: public void test26_OpenIDConnectAuth() throws Exception { _test26_OpenIDConnectAuth(TokenStoreFactory.build(getTestContext("db"))); } public void test26_OpenIDConnectAuthMem() throws Exception { _test26_OpenIDConnectAuth(new MemTokenStore()); } public void _test26_OpenIDConnectAuth(TokenStore tokenStore) throws Exception { if (!syncgatewayTestsEnabled() || !isSQLiteDB()) return; final URL remoteDbURL = getRemoteTestDBURL("openid_db"); assertNotNull(remoteDbURL); OpenIDConnectAuthorizer.forgetIDTokensForServer(remoteDbURL, tokenStore); Authenticator auth = AuthenticatorFactory.createOpenIDConnectAuthenticator( new OIDCLoginCallback() { @Override public void callback(URL login, URL authBase, OIDCLoginContinuation cont) { assertValidOIDCLogin(login, authBase, remoteDbURL); // Fake a form submission to the OIDC test provider, to get an auth URL redirect: URL authURL = loginToOIDCTestProvider(remoteDbURL); assertNotNull(authURL); Log.e(TAG, "**** Callback handing control back to authenticator..."); cont.callback(authURL, null); } }, tokenStore); Throwable authError = pullWithOIDCAuth(auth, "pupshaw"); assertNull(authError); // Now try again; this should use the ID token from the keychain and/or a session cookie: Log.v(TAG, "**** Second replication..."); final AtomicBoolean callbackInvoked = new AtomicBoolean(false); auth = AuthenticatorFactory.createOpenIDConnectAuthenticator( new OIDCLoginCallback() { @Override public void callback(URL login, URL authBase, OIDCLoginContinuation cont) { assertValidOIDCLogin(login, authBase, remoteDbURL); assertFalse(callbackInvoked.get()); callbackInvoked.set(true); cont.callback(null, null); // cancel } }, tokenStore); authError = pullWithOIDCAuth(auth, "pupshaw"); assertNull(authError); assertFalse(callbackInvoked.get()); } public void test27_OpenIDConnectAuth_ExpiredIDToken() throws Exception { _test27_OpenIDConnectAuth_ExpiredIDToken(TokenStoreFactory.build(getTestContext("db"))); } public void test27_OpenIDConnectAuth_ExpiredIDTokenMem() throws Exception { _test27_OpenIDConnectAuth_ExpiredIDToken(new MemTokenStore()); } public void _test27_OpenIDConnectAuth_ExpiredIDToken(TokenStore tokenStore) throws Exception { if (!syncgatewayTestsEnabled() || !isSQLiteDB()) return; final URL remoteDbURL = getRemoteTestDBURL("openid_db"); assertNotNull(remoteDbURL); OpenIDConnectAuthorizer.forgetIDTokensForServer(remoteDbURL, tokenStore); final AtomicBoolean callbackInvoked = new AtomicBoolean(false); Authenticator auth = AuthenticatorFactory.createOpenIDConnectAuthenticator( new OIDCLoginCallback() { @Override public void callback(URL login, URL authBase, OIDCLoginContinuation cont) { assertValidOIDCLogin(login, authBase, remoteDbURL); assertFalse(callbackInvoked.get()); callbackInvoked.set(true); cont.callback(null, null); // cancel } }, tokenStore); // Set bogus ID and refresh tokens, so first the session check will fail, then the attempt // to refresh the ID token will fail. Finally the callback above will be called. ((OpenIDConnectAuthorizer) auth).setIDToken("BOGUS_ID"); ((OpenIDConnectAuthorizer) auth).setRefreshToken("BOGUS_REFRESH"); Throwable authError = pullWithOIDCAuth(auth, null); assertTrue(callbackInvoked.get()); assertTrue(authError instanceof RemoteRequestResponseException); RemoteRequestResponseException rrre = (RemoteRequestResponseException) authError; assertEquals(RemoteRequestResponseException.USER_DENIED_AUTH, rrre.getCode()); } // Use the CBLRestLogin class to log in with OIDC without using a replication public void test28_OIDCLoginWithoutReplicator() throws Exception { // TODO: // RemoteLogin is not implemented yet. } private URL loginToOIDCTestProvider(URL remoteDbURL) { // Fake a form submission to the OIDC test provider, to get an auth URL redirect: try { PersistentCookieJar cookieStore = database.getPersistentCookieStore(); CouchbaseLiteHttpClientFactory factory = new CouchbaseLiteHttpClientFactory(cookieStore); // Don't redirect factory.setFollowRedirects(false); URL formURL = new URL(remoteDbURL.toExternalForm() + "/_oidc_testing/authenticate?client_id=CLIENTID&redirect_uri=http%3A%2F%2F" + getSyncGatewayHost() + "%3A4984%2Fopenid_db%2F_oidc_callback&response_type=code&scope=openid+email&state="); Map<String, String> formData = new HashMap<String, String>(); formData.put("username", "pupshaw"); formData.put("authenticated", "true"); final Map<String, Object> results = new HashMap<String, Object>(); RemoteRequest rq = new RemoteFormRequest(factory, "POST", formURL, false, formData, null, new RemoteRequestCompletion() { @Override public void onCompletion(Response httpResponse, Object result, Throwable error) { results.put("response", httpResponse); results.put("result", result); results.put("error", error); } }); rq.setAuthenticator(null); Thread t = new Thread(rq); t.start(); t.join(); assertTrue(results.containsKey("error")); assertTrue(results.get("error") instanceof RemoteRequestResponseException); RemoteRequestResponseException rrre = (RemoteRequestResponseException) results.get("error"); assertEquals(302, rrre.getCode()); Response response = (Response) results.get("response"); String authURLStr = response.header("Location"); Log.e(TAG, "Redirected to: %s", authURLStr); assertNotNull(authURLStr); return new URL(authURLStr); } catch (Exception ex) { Log.e(TAG, "Error in loginToOIDCTestProvider() remoteDbURL=<%s>", ex, remoteDbURL); return null; } } private void assertValidOIDCLogin(URL login, URL authBase, URL remoteDbURL) { Log.w(TAG, "*** Login callback invoked with login URL: <%s>, authBase: <%s>", login, authBase); assertNotNull(login); assertEquals(remoteDbURL.getHost(), login.getHost()); assertEquals(remoteDbURL.getPort(), login.getPort()); assertEquals(remoteDbURL.getPath() + "/_oidc_testing/authorize", login.getPath()); assertNotNull(authBase); assertEquals(remoteDbURL.getHost(), authBase.getHost()); assertEquals(remoteDbURL.getPort(), authBase.getPort()); assertEquals(remoteDbURL.getPath() + "/_oidc_callback", authBase.getPath()); } private Throwable pullWithOIDCAuth(Authenticator auth, String username) throws Exception { URL remoteDbURL = getRemoteTestDBURL("openid_db"); if (remoteDbURL == null) return null; Replication repl = database.createPullReplication(remoteDbURL); repl.setAuthenticator(auth); runReplication(repl); if(username != null && repl.getLastError() == null) // SG namespaces the username by prefixing it with the hash of // the identity provider's registered name (given in the SG config file.) assertTrue(repl.getUsername().endsWith(username)); return repl.getLastError(); } }