Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Commit

Permalink
Merge pull request #622 from stormpath/4.0.0
Browse files Browse the repository at this point in the history
Release 4.0.0
  • Loading branch information
robertjd authored Jul 21, 2017
2 parents a1dac39 + e4b3125 commit aed8d26
Show file tree
Hide file tree
Showing 54 changed files with 2,233 additions and 1,528 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ docs/_build
node_modules
.coveralls.yml
*.log
.env
.env
.vscode
.DS_Store
7 changes: 5 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
language: node_js
sudo: false
node_js:
- '0.10'
- '0.12'
- '4'
- '6'
install:
Expand Down Expand Up @@ -35,6 +33,11 @@ env:
global:
- secure: M4UcXsvhjYXupsD0D+bAWdopk9JjOxKnHi5OcqCSZ4lCn6XZ3KlxMQC/VM8Ryhe3Y5dAcNnJOJSE0eeK+ojZERY2UcozYSjEIdTZjGDI1L4HM538GeRz8wLEIG18gRdSjsyVGjAOed+eZ/MQwo2xyIjVPjJBFJuy2Djtt4fi1sw=
- secure: LC2v9W0Nx8oviv+AajnXV1DOaiht+/1Hy92loa4a4Lbwz/NW0DmM9k1ollBFHpBhWVai3HaZKPl0XqdHqdq1fA2HppdJv8YbT/Hwkj1WdX+LRE/VUNevVc+MszlDJq1FOrgaEjiKyBqQP07RkZl4tg2kwpIET2Q0zmCubE7pezo=
- secure: Uiqmlmp4O9noUngjFvl7hws+LpaHvy3lcwAAg4ecEotyE0T4b+kd1tvN5zSA2VEohPy/ynQK8zjevHqNoQzc4rUxTUsCijk1fnyOqrowvYrnSC0jwjkp4FaDaPk6HyEhGA6N37hl3km38iD09eqLwa4qA7SYgZEi5qRXLhnQxjo=
- secure: h85DxO0pgB+tnszJIwfd/BDtmi1NKVfNuZertF3jitdPFFktlqF2ecDiAqgINpI4lNiJYac8eF8isTZn2Unh2K2SyElkZZfmhifz8n5MHqywidNYQKDz8BMBGELOxNQVkPgevPR5VU1eZoYUMDRgetC6hCp3iaRGoF6Xv+kAXl4=
- secure: mBl0PWZag9Ji9jAYOEhCc9uORN9sGbbHD1IBkJD3kZ++tpMJO33ZUDHQix5BRSohhPgVqDm79XHpJiJ8PSIkQfvyMZNlWjGKqd/J9bdZbxSfO6+PdcSwS38Sr4hQsZyhYXhmragKzJI10vWBVJ/eDchYNLZfDaTjrPk7cSn9iPo=
- secure: OlgbFdpSfZsyNN2utPJ3NUOUDjfx3gnrfmnYxJ/6LCB3Dp3Rfh+hrijWnHbg+GRhmaVjWYSd5iR419bn24hQToRmDaxwtfFreOdcx24D6Tak8tXWNo7I5B+lGpTG1lIdGWrOPW8w4MvcQY76HbbaHBZoA7dDiYtLI30uu8LgZ2Y=
- secure: Z2+1Ln/B3LcuYgrF05qwF32BZzypRwCMgEXZPNtPbI7vL6qYbn5MCg2uv023OjvDd58LPwtwwaBxggLomoexiq1YHafadMXPiT6zGCeY72jHTQlVQN17WHYLfJ/1qzyKKsoFhN5Aqcved8cC1cOl8LoaqFUxswVLNdCXEGAbEhc=
matrix:
include:
- env: BUILD_DOCS=true
Expand Down
329 changes: 328 additions & 1 deletion docs/changelog.rst

Large diffs are not rendered by default.

131 changes: 127 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
'use strict';

var async = require('async');
var path = require('path');
var stormpath = require('stormpath');
var stormpathConfig = require('stormpath-config');
var uuid = require('uuid');
var configStrategy = stormpathConfig.strategy;

function DefaultJwksCacheManager(defaultJwksCacheManagerConfig) {
defaultJwksCacheManagerConfig = defaultJwksCacheManagerConfig || {};
this.ttl = defaultJwksCacheManagerConfig.ttl;
this.jwks = null;
}
DefaultJwksCacheManager.prototype.getJwks = function getJwks() {
var now = new Date().getTime();
if (now > (this.lastSet + this.ttl)) {
this.jwks = null;
}

return this.jwks;
};
DefaultJwksCacheManager.prototype.setJwks = function setJwks(jwks) {
this.lastSet = new Date().getTime();
this.jwks = jwks;
};


