Skip to content

Commit

Permalink
🎨 public client registration updates (#7690)
Browse files Browse the repository at this point in the history
* 🎨  use updateClient function to update redirectUri

refs #7654

* 🎨  name instead of clientName
* 🎨  config.get('theme:title') for client name

- initial read can happen from config

* ✨  register public client: client name and description

- no update yet
- for initial client creation
- we forward title/description to Ghost Auth
- TODO: use settings-cache when merged

* ✨  store blog_uri in db
* 🎨  passport logic changes

- use updateClient instead of changeCallbackURL
- be able to update: blog title, blog description, redirectUri and blogUri
- remove retries, they get implemented in passport-ghost soon
- reorder logic a bit

* 🛠  passport-ghost 1.2.0

* 🎨  tests: extend DataGenerator createClient

- set some defaults

* 🎨  tests

- extend tests
- 👻

* ✨  run auth.init in background

- no need to block the bootstrap process
- if client can't be registered, you will see an error
- ensure Ghost-Admin renders correctly

* 🛠   passport-ghost 1.3.0

- retries

* 🎨  use client_uri in Client Schema

- adapt changes
- use blog_uri only when calling the passport-ghost instance
- Ghost uses the client_uri notation to improve readability

* ✨  read blog title/description from settings cache

* 🚨  Ghost Auth returns email instead of email_address

- adapt Ghost
  • Loading branch information
kirrg001 authored and ErisDS committed Nov 8, 2016
1 parent 0d0542c commit 0a744c2
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 109 deletions.
4 changes: 2 additions & 2 deletions core/server/api/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ configuration = {
configuration.clientId = result.ghostAdmin.get('slug');
configuration.clientSecret = result.ghostAdmin.get('secret');

if (result.ghostAuth) {
configuration.ghostAuthId = result.ghostAuth.get('uuid');
if (config.get('auth:type') === 'ghost') {
configuration.ghostAuthId = result.ghostAuth && result.ghostAuth.get('uuid') || 'not-available';
configuration.ghostAuthUrl = config.get('auth:url');
}

Expand Down
8 changes: 4 additions & 4 deletions core/server/auth/auth-strategies.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ strategies = {
}

return models.User.add({
email: profile.email_address,
name: profile.email_address,
email: profile.email,
name: profile.email,
password: utils.uid(50),
roles: invite.toJSON().roles
}, options);
Expand All @@ -117,13 +117,13 @@ strategies = {
}

return models.User.edit({
email: profile.email_address,
email: profile.email,
status: 'active'
}, _.merge({id: owner.id}, options));
});
};

