Custom Authentication with Couchbase Mobile
Couchbase Mobile extends Couchbase to the edge, securely managing and syncing data from any cloud to every mobile device. Couchbase Mobile features an embedded database – Couchbase Lite – with SQL and full-text search for JSON, built-in peer-to-peer synchronization, and end-to-end security from cloud to edge.
Couchbase Mobile also features a secure web gateway – Sync Gateway – that enables data access and synchronization over the web and supports multiple authentication methods. One of these methods is custom authentication where an App Server handles the authentication with an external system.
This blog post will guide you through the custom authentication flow with examples of how to implement the App Server code and the mobile app code. An OpenLDAP server is used in this example, but the flow applies to any external authentication provider.
The example App Server is a simple node.js application and the mobile app code samples use the Couchbase Lite Android SDK.
Prerequisites
This blog assumes familiarity with Couchbase Server, Sync Gateway, and Couchbase Lite.
You will need an operational Sync Gateway 2.5 & Couchbase Server EE 6.x installation. If needed, the following links can get you up and running quickly:
All code from this blog is available in the following Git repository: https://github.com/dugbonsai/sg-custom-auth
Custom Authentication Flow
The following diagram shows an example of custom authentication.
- The mobile app user attempts to log in with their credentials through a REST interface exposed by the App Server.
- The App Server authenticates the user against an OpenLDAP Server.
- If the user is not authenticated, the user is returned to the login UI.
- If the user is authenticated, the App Server gets the user information from Sync Gateway through a REST interface.
- If the user does exist in Sync Gateway skip to step 8.
- If the user does not exist in Sync Gateway, the App Server creates a new user in Sync Gateway through a REST interface.
- If the user was not created in Sync Gateway, the App Server returns an error and the user is returned to the login UI.
- If the user was created in Sync Gateway, the App Server creates a new session for the user in Sync Gateway through a REST interface.
- If the session was not created in Sync Gateway, the App Server returns an error and the user is returned to the login UI.
- If the session was created in Sync Gateway, the App Server returns the session ID.
- The mobile app stores the session ID and creates a Couchbase Lite replicator using the session ID.
- If replication fails due to an expired session in Sync Gateway, the user is returned to the login UI.
OpenLDAP Configuration
If you already have an LDAP server configured you can skip this section. If you do not have an LDAP server configured, these instructions will walk you through configuring OpenLDAP on Ubuntu 18.04.
- Create an LDIF file (ldap_data.ldif) with information to populate the LDAP database (click here for more details on LDIF files). The instructions for configuring OpenLDAP above also contain instructions for creating the LDIF file to populate the LDAP database. In this example, user mobileuser (starting on line 13) will be used when authenticating from the mobile app.
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 |
dn: ou=People,dc=example,dc=com objectClass: organizationalUnit ou: People dn: ou=Groups,dc=example,dc=com objectClass: organizationalUnit ou: Groups dn: cn=department,ou=Groups,dc=example,dc=com objectClass: posixGroup cn: subgroup gidNumber: 5000 dn: uid=mobileuser,ou=People,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount uid: mobileuser sn: User givenName: Mobile cn: Mobile User displayName: mobileuser uidNumber: 10000 gidNumber: 5000 userPassword: password gecos: Mobile User loginShell: /bin/bash homeDirectory: /home/mobileuser |
- Add the initial data to the OpenLDAP database:
1 |
$ ldapadd -x -D cn=admin,dc=example,dc=com -W -f ldap_data.ldif |
- Test the setup by searching for mobileuser in the OpenLDAP directory:
1 |
$ ldapsearch -x -LLL -b dc=example,dc=com 'uid=mobileuser' |
You will see the information for mobileuser:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
dn: uid=mobileuser,ou=People,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount uid: mobileuser sn: User givenName: Mobile cn: Mobile User displayName: mobileuser uidNumber: 10000 gidNumber: 5000 gecos: Mobile User loginShell: /bin/bash homeDirectory: /home/mobileuser |
App Server Implementation
The App Server is implemented as a node.js application. The full App Server code is available in auth.js in the following Git repository: sg-custom-auth. The App Server uses the passport-ldapauth package for LDAP authentication. The following code snippets highlight the call to authenticate against the OpenLDAP server and the Sync Gateway REST API calls.
The App Server must handle calls to POST /login. This endpoint is called from the mobile app with the user credentials stored in the request body in username and password. The App Server uses these credentials to authenticate against the OpenLDAP server.
The value of searchBase must match the domain components defined in your LDAP server. dc=example,dc=com was used above so the same values are used here (line 12).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var express = require('express'), passport = require('passport'), bodyParser = require('body-parser'), LdapStrategy = require('passport-ldapauth'), curl = require('curl-request'); var syncGatewayEndpoint = 'http://<Sync Gateway Host>:4985/<db>'; var app = express(); var OPTS = { server: { url: 'ldap://<OpenLDAP Host>:389', searchBase: 'dc=example,dc=com', // MUST match LDAP directory searchFilter: '(uid={{username}})' } }; passport.use(new LdapStrategy(OPTS)); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: false})); app.use(passport.initialize()); app.post('/login', passport.authenticate('ldapauth', {session: false}), function(req, res) { // user successfully authenticated against LDAP } |
The following steps will be executed if the user is authenticated.
Get user information from Sync Gateway
Get user information from Sync Gateway by calling GET /{db}/_user/{name}. If the user exists in Sync Gateway, the response code is 200. If the user does not exist in Sync Gateway, the response code is 404.
1 2 3 4 5 6 7 8 9 10 |
getUser = new(curl); getUser.setHeaders(['Content-Type: application/json']) .get(syncGatewayEndpoint + '/_user/' + req.body.username) .then(({statusCode, body, headers}) => { if (statusCode == 404) { // status == 404: user does not exist } else if (statusCode == 200) { // status == 200: user exists } }) |
If the user does not exist in Sync Gateway, create a new user
Create a new user in Sync Gateway by calling POST /{db}/_user/. If the user was successfully created in Sync gateway, the response code is 201.
1 2 3 4 5 6 7 8 9 10 11 |
postUser = new(curl); postUser.setHeaders(['Content-Type: application/json']) .setBody('{"name": "' + req.body.username + '","password": "' + req.body.password + '"}') .post(syncGatewayEndpoint + '/_user/') .then(({statusCode, body, headers}) => { if (statusCode == 201) { // status == 201: success } else { // user could not be created } }) |
Create a session for the user in Sync Gateway
Create a session for the user in Sync Gateway by calling POST /{db}/_session. In this example, the session will expire after 30 minutes. If the session was successfully created in Sync Gateway, the response code is 200. The JSON response contains the following values:
session_id: the ID for the new session. This will be used by the mobile app for authentication with Sync Gateway.
expires: timestamp when the session expires. This is not used in the mobile app.
cookie_name: name of the session cookie. This is not used in the mobile app.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
resBody = {statusCode: 200}; postSession = new(curl); postSession.setHeaders(['Content-Type: application/json']) .setBody('{"name": "' + request.body.username + '","ttl": 1800}') .post(syncGatewayEndpoint + '/_session') .then(({statusCode, body, headers}) => { if (statusCode == 200) { // status == 200: success resBody.session_id = body.session_id; resBody.expires = body.expires; resBody.cookie_name = body.cookie_name; // send success response back to client response.send(JSON.stringify(resBody)); } else { // session could not be created } }) |
Testing Authentication
To test the authentication from the App Server, start it using the following command:
1 |
$ node auth.js |
After it starts you will see the following:
1 |
Listening on port 8080 |
You can test the app server using a simple curl command as follows (or Postman, etc.):
1 |
$ curl -d "username=mobileuser&password=password" -X POST http://<AppServer host>:8080/login |
You will see output similar to the following:
1 |
{"statusCode":200,"session_id":"c149012eaa8d0cf15b1b4110cf0a2fec259ef726","expires":"2019-08-01T13:47:56.311076773Z","cookie_name":"SyncGatewaySession"} |
If the user does not exist in LDAP, the response will be:
1 |
Unauthorized |
The output from the App Server will look like this:
1 2 3 4 5 |
mobileuser has been authenticated with LDAP creating user mobileuser in Sync Gateway user mobileuser created in Sync Gateway create session for user mobileuser created session for mobileuser |
Mobile App Implementation
All that is left to do is make the appropriate calls from the Mobile App to the App Server and Sync Gateway. The mobile app uses the Volley framework to make the REST call to the App Server. The following code snippets highlight the call to the App Server and the Couchbase Lite code to authenticate using the session_id.
Authenticate mobile user
Authenticate user with App Server by calling POST /login/
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 |
JSONObject reqBody = new JSONObject(); reqBody.put("username", <user supplied username>); reqBody.put("password", <user supplied password>); String url = <App Server host>:8080/login; RequestQueue queue = Volley.newRequestQueue(<context>); JsonRequest<JSONObject> jsonRequest = new JsonObjectRequest( Request.Method.POST, url, reqBody, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { // get session_id from response try { String sessionID = response.getString("session_id"); // Store session_id } catch (JSONException je) { // Handle exception } } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // authentication failed } }); queue.add(jsonRequest); |
Create one-shot replication
Create one-shot replication using saved session_id (line 13) and re-authenticate if Sync Gateway session has expired.
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 |
String syncGatewayEndpoint = "ws://<Sync Gateway Host>:4984/{db}"; URI url = null; try { url = new URI(mSyncGatewayEndpoint); } catch (URISyntaxException e) { e.printStackTrace(); return; } ReplicatorConfiguration config = new ReplicatorConfiguration(database, new URLEndpoint(url)); config.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL); config.setContinuous(false); config.setAuthenticator(new SessionAuthenticator(sessionID)); Replicator replicator = new Replicator(config); replicator.addChangeListener(new ReplicatorChangeListener() { @Override public void changed(ReplicatorChange change) { CouchbaseLiteException error = change.getStatus().getError(); if (error != null) { if (error.getCode() == 10401) { // session expired; re-authenticate } } ... } }); replicator.start(); |
What’s Next
If you’re new to Couchbase, take advantage of our free, online training available at https://learn.couchbase.com to learn more.
More mobile tutorials can be found on the Couchbase Tutorials web page.