0

Has anyone successfully navigated Jawbone's OAuth2.0 authentication for their REST API?

I am unable to figure out how to access and send the authorization_code in order to obtain the access_token (steps 4 & 5 in the Jawbone API Authorization Documentation). I want to reuse the access_token for subsequent (AJAX-style) calls and avoid asking the user to reauthorize each time.

Each call of the API (get.sleeps) requires a full round trip of the auth process including this reauthorization to get an authorization_token (screen shot). Both the Jawbone and Passport Documentation is vague on this point.

enter image description here

My stack involves, node.js, the jawbone-up NPM, express.js and passport.js. The Passport Strategy for Jawbone appears to work correctly as I get valid data back.

The jawbone-up NPM explicitly does not help maintain the session (access_token), saying "This library does not assist in getting an access_token through OAuth..."

QUESTION: how do I actually use the OAUTH access_token in the API call? Can someone show me some code to do this?

Thanks

var dotenv = require('dotenv').load(),
    express = require('express'),
    app = express(),
    ejs = require('ejs'),
    https = require('https'),
    fs = require('fs'),
    bodyParser = require('body-parser'),
    passport = require('passport'),
    JawboneStrategy = require('passport-oauth').OAuth2Strategy,
    port = 5000,
    jawboneAuth = {
       clientID: process.env.JAWBONE_CLIENT_ID,
       clientSecret: process.env.JAWBONE_CLIENT_SECRET,
       authorizationURL: process.env.JAWBONE_AUTH_URL,
       tokenURL: process.env.JAWBONE_AUTH_TOKEN_URL,
       callbackURL: process.env.JAWBONE_CALLBACK_URL 
    },
    sslOptions = {
        key: fs.readFileSync('./server.key'),
        cert: fs.readFileSync('./server.crt')
    };
    app.use(bodyParser.json());
    app.use(express.static(__dirname + '/public'));
    app.set('view engine', 'ejs');
    app.set('views', __dirname + '/views');

// ----- Passport set up ----- //
app.use(passport.initialize());

app.get('/', 
    passport.authorize('jawbone', {
        scope: ['basic_read','sleep_read'],
        failureRedirect: '/'
    })
);

app.get('/done',
    passport.authorize('jawbone', {
        scope: ['basic_read','sleep_read'],
        failureRedirect: '/'
    }), function(req, res) {
        res.render('userdata', req.account);
    }
);

passport.use('jawbone', new JawboneStrategy({
    clientID: jawboneAuth.clientID,
    clientSecret: jawboneAuth.clientSecret,
    authorizationURL: jawboneAuth.authorizationURL,
    tokenURL: jawboneAuth.tokenURL,
    callbackURL: jawboneAuth.callbackURL
}, function(token, refreshToken, profile, done) {
    var options = {
            access_token: token,
            client_id: jawboneAuth.clientID,
            client_secret: jawboneAuth.clientSecret
        },
        up = require('jawbone-up')(options);

    up.sleeps.get({}, function(err, body) {
        if (err) {
            console.log('Error receiving Jawbone UP data');
        } else {
        var jawboneData = JSON.parse(body).data;
        console.log(jawboneData);
        return done(null, jawboneData, console.log('Jawbone UP data ready to be displayed.'));
        }
    });
}));
// HTTPS
var secureServer = https.createServer(sslOptions, app).listen(port, function(){
    console.log('UP server listening on ' + port);
});
Colin
  • 930
  • 3
  • 19
  • 42

2 Answers2

0

The Jawbone access token expires in 1 year so you definitely don't need to re-authenticate the user each time. Also you are provided with a refresh_token as well, so you can refresh the access token when needed.

Once you have the access_token you have to store it somewhere, preferably in some sort of a database or a file storage for later use, then you use that token for each request made to the Jawbone REST API.

The jawbone-up module uses request internally, so I'm going to show you how to make a request with it (it should be pretty much the same with any other module).

Here is how you can get the user's profile (the most basic API call):

var request = require('request')
request.get({
  uri:'https://jawbone.com/nudge/api/v.1.1/users/@me',
  auth:{bearer:'[ACCESS_TOKEN]'},
  json:true
}, function (err, res, body) {
  // body is a parsed JSON object containing the response data
})

There is another module called Purest which also uses request internally, but hides some of the complexity around using a REST API. Here is how the same request would look like using that module:

