mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05: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:
parent
ce945fbc91
commit
a224154420
2 changed files with 120 additions and 0 deletions
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue