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

Rework registration to work with Okta #611

Merged
merged 1 commit into from
Apr 7, 2017
Merged
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
58 changes: 48 additions & 10 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ configuration must be removed. In it's place you will need to configure the
following properties:

- **API Token**: similar to the Stormpath API Key, this is a secret that is used
to secure the communication with the Okta platorm.
to secure the communication with the Okta platform.
- **Application Id**: This is the Okta Application that represents your application.
The migration tool should have created this automatically for you.
- **Org**: In Stormpath you had a Tenant, and in Okta you have an Org. Every
Expand Down Expand Up @@ -52,22 +52,60 @@ Or through the following environment variables:

**Breaking Changes**

- ``req.app.get('stormpathApplication')`` will be undefined.
- Subdomain-based multi-tenancy, as introduced by version 3.2.0, will not be supported. If you are using this feature please contact [email protected] so that we can help you find a solution.

- ``req.app.get('stormpathApplication')`` will now be an Okta application, which does not have the same capabilities as a Stormpath application. We will provide more information on this in the future, likely as a changelog to the underlying Node SDK.

- Custom data properties must be declared on the Okta User Schema. If you have used the `web.register.form.fields` configuration to add custom properties to your registration form, you will need to use the Okta Admin Console to add these to the user schema. This can be found under Directory -> Profile Editor.

- Email verification has several major changes:

- You will have to send the email verification message to your users. Stormpath was able to send this email for you, but this is not yet available in Okta. We've provided a new option for you to pass an ``emailVerificationHandler``, this handler will be called when a new user registers, or when a user is asking for the verification email to be re-sent. This function is passed the account, which will have the email verification token that you need to send to the user. See example below.

- Email verification is now a global configuration, rather than a per-directory option. It must be enabled with the ``web.register.emailVerificationRequired`` option.

- The email verification token is still found on the ``account.emailVerificationToken.href`` property like before, but it no longer has a full URL in front of it. We've retained an initial forward slash in case you were using this as part of a Regular Expression when looking for the token.

Here is how the configuration for email verification should now look:

.. code-block:: javascript

app.use(stormpath.init(app, {
web: {
register: {
emailVerificationRequired: true
}
},
emailVerificationHandler: function(account) {
/**
* Drop the initial slash from the token, then append it to the verification URL
* of your application. Then send this link the user by email.
*/

var token = account.emailVerificationToken.href.slice(1);

var verificationUrl = 'http://www.example.com/verify?sptoken=' + token;

var userEmail = account.email;

var message = 'To verify you account please visit: ' + verificationUrl;

sendEmail(userEmail, message); // pseudo code, sendEmail must be provided by you
},

}));

**Potentially Breaking Changes**

- Okta uses an API Token to authenticate its API, similar to Stormpath's API Key ID/Secret. However the Okta API Tokens will expire in 30 days if they are not used. This means that if your application is not used for 30 days it will fail because the API Token will no longer be valid.

- ``req.user`` is now populated from the Okta User, which will contain a new set
of default properties that Stormpath did not have. We've copied the relevent
Okta properties onto their Stormpath counterparts (e.g. firstNamae, lastName,
and customData), however there will be new properties that did not exist before.
of default properties that Stormpath did not have. We've copied the relevant
Okta properties onto their Stormpath counterparts (e.g. ``firstName``, ``lastName``,
and ``customData``), however there will be new properties that did not exist before.
Please evaluate how you are using ``req.user`` to ensure that the new properties
won't break your code.

- ``req.user.emailVerificationStatus`` is now derived from the Okta User status.
If an account has reached the ``ACTIVE`` state, we consider `emailVerificationStatus`
to be ``VERIFIED``. This may not match any custom logic you had around Stormpath's
email verification feature. Please see the `Okta User Status`_ documentation for
more information about then new status scheme.

Version 3.2.0
-------------
Expand Down
142 changes: 106 additions & 36 deletions lib/controllers/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

var async = require('async');
var url = require('url');
var uuid = require('uuid');

var Account = require('stormpath/lib/resource/Account');
var helpers = require('../helpers');

/**
Expand Down Expand Up @@ -92,6 +94,24 @@ function applyDefaultAccountFields(stormpathConfig, req) {
}
}

/**
* Translates user creation errors into an end-user friendly userMessage
* @param {*} err
*/
function userCreationErrorMapper(err) {
if (err && err.errorCauses) {
err.errorCauses.forEach(function (cause) {
if (cause.errorSummary === 'login: An object with this field already exists in the current organization') {
err.userMessage = 'An account with that email address already exists.';
} else if (!err.userMessage) {
// This clause allows the first error cause to be returned to the user
err.userMessage = cause.errorSummary;
}
});
}
return err;
}

/**
* Register a new user -- either via a JSON API, or via a browser.
*
Expand All @@ -103,10 +123,11 @@ function applyDefaultAccountFields(stormpathConfig, req) {
*/
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 emailVerificationHandler = config.emailVerificationHandler;
var logger = req.app.get('stormpathLogger');
var view = config.web.register.view;
var accountStore = req.organization || application;

