From b218e1fda0dfde41308b3f22a2f7229e3e875e97 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 4 Dec 2015 11:17:50 -0800 Subject: [PATCH] Expose configuration for local validation of access tokens --- docs/authentication.rst | 83 ++++++++++++++++++++++++++++++----- lib/config.yml | 1 + lib/helpers/get-user.js | 5 +++ test/helpers/test-get-user.js | 56 +++++++++++++++++++++++ 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index 0a2ff7dc..ae73b0f0 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -37,19 +37,82 @@ To use cookie authentication, simply use the ``loginRequired`` middleware:: }); Behind the scenes we are issuing a OAuth2 Access Token and Refresh token for -the user, and storing them in secure, HTTPS-Only cookies. The maximum -lifetime of the cookies is controlled by the expiration time of the Refresh -Token. +the user, and storing them in secure, HTTPS-Only cookies. After the user has +logged in, these cookies will be supplied on every request. Our library will +assert that the access token is valid. If the access token is expired, we will +attempt to refresh it with the refresh token. -If you need to change the expiration time of the Refresh Token, please login -to the Stormpath Admin Console and navigate to the OAuth policy of your -Stormpath Application. Then change the expiration time of the Refresh Token. .. note:: - Express-Stormpath's session management will not interfere with any existing - session middleware you might have. The sessions that Stormpath uses are - exclusively used for Stormpath's purposes, so it's safe to create your own - separate sessions if needed. + Express-Stormpath's OAuth2 cookie feature will not interfere with any + existing cookie-based session middleware you might have. The cookies that + Stormpath creates are used exclusively for Stormpath's purposes, so it's + safe to create your own separate sessions if needed. + + +Setting Token Expiration Time +............................. + +If you need to change the expiration time of the access token or refresh Token, +please login to the Stormpath Admin Console and navigate to the OAuth policy of +your Stormpath Application. There you will find the settings for each token. + +Token Validation Strategy +......................... + +When a request comes into your server, this library will use the access token +and refresh token cookies to make an authentication decision. The default +validation strategy works like this: + +- Validate the signature and expiration time of the access token. If the access + token is expired, attempt to get a new one by using the refresh token. + +- If the access token is expired and cannot be refreshed, deny the request + +- If the access token is not expired and the signature is valid, the library + makes a request to the Stormpath API to assert that the access token has not + been revoked and that the associated account still exists and is not disabled. + +In the last step, the API request will add a network request to the +authentication process. If this is not desirable (for performance reasons), +you can opt-in to `local` validation. In this situation, our library only +checks the signature of the token and does not make the extra request to the +Stormpath API to assert the token and the account. + +You can opt-in to local validation with this configuration: + +.. code-block:: javascript + + { + web: { + oauth2: { + password: { + validationStrategy: 'local' + } + } + } + } + +.. warning:: + + When using local validation, your server will not be aware of token revocation + or any changes to the associated Stormpath account. **This is a security + risk.** + + There are two suggested strategies for dealing with this risk: + + * Use a short expiration time for your access tokens (such as one hour or + less). This will limit the amount of time that the access token can be used + for validation. Our library *always* makes a request to the Stormpath API when + we attempt to refresh an access token, so the refresh attempt will fail + at this time if the refresh token has been revoked. + + * Maintain a blacklist of revoked tokens, in your local application cache. + Implement a middleware function that asserts that the access token is not + in this cache, and reject the request if true. We may implement this as + a convenience feature in the future. + + Issuing API Keys diff --git a/lib/config.yml b/lib/config.yml index a5e07a60..1b77fd69 100644 --- a/lib/config.yml +++ b/lib/config.yml @@ -9,6 +9,7 @@ web: ttl: 3600 password: enabled: true + validationStrategy: stormpath accessTokenCookie: name: "access_token" httpOnly: true diff --git a/lib/helpers/get-user.js b/lib/helpers/get-user.js index 4b0570e6..b1c9c3ec 100644 --- a/lib/helpers/get-user.js +++ b/lib/helpers/get-user.js @@ -30,6 +30,7 @@ var expandAccount = require('./expand-account'); 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'); // In the event this has already been run (this can happen due to Express @@ -66,6 +67,10 @@ module.exports = function (req, res, next) { } else if (req.cookies && req.cookies.access_token) { var authenticator = new stormpath.JwtAuthenticator(application); + if (config.web.oauth2.password.validationStrategy === 'local') { + authenticator.withLocalValidation(); + } + authenticator.authenticate(req.cookies.access_token, function (err, authenticationResult) { if (err) { logger.info('Failed to authenticate the request. Invalid access_token found.'); diff --git a/test/helpers/test-get-user.js b/test/helpers/test-get-user.js index 701672ee..41828e6e 100644 --- a/test/helpers/test-get-user.js +++ b/test/helpers/test-get-user.js @@ -634,6 +634,62 @@ describe('getUser', function () { }); }); + it('should use local validation, if specified by configuration', function (done) { + + var app = helpers.createStormpathExpressApp({ + application: stormpathApplication, + website: true, + web: { + oauth2: { + password: { + validationStrategy: 'local' + } + } + } + }); + + app.get('/', getUser, function (req, res) { + res.json(req.user); + }); + + app.on('stormpath.ready', function () { + + var agent = request.agent(app); + + async.series([ + function (callback) { + stormpathAccount.status = 'ENABLED'; + stormpathAccount.save(callback); + }, + function (callback) { + agent + .post('/login') + .send({ + login: accountData.email, + password: accountData.password + }) + .expect(302) + .end(callback); + }, + function (callback) { + var a = new Date(); + agent + .get('/') + .expect(200) + .end(function (err, res) { + if (err) { + return callback(err); + } + var b = new Date(); + assert((b - a) < 20, 'Validation took too long - does not appear to be local validation'); + assert.equal(res.body.email, accountData.email); + callback(); + }); + } + ], done); + }); + }); + it('should set req.user and res.locals.user if an invalid access_token cookie is present with a valid refresh_token cookie', function (done) { var app = createFakeExpressApp(); var agent = request.agent(app);