// Factory method to create a client using a configuration only.
// The configuration provided to this factory is the final configuration.
function ClientFactory(config) {
Expand All @@ -14,15 +35,117 @@ function ClientFactory(config) {
])
);
}
/**
* Fetches authorization server and client configuration from Okta, requires
* an already defined okta.org and okta.applicationId
*/
function OktaConfigurationStrategy() {

}
OktaConfigurationStrategy.prototype.process = function process(config, callback) {
var client = new ClientFactory(config);
var applicationCredentialsResourceUrl = '/internal/apps/' + config.application.id + '/settings/clientcreds';

async.parallel({
applicationResource: client.getApplication.bind(client, '/apps/' + config.application.id),
applicationCredentialsResource: client.getResource.bind(client, applicationCredentialsResourceUrl),
idps: client.getResource.bind(client, '/idps')
}, function (err, results) {

if (err) {
return callback(err);
}

/**
* Copy the authorization server ID to it's new location on the applicatin's profile object.
*/

var authServerIdAtOldLocation = results.applicationResource.settings.notifications.vpn.message;
var authServerIdAtNewLocation = results.applicationResource.profile && results.applicationResource.profile.forAuthorizationServerId;

config.authorizationServerId = authServerIdAtNewLocation || authServerIdAtOldLocation;

if (!authServerIdAtNewLocation) {
if (!results.applicationResource.profile) {
results.applicationResource.profile = {};
}
results.applicationResource.profile.forAuthorizationServerId = authServerIdAtOldLocation;
results.applicationResource.save(function (err) {
if (err) {
console.error(err); // eslint-disable-line no-console
}
console.log('Persisted authorization server ID to new location on application.settings'); // eslint-disable-line no-console
});
}

config.authorizationServerClientId = results.applicationCredentialsResource.client_id;
config.authorizationServerClientSecret = results.applicationCredentialsResource.client_secret;

var idps = results.idps.items.filter(function (idp) {
return ['LINKEDIN', 'FACEBOOK', 'GOOGLE'].indexOf(idp.type) > -1;
});

var idpConfiguration = idps.reduce(function (idpConfiguration, idp) {
var providerId = idp.type.toLowerCase();
var providedConfig = config.web.social[providerId] || {};

var clientId = idp.protocol.credentials.client.client_id;

var redirectUri = '/callbacks/' + providerId;

var scope = providedConfig.scope || idp.protocol.scopes.join(' ');

var authorizeUriParams = {
client_id: config.authorizationServerClientId,
idp: idp.id,
response_type: 'code',
response_mode: 'query',
scope: scope,
redirect_uri: '{redirectUri}', // Leave this here for now, will be replaced when a view is requested
nonce: uuid.v4(),
state: '{state}' // Leave this here for now, will be replaced when a view is requested
};

var authorizeUri = config.org + 'oauth2/' + config.authorizationServerId + '/v1/authorize?';

authorizeUri += Object.keys(authorizeUriParams).reduce(function (queryString, param) {
return queryString += '&' + param + '=' + authorizeUriParams[param];
}, '');

idpConfiguration[providerId] = {
clientId: clientId,
clientSecret: idp.protocol.credentials.client.client_secret,
enabled: idp.status === 'ACTIVE',
providerId: providerId,
providerType: providerId,
scope: scope,
uri: redirectUri, // for back compat if custom templates are dep
redirectUri: redirectUri,
authorizeUri: authorizeUri
};

return idpConfiguration;
}, {});

config.web.social = idpConfiguration;

if (config.web.refreshTokenCookie.maxAge) {
config.web.refreshTokenCookie.maxAge = parseInt(config.web.refreshTokenCookie.maxAge, 10);
}

config.jwksCacheManager = config.jwksCacheManager || new DefaultJwksCacheManager(config.web.defaultJwksCacheManagerConfig);

callback(null, config);

});
};

module.exports = function (config) {
var configLoader = stormpath.configLoader(config);

// Load our integration config.
configLoader.prepend(new configStrategy.LoadFileConfigStrategy(path.join(__dirname, '/config.yml'), true));
configLoader.add(new configStrategy.EnrichIntegrationConfigStrategy(config));
configLoader.add(new configStrategy.EnrichClientFromRemoteConfigStrategy(ClientFactory));
configLoader.add(new configStrategy.EnrichIntegrationFromRemoteConfigStrategy(ClientFactory));
configLoader.add(new OktaConfigurationStrategy(ClientFactory));

return new stormpath.Client(configLoader);
};
};
21 changes: 16 additions & 5 deletions lib/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ web:
# this library. If not defined, we will default to /
basePath: null

# Determines how long Jwks (used for password-grant access token validation) should be cached
defaultJwksCacheManagerConfig:
ttl: 60000 # milliseconds

domainName: null # Required if using subdomain-based multi-tenancy

multiTenancy:
Expand Down Expand Up @@ -55,7 +59,10 @@ web:
domain: null