var Purest = require('purest')
var jawbone = new Purest({provider:'jawbone'})
jawbone.get('users/@me', {
  auth:{bearer:'[ACCESS_TOKEN]'}
}, function (err, res, body) {
  // body is a parsed JSON object containing the response data
})

Alternatively for authenticating the user (getting the access_token) you can use another module called Grant which I personally use, but either one should work.

simo
  • 15,078
  • 7
  • 45
  • 59
  • Simo, I've not come across these modules before but will start some reading. Do you have any sample code to handle an expired `access_token`? It's probably my own inexperience but I'm finding the Passportjs and Jawbone documentation pretty unhelpful.... – Colin Jul 18 '15 at 17:54
  • Using request `request.post({uri:'https://jawbone.com/auth/oauth2/token', form:{grant_type:'refresh_token',refresh_token:'..',client_id:'..',client_secret:'..'}}, function(e,r,b){})` :) – simo Jul 18 '15 at 19:23
  • Simo, I feel bad as this was a good answer but I couldn't get Grant to work. I kept getting stuck with a "invalid_redirect" error. I may post another question on this though - if I can get this to work it is a very clean and elegant module. – Colin Jul 19 '15 at 17:26
  • No problem, I'm glad you find this solution elegant. If you are still interested in it, I probably forgot to point you to the article about it [here](https://scotch.io/tutorials/implement-oauth-into-your-express-koa-or-hapi-applications-using-grant). Take your time, your OAuth app's redirect config, the Grant's server.host config and the host from which you initiate the flow should match - that's the bug you are getting. – simo Jul 19 '15 at 18:40
  • I created a new question here showing my config confusion - http://stackoverflow.com/questions/31504030/npm-grant-oauth-middleware-invalid-redirect-error – Colin Jul 19 '15 at 21:19
0

You weren't too far off, you were already getting the token. To make your code work a few steps are needed:

Add the concept of a "session", data that exists from request to request as a global variable. When you do a full web app use express-sessions and passport-sessions and implement user management. But for now we just add a global for a single user state.

var demoSession = {
    accessToken: '',
    refreshToken: ''
};

Pass in a user object in the done() of JawboneStrategy. This is because the "authorize" feature of passport is expecting a user to exist in the session. It attaches the authorize results to this user. Since we are just testing the API just pass in an empty user.

// Setup the passport jawbone authorization strategy
passport.use('jawbone', new JawboneStrategy({
    clientID: jawboneAuth.clientID,
    clientSecret: jawboneAuth.clientSecret,
    authorizationURL: jawboneAuth.authorizationURL,
    tokenURL: jawboneAuth.tokenURL,
    callbackURL: jawboneAuth.callbackURL
}, function(accessToken, refreshToken, profile, done) {
    // we got the access token, store it in our temp session
    demoSession.accessToken = accessToken;
    demoSession.refreshToken = refreshToken;
    var user = {}; // <-- need empty user
    done(null, user);
    console.dir(demoSession);
}));

Use a special page to show the data "/data". Add a route to separate the auth from the display of service.

app.get('/done', passport.authorize('jawbone', {
        scope: ['basic_read','sleep_read'],
        failureRedirect: '/'
    }), function(req, res) {
        res.redirect('/data');
    }
);

Lastly the Jawbone Up sleeps API is a little tricky. you have to add a YYYYMMDD string to the request:

app.get('/data', function(req, res) {

    var options = {
        access_token: demoSession.accessToken,
        client_id: jawboneAuth.clientID,
        client_secret: jawboneAuth.clientSecret
    };
    var up = require('jawbone-up')(options);

    // we need to add date or sleep call fails
    var yyyymmdd = (new Date()).toISOString().slice(0, 10).replace(/-/g, "");
    console.log('Getting sleep for day ' + yyyymmdd);

    up.sleeps.get({date:yyyymmdd}, function(err, body) {
        if (err) {
            console.log('Error receiving Jawbone UP data');
        } else {
            try {
                var result = JSON.parse(body);
                console.log(result);
                res.render('userdata', {
                    requestTime: result.meta.time,
                    jawboneData: JSON.stringify(result.data)
                });
            }
            catch(err) {
                res.render('userdata', {
                    requestTime: 0,
                    jawboneData: 'Unknown result'
                });
            }

        }
    });
});

I have created a gist that works for me here thats based on your code: https://gist.github.com/longplay/65056061b68f730f1421

longplay
  • 206
  • 3
  • 7