Heads up… this article is old! For an updated version of this article, see Tutorial: Build a Basic CRUD App with Node.js on the Okta developer blog.

Update Building for mobile not web? Check out our latest tutorial Build a REST API for Your Mobile Apps Using Node.js. Also, these code examples have been updated to reflect the 3.0 release of the express-stormpath integration.

Here at Stormpath we <heart> Node.js – it’s so much fun to build with! We’ve built several libraries to help node developers achieve user management nirvana in your applications.

If you’ve built a web app before, you know that all the “user stuff” is a royal pain. Stormpath gives developers all that “user stuff” out-of-the-box so you can get on with what you really care about – your app! By the time you’re done with this tutorial (less than 15 minutes, I promise), you’ll have a fully-working Express app.

We will focus on our Express-Stormpath library to roll out a simple Express.js web application, with a complete user registration and login system, with these features:

Login and Registration pages

Password reset workflows

A profile page for your logged in users

A customizable home page

The ability to add other Stormpath features in our Express-Stormpath library (API authentication, SSO, social login, and more)

In this demo we will be using Express 4.0, we’ll discuss some of the great features of Express 4.0 as we go along. I will be using my Mac, the Terminal app, and Sublime Text for a text editor.

What is Stormpath?

Stormpath is an API service that allows developers to create, edit, and securely store

user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

In short: we make user account management a lot easier, more secure, and more

scalable than what you’re probably used to.

Ready to get started? Register for a free developer account!

Create your Node.js application

Got your Stormpath developer account? Great! Let’s get started.. vroom vroom

If you don’t already have Node.js on your system you should head over to Node.org and install it on your computer. In our examples we will be using a Mac, all commands you see should be entered in your Terminal (without the $ in front – that’s a symbol

to let you know that these are terminal commands)

Step one is to create a folder for this project and change into that directory:

$ mkdir my-webapp $ cd my-webapp 1 2 3 $ mkdir my - webapp $ cd my - webapp

Now that we are in the folder we will want to create a package.json file for this project. This file is used by Node.js to keep track of what libraries (aka modules) your project depends on. To create the file:

$ npm init 1 2 $ npm init

You will be asked a series of questions, for most of them you can just press enter to allow the default value to be used. Here is what I chose, I decided to call my main file server.js , I set my own description and set the license to MIT – everything else I just pressed enter on:

