1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00

attempt expired session restore on network request (#327)

refs TryGhost/Ghost#5202

We can get into a situation where the app is left open without a
network connection and the token subsequently expires, this will
result in the next network request returning a 401 and killing the
session. This is an attempt to detect that and restore the session
using the stored refresh token before continuing with the request

- wrap ajax requests in a session restore request if we detect an expired `access_token`
This commit is contained in:
Kevin Ansfield 2016-10-17 14:32:22 +01:00 committed by Katharina Irrgang
parent 4506acb389
commit 8881513c7d
2 changed files with 120 additions and 0 deletions

View file

@ -133,12 +133,38 @@ let ajaxService = AjaxService.extend({
// ember-ajax recognises `application/vnd.api+json` as a JSON-API request
// and formats appropriately, we want to handle `application/json` the same
_makeRequest(hash) {
let isAuthenticated = this.get('session.isAuthenticated');
let isGhostRequest = hash.url.indexOf('/ghost/api/') !== -1;
let isTokenRequest = isGhostRequest && hash.url.match(/authentication\/(?:token|ghost)/);
let tokenExpiry = this.get('session.authenticated.expires_at');
let isTokenExpired = tokenExpiry < (new Date()).getTime();
if (isJSONContentType(hash.contentType) && hash.type !== 'GET') {
if (typeof hash.data === 'object') {
hash.data = JSON.stringify(hash.data);
}
}
// we can get into a situation where the app is left open without a
// network connection and the token subsequently expires, this will
// result in the next network request returning a 401 and killing the
// session. This is an attempt to detect that and restore the session
// using the stored refresh token before continuing with the request
//
// TODO:
// - this might be quite blunt, if we have a lot of requests at once
// we probably want to queue the requests until the restore completes
// BUG:
// - the original caller gets a rejected promise with `undefined` instead
// of the AjaxError object when session restore fails. This isn't a
// huge deal because the session will be invalidated and app reloaded
// but it would be nice to be consistent
if (isAuthenticated && isGhostRequest && !isTokenRequest && isTokenExpired) {
return this.get('session').restore().then(() => {
return this._makeRequest(hash);
});
}
return this._super(...arguments);
},

View file

@ -14,6 +14,8 @@ import {
isUnsupportedMediaTypeError
} from 'ghost-admin/services/ajax';
import config from 'ghost-admin/config/environment';
import Service from 'ember-service';
import RSVP from 'rsvp';
function stubAjaxEndpoint(server, response = {}, code = 200) {
server.get('/test/', function () {
@ -176,5 +178,97 @@ describeModule(
done();
});
});
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
describe('session handling', function () {
let successfulRequest = false;
let sessionStub = Service.extend({
isAuthenticated: true,
restoreCalled: false,
authenticated: null,
init() {
this.authenticated = {
expires_at: (new Date()).getTime() - 10000,
refresh_token: 'RefreshMe123'
};
},
restore() {
this.restoreCalled = true;
this.authenticated.expires_at = (new Date()).getTime() + 10000;
return RSVP.resolve();
},
authorize() {
}
});
beforeEach(function () {
server.get('/ghost/api/v0.1/test/', function () {
return [
200,
{'Content-Type': 'application/json'},
JSON.stringify({
success: true
})
];
});
server.post('/ghost/api/v0.1/authentication/token', function () {
return [
401,
{'Content-Type': 'application/json'},
JSON.stringify({})
];
});
});
it('can restore an expired session', function (done) {
let ajax = this.subject();
ajax.set('session', sessionStub.create());
ajax.request('/ghost/api/v0.1/test/');
ajax.request('/ghost/api/v0.1/test/').then((result) => {
expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true;
expect(result.success, 'result.success').to.be.true;
done();
}).catch((error) => {
expect(true, 'request failed').to.be.false;
done();
});
});
it('errors correctly when session restoration fails', function (done) {
let ajax = this.subject();
let invalidateCalled = false;
ajax.set('session', sessionStub.create());
ajax.set('session.restore', function () {
this.set('restoreCalled', true);
return ajax.post('/ghost/api/v0.1/authentication/token');
});
ajax.set('session.invalidate', function () {
invalidateCalled = true;
});
stubAjaxEndpoint(server, {}, 401);
ajax.request('/ghost/api/v0.1/test/').then(() => {
expect(true, 'request was successful').to.be.false;
done();
}).catch((error) => {
// TODO: fix the error return when a session restore fails
// expect(isUnauthorizedError(error)).to.be.true;
expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true;
expect(successfulRequest, 'successfulRequest').to.be.false;
expect(invalidateCalled, 'invalidateCalled').to.be.true;
done();
});
});
});
}
);