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

Commit aed8d26

Browse files
authored
Merge pull request #622 from stormpath/4.0.0
Release 4.0.0
2 parents a1dac39 + e4b3125 commit aed8d26

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2233
-1528
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ docs/_build
33
node_modules
44
.coveralls.yml
55
*.log
6-
.env
6+
.env
7+
.vscode
8+
.DS_Store

.travis.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
language: node_js
22
sudo: false
33
node_js:
4-
- '0.10'
5-
- '0.12'
64
- '4'
75
- '6'
86
install:
@@ -35,6 +33,11 @@ env:
3533
global:
3634
- secure: M4UcXsvhjYXupsD0D+bAWdopk9JjOxKnHi5OcqCSZ4lCn6XZ3KlxMQC/VM8Ryhe3Y5dAcNnJOJSE0eeK+ojZERY2UcozYSjEIdTZjGDI1L4HM538GeRz8wLEIG18gRdSjsyVGjAOed+eZ/MQwo2xyIjVPjJBFJuy2Djtt4fi1sw=
3735
- secure: LC2v9W0Nx8oviv+AajnXV1DOaiht+/1Hy92loa4a4Lbwz/NW0DmM9k1ollBFHpBhWVai3HaZKPl0XqdHqdq1fA2HppdJv8YbT/Hwkj1WdX+LRE/VUNevVc+MszlDJq1FOrgaEjiKyBqQP07RkZl4tg2kwpIET2Q0zmCubE7pezo=
36+
- secure: Uiqmlmp4O9noUngjFvl7hws+LpaHvy3lcwAAg4ecEotyE0T4b+kd1tvN5zSA2VEohPy/ynQK8zjevHqNoQzc4rUxTUsCijk1fnyOqrowvYrnSC0jwjkp4FaDaPk6HyEhGA6N37hl3km38iD09eqLwa4qA7SYgZEi5qRXLhnQxjo=
37+
- secure: h85DxO0pgB+tnszJIwfd/BDtmi1NKVfNuZertF3jitdPFFktlqF2ecDiAqgINpI4lNiJYac8eF8isTZn2Unh2K2SyElkZZfmhifz8n5MHqywidNYQKDz8BMBGELOxNQVkPgevPR5VU1eZoYUMDRgetC6hCp3iaRGoF6Xv+kAXl4=
38+
- secure: mBl0PWZag9Ji9jAYOEhCc9uORN9sGbbHD1IBkJD3kZ++tpMJO33ZUDHQix5BRSohhPgVqDm79XHpJiJ8PSIkQfvyMZNlWjGKqd/J9bdZbxSfO6+PdcSwS38Sr4hQsZyhYXhmragKzJI10vWBVJ/eDchYNLZfDaTjrPk7cSn9iPo=
39+
- secure: OlgbFdpSfZsyNN2utPJ3NUOUDjfx3gnrfmnYxJ/6LCB3Dp3Rfh+hrijWnHbg+GRhmaVjWYSd5iR419bn24hQToRmDaxwtfFreOdcx24D6Tak8tXWNo7I5B+lGpTG1lIdGWrOPW8w4MvcQY76HbbaHBZoA7dDiYtLI30uu8LgZ2Y=
40+
- secure: Z2+1Ln/B3LcuYgrF05qwF32BZzypRwCMgEXZPNtPbI7vL6qYbn5MCg2uv023OjvDd58LPwtwwaBxggLomoexiq1YHafadMXPiT6zGCeY72jHTQlVQN17WHYLfJ/1qzyKKsoFhN5Aqcved8cC1cOl8LoaqFUxswVLNdCXEGAbEhc=
3841
matrix:
3942
include:
4043
- env: BUILD_DOCS=true

docs/changelog.rst

Lines changed: 328 additions & 1 deletion
Large diffs are not rendered by default.

lib/client.js

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
'use strict';
22

3+
var async = require('async');
34
var path = require('path');
45
var stormpath = require('stormpath');
56
var stormpathConfig = require('stormpath-config');
7+
var uuid = require('uuid');
68
var configStrategy = stormpathConfig.strategy;
79

10+
function DefaultJwksCacheManager(defaultJwksCacheManagerConfig) {
11+
defaultJwksCacheManagerConfig = defaultJwksCacheManagerConfig || {};
12+
this.ttl = defaultJwksCacheManagerConfig.ttl;
13+
this.jwks = null;
14+
}
15+
DefaultJwksCacheManager.prototype.getJwks = function getJwks() {
16+
var now = new Date().getTime();
17+
if (now > (this.lastSet + this.ttl)) {
18+
this.jwks = null;
19+
}
20+
21+
return this.jwks;
22+
};
23+
DefaultJwksCacheManager.prototype.setJwks = function setJwks(jwks) {
24+
this.lastSet = new Date().getTime();
25+
this.jwks = jwks;
26+
};
27+
28+
829
// Factory method to create a client using a configuration only.
930
// The configuration provided to this factory is the final configuration.
1031
function ClientFactory(config) {
@@ -14,15 +35,117 @@ function ClientFactory(config) {
1435
])
1536
);
1637
}
38+
/**
39+
* Fetches authorization server and client configuration from Okta, requires
40+
* an already defined okta.org and okta.applicationId
41+
*/
42+
function OktaConfigurationStrategy() {
43+
44+
}
45+
OktaConfigurationStrategy.prototype.process = function process(config, callback) {
46+
var client = new ClientFactory(config);
47+
var applicationCredentialsResourceUrl = '/internal/apps/' + config.application.id + '/settings/clientcreds';
48+
49+
async.parallel({
50+
applicationResource: client.getApplication.bind(client, '/apps/' + config.application.id),
51+
applicationCredentialsResource: client.getResource.bind(client, applicationCredentialsResourceUrl),
52+
idps: client.getResource.bind(client, '/idps')
53+
}, function (err, results) {
54+
55+
if (err) {
56+
return callback(err);
57+
}
58+
59+
/**
60+
* Copy the authorization server ID to it's new location on the applicatin's profile object.
61+
*/
62+
63+
var authServerIdAtOldLocation = results.applicationResource.settings.notifications.vpn.message;
64+
var authServerIdAtNewLocation = results.applicationResource.profile && results.applicationResource.profile.forAuthorizationServerId;
65+
66+
config.authorizationServerId = authServerIdAtNewLocation || authServerIdAtOldLocation;
67+
68+
if (!authServerIdAtNewLocation) {
69+
if (!results.applicationResource.profile) {
70+
results.applicationResource.profile = {};
71+
}
72+
results.applicationResource.profile.forAuthorizationServerId = authServerIdAtOldLocation;
73+
results.applicationResource.save(function (err) {
74+
if (err) {
75+
console.error(err); // eslint-disable-line no-console
76+
}
77+
console.log('Persisted authorization server ID to new location on application.settings'); // eslint-disable-line no-console
78+
});
79+
}
80+
81+
config.authorizationServerClientId = results.applicationCredentialsResource.client_id;
82+
config.authorizationServerClientSecret = results.applicationCredentialsResource.client_secret;
83+
84+
var idps = results.idps.items.filter(function (idp) {
85+
return ['LINKEDIN', 'FACEBOOK', 'GOOGLE'].indexOf(idp.type) > -1;
86+
});
87+
88+
var idpConfiguration = idps.reduce(function (idpConfiguration, idp) {
89+
var providerId = idp.type.toLowerCase();
90+
var providedConfig = config.web.social[providerId] || {};
91+
92+
var clientId = idp.protocol.credentials.client.client_id;
93+
94+
var redirectUri = '/callbacks/' + providerId;
95+
96+
var scope = providedConfig.scope || idp.protocol.scopes.join(' ');
97+
98+
var authorizeUriParams = {
99+
client_id: config.authorizationServerClientId,
100+
idp: idp.id,
101+
response_type: 'code',
102+
response_mode: 'query',
103+
scope: scope,
104+
redirect_uri: '{redirectUri}', // Leave this here for now, will be replaced when a view is requested
105+
nonce: uuid.v4(),
106+
state: '{state}' // Leave this here for now, will be replaced when a view is requested
107+
};
108+
109+
var authorizeUri = config.org + 'oauth2/' + config.authorizationServerId + '/v1/authorize?';
110+
111+
authorizeUri += Object.keys(authorizeUriParams).reduce(function (queryString, param) {
112+
return queryString += '&' + param + '=' + authorizeUriParams[param];
113+
}, '');
114+
115+
idpConfiguration[providerId] = {
116+
clientId: clientId,
117+
clientSecret: idp.protocol.credentials.client.client_secret,
118+
enabled: idp.status === 'ACTIVE',
119+
providerId: providerId,
120+
providerType: providerId,
121+
scope: scope,
122+
uri: redirectUri, // for back compat if custom templates are dep
123+
redirectUri: redirectUri,
124+
authorizeUri: authorizeUri
125+
};
126+
127+
return idpConfiguration;
128+
}, {});
129+
130+
config.web.social = idpConfiguration;
131+
132+
if (config.web.refreshTokenCookie.maxAge) {
133+
config.web.refreshTokenCookie.maxAge = parseInt(config.web.refreshTokenCookie.maxAge, 10);
134+
}
135+
136+
config.jwksCacheManager = config.jwksCacheManager || new DefaultJwksCacheManager(config.web.defaultJwksCacheManagerConfig);
137+
138+
callback(null, config);
139+
140+
});
141+
};
17142

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

21146
// Load our integration config.
22147
configLoader.prepend(new configStrategy.LoadFileConfigStrategy(path.join(__dirname, '/config.yml'), true));
23-
configLoader.add(new configStrategy.EnrichIntegrationConfigStrategy(config));
24-
configLoader.add(new configStrategy.EnrichClientFromRemoteConfigStrategy(ClientFactory));
25-
configLoader.add(new configStrategy.EnrichIntegrationFromRemoteConfigStrategy(ClientFactory));
148+
configLoader.add(new OktaConfigurationStrategy(ClientFactory));
26149

27150
return new stormpath.Client(configLoader);
28-
};
151+
};

lib/config.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ web:
44
# this library. If not defined, we will default to /
55
basePath: null
66

7+
# Determines how long Jwks (used for password-grant access token validation) should be cached
8+
defaultJwksCacheManagerConfig:
9+
ttl: 60000 # milliseconds
10+
711
domainName: null # Required if using subdomain-based multi-tenancy
812

913
multiTenancy:
@@ -55,7 +59,10 @@ web:
5559
domain: null
5660

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

79+
# The order of locations that getUser() will search for an access token when
80+
# attempting to resolve the user for the request
81+
getUser:
82+
accessTokenSearchLocations:
83+
- header
84+
- cookie
85+
7286
register:
7387
enabled: true
7488
uri: "/register"
@@ -218,13 +232,10 @@ web:
218232
social:
219233
facebook:
220234
uri: "/callbacks/facebook"
221-
scope: "email"
222-
github:
223-
uri: "/callbacks/github"
224-
scope: "user:email"
235+
scope: "email openid profile"
225236
google:
226237
uri: "/callbacks/google"
227-
scope: "email profile"
238+
scope: "email profile openid"
228239
linkedin:
229240
uri: "/callbacks/linkedin"
230241
scope: "r_basicprofile r_emailaddress"

lib/controllers/change-password.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,54 @@
22

33
var forms = require('../forms');
44
var helpers = require('../helpers');
5+
var oktaErrorTransformer = require('../okta/error-transformer');
56

