Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ web:
password:
enabled: true
validationStrategy: "local"
revoke:
enabled: true
uri: '/oauth/revoke'

accessTokenCookie:

Expand Down
45 changes: 29 additions & 16 deletions lib/controllers/get-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ module.exports = function (req, res) {
res.json(authResult.accessTokenResponse);
}

function resolveClientCredentialsAuthFields(req) {
var authHeader = req && req.headers && req.headers.authorization;

if (authHeader && authHeader.match(/Basic/i)) {
var authorization = authHeader.split(' ').pop();
var parts = new Buffer(authorization, 'base64').toString('utf8').split(':');

req.body.apiKey = {
id: parts[0],
secret: parts[1]
};
} else if (req.body && req.body.client_id && req.body.client_secret) {
req.body.apiKey = {
id: req.body.client_id,
secret: req.body.client_secret
};
}
}

function continueWithHandlers(authResult, preHandler, postHandler, onCompleted) {
var options = req.body || {};

Expand Down Expand Up @@ -88,8 +107,18 @@ module.exports = function (req, res) {
break;
case 'password':
case 'refresh_token':
case 'client_credentials':
var authenticator = new stormpath.OAuthAuthenticator(application);

if (config.web.scopeFactory) {
authenticator.setScopeFactory(config.web.scopeFactory);
authenticator.setScopeFactorySigningKey(config.client.apiKey.secret);
}

if (grantType === 'client_credentials') {
resolveClientCredentialsAuthFields(req);
}

authenticator.authenticate(req, function (err, authResult) {
if (err) {
return writeErrorResponse(err);
Expand All @@ -108,22 +137,6 @@ module.exports = function (req, res) {
});
break;

case 'client_credentials':
application.authenticateApiRequest({
request: req,
ttl: config.web.oauth2.client_credentials.accessToken.ttl,
scopeFactory: function (account, requestedScopes) {
return requestedScopes;
}
}, function (err, authResult) {
if (err) {
return writeErrorResponse(err);
}

res.json(authResult.tokenResponse);
});
break;

default:
writeErrorResponse({
error: 'unsupported_grant_type'
Expand Down
3 changes: 2 additions & 1 deletion lib/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ module.exports = {
login: require('./login'),
logout: require('./logout'),
register: require('./register'),
verifyEmail: require('./verify-email')
verifyEmail: require('./verify-email'),
revokeToken: require('./revoke-token')
};
134 changes: 134 additions & 0 deletions lib/controllers/revoke-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use strict';

var middleware = require('../middleware');
var nJwt = require('njwt');

/**
* Revokes an OAuth token (an access token or a refresh token). When an access
* token is revoked, the associated access token is revoked as well.
*
* The URL this controller is bound to can be controlled via express-stormpath
* settings.
*
* @method
*
* @param {Object} req - The http request.
* @param {Object} res - The http response.
*/
module.exports = function (req, res) {
var config = req.app.get('stormpathConfig');
var logger = req.app.get('stormpathLogger');

var token = req.body.token;
var jwtSigningKey = config.client.apiKey.secret;

function writeErrorResponse(err) {
var error = {
error: err.error,
message: err.userMessage || err.message
};

logger.info('An OAuth token revoke failed due to an improperly formed request.');

return res.status(err.status || err.statusCode || 400).json(error);
}

/**
* @private
*
* Given a token's resource ID and its type (access or refresh),
* retrieves a token with that ID if and only if it is one of the tokens
* belonging to this user. If there is no such token, the callback is called
* with no data nonetheless, due to how RFC 7009 defines this case.
*
* @param {String} tokenId Token resource identifier
* @param {String} tokenType The type of the token, `access` or `refresh`
* @param {Function} callback Function to be called after completion
*/
function loadTokenForUser(tokenId, tokenType, callback) {
var getTokens;
switch (tokenType) {
case 'access':
getTokens = req.user.getAccessTokens.bind(req.user);
break;
case 'refresh':
getTokens = req.user.getRefreshTokens.bind(req.user);
break;
default:
return writeErrorResponse({
error: 'unsupported_token_type'
});
}

getTokens(function (err, collection) {
if (err) {
return callback(err);
}

var validTokens = collection.items.filter(function (token) {
return token.href.indexOf(tokenId) !== -1;
});

if (validTokens.length) {
return callback(null, validTokens[0]);
}

// RFC 7009 states that if there is no token, it counts as already invalidated,
// and the process should proceed as it were found and invalidated.
callback();
});

}

/**
* @private
*
* Unpacks a token from its compact form and retrieves the correct token resource
* belonging to the current user from that data, if there is one such token.
*
* @param {String} compactToken Token in compact string form
* @param {Function} callback Function to be called after completion
*/
function getTokenResource(compactToken, callback) {
nJwt.verify(compactToken, jwtSigningKey, function (err, parsedToken) {
if (err) {
return callback(); // Ignore failure, means token is already invalid
}

var tokenType = parsedToken.header.stt;
var tokenId = parsedToken.body.jti;

loadTokenForUser(tokenId, tokenType, callback);
});
}

middleware.apiAuthenticationRequired(req, res, function (err) {
if (err) {
return writeErrorResponse(err);
}

if (!token) {
return writeErrorResponse({
error: 'invalid_request'
});
}

getTokenResource(token, function (err, resource) {
if (err) {
return writeErrorResponse(err);
}

if (!resource) {
return res.status(200).end();
}

resource.delete(function (err) {
if (err) {
return writeErrorResponse(err);
}

res.status(200).end();
});
});
});
};
3 changes: 2 additions & 1 deletion lib/middleware/api-authentication-required.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var stormpath = require('stormpath');
*/
module.exports = function (req, res, next) {
var application = req.app.get('stormpathApplication');
var client = req.app.get('stormpathClient');
var config = req.app.get('stormpathConfig');
var logger = req.app.get('stormpathLogger');

Expand Down Expand Up @@ -64,7 +65,7 @@ module.exports = function (req, res, next) {
var isBasic = req.headers.authorization && req.headers.authorization.match(/Basic .+/);

if (token) {
var authenticator = new stormpath.JwtAuthenticator(application);
var authenticator = new stormpath.StormpathAccessTokenAuthenticator(client);
if (config.web.oauth2.password.validationStrategy === 'local') {
authenticator.withLocalValidation();
}
Expand Down
4 changes: 4 additions & 0 deletions lib/stormpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ module.exports.init = function (app, opts) {

if (web.oauth2.enabled) {
router.all(web.oauth2.uri, bodyParser.form(), stormpathMiddleware, controllers.getToken);

if (web.oauth2.revoke.enabled) {
addPostRoute(web.oauth2.revoke.uri, controllers.revokeToken);
}
}

client.getApplication(config.application.href, function (err, application) {
Expand Down
Loading