From af48083fc10834ca8b4f5bf73e2c4856a5bcab70 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Mon, 5 Dec 2016 16:07:31 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=20new=20route=20to=20finish=20Ghos?= =?UTF-8?q?t=20setup=20when=20a=20one=20time=20access=20token=20is=20avail?= =?UTF-8?q?able=20in=20Ghost-Admin=20(#7765)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no issue - added a new route --- core/server/api/authentication.js | 104 +++++++++++++---- core/server/routes/api.js | 4 + core/server/translations/en.json | 3 +- .../routes/api/authentication_spec.js | 107 ++++++++++++++++-- 4 files changed, 185 insertions(+), 33 deletions(-) diff --git a/core/server/api/authentication.js b/core/server/api/authentication.js index 937aa66fc51f..334503838aaa 100644 --- a/core/server/api/authentication.js +++ b/core/server/api/authentication.js @@ -1,16 +1,16 @@ -var _ = require('lodash'), - validator = require('validator'), - pipeline = require('../utils/pipeline'), - dataProvider = require('../models'), - settings = require('./settings'), - mail = require('./../mail'), - apiMail = require('./mail'), - globalUtils = require('../utils'), - utils = require('./utils'), - errors = require('../errors'), - events = require('../events'), - config = require('../config'), - i18n = require('../i18n'), +var _ = require('lodash'), + validator = require('validator'), + pipeline = require('../utils/pipeline'), + dataProvider = require('../models'), + settings = require('./settings'), + mail = require('./../mail'), + apiMail = require('./mail'), + globalUtils = require('../utils'), + utils = require('./utils'), + errors = require('../errors'), + events = require('../events'), + config = require('../config'), + i18n = require('../i18n'), authentication; /** @@ -379,15 +379,17 @@ authentication = { } function formatResponse(isSetup) { - return {setup: [ - { - status: isSetup, - // Pre-populate from config if, and only if the values exist in config. - title: config.title || undefined, - name: config.user_name || undefined, - email: config.user_email || undefined - } - ]}; + return { + setup: [ + { + status: isSetup, + // Pre-populate from config if, and only if the values exist in config. + title: config.title || undefined, + name: config.user_name || undefined, + email: config.user_email || undefined + } + ] + }; } tasks = [ @@ -553,6 +555,64 @@ authentication = { ]; return pipeline(tasks, tokenDetails, localOptions); + }, + + /** + * search for token and check expiry + * + * finally delete old temporary token + * + * send welcome mail + * --> @TODO: in case user looses the redirect, no welcome mail :( + * --> send email on first login! + */ + onSetupStep3: function (data) { + var oldAccessToken, oldAccessTokenData, options = {context: {internal: true}}; + + return dataProvider.Accesstoken + .findOne({ + token: data.token + }) + .then(function (_oldAccessToken) { + if (!_oldAccessToken) { + throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')); + } + + oldAccessToken = _oldAccessToken; + oldAccessTokenData = oldAccessToken.toJSON(); + + if (oldAccessTokenData.expires < Date.now()) { + throw new errors.NoPermissionError(i18n.t('errors.middleware.oauth.tokenExpired')); + } + + var newAccessToken = globalUtils.uid(256), + refreshToken = globalUtils.uid(256), + newAccessExpiry = Date.now() + globalUtils.ONE_HOUR_MS, + refreshExpires = Date.now() + globalUtils.ONE_WEEK_MS; + + return dataProvider.Accesstoken.add({ + token: newAccessToken, + user_id: oldAccessTokenData.user_id, + client_id: oldAccessTokenData.client_id, + expires: newAccessExpiry + }, options) + .then(function then() { + return dataProvider.Refreshtoken.add({ + token: refreshToken, + user_id: oldAccessTokenData.user_id, + client_id: oldAccessTokenData.client_id, + expires: refreshExpires + }, options); + }).then(function then() { + return oldAccessToken.destroy(); + }).then(function () { + return { + access_token: newAccessToken, + refresh_token: refreshToken, + expires_in: newAccessExpiry + }; + }); + }); } }; diff --git a/core/server/routes/api.js b/core/server/routes/api.js index 9fdd8a706ec3..b66a2db9005e 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -144,6 +144,10 @@ apiRoutes = function apiRoutes(middleware) { middleware.spamPrevention.forgotten, api.http(api.authentication.generateResetToken) ); + + // ## Endpoint to exchange a one time access-token with a pair of AT/RT + router.post('/authentication/setup/three', middleware.api.authenticateClient, api.http(api.authentication.onSetupStep3)); + router.put('/authentication/passwordreset', api.http(api.authentication.resetPassword)); router.post('/authentication/invitation', api.http(api.authentication.acceptInvitation)); router.get('/authentication/invitation', api.http(api.authentication.isInvitation)); diff --git a/core/server/translations/en.json b/core/server/translations/en.json index 4bb8802bab81..6bbce4ba7049 100644 --- a/core/server/translations/en.json +++ b/core/server/translations/en.json @@ -79,7 +79,8 @@ "oauth": { "invalidClient": "Invalid client.", "invalidRefreshToken": "Invalid refresh token.", - "refreshTokenExpired": "Refresh token expired." + "refreshTokenExpired": "Refresh token expired.", + "tokenExpired": "Token expired." }, "privateblogging": { "wrongPassword": "Wrong password" diff --git a/core/test/functional/routes/api/authentication_spec.js b/core/test/functional/routes/api/authentication_spec.js index b7fa7494b8c1..f63e389de77e 100644 --- a/core/test/functional/routes/api/authentication_spec.js +++ b/core/test/functional/routes/api/authentication_spec.js @@ -1,12 +1,13 @@ -var supertest = require('supertest'), - should = require('should'), - testUtils = require('../../../utils'), - user = testUtils.DataGenerator.forModel.users[0], - ghost = require('../../../../../core'), - config = require('../../../../../core/server/config'), +var supertest = require('supertest'), + should = require('should'), + testUtils = require('../../../utils'), + user = testUtils.DataGenerator.forModel.users[0], + ghost = require('../../../../../core'), + models = require('../../../../../core/server/models'), + config = require('../../../../../core/server/config'), request; -describe('Authentication API', function () { +describe.only('Authentication API', function () { var accesstoken = ''; before(function (done) { @@ -38,7 +39,7 @@ describe('Authentication API', function () { client_id: 'ghost-admin', client_secret: 'not_available' }).expect('Content-Type', /json/) - // TODO: make it possible to override oauth2orize's header so that this is consistent + // TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', 'no-store') .expect(200) .end(function (err, res) { @@ -111,7 +112,7 @@ describe('Authentication API', function () { client_id: 'ghost-admin', client_secret: 'not_available' }).expect('Content-Type', /json/) - // TODO: make it possible to override oauth2orize's header so that this is consistent + // TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', 'no-store') .expect(200) .end(function (err, res) { @@ -127,7 +128,7 @@ describe('Authentication API', function () { client_id: 'ghost-admin', client_secret: 'not_available' }).expect('Content-Type', /json/) - // TODO: make it possible to override oauth2orize's header so that this is consistent + // TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', 'no-store') .expect(200) .end(function (err, res) { @@ -163,4 +164,90 @@ describe('Authentication API', function () { done(); }); }); + + it('exchange one time access token on setup', function (done) { + testUtils.fixtures.insertAccessToken({ + expires: Date.now() + 3600000, + token: 'one-time-token', + user_id: 1, + client_id: 2 + }).then(function () { + request.post(testUtils.API.getApiQuery('authentication/setup/three')) + .set('Origin', config.url) + .send({ + token: 'one-time-token', + client_id: 'ghost-admin', + client_secret: 'not_available' + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.exist(res.body.access_token); + should.exist(res.body.refresh_token); + should.exist(res.body.expires_in); + + models.Accesstoken.findOne({ + token: 'one-time-token' + }).then(function (found) { + should.not.exist(found); + done(); + }); + }); + }).catch(done); + }); + + it('[failure] wrong AT', function (done) { + request.post(testUtils.API.getApiQuery('authentication/setup/three')) + .set('Origin', config.url) + .send({ + token: 'wrong', + client_id: 'ghost-admin', + client_secret: 'not_available' + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(403) + .end(function (err, res) { + if (err) { + return done(err); + } + should.not.exist(res.body.access_token); + should.not.exist(res.body.refresh_token); + should.not.exist(res.body.expires_in); + + done(); + }); + }); + + it('[failure] expired AT', function (done) { + testUtils.fixtures.insertAccessToken({ + expires: Date.now() - 1000, + token: 'one-time-token', + user_id: 1, + client_id: 2 + }).then(function () { + request.post(testUtils.API.getApiQuery('authentication/setup/three')) + .set('Origin', config.url) + .send({ + token: 'one-time-token', + client_id: 'ghost-admin', + client_secret: 'not_available' + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(403) + .end(function (err) { + if (err) { + return done(err); + } + + done(); + }); + }).catch(done); + }); });