67
/**
7-
* Allow a user to change his password.
8+
* Uses the AuthN API to complete a password reset workflow.
9+
*
10+
* Use application.sendPasswordResetEmail() to get recoveryTokenResponse
11+
*
12+
* @param {*} client stormpath client instance
13+
* @param {*} recoveryTokenResource the response from /authn/recovery/password
14+
* @param {*} newPassword new password that the end-user has provided
15+
* @param {*} callback
16+
*/
17+
function resetPasswordWithRecoveryToken(client, recoveryTokenResource, newPassword, callback) {
18+
19+
var userHref = '/users/' + recoveryTokenResource._embedded.user.id;
20+
21+
client.getAccount(userHref, function (err, account) {
22+
23+
if (err) {
24+
return callback(err);
25+
}
26+
27+
var href = '/authn/recovery/answer';
28+
var body = {
29+
stateToken: recoveryTokenResource.stateToken,
30+
answer: account.profile.stormpathMigrationRecoveryAnswer
31+
};
32+
33+
client.createResource(href, body, function (err, result) {
34+
35+
if (err) {
36+
return callback(err);
37+
}
38+
39+
var href = '/authn/credentials/reset_password';
40+
var body = {
41+
stateToken: result.stateToken,
42+
newPassword: newPassword
43+
};
44+
45+
client.createResource(href, body, callback);
46+
47+
});
48+
});
49+
}
50+
51+
/**
52+
* Allow a user to change their password.
853
*
954
* This can only happen if a user has reset their password, received the
1055
* password reset email, then clicked the link in the email which redirects them
@@ -21,6 +66,7 @@ var helpers = require('../helpers');
2166
*/
2267
module.exports = function (req, res, next) {
2368
var application = req.app.get('stormpathApplication');
69+
var client = req.app.get('stormpathClient');
2470
var config = req.app.get('stormpathConfig');
2571
var logger = req.app.get('stormpathLogger');
2672
var sptoken = req.query.sptoken || req.body.sptoken;
@@ -39,6 +85,7 @@ module.exports = function (req, res, next) {
3985
application.verifyPasswordResetToken(sptoken, function (err, result) {
4086
if (err) {
4187
logger.info('A user attempted to reset their password with a token, but that token verification failed.');
88+
err = oktaErrorTransformer(err);
4289
return helpers.writeJsonError(res, err);
4390
}
4491

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

50-
result.password = req.body.password;
51-
52-
return result.save(function (err) {
97+
return resetPasswordWithRecoveryToken(client, result, req.body.password, function (err) {
5398
if (err) {
5499
logger.info('A user attempted to reset their password, but the password change itself failed.');
100+
err = oktaErrorTransformer(err);
55101
return helpers.writeJsonError(res, err);
56102
}
57103

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

91137
result.password = form.data.password;
92138

93-
result.save(function (err) {
139+
return resetPasswordWithRecoveryToken(client, result, form.data.password, function (err) {
94140
if (err) {
95141
logger.info('A user attempted to reset their password, but the password change itself failed.');
142+
err = oktaErrorTransformer(err);
96143
viewData.error = err.userMessage;
97144
return helpers.render(req, res, view, viewData);
98145
}

lib/controllers/current-user.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use strict';
22

3-
var _ = require('lodash');
4-
53
var expandAccount = require('../helpers').expandAccount;
4+
var strippedAccount = require('../helpers').strippedAccount;
65

76
function getStrippedOrganization(organization) {
87
return {
@@ -31,21 +30,13 @@ function currentUser(req, res) {
3130
});
3231

3332
// All other properties, that have not been expanded, should be removed.
34-
var strippedAccount = _.clone(expandedAccount);
35-
36-
Object.keys(strippedAccount).forEach(function (property) {
37-
var expandable = !!config.web.me.expand[property];
38-
if (strippedAccount[property] && strippedAccount[property].href && !expandable) {
39-
delete strippedAccount[property];
40-
}
41-
});
4233

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

4637
res.json({
4738
organization: strippedOrganization,
48-
account: strippedAccount
39+
account: strippedAccount(expandedAccount, config.web.me.expand)
4940
});
5041
});
5142
}

0 commit comments

Comments
 (0)