# Refresh Token Cookie has same options as the Access Token Cookie (above).
# Use the maxAge value to set the expiration time of this cookie. If not
# specified the cookie will become a session cookie
refreshTokenCookie:
maxAge: null #milliseconds
name: "refresh_token"
httpOnly: true
secure: null
Expand All @@ -69,6 +76,13 @@ web:
- application/json
- text/html

# The order of locations that getUser() will search for an access token when
# attempting to resolve the user for the request
getUser:
accessTokenSearchLocations:
- header
- cookie

register:
enabled: true
uri: "/register"
Expand Down Expand Up @@ -218,13 +232,10 @@ web:
social:
facebook:
uri: "/callbacks/facebook"
scope: "email"
github:
uri: "/callbacks/github"
scope: "user:email"
scope: "email openid profile"
google:
uri: "/callbacks/google"
scope: "email profile"
scope: "email profile openid"
linkedin:
uri: "/callbacks/linkedin"
scope: "r_basicprofile r_emailaddress"
Expand Down
57 changes: 52 additions & 5 deletions lib/controllers/change-password.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,54 @@

var forms = require('../forms');
var helpers = require('../helpers');
var oktaErrorTransformer = require('../okta/error-transformer');

/**
* Allow a user to change his password.
* Uses the AuthN API to complete a password reset workflow.
*
* Use application.sendPasswordResetEmail() to get recoveryTokenResponse
*
* @param {*} client stormpath client instance
* @param {*} recoveryTokenResource the response from /authn/recovery/password
* @param {*} newPassword new password that the end-user has provided
* @param {*} callback
*/
function resetPasswordWithRecoveryToken(client, recoveryTokenResource, newPassword, callback) {

var userHref = '/users/' + recoveryTokenResource._embedded.user.id;

client.getAccount(userHref, function (err, account) {

if (err) {
return callback(err);
}

var href = '/authn/recovery/answer';
var body = {
stateToken: recoveryTokenResource.stateToken,
answer: account.profile.stormpathMigrationRecoveryAnswer
};

client.createResource(href, body, function (err, result) {

if (err) {
return callback(err);
}

var href = '/authn/credentials/reset_password';
var body = {
stateToken: result.stateToken,
newPassword: newPassword
};

client.createResource(href, body, callback);

});
});
}

/**
* Allow a user to change their password.
*
* This can only happen if a user has reset their password, received the
* password reset email, then clicked the link in the email which redirects them
Expand All @@ -21,6 +66,7 @@ var helpers = require('../helpers');
*/
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');
var sptoken = req.query.sptoken || req.body.sptoken;
Expand All @@ -39,6 +85,7 @@ module.exports = function (req, res, next) {
application.verifyPasswordResetToken(sptoken, function (err, result) {
if (err) {
logger.info('A user attempted to reset their password with a token, but that token verification failed.');
err = oktaErrorTransformer(err);
return helpers.writeJsonError(res, err);
}

Expand All @@ -47,11 +94,10 @@ module.exports = function (req, res, next) {
return res.end();
}

result.password = req.body.password;

return result.save(function (err) {
return resetPasswordWithRecoveryToken(client, result, req.body.password, function (err) {
if (err) {
logger.info('A user attempted to reset their password, but the password change itself failed.');
err = oktaErrorTransformer(err);
return helpers.writeJsonError(res, err);
}

Expand Down Expand Up @@ -90,9 +136,10 @@ module.exports = function (req, res, next) {

result.password = form.data.password;

result.save(function (err) {
return resetPasswordWithRecoveryToken(client, result, form.data.password, function (err) {
if (err) {
logger.info('A user attempted to reset their password, but the password change itself failed.');
err = oktaErrorTransformer(err);
viewData.error = err.userMessage;
return helpers.render(req, res, view, viewData);
}
Expand Down
13 changes: 2 additions & 11 deletions lib/controllers/current-user.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use strict';

var _ = require('lodash');

var expandAccount = require('../helpers').expandAccount;
var strippedAccount = require('../helpers').strippedAccount;

function getStrippedOrganization(organization) {
return {
Expand Down Expand Up @@ -31,21 +30,13 @@ function currentUser(req, res) {
});

// All other properties, that have not been expanded, should be removed.
var strippedAccount = _.clone(expandedAccount);

Object.keys(strippedAccount).forEach(function (property) {
var expandable = !!config.web.me.expand[property];
if (strippedAccount[property] && strippedAccount[property].href && !expandable) {
delete strippedAccount[property];
}
});

var strippedOrganization = req.organization ?
getStrippedOrganization(req.organization) : null;

res.json({
organization: strippedOrganization,
account: strippedAccount
account: strippedAccount(expandedAccount, config.web.me.expand)
});
});
}
Expand Down
Loading

0 comments on commit aed8d26

Please sign in to comment.