models.User.getByEmail(profile.email_address, options)
models.User.getByEmail(profile.email, options)
.then(function fetchedUser(user) {
if (user) {
return user;
Expand Down
198 changes: 124 additions & 74 deletions core/server/auth/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,95 +2,130 @@ var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy
BearerStrategy = require('passport-http-bearer').Strategy,
GhostOAuth2Strategy = require('passport-ghost').Strategy,
passport = require('passport'),
_ = require('lodash'),
debug = require('debug')('ghost:auth'),
Promise = require('bluebird'),
authStrategies = require('./auth-strategies'),
errors = require('../errors'),
events = require('../events'),
logging = require('../logging'),
models = require('../models'),
_private = {
retryTimeout: 3000,
retries: 10
};
_private = {};

_private.registerClient = function (options) {
/**
* Update client name and description if changes in the blog settings
*/
_private.registerEvents = function registerEvents() {
events.on('settings.edited', function onSettingsChanged(settingModel) {
var titleHasChanged = settingModel.attributes.key === 'title' && settingModel.attributes.value !== settingModel._updatedAttributes.value,
descriptionHasChanged = settingModel.attributes.key === 'description' && settingModel.attributes.value !== settingModel._updatedAttributes.value,
options = {
ghostOAuth2Strategy: passport._strategies.ghost
};

if (!titleHasChanged && !descriptionHasChanged) {
return;
}

if (titleHasChanged) {
options.clientName = settingModel.attributes.value;
debug('Ghost Auth Client title has changed: ' + options.clientName);
}

if (descriptionHasChanged) {
options.clientDescription = settingModel.attributes.value;
debug('Ghost AuthClient description has changed: ' + options.clientDescription);
}

_private.updateClient(options).catch(function onUpdatedClientError(err) {
// @TODO: see https://github.com/TryGhost/Ghost/issues/7627
if (_.isArray(err)) {
err = err[0];
}

logging.error(err);
});
});
};

/**
* smart function
*/
_private.updateClient = function updateClient(options) {
var ghostOAuth2Strategy = options.ghostOAuth2Strategy,
redirectUri = options.redirectUri,
clientUri = options.clientUri,
clientName = options.clientName,
redirectUri = options.redirectUri;
clientDescription = options.clientDescription;

return models.Client.findOne({slug: 'ghost-auth'}, {context: {internal: true}})
.then(function fetchedClient(client) {
// CASE: Ghost Auth client is already registered
if (client) {
if (client.get('redirection_uri') === redirectUri) {
return {
client_id: client.get('uuid'),
client_secret: client.get('secret')
};
}

debug('Update ghost client callback url...');
return ghostOAuth2Strategy.changeCallbackURL({
callbackURL: redirectUri,
clientId: client.get('uuid'),
clientSecret: client.get('secret')
}).then(function changedCallbackURL() {
client.set('redirection_uri', redirectUri);
return client.save(null, {context: {internal: true}});
}).then(function updatedClient() {
return {
client_id: client.get('uuid'),
client_secret: client.get('secret')
};
});
}
.then(function (client) {
// CASE: we have to create the client
if (!client) {
debug('Client does not exist');

return ghostOAuth2Strategy.registerClient({
name: clientName,
description: clientDescription
}).then(function registeredRemoteClient(credentials) {
debug('Registered remote client: ' + JSON.stringify(credentials));

return ghostOAuth2Strategy.registerClient({clientName: clientName})
.then(function addClient(credentials) {
return models.Client.add({
name: 'Ghost Auth',
name: credentials.name,
description: credentials.description,
slug: 'ghost-auth',
uuid: credentials.client_id,
secret: credentials.client_secret,
redirection_uri: redirectUri
redirection_uri: credentials.redirect_uri,
client_uri: credentials.blog_uri
}, {context: {internal: true}});
})
.then(function returnClient(client) {
}).then(function addedLocalClient(client) {
debug('Added local client: ' + JSON.stringify(client.toJSON()));

return {
client_id: client.get('uuid'),
client_secret: client.get('secret')
};
});
});
};
}

_private.startPublicClientRegistration = function startPublicClientRegistration(options) {
return new Promise(function (resolve, reject) {
(function retry(retries) {
options.retryCount = retries;

_private.registerClient(options)
.then(resolve)
.catch(function publicClientRegistrationError(err) {
logging.error(err);

if (options.retryCount < 0) {
return reject(new errors.IncorrectUsageError({
message: 'Public client registration failed: ' + err.code || err.message,
context: 'Please verify that the url can be reached: ' + options.ghostOAuth2Strategy.url
}));
}

debug('Trying to register Public Client...');
var timeout = setTimeout(function () {
clearTimeout(timeout);

options.retryCount = options.retryCount - 1;
retry(options.retryCount);
}, _private.retryTimeout);
});
})(_private.retries);
});
// CASE: nothing changed
if (client.get('redirection_uri') === redirectUri &&
client.get('name') === clientName &&
client.get('description') === clientDescription &&
client.get('client_uri') === clientUri) {
debug('Client did not change');

return {
client_id: client.get('uuid'),
client_secret: client.get('secret')
};
}

debug('Update client...');
return ghostOAuth2Strategy.updateClient(_.omit({
clientId: client.get('uuid'),
clientSecret: client.get('secret'),
redirectUri: redirectUri,
blogUri: clientUri,
name: clientName,
description: clientDescription
}, _.isUndefined)).then(function updatedRemoteClient(updatedRemoteClient) {
debug('Update remote client: ' + JSON.stringify(updatedRemoteClient));

client.set('redirection_uri', updatedRemoteClient.redirect_uri);
client.set('client_uri', updatedRemoteClient.blog_uri);
client.set('name', updatedRemoteClient.name);
client.set('description', updatedRemoteClient.description);

return client.save(null, {context: {internal: true}});
}).then(function updatedLocalClient() {
return {
client_id: client.get('uuid'),
client_secret: client.get('secret')
};
});
});
};

/**
Expand All @@ -101,9 +136,10 @@ _private.startPublicClientRegistration = function startPublicClientRegistration(
exports.init = function initPassport(options) {
var authType = options.authType,
clientName = options.clientName,
clientDescription = options.clientDescription,
ghostAuthUrl = options.ghostAuthUrl,
redirectUri = options.redirectUri,
blogUri = options.blogUri;
clientUri = options.clientUri;

return new Promise(function (resolve, reject) {
passport.use(new ClientPasswordStrategy(authStrategies.clientPasswordStrategy));
Expand All @@ -114,22 +150,36 @@ exports.init = function initPassport(options) {
}

var ghostOAuth2Strategy = new GhostOAuth2Strategy({
callbackURL: redirectUri,
blogUri: blogUri,
redirectUri: redirectUri,
blogUri: clientUri,
url: ghostAuthUrl,
passReqToCallback: true
}, authStrategies.ghostStrategy);

_private.startPublicClientRegistration({
_private.updateClient({
ghostOAuth2Strategy: ghostOAuth2Strategy,
clientName: clientName,
redirectUri: redirectUri
clientDescription: clientDescription,
redirectUri: redirectUri,
clientUri: clientUri
}).then(function setClient(client) {
debug('Public Client Registration was successful');

ghostOAuth2Strategy.setClient(client);
passport.use(ghostOAuth2Strategy);
_private.registerEvents();
return resolve({passport: passport.initialize()});
}).catch(reject);
}).catch(function onError(err) {
// @TODO: see https://github.com/TryGhost/Ghost/issues/7627
if (_.isArray(err)) {
err = err[0];
}

debug('Public registration failed:' + err.message);

return reject(new errors.GhostError({
err: err,
context: 'Public client registration failed',
help: 'Please verify the configured url: ' + ghostOAuth2Strategy.url
}));
});
});
};
1 change: 1 addition & 0 deletions core/server/data/schema/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ module.exports = {
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
secret: {type: 'string', maxlength: 150, nullable: false},
redirection_uri: {type: 'string', maxlength: 2000, nullable: true},
client_uri: {type: 'string', maxlength: 2000, nullable: true},
logo: {type: 'string', maxlength: 2000, nullable: true},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'development'},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'ua', validations: {isIn: [['ua', 'web', 'native']]}},
Expand Down
12 changes: 8 additions & 4 deletions core/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var debug = require('debug')('ghost:boot:init'),
Promise = require('bluebird'),
KnexMigrator = require('knex-migrator'),
config = require('./config'),
logging = require('./logging'),
i18n = require('./i18n'),
api = require('./api'),
models = require('./models'),
Expand Down Expand Up @@ -115,15 +116,18 @@ function init(options) {

debug('Express Apps done');

return auth.init({
// runs asynchronous
auth.init({
authType: config.get('auth:type'),
ghostAuthUrl: config.get('auth:url'),
redirectUri: utils.url.urlJoin(utils.url.getBaseUrl(), 'ghost', '/'),
blogUri: utils.url.urlJoin(utils.url.getBaseUrl(), '/'),
// @TODO: set blog title
clientName: utils.url.getBaseUrl()
clientUri: utils.url.urlJoin(utils.url.getBaseUrl(), '/'),
clientName: api.settings.getSettingSync('title'),
clientDescription: api.settings.getSettingSync('description')
}).then(function (response) {
parentApp.use(response.auth);
}).catch(function onAuthError(err) {
logging.error(err);
});
}).then(function () {
debug('Auth done');
Expand Down
10 changes: 5 additions & 5 deletions core/test/unit/auth/auth-strategies_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe('Auth Strategies', function () {
it('with invite, but with wrong invite token', function (done) {
var ghostAuthAccessToken = '12345',
req = {body: {inviteToken: 'wrong'}},
profile = {email_address: 'test@example.com'};
profile = {email: 'test@example.com'};

userByEmailStub.returns(Promise.resolve(null));
inviteStub.returns(Promise.reject(new errors.NotFoundError()));
Expand All @@ -231,7 +231,7 @@ describe('Auth Strategies', function () {
it('with correct invite token, but expired', function (done) {
var ghostAuthAccessToken = '12345',
req = {body: {inviteToken: 'token'}},
profile = {email_address: 'test@example.com'};
profile = {email: 'test@example.com'};

userByEmailStub.returns(Promise.resolve(null));
inviteStub.returns(Promise.resolve(Models.Invite.forge({
Expand All @@ -252,7 +252,7 @@ describe('Auth Strategies', function () {
it('with correct invite token', function (done) {
var ghostAuthAccessToken = '12345',
req = {body: {inviteToken: 'token'}},
invitedProfile = {email_address: 'test@example.com'},
invitedProfile = {email: 'test@example.com'},
invitedUser = {id: 2},
inviteModel = Models.Invite.forge({
id: 1,
Expand Down Expand Up @@ -282,7 +282,7 @@ describe('Auth Strategies', function () {
it('setup', function (done) {
var ghostAuthAccessToken = '12345',
req = {body: {}},
ownerProfile = {email_address: 'test@example.com'},
ownerProfile = {email: 'test@example.com'},
owner = {id: 2};

userByEmailStub.returns(Promise.resolve(null));
Expand Down Expand Up @@ -313,7 +313,7 @@ describe('Auth Strategies', function () {
it('auth', function (done) {
var ghostAuthAccessToken = '12345',
req = {body: {}},
ownerProfile = {email_address: 'test@example.com'},
ownerProfile = {email: 'test@example.com'},
owner = {id: 2};

userByEmailStub.returns(Promise.resolve(owner));
Expand Down
Loading

0 comments on commit 0a744c2

Please sign in to comment.