function handlePreRegistration(formData, callback) {
var preRegistrationHandler = config.preRegistrationHandler;
Expand Down Expand Up @@ -154,46 +175,63 @@ module.exports = function (req, res, next) {
}

helpers.prepAccountData(req.body, config, function (accountData) {
if (req.organization) {
accountData.accountStore = {
href: req.organization.href
};
}

accountStore.createAccount(accountData, function (err, account) {
if (err) {
return helpers.writeJsonError(res, err);
}
var oktaUser = Account.prototype.toOktaUser.call(accountData, {
customDataStrategy: config.customDataStrategy
});

if (config.web.register.autoLogin) {
var options = {
username: req.body.email,
password: req.body.password
};
var options = {
activate: true
};

if (req.organization) {
options.accountStore = req.organization.href;
}
oktaUser.profile.emailVerificationStatus = 'UNVERIFIED';
oktaUser.profile.emailVerificationToken = uuid.v4();

return helpers.authenticate(options, req, res, function (err, account, authResult) {
if (err) {
return helpers.writeJsonError(res, err);
}
if (config.web.register.emailVerificationRequired) {
options.activate = false;
}

helpers.createSession(authResult, account, req, res);
client.createUser(oktaUser, options, function (err, account) {

handleResponse(account, defaultJsonResponse);
});
if (err) {
return helpers.writeJsonError(res, userCreationErrorMapper(err));
}

helpers.expandAccount(account, config.expand, logger, function (err, expandedAccount) {
client.createResource(application.href + '/users', {
id: account.id,
scope: 'USER',
credentials: {
userName: account.email
}
}, function (err) {

if (err) {
return helpers.writeJsonError(res, err);
}

req.user = expandedAccount;
if (config.web.register.emailVerificationRequired) {
emailVerificationHandler(account);
}

if (config.web.register.autoLogin) {
var options = {
username: req.body.email,
password: req.body.password
};

handleResponse(expandedAccount, defaultJsonResponse);
return helpers.authenticate(options, req, res, function (err, account, authResult) {
if (err) {
return helpers.writeJsonError(res, err);
}

helpers.createSession(authResult, account, req, res);

handleResponse(account, defaultJsonResponse);
});
}

req.user = account;
handleResponse(account, defaultJsonResponse);
});
});
});
Expand Down Expand Up @@ -261,15 +299,49 @@ module.exports = function (req, res, next) {
};
}

accountStore.createAccount(accountData, function (err, account) {
var oktaUser = Account.prototype.toOktaUser.call(accountData, {
customDataStrategy: config.customDataStrategy
});

var options = {
activate: true
};

oktaUser.profile.emailVerificationStatus = 'UNVERIFIED';
oktaUser.profile.emailVerificationToken = uuid.v4();

if (config.web.register.emailVerificationRequired) {
options.activate = false;
}

client.createUser(oktaUser, options, function (err, account) {


if (err) {
logger.info('A user tried to create a new account, but this operation failed with an error message: ' + err.developerMessage);
callback(err);
} else {
return callback(userCreationErrorMapper(err));
}

client.createResource(application.href + '/users', {
id: account.id,
scope: 'USER',
credentials: {
userName: account.email
}
}, function (err) {
if (err) {
return callback(err);
}

if (config.web.register.emailVerificationRequired) {
emailVerificationHandler(account);
}

res.locals.user = account;
req.user = account;
callback(null, account);
}
});

});
});
}
Expand Down Expand Up @@ -303,11 +375,9 @@ module.exports = function (req, res, next) {
});
}

helpers.expandAccount(account, config.expand, logger, function (err, expandedAccount) {
req.user = expandedAccount;
req.user = account;
handleResponse(account, defaultCreatedHtmlResponse);

handleResponse(expandedAccount, defaultCreatedHtmlResponse);
});
});
break;

Expand Down
15 changes: 13 additions & 2 deletions lib/controllers/verify-email.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

var Account = require('stormpath/lib/resource/Account');
var forms = require('../forms');
var helpers = require('../helpers');

Expand All @@ -24,6 +25,7 @@ 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 emailVerificationHandler = config.emailVerificationHandler;
var logger = req.app.get('stormpathLogger');

helpers.handleAcceptRequest(req, res, {
Expand All @@ -40,7 +42,7 @@ module.exports = function (req, res, next) {
};
}

application.resendVerificationEmail(options, function (err) {
application.resendVerificationEmail(options, function (err, account) {
// Code 2016 means that an account does not exist for the given email
// address. We don't want to leak information about the account list,
// so allow this continue without error.
Expand All @@ -49,8 +51,13 @@ module.exports = function (req, res, next) {
return helpers.writeJsonError(res, err);
}

if (account) {
emailVerificationHandler(new Account(account));
}

res.end();
});

break;

case 'GET':
Expand Down Expand Up @@ -99,7 +106,7 @@ module.exports = function (req, res, next) {
};
}

application.resendVerificationEmail(options, function (err) {
application.resendVerificationEmail(options, function (err, account) {
// Code 2016 means that an account does not exist for the given email
// address. We don't want to leak information about the account
// list, so allow this continue without error.
Expand All @@ -108,6 +115,10 @@ module.exports = function (req, res, next) {
return helpers.render(req, res, view, { error: err.message, form: form });
}

if (account) {
emailVerificationHandler(new Account(account));
}

res.redirect(config.web.login.uri + '?status=unverified');
});
},
Expand Down
21 changes: 18 additions & 3 deletions lib/stormpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ module.exports.init = function (app, opts) {
config.expand.customData = true;
}

config.emailVerificationHandler = config.emailVerificationHandler || function () {};

if (config.web.register.emailVerificationRequired) {
config.web.verifyEmail.enabled = true;
}

function localsMiddleware(req, res, next) {
// Helper for getting the current URL.
res.locals.url = req.protocol + '://' + helpers.getHost(req);
Expand Down Expand Up @@ -227,9 +233,18 @@ module.exports.init = function (app, opts) {
helpers.getFormViewModel('login', config, function () {});
helpers.getFormViewModel('register', config, function () {});

// app.set('oktaApplication', results.application);
isClientReady = true;
app.emit('stormpath.ready');


client.getApplication('/apps/' + config.application.id, function (err, application) {
if (err) {
throw err;
}
app.set('stormpathApplication', application);
isClientReady = true;
app.emit('stormpath.ready');
});



});

Expand Down
Loading