Using JWT with Sync Gateway?

#1

My angular + ionic + node app uses JWT for token based auth, users login / join using an app specific username and password. I want to add sync gateway to the stack, but looking at the auth section here: http://developer.couchbase.com/mobile/develop/guides/sync-gateway/administering-sync-gateway/authenticating-users/index.html#custom--indirect--authentication, I’m not sure if it is possible to use the Bearer token with sync gateway. Perhaps I need both Bearer token and Set-Cookie header, the Bearer token to use protected node app apis, and the Set-Cookie header when doing the syncing… Any advice / tips?

1 Like
#2

Hey Derek, I hope this is helpful, but I use JWTs as well and this is my process:

All requests are proxied by Nginx, so Sync Gateway exists at /sync and my api is at /api/v1/

To login, I send a request to /api/v1/account/login with username/password (for example)

  • Internally, the user/pass are authenticated, and if valid, a JWT is created to be used for other API calls (logout, some info not included in SG)
  • If the user/pass is valid, I do a request to SG over the Admin port (4985) to get a sync gateway cookie (if that returns a certain error, I create a new user and then re-request the cookie)
  • My app server waits on the cookie, and when received, it completes the login process for the user by attaching the cookie to the response, as well as (in my case) embeddeding the JWT in the response body.

I setup my Couchbase Mobile session to use the cookie in the push/pull replication headers. Otherwise, I have a request interceptor which includes my JWT for my REST API calls.

So far hasn’t run into any problems. If the cookie expires, I re-login/re-request a cookie (in my case, there is no difference to these two functionality-wise)

1 Like
#3

Hey SureshJoshi,

Thanks for taking the time out to respond. Yeah that’s really helpful, was planning on trying pretty much the exact same setup… It’s great to hear that you’ve had it working without issue, gives me confidence to follow suit. Speaking of cookie expiration, do you have your token and cookie set to the same ttl / expiration time?

#4

Well, for no particular reason, my cookie and token are vastly different, but I’ll probably line them up, and set them to expire fairly far in the future - however, I haven’t decided if I want to setup a refresh token flow yet, so that would affect that.

#5

Okay, sounds good. I’m pretty new sync gateway… hit a few snags, firstly I didn’t realise the passwords were getting bcrypt’d by default when they are saved, handy, and that any additional custom fields are not saved, hummm… Also I didn’t realise you can’t get the user’s password back in a get request (seems like you should given your on admin port)… Not sure how you’re meant to check that the passwords match on a login attempt, do you have two user docs? I was hoping I could just have the one that worked with sync gateway and contained all my apps custom user fields. I want to store the users in the cb db, and have a username/email and password login system that looks up these users. Not using facebook, persona etc.

I’ve been experimenting, here a sample of my /api/v0/auth code:

'use strict';

var _ = require('lodash'),
    utils = require('module/utils'),
    express = require('express'),
    router = express.Router(),
    config = require('module/config'),
    jwt = require('jsonwebtoken'),
    sgClientAdmin = require('request-json').createClient(config.SG_CONFIG.ADMIN_ENDPOINT),
    sgClientPublic = require('request-json').createClient(config.SG_CONFIG.PUBLIC_ENDPOINT);


// Seems like the passwords are being crypt'd twice... as CB does it by default!

router.post('/register', function (req, res, next) {
    var user = req.body;
    utils.cryptPassword(user.password, function (err, hash) {
        user.password = hash;
        console.log('hash', hash);
        createSyncUser(req, res, next);
    });
});

