In this post I’m going to write about an important new feature in Couchbase Mobile version 1.3, OpenID Connect (OIDC) support.

Introduction

Couchbase Mobile has an number of important security features. Sync Gateway acts as the intermediary that allows Couchbase Lite to replicate data to Couchbase Server. Naturally many applications want to authenticate users and control what they can do through Sync Gateway. OpenID Connect offers an option that simultaneously simplifies adding authentication, gives developers a big step up in integration options, and removes the headache of supporting the infrastructure needed.

The Goal

Let me clarify a little. In a typical scenario, an application uses Couchbase Lite as its primary data store. Information gets read from and written to the local database. The application sets up replication to a backend server through a Sync Gateway instance running in the cloud.

Often, as the application developer, you want to tie data to a unique, verifiable identity. Details of the identity don’t really matter. Managing the infrastructure becomes unnecessary overhead. OpenID Connect provides the way to offload this to other trusted sources.

Let’s spell out all the actors. We have the user, the application, Couchbase Lite (integrated in the application), our Sync Gateway instance, and OpendID Provider (OP).

Sync Gateway controls authorizing any changes during replication. The goal then, is to have the user log in on the application by authenticating themselves to the OP. The OP provides information back the application can then use to provide Sync Gateway proof of the user’s identity. We’ll walk through all the steps for this.

OpendID Connect

Many services already exist that require users to create an account and so on. The OpenID Foundation formed to build standards to open up the authentication portions of these systems to outside use. This effort produced the OpenID Connect specification. In the words of the OpenID Foundation:

OpenID Connect is an interoperable authentication protocol based on the OAuth 2.0 family of specifications. It uses straightforward REST/JSON message flows with a design goal of “making simple things simple and complicated things possible”. It’s uniquely easy for developers to integrate, compared to any preceding Identity protocol.

To find out more about OpenID Connect, you can take a look at the links above. I also found this write-up valuable.

Flows

OpenID Connect follows three possible “flows”. A flow specifies the back and forth stages of the protocol, and all the details of what parameters are necessary and what they mean. We’ll talk about the Authorization Code Flow (auth flow) here. The auth flow sets up refresh capabilities, so if you don’t want to bother a user to sign in too often, it’s the one to use.

Couchbase has implemented classes to abstract out much of the complexity of OpendID Connect. Like other parts of Couchbase Lite, the authentication classes handle a lot behind the scenes, especially the network calls.

Still, a full code sample is too much for one blog post. I’ll provide simplified examples. To see a complete application, take a look at the GrocerySync projects under couchbaselabs on Github. Here are links for Android and iOS. (Note as of this writing you need to switch to the openid branch.)

Let’s walk through the key steps in implementing authentication in an Android app. We’ll use Google as our OpenID Provider (OP).

On Android, you can implement something directly, use the Google Sign-In client library, or use the wrappers built into Couchbase Lite. We’ll use the Couchbase Lite wrappers. To understand the underlying code better, you might want to check out Google’s documentation on implementing OpenID Connect. You can find it here.

Replication

In Couchbase, we refer to syncing database changes as replication. No surprise then, that authentication ties deeply to replication. Briefly, to sync, you create replication objects, set some options, then fire them up, either in one-shot or continuous mode.

You typically do authentication by creating an Authenticator object and assigning it to a Replication object. Couchbase Lite has factories for creating different types of authenticators. You can read more about replications, Replication objects, and authenicators here.

Code

In my simple example app, the first Activity on launch presents two buttons, one to sign in, and one to sign out. I’ll refer to this as the main activity. If the user isn’t signed in, clicking the sign in button launches an activity with a web view to handle the sign in process. I’ll call that the login activity. Storing the credentials happens after returning to the main activity.

The code comprises three key sections, spread across these two activities.

Prepare a Replication

Replications have a “direction”. A pull replication transfers data from a Sync Gateway, while a push sends data to one. Here I just set up a pull replication, to highlight the authentication piece. The code is broken out, but gets called during onCreate in the main activity.