Press ^C at any time to quit. name: (my-webapp) version: (0.0.0) description: Website for my new app entry point: (index.js) server.js test command: git repository: keywords: author: license: (ISC) MIT About to write to /private/tmp/my-webapp/package.json: { "name": "my-webapp", "version": "0.0.0", "description": "Website for my new app", "main": "server.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "author": "", "license": "MIT" } Is this ok? (yes) yes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Press ^ C at any time to quit . name : ( my - webapp ) version : ( 0.0.0 ) description : Website for my new app entry point : ( index . js ) server . js test command : git repository : keywords : author : license : ( ISC ) MIT About to write to / private / tmp / my - webapp / package . json : { "name" : "my-webapp" , "version" : "0.0.0" , "description" : "Website for my new app" , "main" : "server.js" , "scripts" : { "test" : "echo " Error : no test specified " && exit 1" } , "author" : "" , "license" : "MIT" } Is this ok ? ( yes ) yes

With that I will now have a package.json file in my folder. I can take

a look at what’s in it:

$ cat package.json { "name": "my-webapp", "version": "0.0.0", "description": "Website for my new app", "main": "server.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "author": "", "license": "MIT" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cat package . json { "name" : "my-webapp" , "version" : "0.0.0" , "description" : "Website for my new app" , "main" : "server.js" , "scripts" : { "test" : "echo " Error : no test specified " && exit 1" } , "author" : "" , "license" : "MIT" }

Looks good! Now let’s install the libraries we want to use. You can install them all with this command:

$ npm i express express-stormpath cookie-parser csurf jade forms xtend body-parser --save 1 2 $ npm i express express - stormpath cookie - parser csurf jade forms xtend body - parser -- save

The save option will add this module to your dependencies in package.json. Here is what each module does:

Express.js is the web framework that everything else is built on.

Express-stormpath provides convenience features that can be tied in to the Express app, making it very easy to use Stormpath’s features in Express.

Csurf adds CSRF protection to our forms.

Cookie-Parser is used to read the cookies that are created by the Csurf library.

Forms is a module that will take the pain out of validating HTML forms.

Jade is a templating engine for writing HTML pages.

Xtend is a utility library that makes it easy to copy properties from one JavaScript object to another.

Gather your API Credentials and Application Href

The connection between your app and Stormpath is secured with “API Key Pair”. You will provide these keys to your web app and it will use them when it communicates with Stormpath. You can download your API key pair in our Admin Console. After you login you can download your API key pair from the home page, it will download the apiKey.properties file.

While you are in the Admin Console you want to get the href for your default Stormpath Application. In Stormpath, an Application object is used to link your web app to your user stores inside Stormpath. All new developer accounts have an app called “My Application”. Click on “Applications” in the Admin Console, then click on “My Application”.

For this demonstration we will export these settings to your environment, so please run these commands in your terminal:

Unix/Linux/Mac:

export STORMPATH_CLIENT_APIKEY_ID=xxxx export STORMPATH_CLIENT_APIKEY_SECRET=xxxx export STORMPATH_APPLICATION_HREF=xxxx 1 2 3 4 export STORMPATH_CLIENT_APIKEY_ID = xxxx export STORMPATH_CLIENT_APIKEY_SECRET = xxxx export STORMPATH_APPLICATION_HREF = xxxx

Windows:

set STORMPATH_CLIENT_APIKEY_ID=xxxx set STORMPATH_CLIENT_APIKEY_SECRET=xxxx set STORMPATH_APPLICATION_HREF=xxxx 1 2 3 4 set STORMPATH_CLIENT_APIKEY_ID = xxxx set STORMPATH_CLIENT_APIKEY_SECRET = xxxx set STORMPATH_APPLICATION_HREF = xxxx

Now these settings will be automatically available to our server.

Writing the application entry (server.js)

It’s time to create server.js, this will be the entry point for your server application. You can do that from Sublime Text or you can do this in the terminal:

$ touch server.js 1 2 $ touch server . js

Now open that file in Sublime Text and put the following block of code in it:

var express = require('express'); var stormpath = require('express-stormpath'); var app = express(); app.set('views', './views'); app.set('view engine', 'jade'); app.use(stormpath.init(app, { expand: { customData: true } })); app.get('/', stormpath.getUser, function(req, res) { res.render('home', { title: 'Welcome' }); }); app.on('stormpath.ready',function(){ console.log('Stormpath Ready'); }); app.listen(3000); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var express = require ( 'express' ) ; var stormpath = require ( 'express-stormpath' ) ; var app = express ( ) ; app . set ( 'views' , './views' ) ; app . set ( 'view engine' , 'jade' ) ; app . use ( stormpath . init ( app , { expand : { customData : true } } ) ) ; app . get ( '/' , stormpath . getUser , function ( req , res ) { res . render ( 'home' , { title : 'Welcome' } ) ; } ) ; app . on ( 'stormpath.ready' , function ( ) { console . log ( 'Stormpath Ready' ) ; } ) ; app . listen ( 3000 ) ;

In this example we’ve enabled auto-expansion of custom data – this will come in handy later when we build the profile page.

There are many more options that can be passed, and we won’t cover all of them in this demo. Please seee the Express-Stormpath Documentation for a full list

Create your home page

Let’s get the easy stuff out of the way: your home page. Create a views directory and then create a Jade file for the home page:

$ mkdir views $ touch views/home.jade 1 2 3 $ mkdir views $ touch views / home . jade

Now open that file in Sublime Text and put the following in it:

html head title=title link(href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css', rel='stylesheet') body div.container div.jumbotron h1 Hello! if user p Welcome, #{user.fullName} p a.small(href="profile") Edit my profile form(action='/logout', method='POST') button.btn.btn-default(type="submit") Logout else p Welcome to my app, ready to get started? p a.btn.btn-primary(href="/login") Login now p span.small Don't have an account? span a.small(href="/register") Register now 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 html head title = title link ( href = '//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css' , rel = 'stylesheet' ) body div . container div . jumbotron h1 Hello ! if user p Welcome , #{user.fullName} p a . small ( href = "profile" ) Edit my profile form ( action = '/logout' , method = 'POST' ) button . btn . btn - default ( type = "submit" ) Logout else p Welcome to my app , ready to get started ? p a . btn . btn - primary ( href = "/login" ) Login now p span . small Don ' t have an account ? span a . small ( href = "/register" ) Register now

This is a simple view that will prompt a new visitor to log in, or greet a registered user if they have already logged in.

With that… we’ve got something we can look at!

Run your app – It’s Aliiiive!

I kid you not: your application is ready to be used. Just run this command to start the server:

$ node server.js 1 2 $ node server . js

This will start your app which is now running as a web server on your computer. You can now open this link in your browser:

http://localhost:3000

You should see your home page now:

Go ahead, try it out! Create an account, you will be redirected back to the home page and shown your name. Then logout and login again, same thing! Pretty amazing, right??

Pro tip: use a file watcher

As we move forward we will be editing your server files. You will need to restart the server each time. You can kill the server by typing Ctrl + C in your Terminal. But I suggest using a “watcher” that will do this for you.

I really like the Nodemon tool. You can install it globally (it will always be ready for you!) with this command:

$ npm install -g nodemon 1 2 $ npm install - g nodemon

After installation, you can then run this command:

$ nodemon server.js 1 2 $ nodemon server . js

This will start your server and watch for any file changes. Nodemon will automatically restart your server if you change any files – sweet!

Create the profile page

A common feature of most sites is a “Dashboard” or “profile” page – a place where your visitor provide some essential information.

For example purposes, we’re going to build a profile page that allows you to collect a shipping address from your visitors. We will leverage Custom Data, one of the most powerful features of stormpath

To begin, let’s create a new view for this dashboard:

$ touch views/profile.jade 1 2 $ touch views / profile . jade

And a JavaScript file where the route handler will live:

$ touch profile.js 1 2 $ touch profile . js

Now we’ve got some copy-and-paste work to do. These two files are pretty big, so we’ll explain them after the paste.

Paste this into profile.js :

var bodyParser = require('body-parser'); var cookieParser = require('cookie-parser'); var csurf = require('csurf'); var express = require('express'); var extend = require('xtend'); var forms = require('forms'); var collectFormErrors = require('express-stormpath/lib/helpers').collectFormErrors; // Declare the schema of our form: var profileForm = forms.create({ givenName: forms.fields.string({ required: true }), surname: forms.fields.string({ required: true }), streetAddress: forms.fields.string(), city: forms.fields.string(), state: forms.fields.string(), zip: forms.fields.string() }); // A render function that will render our form and // provide the values of the fields, as well // as any situation-specific locals function renderForm(req,res,locals){ res.render('profile', extend({ title: 'My Profile', csrfToken: req.csrfToken(), givenName: req.user.givenName, surname: req.user.surname, streetAddress: req.user.customData.streetAddress, city: req.user.customData.city, state: req.user.customData.state, zip: req.user.customData.zip },locals||{})); } // Export a function which will create the // router and return it module.exports = function profile(){ var router = express.Router(); router.use(cookieParser()); router.use(bodyParser.urlencoded({ extended: true })); router.use(csurf({ cookie: true })); // Capture all requests, the form library will negotiate // between GET and POST requests router.all('/', function(req, res) { profileForm.handle(req,{ success: function(form){ // The form library calls this success method if the // form is being POSTED and does not have errors // The express-stormpath library will populate req.user, // all we have to do is set the properties that we care // about and then cal save() on the user object: req.user.givenName = form.data.givenName; req.user.surname = form.data.surname; req.user.customData.streetAddress = form.data.streetAddress; req.user.customData.city = form.data.city; req.user.customData.state = form.data.state; req.user.customData.zip = form.data.zip; req.user.customData.save(); req.user.save(function(err){ if(err){ if(err.developerMessage){ console.error(err); } renderForm(req,res,{ errors: [{ error: err.userMessage || err.message || String(err) }] }); }else{ renderForm(req,res,{ saved:true }); } }); }, error: function(form){ // The form library calls this method if the form // has validation errors. We will collect the errors // and render the form again, showing the errors // to the user renderForm(req,res,{ errors: collectFormErrors(form) }); }, empty: function(){ // The form library calls this method if the // method is GET - thus we just need to render // the form renderForm(req,res); } }); }); // This is an error handler for this router router.use(function (err, req, res, next) { // This handler catches errors for this router if (err.code === 'EBADCSRFTOKEN'){ // The csurf library is telling us that it can't // find a valid token on the form if(req.user){ // session token is invalid or expired. // render the form anyways, but tell them what happened renderForm(req,res,{ errors:[{error:'Your form has expired. Please try again.'}] }); }else{ // the user's cookies have been deleted, we dont know // their intention is - send them back to the home page res.redirect('/'); } }else{ // Let the parent app handle the error return next(err); } }); return router; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 var bodyParser = require ( 'body-parser' ) ; var cookieParser = require ( 'cookie-parser' ) ; var csurf = require ( 'csurf' ) ; var express = require ( 'express' ) ; var extend = require ( 'xtend' ) ; var forms = require ( 'forms' ) ; var collectFormErrors = require ( 'express-stormpath/lib/helpers' ) . collectFormErrors ; // Declare the schema of our form: var profileForm = forms . create ( { givenName : forms . fields . string ( { required : true } ) , surname : forms . fields . string ( { required : true } ) , streetAddress : forms . fields . string ( ) , city : forms . fields . string ( ) , state : forms . fields . string ( ) , zip : forms . fields . string ( ) } ) ; // A render function that will render our form and // provide the values of the fields, as well // as any situation-specific locals function renderForm ( req , res , locals ) { res . render ( 'profile' , extend ( { title : 'My Profile' , csrfToken : req . csrfToken ( ) , givenName : req . user . givenName , surname : req . user . surname , streetAddress : req . user . customData . streetAddress , city : req . user . customData . city , state : req . user . customData . state , zip : req . user . customData . zip } , locals | | { } ) ) ; } // Export a function which will create the // router and return it module . exports = function profile ( ) { var router = express . Router ( ) ; router . use ( cookieParser ( ) ) ; router . use ( bodyParser . urlencoded ( { extended : true } ) ) ; router . use ( csurf ( { cookie : true } ) ) ; // Capture all requests, the form library will negotiate // between GET and POST requests router . all ( '/' , function ( req , res ) { profileForm . handle ( req , { success : function ( form ) { // The form library calls this success method if the // form is being POSTED and does not have errors // The express-stormpath library will populate req.user, // all we have to do is set the properties that we care // about and then cal save() on the user object: req . user . givenName = form . data . givenName ; req . user . surname = form . data . surname ; req . user . customData . streetAddress = form . data . streetAddress ; req . user . customData . city = form . data . city ; req . user . customData . state = form . data . state ; req . user . customData . zip = form . data . zip ; req . user . customData . save ( ) ; req . user . save ( function ( err ) { if ( err ) { if ( err . developerMessage ) { console . error ( err ) ; } renderForm ( req , res , { errors : [ { error : err . userMessage | | err . message | | String ( err ) } ] } ) ; } else { renderForm ( req , res , { saved : true } ) ; } } ) ; } , error : function ( form ) { // The form library calls this method if the form // has validation errors. We will collect the errors // and render the form again, showing the errors // to the user renderForm ( req , res , { errors : collectFormErrors ( form ) } ) ; } , empty : function ( ) { // The form library calls this method if the // method is GET - thus we just need to render // the form renderForm ( req , res ) ; } } ) ; } ) ; // This is an error handler for this router router . use ( function ( err , req , res , next ) { // This handler catches errors for this router if ( err . code === 'EBADCSRFTOKEN' ) { // The csurf library is telling us that it can't // find a valid token on the form if ( req . user ) { // session token is invalid or expired. // render the form anyways, but tell them what happened renderForm ( req , res , { errors : [ { error : 'Your form has expired. Please try again.' } ] } ) ; } else { // the user's cookies have been deleted, we dont know // their intention is - send them back to the home page res . redirect ( '/' ) ; } } else { // Let the parent app handle the error return next ( err ) ; } } ) ; return router ; } ;

Paste this into profile.jade :

html head title=title link( href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css', rel='stylesheet' ) body div.container div.page-header h1 My Profile if errors each error in errors div.alert.alert-danger span #{error.error} if saved div.alert.alert-success span Your profile has been saved form.login-form.form-horizontal(method='post', role='form') input(name='_csrf', type='hidden', value=csrfToken) div.form-group label.col-sm-4 First Name div.col-sm-8 input.form-control( placeholder='Your first name', required=true, name='givenName', type='text', value=givenName) div.form-group label.col-sm-4 Last Name div.col-sm-8 input.form-control(placeholder='Your last name', required=true, name='surname', type='text', value=surname) div.form-group label.col-sm-4 Street address div.col-sm-8 input.form-control(placeholder='e.g. 123 Sunny Ave', required=true, name='streetAddress', type='text', value=streetAddress) div.form-group label.col-sm-4 City div.col-sm-8 input.form-control(placeholder='e.g. City', required=true, name='city', type='text', value=city) div.form-group label.col-sm-4 State div.col-sm-8 input.form-control(placeholder='e.g. CA', required=true, name='state', type='text', value=state) div.form-group label.col-sm-4 ZIP div.col-sm-8 input.form-control(placeholder='e.g. 94116', required=true, name='zip', type='text', value=zip) div.form-group div.col-sm-offset-4.col-sm-8 button.login.btn.btn-primary(type='submit') Save div.pull-right a(href="/") Return to home page 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 html head title = title link ( href = '//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css' , rel = 'stylesheet' ) body div . container div . page - header h1 My Profile if errors each error in errors div . alert . alert - danger span #{error.error} if saved div . alert . alert - success span Your profile has been saved form . login - form . form - horizontal ( method = 'post' , role = 'form' ) input ( name = '_csrf' , type = 'hidden' , value = csrfToken ) div . form - group label . col - sm - 4 First Name div . col - sm - 8 input . form - control ( placeholder = 'Your first name' , required = true , name = 'givenName' , type = 'text' , value = givenName ) div . form - group label . col - sm - 4 Last Name div . col - sm - 8 input . form - control ( placeholder = 'Your last name' , required = true , name = 'surname' , type = 'text' , value = surname ) div . form - group label . col - sm - 4 Street address div . col - sm - 8 input . form - control ( placeholder = 'e.g. 123 Sunny Ave' , required = true , name = 'streetAddress' , type = 'text' , value = streetAddress ) div . form - group label . col - sm - 4 City div . col - sm - 8 input . form - control ( placeholder = 'e.g. City' , required = true , name = 'city' , type = 'text' , value = city ) div . form - group label . col - sm - 4 State div . col - sm - 8 input . form - control ( placeholder = 'e.g. CA' , required = true , name = 'state' , type = 'text' , value = state ) div . form - group label . col - sm - 4 ZIP div . col - sm - 8 input . form - control ( placeholder = 'e.g. 94116' , required = true , name = 'zip' , type = 'text' , value = zip ) div . form - group div . col - sm - offset - 4.col - sm - 8 button . login . btn . btn - primary ( type = 'submit' ) Save div . pull - right a ( href = "/" ) Return to home page

Breaking down your app

You’ve just created an Express Router. Saywha? I really like how the Express maintainers have described this:

A router is an isolated instance of middleware and routes. Routers can be thought of as "mini" applications, capable only of performing middleware and routing functions. Every express application has a built-in app router. 1 2 3 4 5 A router is an isolated instance of middleware and routes . Routers can be thought of as "mini" applications , capable only of performing middleware and routing functions . Every express application has a built - in app router .

… saywha?

In my words: Express 4.0 encourages you to break up your app into “mini apps”. This makes everything much easier to understand and maintain. This is what we’ve done with the profile.js file — we’ve created a “mini app” which handles JUST the details associated with the profile page.

Don’t believe me? Read on.

Plug in your profile page

Because we followed the Router pattern, it’s now this simple to add the profile page to your existing server.js file (put it right above the call to app.on('stormpath.ready' ):

app.use('/profile',stormpath.loginRequired,require('./profile')()); 1 2 app . use ( '/profile' , stormpath . loginRequired , require ( './profile' ) ( ) ) ;

Omg. Yes. YES. You’ve just decoupled the implentation of a route from it’s addressing. Holy grail? Almost. Awesome? Most Def. (By the way, you’ve also forced authentication on this route, using Stormpath, nice!)

Restart your sever and visit /profile , you should see the form now:

Breaking down your app – for real

Okay, there’s a LOT more to talk about here. So let me cover the important points:

The profile.js file is a builder or constructor, so to speak. You have to invoke it as a method in order to get the router out of it. That’s why we have that empty () after the require('./profile') statement. Why bother? Because with this pattern you can pass in any options that may be required for this router. At the moment we don’t have any, but who knows what the future holds? Doing this give you room to use this router in multiple web apps and factor out any app-specific config.

file is a builder or constructor, so to speak. You have to invoke it as a method in order to get the router out of it. That’s why we have that empty after the statement. Why bother? Because with this pattern you can pass in any options that may be required for this router. At the moment we don’t have any, but who knows what the future holds? Doing this give you room to use this router in multiple web apps and factor out any app-specific config. We are using the forms library to create a schema for the profile form. This is a good practice because it separates the way in which we validate from the formv from the way in which the form is displayed.

library to create a schema for the profile form. This is a good practice because it separates the way in which we validate from the formv from the way in which the form is displayed. We have a renderForm function which is responsible for creating the view model of the form — this model is passed down to the Jade layer, so that profile.jade has all the properties it needs for rendering the form. This render function ensures that our template layer doesn’t blow up with missing values

function which is responsible for creating the of the form — this model is passed down to the Jade layer, so that has all the properties it needs for rendering the form. This render function ensures that our template layer doesn’t blow up with missing values We are using the Csurf library to add CSRF tokens to the form as a security measure. This is done automaticaly for the default forms (login, registration, password reset), but because this is a new, custom router, we have to setup those details manually

We reach into the Express-Stormpath library to grab our collectFormErrors function, a handy utility for pulling validation errors out of the response we get from the forms library. Note to self: PR that in to forms library!

function, a handy utility for pulling validation errors out of the response we get from the forms library. Note to self: PR that in to forms library! We make use of the loginRequired middleware to ensure that users are logged in before they can use this profile page

Wrapping it up

Alas, we’ve reached the end of this tutorial. You now have a web app that can reigster new users and allow them to provide you with a shipping address, pretty sweet right?

Following the profile example you now have everything you need to start building other pages in your application. As you build those pages, I’m sure you’ll want to take advantage of some other great features, such as:

Those are just a few of my favorites, but there is so much more!

Please read the Express-Stormpath Product Guide for details on how to implement all these amazing features — and don’t hesitate to reach out to us!

WE LOVE WEB APPS and we want your user management experience to be 10x better than you ever imagined.

-robert out

Like what you see? Follow @gostormpath to keep up with the latest releases.