Using StormPath Authentication in Synchro

StormPath

StormPath offers an Identity as a Service solution that's very easy to integrate into Node.js applications. They handle creating and managing your user accounts, and logging users into your app/service. In this blog entry, we'll show you how to integrate StormPath into a Synchro app. Users of your native mobile app created with Synchro will be able authenticate from iOS, Android, and Win/WinPhone using just a small amount of cross-platform JavaScript.

Please review the StormPath Node.js Quickstart for an overview of StormPath integration with Node.js apps.

The following assumes you have a StormPath account set up and ready to go, and that you have the configuration information for that account close at hand.

Install StormPath

In your Synchro app directory, install the StormPath NPM module.

$ npm install stormpath --save

Create a login page

Your login page will typically be the menu/landing page of your Synchro app. It will contain login controls and any other landing/welcome information you desire, and upon successful authentication, will take the user to the main page of your app.

All of the code in the sections below will be contained in the login page Synchro module that you create.

Require and configure StormPath

var stormpath = require('stormpath');  
var apiKey = new stormpath.ApiKey(  
    process.env['STORMPATH_CLIENT_APIKEY_ID'],
    process.env['STORMPATH_CLIENT_APIKEY_SECRET']
);

var client = new stormpath.Client({ apiKey: apiKey });  
var applicationHref = process.env['STORMPATH_APPLICATION_HREF'];  

Create Your Authentication UX

Your View will contain the account credential fields for login, including username and password, and adding givenName, surname, email for new account creation. These fields will all be bound to similar fields in the ViewModel. The account creation fields will be hidden unless the users chooses the Create option (and the ViewModel create value is set to true).

You will also add buttons for Login and Create, which are bound to corresponding commands (see below).

You may choose to have two different sets of controls to show logged-in versus logged-out state (using data binding to show the correct set of controls).

exports.View = {  
    title: "App Login",
    elements:
    [
        { control: "stackpanel", orientation: "Vertical", height: "*", width: "*", contents: [
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", margin: { top: 100 }, contents: [
                { control: "text", value: "Username", width: "*", verticalAlignment: "Center" },
                { control: "edit", binding: "username", width: "*", placeholder: "username", verticalAlignment: "Center" }
            ]},
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", contents: [
                { control: "text", value: "Password", width: "*", verticalAlignment: "Center" },
                { control: "password", binding: "password", width: "*", placeholder: "password", verticalAlignment: "Center" }
            ]},   
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", visibility: "{create}", contents: [
                { control: "text", value: "First Name", width: "*", verticalAlignment: "Center" },
                { control: "edit", binding: "firstname", width: "*", placeholder: "first name", verticalAlignment: "Center" }
            ]},     
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", visibility: "{create}", contents: [
                { control: "text", value: "Last Name", width: "*", verticalAlignment: "Center" },
                { control: "edit", binding: "lastname", width: "*", placeholder: "last name", verticalAlignment: "Center" }
            ]},              
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", visibility: "{create}", contents: [
                { control: "text", value: "Email", width: "*", verticalAlignment: "Center" },
                { control: "edit", binding: "email", width: "*", placeholder: "email", verticalAlignment: "Center" }
            ]},               
            { control: "stackpanel", orientation: "Horizontal", width: "400", horizontalAlignment: "Center", contents: [
                { control: "button", caption: "Login", width: "*", binding: "login" },
                { control: "button", caption: "Create", width: "*", binding: "create" }   
            ]}                         
        ]}
    ]
};

exports.InitializeViewModel = function(context, session) {  
    var viewModel =
    {
        username: "",
        password: "",
        email: "",
        firstname: "",
        lastname: "",
        create: false
    }
    return viewModel;
};

Add Commands for Login and Create Account

The code below shows how you can call the StormPath API to authenticate a user, or to create a new user account. In both cases, the only status/error reporting (to the end user) comes from the StormPath API (their messages are nicely formatted, and end-user ready).

exports.Commands = {  
    login: function* (context, session, viewModel) {
        var authRequest = {
            username: viewModel.username,
            password: viewModel.password
        };

        var application = yield Synchro.yieldAwaitable(context, function(callback) { 
            client.getApplication(applicationHref, callback); 
        });

        try {
            var result = yield Synchro.yieldAwaitable(context, function(callback) {
                application.authenticateAccount(authRequest, callback); 
            });
            var account = yield Synchro.yieldAwaitable(context, function(callback) { 
                result.getAccount(callback); 
            });
            session.account = account;
            Synchro.pushAndNavigateTo(context, "yourMainPage");
        } catch (err) {
            var messageBox = { title: "Login Error", message: err.userMessage, options: [{ label: "Ok" }] };
            Synchro.showMessage(context, messageBox);
        }
    },
    create: function* (context, session, viewModel) {
        viewModel.create = true;

        var account = {
            givenName: viewModel.firstname,
            surname: viewModel.lastname,
            username: viewModel.username,
            email: viewModel.email,
            password: viewModel.password
        };

        var application = yield Synchro.yieldAwaitable(context, function(callback) { 
            client.getApplication(applicationHref, callback); 
        });

        try {
            var account = yield Synchro.yieldAwaitable(context, function(callback) { 
                application.createAccount(account, callback); 
            });
            session.account = account;
            Synchro.pushAndNavigateTo(context, "yourMainPage");
        } catch (err) {
            var messageBox = { title: "Create Account Error", message: err.userMessage, options: [{ label: "Ok" }] };
            Synchro.showMessage(context, messageBox);
        }
    }
};

That's it! You have a fully working user authentication and account management facility using StormPath!

Further Exercises

Depending on how your app is structured, you may want to consider installing an authentication hook using our Application Hooks facility. Here is an example of what that might look like:

exports.BeforeInitializeViewModel = function(route, routeModule, context, session, params, state) {  
    if (!session.account) {
        Synchro.navigateTo(context, "login");
    }
};

That will check your authentication status on every page, and if not authenticated, will direct you to the login page to authenticate. This is not strictly necessary unless your login can expire or otherwise be invalidated after it is initially established (time based expiration/logout, account revocation, etc).