private Replication pull; private void prepareToReplicate() { Database db = Runtime.getDb(); try { pull = db.createPullReplication(new URL(Runtime.getSyncUrl())); } catch (MalformedURLException ex) { ex.printStackTrace(); } pull.setContinuous(true); Authenticator authenticator = OpenIDConnectAuthenticatorFactory .createOpenIDConnectAuthenticator(new OIDCLoginCallback() { @Override public void callback(URL loginURL, URL redirectURL, OIDCLoginContinuation loginContinuation) { MainActivity.this.loginContinuation = loginContinuation; Intent loginIntent = new Intent(getApplicationContext(), Login.class); loginIntent.putExtra(Login.LOGIN_URL_KEY, loginURL); loginIntent.putExtra(Login.REDIRECT_URL_KEY, redirectURL); startActivityForResult(loginIntent, LOGIN_REQUEST); } }, new AndroidContext(getApplicationContext())); pull.setAuthenticator(authenticator); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private Replication pull ; private void prepareToReplicate ( ) { Database db = Runtime . getDb ( ) ; try { pull = db . createPullReplication ( new URL ( Runtime . getSyncUrl ( ) ) ) ; } catch ( MalformedURLException ex ) { ex . printStackTrace ( ) ; } pull . setContinuous ( true ) ; Authenticator authenticator = OpenIDConnectAuthenticatorFactory . createOpenIDConnectAuthenticator ( new OIDCLoginCallback ( ) { @Override public void callback ( URL loginURL , URL redirectURL , OIDCLoginContinuation loginContinuation ) { MainActivity . this . loginContinuation = loginContinuation ; Intent loginIntent = new Intent ( getApplicationContext ( ) , Login . class ) ; loginIntent . putExtra ( Login . LOGIN_URL_KEY , loginURL ) ; loginIntent . putExtra ( Login . REDIRECT_URL_KEY , redirectURL ) ; startActivityForResult ( loginIntent , LOGIN_REQUEST ) ; } } , new AndroidContext ( getApplicationContext ( ) ) ) ; pull . setAuthenticator ( authenticator ) ; }

The critical part, for our purposes, lies in creating the authenticator. Don’t let the length of the code lines fool you. It’s all very straightforward. It might help to examine the code from the inside out.

Take a look at the body of the callback method. It takes three input parameters. One it saves for later. Two it puts into an Intent . That intent kicks off the login activity and asks to receive a result.

All the formulating of the URLs the way OpenID needs them has been done for you. For example, when I checked the loginURL, it was well over 400 characters long with what looked like more than 15 parameters! Having implemented an OAuth 2 flow myself managing the network calls and all, I can definitely say this is a lot nicer.

We’ll see later how the Sync Gateway configuration feeds into this flow. For the moment, if you do examine the URLs, know Sync Gateway supplies portions in response to the initial unauthenticated kick off of a replication.

The third parameter to the callback method (the OIDCLoginContinuation interface implementation) is supplied by Couchbase Lite. This is the hook that you use to pass back the results of the sign on. We’ll explore it in more detail in a moment.

Login Activity

As you can probably guess, many OpenID providers work off web pages. The login activity consists of a standard web view. (Using a web view has significant security implications. Please see the special note at the end about security for details.) You need to set up navigation and enable JavaScript. You can read all about it in the Android Guide to building web apps. We only need to look at intercepting URL loading in detail. Here’s the code.

private class LoginWebClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri uri = Uri.parse(url); if (uri.getHost().contentEquals(redirectURL.getHost())) { Intent response = new Intent(); response.setData(uri); setResult(RESULT_OK, response); finish(); return(true); // true => application will handle this } return(false); // Webview will load url } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private class LoginWebClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading ( WebView view , String url ) { Uri uri = Uri . parse ( url ) ; if ( uri . getHost ( ) . contentEquals ( redirectURL . getHost ( ) ) ) { Intent response = new Intent ( ) ; response . setData ( uri ) ; setResult ( RESULT_OK , response ) ; finish ( ) ; return ( true ) ; // true => application will handle this } return ( false ) ; // Webview will load url } }

(Note Android Studio may give you a warning about shouldOverrideUrlLoading being deprecated. As of this writing, this version is still the most widely supported.)

When you supply a WebViewClient , Android uses it to hook all new page loads. OpenID encodes the result of an authentication attempt in the form of a URL with, once again, a bunch of parameters embedded. Our WebViewClient waits to spot the redirect URL, packages that up, and ends the login activity, returning back to our main activity. You may want to look into handling this with an AsyncTask to add extras like a progress bar.

Finishing up

The login activity passes the response url back to the main activity. To complete the authentication process (including storing credentials), Couchbase Lite supplies its own callback, one that implements the OIDCLoginContinuation interface. The interface takes two arguments, the result url, and an exception. The comments in the code snippet explain the semantics of the arguments, including how combinations of null and non-null values indicate success or other outcomes.

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (LOGIN_REQUEST == requestCode) { URL url = null; Exception error = null; if (RESULT_OK == resultCode) { try { String response = data.getData().toString(); url = new URL(response); } catch (MalformedURLException ex) { ex.printStackTrace(); } } else if (RESULT_CANCELED != resultCode) { error = new Exception("Login failed."); } // url = auth redirect => success // url = null, ex = error => error // url = null, ex = null => canceled loginContinuation.callback(url, error); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override protected void onActivityResult ( int requestCode , int resultCode , Intent data ) { if ( LOGIN_REQUEST == requestCode ) { URL url = null ; Exception error = null ; if ( RESULT_OK == resultCode ) { try { String response = data . getData ( ) . toString ( ) ; url = new URL ( response ) ; } catch ( MalformedURLException ex ) { ex . printStackTrace ( ) ; } } else if ( RESULT_CANCELED ! = resultCode ) { error = new Exception ( "Login failed." ) ; } // url = auth redirect => success // url = null, ex = error => error // url = null, ex = null => canceled loginContinuation . callback ( url , error ) ; } }

I handle errors simplistically for illustration. You will likely want to do something more sophisticated. The response url can contain error information if something goes wrong. You can use that to give the user more detailed insight into what failed.

App Code Summary

Using the Couchbase Lite classes, we have a very simple flow. We set up an authenticator and attach it to a replication. We start the replication, possibly in response to a user action (i.e. clicking the sign-in button).

The authenticator hands us back control along with three pre-formed elements, two urls and a Couchbase Lite supplied callback. We proceed with the authentication however we choose. And, finally, we need to finish off by calling the callback we received.

Tips and Tricks

User ID

We didn’t show it in the code, but often you’ll want to assign some kind of user id based on information in the OpenID response. You can use the id to create a Sync Gateway channel to filter documents, for example.

Signing out

You can clear the credentials from a replication by calling the clearAuthenticationStores method. If you used a web view, you may also want to get rid of any session cookies. This code snippet shows how to do that with Android API 21 or later.

pull.clearAuthenticationStores(); CookieManager.getInstance().removeAllCookies(null); CookieManager.getInstance().flush(); 1 2 3 pull . clearAuthenticationStores ( ) ; CookieManager . getInstance ( ) . removeAllCookies ( null ) ; CookieManager . getInstance ( ) . flush ( ) ;

Testing

You can test your code using an emulator and a Sync Gateway instance running on the same machine. Common emulators like the one supplied with Android or the one by Genymotion will allow you to connect to a service on your machine using a special IP address. Google’s OP will often reject these in urls, though. I used a trick where Sync Gateway specifies localhost as its address. Then in the onActivityResult call, I substitute the IP address required by the emulator with this line:

response = response.replaceFirst(Runtime.EMULATOR_HOST, Runtime.EMULATOR_IP); 1 response = response . replaceFirst ( Runtime . EMULATOR_HOST , Runtime . EMULATOR_IP ) ;

Configuring Sync Gateway

We’re done with the application code. Let’s take a brief look at configuring Sync Gateway to use OIDC. This sample configuration shows how to configure GoogleAuthFlow as a provider. You’ll need to generate your own client_id and validation_key for production. See the Google documentation and links for details. Find out more about Sync Gateway and how to configure it in the Couchbase documentation here.

{ "log": ["*"], "databases": { "grocery-sync": { "server": "walrus:.", "users": { "GUEST": {"disabled": true} }, "unsupported": { "oidc_test_provider": { "enabled": true } }, "oidc": { "providers": { "GoogleAuthFlow": { "issuer":"https://accounts.google.com", "client_id":"31919031332-8ea1795ckkphb7hmg6i4ul0blcpq8oq5.apps.googleusercontent.com", "validation_key":"OCIbokd6-SE8LMZE_vQsq8F5", "callback_url":"http://localhost:4984/grocery-sync/_oidc_callback", "register":true } }, "default_provider": "GoogleAuthFlow" }, "sync": ` function(doc, oldDoc) { var username = doc.owner; if (!username) username = oldDoc.owner; if (!username) throw({forbidden : "item must have an owner"}); var channelName = "ch-" + username; access(username, channelName); channel(channelName); } ` } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 { "log" : [ "*" ] , "databases" : { "grocery-sync" : { "server" : "walrus:." , "users" : { "GUEST" : { "disabled" : true } } , "unsupported" : { "oidc_test_provider" : { "enabled" : true } } , "oidc" : { "providers" : { "GoogleAuthFlow" : { "issuer" : "https://accounts.google.com" , "client_id" : "31919031332-8ea1795ckkphb7hmg6i4ul0blcpq8oq5.apps.googleusercontent.com" , "validation_key" : "OCIbokd6-SE8LMZE_vQsq8F5" , "callback_url" : "http://localhost:4984/grocery-sync/_oidc_callback" , "register" : true } } , "default_provider" : "GoogleAuthFlow" } , "sync" : ` function ( doc , oldDoc ) { var username = doc . owner ; if ( ! username ) username = oldDoc . owner ; if ( ! username ) throw ( { forbidden : "item must have an owner" } ) ; var channelName = "ch-" + username ; access ( username , channelName ) ; channel ( channelName ) ; } ` } } }

Special Note – Security

In part, this blog outlines using a web view as part of the authentication process. This approach has drawbacks, including the possibility that the app can eavesdrop on the authentication process. For increased security, you may wish to consider using the system browser instead. For more details, I recommend reading the latest version of this IETF Internet-Draft. Security tends to be a moving target. If you’re dealing with highly sensitive information, take the time to read up on current best practices.

Postscript