router.post('/login', function (req, res, next) {

    var user = req.body;

    if ((!user.email || !user.password) || (!user.email.trim().length || !user.password.trim().length)) {
        invalidCredentials(req);
        return;
    }

    utils.cryptPassword(user.password, function (err, hash) {

        user.password = hash;

        sgClientAdmin.get('_user/' + user.email, function (error, response, body) {

            console.log('\tRequest get _user/' + user.email);
            console.log('error', error);
            console.log('body', body);

            var errorMsg;

            if (error) {
                errorMsg = 'An error occurred getting user, ' + error;
            } else if (response.statusCode === 200) {

                console.log('Success! Logged user in..., user.password:', user.password, ', body.password:' + body.password);

                req.body = body;

                // THIS WILL NEVER WORK AS SYNC GATEWAY DOESN'T GIVE THE PASSWORD BACK!
                utils.comparePassword(user.password, body.password, function (err, isPasswordMatch) {
                    if (isPasswordMatch) {
                        authorised(req, res, next);
                    } else {
                        invalidCredentials(req, res, next);
                    }
                });

            } else {
                errorMsg = 'Unexpected status code ' + response.statusCode + ' when getting user';
            }

            if (errorMsg) {
                req.jsend.error(errorMsg);
            }

        });

    });
});

function createSyncUser(req, res, next) {

    var user = req.body,
        syncUser = {
            email: user.email,
            password: user.password,
            name: user.email,
            // Test adding some custom fields
            bla: ["bla"],
            isX: true
        };

    sgClientAdmin.post('_user/', syncUser, function (error, response, body) {

        console.log('\tRequest post (_user) ...', syncUser);
        console.log('error', error);
        console.log('body', body);

        var errorMsg;

        if (error) {
            errorMsg = 'An error occurred creating user, ' + error;
        } else if (response.statusCode === 409) {
            req.jsend.fail('User with email ' + syncUser.email + ' already exists');
        } else if (response.statusCode === 201) {
            console.log('Success! Added user...');
            req.body = syncUser;
            authorised(req, res, next);
        } else {
            errorMsg = 'Unexpected status code ' + response.statusCode + ' when creating user';
        }

        if (errorMsg) {
            req.jsend.error(errorMsg);
        }
    });
}

function invalidCredentials(req, res, next) {
    req.jsend.fail('Invalid Credentials', 403);
}

function authorised(req, res, next) {

    var user = req.body,
        sessionData = {name: user.email, password: user.password};

    delete user.password; // remove password hash before sending to the client

    // Create a session in sync gateway for this user
    sgClientAdmin.post('_session', sessionData, function (error, response, body) {

        console.log('\tRequest post (sessionData) ...', sessionData);
        console.log('error', error);
        console.log('body', body);

        if (!error && response.statusCode === 200) {

            res.cookie(body.cookie_name, body, {expires: new Date(body.expires)});

            req.jsend.success({
                token: jwt.sign(user, config.JWT_SECRET),
                session: body,
                user: user
            });

        } else {
            req.jsend.error('Unable to create sg session, ' + body.reason);
        }

    });
}

module.exports = router;

You mentioned “Internally, the user/pass are authenticated”, how are you doing that? Where you storing users? Any tips re the above? Can’t find any real examples of user account management using cb + sg. I guess I could just use the couchbase node sdk api for check the user’s password matches…

#6

I think I’m missing something here, I probably just need to use basic auth in the login block calling maybe /_session, to see if the user is authorised, ie. valid password. Not 100%, still learning!

Update:
Okay I think I’ve figured it out. Just needed to make a get request to _session on the public port using basic auth. This automatically checks the password stored in the user doc.

var user = req.body,
        sgClientPublic = requestJson.createClient(config.SG_CONFIG.PUBLIC_ENDPOINT);

    sgClientPublic.setBasicAuth(user.email, user.password);

    sgClientPublic.get('_session', function (error, response, body) {
...

I guess / hope this is the right approach…

The additional user information probably just needs to be saved in a doc that is linked to this user, either by a key ref or perhaps a channel. Looking into that next.

Cheers.

#7

Hi there,

Does this helps you ?

http://ti.eng.br/couchbase-mobile-with-jwt-tokens/

BR,
Thiago Alencar

#8

Thanks, this is exactly what we`ve done also. One question: When you create new sg user you dont really care about sg user password right? Since you authenticate at api endpoint.

#9

It’s been a while, but I think either I set the SG password to the same one that gets authenticated against, or… I throw it away, since I’m already using the admin port.

At some point, I might need to make this into a blog post, just so I can remember what I end up doing!