Skip to content

Commit

Permalink
feat: bearer token support
Browse files Browse the repository at this point in the history
This PR does two simple things:
* look for an 'authToken' field in the sign-in response, and
    store this as hoodie.account.authToken
* send an 'Authorization' header with 'Bearer ' + that token in
    each request to the hoodie-server.

This PR goes together with a PR on the hoodie-server repo, which
at the HAPI level, will:
* strip the couchdb cookie and add it into the JSON response of
    signIn requests.
* read the Authorization header to look for a token of the form
    'Bearer ...', and create a cookie header from the '...', to
    send to the couchdb layer.

set authToken from config at page load

fix hoodiehq/hoodie-server#288 (comment)

BREAKING CHANGE:

Hoodie is not using cookies for authentication anymore and relies
on hoodie-server@^2.0.0

* * *

This commit was sponsored by The Hoodie Firm.
You can hire The Hoodie Firm:

http://go.hood.ie/thehoodiefirm
  • Loading branch information
michielbdejong authored and gr2m committed Sep 23, 2014
1 parent a2bb612 commit 0726487
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 9 deletions.
3 changes: 3 additions & 0 deletions src/hoodie.js
Expand Up @@ -103,6 +103,9 @@ function Hoodie(baseUrl) {
// set username from config (local store)
hoodie.account.username = utils.config.get('_account.username');

// set bearerToken from config (local store)
hoodie.account.bearerToken = utils.config.get('_account.bearerToken');

// init hoodie.remote API
hoodie.remote.init();

Expand Down
28 changes: 22 additions & 6 deletions src/hoodie/account.js
Expand Up @@ -142,8 +142,9 @@ function hoodieAccount(hoodie) {
}

return sendSignUpRequest(username, password)
.done(function() {
.done(function(newUsername, newHoodieId, newBearerToken) {
setUsername(username);
setBearerToken(newBearerToken);
account.trigger('signup', username);
});
};
Expand Down Expand Up @@ -269,13 +270,14 @@ function hoodieAccount(hoodie) {
promise.done(disconnect);
}

return promise.done( function(newUsername, newHoodieId) {
return promise.done( function(newUsername, newHoodieId, newBearerToken) {
if (options.moveData) {
account.trigger('movedata');
}
if (!isReauthenticating && !options.moveData) {
cleanup();
}
setBearerToken(newBearerToken);
if (isReauthenticating) {
if (!isSilent) {
account.trigger('reauthenticated', newUsername);
Expand Down Expand Up @@ -566,6 +568,15 @@ function hoodieAccount(hoodie) {
return config.set('_account.username', newUsername);
}

function setBearerToken(newBearerToken) {
if (account.bearerToken === newBearerToken) {
return;
}

account.bearerToken = newBearerToken;
return config.set('_account.bearerToken', newBearerToken);
}


//
// handle a successful authentication request.
Expand Down Expand Up @@ -680,21 +691,24 @@ function hoodieAccount(hoodie) {
// 'roles': [
// 'mvu85hy',
// 'confirmed'
// ]
// ],
// 'bearerToken': 'dXNlci2Mjow9N2Rh2WyZfioB1ubEsc5n9taWNoaWVsMjo1MzkxOEQKpdFA'
// }
//
// we want to turn it into 'test1', 'mvu85hy' or reject the promise
// in case an error occurred ('roles' array contains 'error' or is empty)
// we want to turn it into 'test1', 'mvu85hy', 'dXNlci2Mjow9N2Rh2WyZfioB1ubEsc5n9taWNoaWVsMjo1MzkxOEQKpdFA'
// or reject the promise in case an error occurred ('roles' array contains 'error' or is empty)
//
function handleSignInSuccess(options) {
options = options || {};

return function(response) {
var newUsername;
var newHoodieId;
var newBearerToken;

newUsername = response.name.replace(/^user(_anonymous)?\//, '');
newHoodieId = response.roles[0];
newBearerToken = response.bearerToken;

//
// if an error occurred, the userDB worker stores it to the $error attribute
Expand Down Expand Up @@ -731,8 +745,9 @@ function hoodieAccount(hoodie) {
}
authenticated = true;

setBearerToken(newBearerToken);
account.fetch();
return resolveWith(newUsername, newHoodieId, options);
return resolveWith(newUsername, newHoodieId, newBearerToken, options);
};
}

Expand Down Expand Up @@ -908,6 +923,7 @@ function hoodieAccount(hoodie) {
account.trigger('cleanup');
authenticated = undefined;
setUsername(undefined);
setBearerToken(undefined);

return resolve();
}
Expand Down
5 changes: 5 additions & 0 deletions src/hoodie/request.js
Expand Up @@ -63,6 +63,11 @@ function hoodieRequest(hoodie) {

defaults.url = url;

if (hoodie.account.bearerToken) {
defaults.headers = {
Authorization: 'Bearer ' + hoodie.account.bearerToken
};
}

// we are piping the result of the request to return a nicer
// error if the request cannot reach the server at all.
Expand Down
1 change: 1 addition & 0 deletions test/mocks/account.js
Expand Up @@ -18,6 +18,7 @@ module.exports = function() {
var api = {
username : 'joe@example.com',
ownerHash : 'hash123',
bearerToken : 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',

anonymousSignUp : this.sandbox.stub().returns(anonymousSignUpDefer.promise()),
authenticate : this.sandbox.stub().returns(authenticateDefer.promise()),
Expand Down
35 changes: 32 additions & 3 deletions test/specs/hoodie/account.spec.js
Expand Up @@ -51,6 +51,7 @@ describe('hoodie.account', function() {
_when('user is logged in as joe@example.com', function() {
beforeEach(function() {
this.account.username = 'joe@example.com';
this.account.bearerToken = 'dXNlci2Mjow9N2Rh2WyZfioB1ubE';
});

_and('session has not been validated yet', function() {
Expand Down Expand Up @@ -89,6 +90,7 @@ describe('hoodie.account', function() {
var promise = this.account.authenticate();
this.requestDefers[0].resolve({
name: 'joe@example.com',
bearerToken: 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
roles: ['hash123', 'confirmed']
});

Expand Down Expand Up @@ -137,10 +139,19 @@ describe('hoodie.account', function() {
expect(this.account.username).to.eql('joe@example.com');
});

it('should set account.bearerToken', function() {
expect(this.account.bearerToken).to.eql('dXNlci2Mjow9N2Rh2WyZfioB1ubE');
});

it('should not set _account.username config', function() {
// because it's already 'joe@example.com'
expect(configMock.set).to.not.be.calledWith('_account.username', 'joe@example.com');
});

it('should not set _account.bearerToken config', function() {
// because it's already 'dXNlci2Mjow9N2Rh2WyZfioB1ubE'
expect(configMock.set).to.not.be.calledWith('_account.bearerToken', 'dXNlci2Mjow9N2Rh2WyZfioB1ubE');
});
}); // returns valid session info for joe@example.com

//
Expand Down Expand Up @@ -526,6 +537,7 @@ describe('hoodie.account', function() {
this.hoodie.store.findAll.defer.resolve([]);
this.requestDefers[1].resolve({
name: 'joe@example.com',
bearerToken: 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
roles: ['hash123', 'confirmed']
});
});
Expand All @@ -534,12 +546,16 @@ describe('hoodie.account', function() {
expect(configMock.set).to.be.calledWith('_account.username', 'joe@example.com');
expect(this.account.username).to.be('joe@example.com');
});
it('should persist new bearerToken', function() {
expect(configMock.set).to.be.calledWith('_account.bearerToken', 'dXNlci2Mjow9N2Rh2WyZfioB1ubE');
expect(this.account.bearerToken).to.be('dXNlci2Mjow9N2Rh2WyZfioB1ubE');
});
it('should trigger `signup` event', function() {
expect(this.account.trigger).to.be.calledWith('signup', 'joe@example.com');
});

it('should resolve its promise', function() {
expect(this.promise).to.be.resolvedWith('joe@example.com', 'hash123', {});
expect(this.promise).to.be.resolvedWith('joe@example.com', 'hash123', 'dXNlci2Mjow9N2Rh2WyZfioB1ubE', {});
});

}); // signIn successful
Expand Down Expand Up @@ -636,6 +652,7 @@ describe('hoodie.account', function() {
beforeEach(function() {
this.requestDefers[0].resolve({
name: 'hash123',
bearerToken: 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
roles: ['hash123', 'confirmed']
});
});
Expand Down Expand Up @@ -894,6 +911,7 @@ describe('hoodie.account', function() {
this.response = {
'ok': true,
'name': 'user/joe@example.com',
'bearerToken': 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
'roles': ['hash123', 'confirmed']
};
this.hoodie.request.defer.resolve(this.response);
Expand All @@ -914,15 +932,22 @@ describe('hoodie.account', function() {
expect(configMock.set).to.be.calledWith('_account.username', 'joe@example.com');
});

it('should set bearerToken', function() {
this.account.signIn('joe@example.com', 'secret');

expect(this.account.username).to.eql('joe@example.com');
expect(configMock.set).to.be.calledWith('_account.username', 'joe@example.com');
});

it('should fetch the _users doc', function() {
this.sandbox.spy(this.account, 'fetch');
this.account.signIn('joe@example.com', 'secret');
expect(this.account.fetch).to.be.called();
});

it('should resolve with username, hoodieId and options', function() {
it('should resolve with username, hoodieId, bearerToken and options', function() {
var promise = this.account.signIn('joe@example.com', 'secret', {foo: 'bar'});
expect(promise).to.be.resolvedWith('joe@example.com', 'hash123', {foo: 'bar'});
expect(promise).to.be.resolvedWith('joe@example.com', 'hash123', 'dXNlci2Mjow9N2Rh2WyZfioB1ubE', {foo: 'bar'});
});

it('should trigger `signin` event', function() {
Expand Down Expand Up @@ -1048,6 +1073,7 @@ describe('hoodie.account', function() {
this.response = {
'ok': true,
'name': 'user/joe@example.com',
'bearerToken': 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
'roles': ['hash123', 'confirmed']
};
this.hoodie.request.defer.resolve(this.response);
Expand Down Expand Up @@ -1078,6 +1104,7 @@ describe('hoodie.account', function() {
this.response = {
'ok': true,
'name': 'user/joe@example.com',
'bearerToken': 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
'roles': ['hash123', 'confirmed']
};
this.hoodie.request.defer.resolve(this.response);
Expand Down Expand Up @@ -2081,6 +2108,7 @@ function validSessionResponse() {
function validSignInResponse() {
return {
name: 'user/joe@example.com',
bearerToken: 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
roles: ['hash123', 'confirmed']
};
}
Expand Down Expand Up @@ -2133,6 +2161,7 @@ function with_session_validated_before(callback) {
var response = {
userCtx: {
name: 'user/joe@example.com',
bearerToken: 'dXNlci2Mjow9N2Rh2WyZfioB1ubE',
roles: ['hash123', 'confirmed']
}
};
Expand Down
4 changes: 4 additions & 0 deletions test/specs/hoodie/request.spec.js
Expand Up @@ -45,6 +45,10 @@ describe('hoodie.request', function () {
expect(this.args.crossDomain).to.be(true);
});

it('should set the Authorization "Bearer ..." header', function() {
expect(this.args.headers.Authorization).to.be('Bearer dXNlci2Mjow9N2Rh2WyZfioB1ubE');
});

_and('baseUrl is not set', function() {
beforeEach(function() {
this.hoodie.baseUrl = undefined;
Expand Down

0 comments on commit 0726487

Please sign in to comment.