Ghost/core/server/services/auth/oauth.js

193 lines
7.4 KiB
JavaScript
Raw Normal View History

var oauth2orize = require('oauth2orize'),
_ = require('lodash'),
passport = require('passport'),
models = require('../../models'),
authUtils = require('./utils'),
spamPrevention = require('../../web/middleware/api/spam-prevention'),
common = require('../../lib/common'),
knex = require('../../data/db').knex,
oauthServer,
oauth;
function exchangeRefreshToken(client, refreshToken, scope, body, authInfo, done) {
knex.transaction(function (transacting) {
var options = {
transacting: transacting
};
return models.Refreshtoken.findOne({token: refreshToken}, _.merge({forUpdate: true}, options))
.then(function then(model) {
if (!model) {
throw new common.errors.NoPermissionError({
message: common.i18n.t('errors.middleware.oauth.invalidRefreshToken')
});
}
var token = model.toJSON();
if (token.expires <= Date.now()) {
throw new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.oauth.refreshTokenExpired')
});
}
// @TODO: this runs outside of the transaction
spamPrevention.userLogin().reset(authInfo.ip, body.refresh_token + 'login');
return authUtils.createTokens({
clientId: token.client_id,
userId: token.user_id,
oldAccessToken: authInfo.accessToken,
oldRefreshToken: refreshToken,
oldRefreshId: token.id
}, options).then(function (response) {
return {
access_token: response.access_token,
expires_in: response.expires_in
};
});
});
}).then(function (response) {
done(null, response.access_token, {expires_in: response.expires_in});
}).catch(function (err) {
if (common.errors.utils.isIgnitionError(err)) {
return done(err, false);
}
done(new common.errors.InternalServerError({
err: err
}), false);
});
}
// We are required to pass in authInfo in order to reset spam counter for user login
function exchangePassword(client, username, password, scope, body, authInfo, done) {
🐛 Fixed error for password authentication with Bearer Token (#9227) refs #8613, refs #9228 - if you send a request to /authentication/token with `grant_type:password` and a Bearer token, Ghost was not able to handle this combination - because it skipped the client authentication, see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/authenticate.js#L13 - and OAuth detects the `grant_type: password` and jumps in the target implementation - the target implementation for password authentication **again** tried to fetch the client and failed, because it relied on the previous client authentication - see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/oauth.js#L40 (client.slug is undefined if client authentication is skipped) - ^ so this is the bug - we **can** skip client authentication for requests to the API to fetch data for example e.g. GET /posts (including Bearer) - so when is a client authentication required? - RFC (https://tools.ietf.org/html/rfc6749#page-38) differentiates between confidential and public clients, Ghost has no implementation for this at the moment - so in theory, public clients don't have to be authenticated, only if the credentials are included - to not invent a breaking change, i decided to only make the client authentication required for password authentication - we could change this in Ghost 2.0 I have removed the extra client request to the database for the password authentication, this is not needed. We already do client password authentication [here](https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/auth-strategies.js#L19); If a Bearer token is present and you have not send a `grant_type` (which signalises OAuth to do authentication), you can skip the client authentication.
2017-11-09 17:11:29 +03:00
if (!client || !client.id) {
return done(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.clientCredentialsNotProvided')
🐛 Fixed error for password authentication with Bearer Token (#9227) refs #8613, refs #9228 - if you send a request to /authentication/token with `grant_type:password` and a Bearer token, Ghost was not able to handle this combination - because it skipped the client authentication, see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/authenticate.js#L13 - and OAuth detects the `grant_type: password` and jumps in the target implementation - the target implementation for password authentication **again** tried to fetch the client and failed, because it relied on the previous client authentication - see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/oauth.js#L40 (client.slug is undefined if client authentication is skipped) - ^ so this is the bug - we **can** skip client authentication for requests to the API to fetch data for example e.g. GET /posts (including Bearer) - so when is a client authentication required? - RFC (https://tools.ietf.org/html/rfc6749#page-38) differentiates between confidential and public clients, Ghost has no implementation for this at the moment - so in theory, public clients don't have to be authenticated, only if the credentials are included - to not invent a breaking change, i decided to only make the client authentication required for password authentication - we could change this in Ghost 2.0 I have removed the extra client request to the database for the password authentication, this is not needed. We already do client password authentication [here](https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/auth-strategies.js#L19); If a Bearer token is present and you have not send a `grant_type` (which signalises OAuth to do authentication), you can skip the client authentication.
2017-11-09 17:11:29 +03:00
}), false);
}
🐛 Fixed error for password authentication with Bearer Token (#9227) refs #8613, refs #9228 - if you send a request to /authentication/token with `grant_type:password` and a Bearer token, Ghost was not able to handle this combination - because it skipped the client authentication, see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/authenticate.js#L13 - and OAuth detects the `grant_type: password` and jumps in the target implementation - the target implementation for password authentication **again** tried to fetch the client and failed, because it relied on the previous client authentication - see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/oauth.js#L40 (client.slug is undefined if client authentication is skipped) - ^ so this is the bug - we **can** skip client authentication for requests to the API to fetch data for example e.g. GET /posts (including Bearer) - so when is a client authentication required? - RFC (https://tools.ietf.org/html/rfc6749#page-38) differentiates between confidential and public clients, Ghost has no implementation for this at the moment - so in theory, public clients don't have to be authenticated, only if the credentials are included - to not invent a breaking change, i decided to only make the client authentication required for password authentication - we could change this in Ghost 2.0 I have removed the extra client request to the database for the password authentication, this is not needed. We already do client password authentication [here](https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/auth-strategies.js#L19); If a Bearer token is present and you have not send a `grant_type` (which signalises OAuth to do authentication), you can skip the client authentication.
2017-11-09 17:11:29 +03:00
// Validate the user
return models.User.check({email: username, password: password})
.then(function then(user) {
return authUtils.createTokens({
clientId: client.id,
userId: user.id
});
})
.then(function then(response) {
spamPrevention.userLogin().reset(authInfo.ip, username + 'login');
return done(null, response.access_token, response.refresh_token, {expires_in: response.expires_in});
})
🐛 Fixed error for password authentication with Bearer Token (#9227) refs #8613, refs #9228 - if you send a request to /authentication/token with `grant_type:password` and a Bearer token, Ghost was not able to handle this combination - because it skipped the client authentication, see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/authenticate.js#L13 - and OAuth detects the `grant_type: password` and jumps in the target implementation - the target implementation for password authentication **again** tried to fetch the client and failed, because it relied on the previous client authentication - see https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/oauth.js#L40 (client.slug is undefined if client authentication is skipped) - ^ so this is the bug - we **can** skip client authentication for requests to the API to fetch data for example e.g. GET /posts (including Bearer) - so when is a client authentication required? - RFC (https://tools.ietf.org/html/rfc6749#page-38) differentiates between confidential and public clients, Ghost has no implementation for this at the moment - so in theory, public clients don't have to be authenticated, only if the credentials are included - to not invent a breaking change, i decided to only make the client authentication required for password authentication - we could change this in Ghost 2.0 I have removed the extra client request to the database for the password authentication, this is not needed. We already do client password authentication [here](https://github.com/TryGhost/Ghost/blob/1.17.0/core/server/auth/auth-strategies.js#L19); If a Bearer token is present and you have not send a `grant_type` (which signalises OAuth to do authentication), you can skip the client authentication.
2017-11-09 17:11:29 +03:00
.catch(function (err) {
done(err, false);
});
}
function exchangeAuthorizationCode(req, res, next) {
if (!req.body.authorizationCode) {
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
}
req.query.code = req.body.authorizationCode;
passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
}
spamPrevention.userLogin().reset(req.authInfo.ip, req.body.authorizationCode + 'login');
authUtils.createTokens({
clientId: req.client.id,
userId: user.id
}).then(function then(response) {
res.json({
access_token: response.access_token,
refresh_token: response.refresh_token,
expires_in: response.expires_in
});
}).catch(function (err) {
next(err);
});
})(req, res, next);
}
oauth = {
init: function init() {
oauthServer = oauth2orize.createServer();
// remove all expired accesstokens on startup
models.Accesstoken.destroyAllExpired();
// remove all expired refreshtokens on startup
models.Refreshtoken.destroyAllExpired();
// Exchange user id and password for access tokens. The callback accepts the
// `client`, which is exchanging the user's name and password from the
// authorization request for verification. If these values are validated, the
// application issues an access token on behalf of the user who authorized the code.
oauthServer.exchange(oauth2orize.exchange.password({userProperty: 'client'},
exchangePassword));
// Exchange the refresh token to obtain an access token. The callback accepts the
// `client`, which is exchanging a `refreshToken` previously issued by the server
// for verification. If these values are validated, the application issues an
// access token on behalf of the user who authorized the code.
oauthServer.exchange(oauth2orize.exchange.refreshToken({userProperty: 'client'},
exchangeRefreshToken));
/**
* Exchange authorization_code for an access token.
* We forward to authorization code to Ghost.org.
*
* oauth2orize offers a default implementation via exchange.authorizationCode, but this function
* wraps the express request and response. So no chance to get access to it.
* We use passport to communicate with Ghost.org. Passport's module design requires the express req/res.
*
* For now it's OK to not use exchange.authorizationCode. You can read through the implementation here:
* https://github.com/jaredhanson/oauth2orize/blob/master/lib/exchange/authorizationCode.js
* As you can see, it does some validation and set's some headers, not very very important,
* but it's part of the oauth2 spec.
*
* @TODO: How to use exchange.authorizationCode in combination of passport?
*/
oauthServer.exchange('authorization_code', exchangeAuthorizationCode);
},
// ### Generate access token Middleware
// register the oauth2orize middleware for password and refresh token grants
generateAccessToken: function generateAccessToken(req, res, next) {
/**
* TODO:
* https://github.com/jaredhanson/oauth2orize/issues/182
* oauth2orize only offers the option to forward request information via authInfo object
*
* Important: only used for resetting the brute count (access to req.ip)
*/
req.authInfo = {
ip: req.ip,
accessToken: authUtils.getBearerAutorizationToken(req)
};
return oauthServer.token()(req, res, function (err) {
if (err && err.status === 400) {
err = new common.errors.BadRequestError({err: err, message: err.message});
}
next(err);
});
}
};
module.exports = oauth;