Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto refresh bearer token #45

Merged
merged 3 commits into from
May 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 39 additions & 34 deletions lib/api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,49 @@ var config = require('./config');
var apiClient = new JSONAPIClient(config.host + '/api', {
'Content-Type': 'application/json',
'Accept': 'application/vnd.api+json; version=1',
});
}, {
beforeEveryRequest: function() {
var auth = require('./auth');
return auth.checkBearerToken();
},

apiClient.handleError = function(response) {
var errorMessage;
if (response instanceof Error) {
throw response;
} else if (typeof response.body === 'object') {
if (response.body.error) {
errorMessage = response.body.error;
if (response.body.error_description) {
errorMessage += ' ' + response.error_description;
}
} else if (Array.isArray(response.body.errors)) {
errorMessage = response.body.errors.map(function(error) {
if (typeof error.message === 'string') {
return error.message;
} else if (typeof error.message === 'object') {
return Object.keys(error.message).map(function(key) {
return key + ' ' + error.message[key];
}).join('\n');
handleError: function(response) {
var errorMessage;
if (response instanceof Error) {
throw response;
} else if (typeof response.body === 'object') {
if (response.body.error) {
errorMessage = response.body.error;
if (response.body.error_description) {
errorMessage += ' ' + response.error_description;
}
}).join('\n');
} else if (Array.isArray(response.body.errors)) {
errorMessage = response.body.errors.map(function(error) {
if (typeof error.message === 'string') {
return error.message;
} else if (typeof error.message === 'object') {
return Object.keys(error.message).map(function(key) {
return key + ' ' + error.message[key];
}).join('\n');
}
}).join('\n');
} else {
errorMessage = 'Unknown error (bad response body)';
}
} else if (response.text.indexOf('<!DOCTYPE') !== -1) {
// Manually set a reasonable error when we get HTML back (currently 500s will do this).
errorMessage = [
'There was a problem on the server.',
response.req.url,
response.status,
response.statusText,
].join(' ');
} else {
errorMessage = 'Unknown error (bad response body)';
errorMessage = 'Unknown error (bad response)';
}
} else if (response.text.indexOf('<!DOCTYPE') !== -1) {
// Manually set a reasonable error when we get HTML back (currently 500s will do this).
errorMessage = [
'There was a problem on the server.',
response.req.url,
response.status,
response.statusText,
].join(' ');
} else {
errorMessage = 'Unknown error (bad response)';
}

throw new Error(errorMessage);
};
throw new Error(errorMessage);
}
});

module.exports = apiClient;
34 changes: 26 additions & 8 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ var JSON_HEADERS = {
};

// We don't want to wait until the token is already expired before refreshing it.
var TOKEN_EXPIRATION_ALLOWANCE = 60 * 1000;
var BEARER_TOKEN_EXPIRATION_ALLOWANCE = 60 * 1000;

module.exports = new Model({
_currentUserPromise: null,

_bearerToken: '',
_bearerRefreshTimeout: NaN,
_bearerTokenExpiration: NaN,
_refreshToken: '',

_getAuthToken: function() {
console.log('Getting auth token');
Expand Down Expand Up @@ -66,19 +68,24 @@ module.exports = new Model({
this._bearerToken = response.access_token;
apiClient.headers.Authorization = 'Bearer ' + this._bearerToken;

var refresh = this._refreshBearerToken.bind(this, response.refresh_token);
var timeToRefresh = (response.expires_in * 1000) - TOKEN_EXPIRATION_ALLOWANCE;
this._bearerRefreshTimeout = setTimeout(refresh, timeToRefresh);
this._bearerTokenExpiration = Date.now() + (response.expires_in * 1000);
Copy link
Contributor

@parrish parrish May 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty minor, but it might be safer to use

this._bearerTokenExpiration = new Date(response.created_at * 1000) + (response.expires_in * 1000);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the way I've got it means the expiration time is relative to the user's clock, and this would make it relative to the server's clock. Isn't that a little safer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're saying. Using Date.now() should be within the allowance anyway.

this._refreshToken = response.refresh_token;

return this._bearerToken;
},

_refreshBearerToken: function(refreshToken) {
_bearerTokenIsExpired: function() {
return Date.now() >= this._bearerTokenExpiration - BEARER_TOKEN_EXPIRATION_ALLOWANCE;
},

_refreshBearerToken: function() {
console.log('Refreshing expired bearer token');

var url = config.host + '/oauth/token';

var data = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
refresh_token: this._refreshToken,
client_id: config.clientAppID,
};

Expand All @@ -96,7 +103,8 @@ module.exports = new Model({
_deleteBearerToken: function() {
this._bearerToken = '';
delete apiClient.headers.Authorization;
clearTimeout(this._bearerRefreshTimeout);
this._bearerTokenExpiration = NaN;
this._refreshToken = '';
console.log('Deleted bearer token');
},

Expand Down Expand Up @@ -184,6 +192,16 @@ module.exports = new Model({
return this._currentUserPromise;
},

checkBearerToken: function() {
var awaitBearerToken;
if (this._bearerTokenIsExpired()) {
awaitBearerToken = this._refreshBearerToken();
} else {
awaitBearerToken = Promise.resolve(this._bearerToken);
}
return awaitBearerToken;
},

signIn: function(credentials) {
var originalArguments = arguments;
return this.checkCurrent().then(function(user) {
Expand Down