home / 2017.03.19 17:30 /aws /cognito /node /react

Logging in to Amazon Cognito

This document describes how to write a simple React UI for logging in to Amazon Cognito using the AWS SDK for JavaScript. We are designing the login form to allow user login, as well as handle user password change scenario when the user is logging in for the first time.

I am approaching this from a node and react beginner point of view, so there will be sections of this document I won’t explain or explain correctly, but I am logging here my best understanding.

Setting up the project

We’ll run a node server that publishes a React UI, a single page application. First step, we need to create a new node project and add dependencies. Create a new folder, open it in a console and run the following commands:

npm init
npm install --save amazon-cognito-identity-js aws-sdk babel bl body-parser express express-react-views react react-dom
npm install --save-dev babel-cli babel-core babel-preset-env babel-preset-es2015 babel-preset-react babel-register

Those are a lot of packages, and I know very little about what they each do. What I do know is that:

The other dependencies are dev dependencies, which are test dependencies or transpilers (converting code from one format to some other format) and do not need to be packaged in a release. All the dev dependencies used here are babel related and are used to interpret the more modern EC2015 and React’s JSX and convert them to JavaScript.

The login scenario

Logging in to Cognito using the Cognito SDK is pretty simple. The steps are:

This is where it may get more complicated. If the user account is already enabled and active, and the credentials you used are correct, you will get a successful response containing some tokens that you can use to prove the user is now authenticated. But sometimes you will get a challenge instead of a successful response. This will happen when a user logs in for the first time and they are forced to change their password before proceeding. This scenario is not very well documented, but the authenticateUser method we are using to login will expect the following callbacks:

The UI

We’re writing the Login form and the whole UI of the app, in a single file (not necessary, not best practice for large apps, but this will do for now). To save you the refactoring trouble later I’ll just ask you to create a views subfolder and an index.jsx file in that subfolder and put all your code there. I will explain why in the server section.

First look at the application class

We start with dependencies at the top of the file. The imports from aws-sdk are commented out because they are no longer necessary, but may be useful in the future.

import {
    Config,
    CognitoIdentityCredentials
} from "aws-sdk";
import {
    CognitoUserPool,
    CognitoUser,
    CognitoUserAttribute,
    AuthenticationDetails
} from "amazon-cognito-identity-js";
import React from 'react';
import http from 'http';
import bl from 'bl';

This is not the old-school require(...) imports, but we’ll use those as well when we set up the server. We can use ES2015 syntax in this file because the server will compile it to plain JavaScript before sending it to the client.

I will start with a very rudimentary Application class that I will extend later in the document. The application will currently just contain a login form:

export default class Application extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div className="application">
                { this.renderLoginForm() }
            </div>
        )
    }

    renderLoginForm() {
        return (<LoginForm userPoolId={this.props.data.userPoolId} clientId={this.props.data.clientId}/>)
    }
}

The application consists of a div that contains a LoginForm component. You can see the LoginForm component expects some attributes: the user pool id and the client id.

The login component

Let’s move on to the login form. This is a large React component with a lot of methods and I will go over each of them in turn, starting with the constructor:

class LoginForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            username: "",
            password: "",
            isFirstLogin: false,
            newPassword: "",
            confirmNewPassword: "",
            passwordsDoNotMatch: false,
            challenge: null,
            cognitoUser: null
        }
        this.login = this.login.bind(this);
        this.changeUsername = this.changeUsername.bind(this);
        this.changePassword = this.changePassword.bind(this);
        this.changeNewPassword = this.changeNewPassword.bind(this);
        this.changeConfirmNewPassword = this.changeConfirmNewPassword.bind(this);

        this.newPasswordRequired = this.newPasswordRequired.bind(this);
        this.notifySuccessfulLogin = this.notifySuccessfulLogin.bind(this);
    }
}

Next we’ll look at the render method. How does the login form look like?

class LoginForm extends React.Component {

    // constructor

    render() {
        return (
            <div className="loginForm"><table><tbody>
                {this.renderMessageRow('loginForm', 'Login')}
                {this.renderFormFieldRow('username', 'text', this.state.username, this.changeUsername)}
                {this.renderFormFieldRow('password', 'password', this.state.password, this.changePassword)}
                {this.renderNewPasswordComponents()}
                {this.renderLoginButtonRow()}
            </tbody></table></div>
        )
    }

    renderFormFieldRow(label, type, value, onChange) {
        return (
            <tr key={label}>
                <td><label>{label}</label></td>
                <td><input type={type} value={value} onChange={onChange}/></td>
            </tr>
        )
    }

    renderLoginButtonRow() {
        return (
            <tr>
                <td colSpan="2"><input type="submit" onClick={this.login} value="login"/></td>
            </tr>
        )
    }

    renderMessageRow(key, message) {
        return (
            <tr key={key}>
                <td colSpan="2">{message}</td>
            </tr>
        )
    }

    renderNewPasswordComponents() {
        if (this.state.isFirstLogin) {
            return [
                renderMessageRow('new password message', 'Because this is the first time you are logging in, you will have to change your password'),
                renderFormFieldRow('new password', 'password', this.state.newPassword, this.changeNewPassword),
                renderFormFieldRow('confirm new password', 'password', this.state.confirmNewPassword, this.changeConfirmNewPassword),
                renderPasswordsDoNotMatchMessage()
            ];
        }
    }

    renderPasswordsDoNotMatchMessage() {
        if (this.state.passwordsDoNotMatch) {
            return renderMessageRow('passwordsDoNotMatchMessage', 'the new password and confirm new password fields do not match or are empty')
        }
    }

}

Directly related to the render methods are the change handlers for the input components, which only change the component state by using the setState method:

class LoginForm extends React.Component {

    // constructor

    // render methods

    changeUsername(event) {
        this.setState({username: event.target.value});
    }

    changePassword(event) {
        this.setState({password: event.target.value});
    }

    changeNewPassword(event) {
        this.setState({newPassword: event.target.value});
    }

    changeConfirmNewPassword(event) {
        this.setState({confirmNewPassword: event.target.value});
    }
}

Now we can move on to the login method:

class LoginForm extends React.Component {

    // constructor

    // render methods

    // change handlers

    login(e) {
        e.preventDefault();

        var cognitoUser = this.getCognitoUser();

        if (this.state.isFirstLogin) {
            if (this.verifyNewPassword()) {
                cognitoUser.completeNewPasswordChallenge(this.state.newPassword.trim(), this.state.challenge, this.getCallbacks())
            } else {
                this.setState({passwordsDoNotMatch: true});
            }
        } else {
            cognitoUser.authenticateUser(this.getAuthenticationDetails(), this.getCallbacks());
        }
    }

    verifyNewPassword() {
        return this.state.newPassword.trim().length > 0 &&
            this.state.newPassword.trim() === this.state.confirmNewPassword.trim();
    }
}

Next we look at the utility methods:

class LoginForm extends React.Component {

    // constructor

    // render methods

    // change handlers

    // login method

    getUserPool() {
        return new CognitoUserPool({
            UserPoolId: this.props.userPoolId,
            ClientId: this.props.clientId,
        });
    }

    getCognitoUser() {
        if (this.state.cognitoUser) {
            return this.state.cognitoUser;
        } else {
            var newCognitoUser = new CognitoUser({
                Username : this.state.username.trim(),
                Pool : this.getUserPool()
            });
            this.setState({cognitoUser: newCognitoUser});
            return newCognitoUser;
        }

    }

    getAuthenticationDetails() {
        return new AuthenticationDetails({
            Username : this.state.username.trim(),
            Password: this.state.password.trim()
        });
    }
}

The last part we need to talk about regarding the login component is how do we handle the events from the login process? We use the callbacks below:

class LoginForm extends React.Component {

    // constructor

    // render methods

    // change handlers

    // login method

    // utility methods

    getCallbacks() {
        return {
            onSuccess: this.notifySuccessfulLogin,
            onFailure: function(err) {
                alert(err);
            },
            newPasswordRequired: this.newPasswordRequired
        }
    }

    newPasswordRequired(result) {
        console.log("new password required");
        delete result.email_verified;
        this.setState({isFirstLogin: true, challenge: result});
    }

    notifySuccessfulLogin(result) {
        console.log(result.idToken.jwtToken);
        this.props.loginHandler(this.state.username.trim(), result.idToken.jwtToken);
    }
}

So, in summary, what this component does is:

Back to the application class

It’s time to go back to the application class and take a look at the actual implementation, the one that can receive a notification from the login component through a handler and then hide the login form and display a main application screen.

export default class Application extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            username: null,
            token: null
        }
        this.handleLoggedIn = this.handleLoggedIn.bind(this);
    }
}

The handleLoggedIn method will accept the username and token obtained by the login component and update the state of the application component.

export default class Application extends React.Component {
    // constructor

    handleLoggedIn(username, token) {
        this.setState({
            username: username,
            token: token
        })
    }
}

Updating the state of the application component will trigger a render of the component, and the render methods detailed below will change the UI from showing the login form to showing the main screen of the app:

export default class Application extends React.Component {
    // constructor

    // handler method

    render() {
        return (
            <div className="application">
                { this.renderLoginForm() }
                { this.renderMainScreen() }
            </div>
        )
    }

    renderLoginForm() {
        if (this.state.token == null) {
            return (<LoginForm userPoolId={this.props.data.userPoolId} clientId={this.props.data.clientId} loginHandler={this.handleLoggedIn}/>)
        }
    }

    renderMainScreen() {
        if (this.state.token) {
            return <div>MAIN SCREEN</div>
        }
    }
}

Setting up the server

Time to set up the main program script, the one that starts everything up and monitors calls from clients. This is the part where we have a lot of third-party libraries that I don’t yet understand, but I will explain things as best I can. Create a program.js file in the root folder of your project. We’ll start with dependencies:

var express = require('express');
var app = express();
var React = require('react');
var ReactDOMServer = require('react-dom/server');
var DOM = React.DOM;
var body = DOM.body;
var div = DOM.div;
var script = DOM.script;
require('babel-core');
var https = require('https');
var bl = require('bl');

var browserify = require('browserify');
var babelify = require("babelify");

Next, we are configuring the server to listen to port 8080 by default, or the first argument we send to the program. We are also setting the view engine of the server to jsx and pluggin in express-react-views to handle interpreting the views we are serving. We let the server know to load the views from the /views folder:

app.set('port', (process.argv[2] || 8080));
app.set('view engine', 'jsx');
app.set('views', __dirname + '/views');
app.engine('jsx', require('express-react-views').createEngine({transformViews: false}));

require('babel-register');

We also instantiate our Application component and we add the initial configuration to the data variable:

var Application = require('./views/index.jsx').default;
console.log(Application);

var data = {
    userPoolId: 'user pool id',
    clientId: 'client id',
    notesUrl: 'https://amazon-staging-api-url/cognitotest/notes'
};

Now let’s prepare the javascript bundle and make the express server send the bundled file when the /bundle.js URL is accessed:

app.use('/bundle.js', function (req, res) {
    res.setHeader('content-type', 'application/javascript');

    browserify({debug: true})
        .transform(babelify.configure({
            presets: ["react", "es2015"],
            compact: false
        }))
        .require("./app.js", {entry: true})
        .bundle()
        .on("error", function(err) {
            console.log(err.message);
            res.end();
        })
        .pipe(res);
});

So what is the app.js file? You will need to create this file in the root of your project and include the following in it:

import React from 'react';
import ReactDOM from 'react-dom';
import Application from './views/index.jsx';

let data = JSON.parse(document.getElementById('initial-data').getAttribute('data-json'));
ReactDOM.render(<Application data={data}/>, document.getElementById("app"));

So this app.js file is just a file importing all required dependencies to run the UI, then parsing the intial-data into an object and using that object to instantiate and render an Application component and add it to the HTML document under the element with id app.

We are using the generated bundle.js in the main application that is accessed at the root path:

app.use('/', function (req, res) {
    var initialData = JSON.stringify(data);
    var markup = ReactDOMServer.renderToString(React.createElement(Application, {data: data}));

    res.setHeader('Content-Type', 'text/html');

    var html = ReactDOMServer.renderToStaticMarkup(body(null,
        div({id: 'app', dangerouslySetInnerHTML: {__html: markup}}),
        script({
            id: 'initial-data',
            type: 'text/plain',
            'data-json': initialData
        }),
        script({src: '/bundle.js'})
    ));

    res.end(html);
});

Finally, we make the express server listen on the port we want:

app.listen(app.get('port'), function() {})

Now, all you have to do is open a shell in the main folder and run node program.js, then open a browser and navigate to localhost:8080. You should have a running application that can handle a login to Cognito scenario. We are done with the login part, but we also want to use the token to access a secured API, which we describe in the next sections.

The main app page

Right now, the main app only shows a simple text. We want our main app to access some secured data using the token we have obtained through the login process. The main screen is a pretty simple component, compared to the login component:

class MainScreen extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            title: "Your notes",
            notes: []
        }
        this.setNotes = this.setNotes.bind(this);
        this.loadNotes = this.loadNotes.bind(this);
        this.collectResponse = this.collectResponse.bind(this);
        this.loadNotes();
    }
}
class MainScreen extends React.Component {
    // constructor

    render() {
        return (
            <div>
                <p>Hello, {this.props.username}, welcome to the app!</p>
                {this.renderNotes()}
            </div>
        )
    }

    renderNotes() {
        var renderedNotes = this.state.notes.map(function(note) {
            return (
                <div key={note.title}>
                    <h2>{note.title}</h2>
                    <p>{note.contents}</p>
                </div>
            )
        })
        return (
            <div>
                <h1>{this.state.title}</h1>
                {renderedNotes}
            </div>
        )
    }
}

Lastly, let’s see how we get the notes:

class MainScreen extends React.Component {
    // constructor

    // render methods

    loadNotes() {
        var options = {
            method: 'GET',
            host: 'localhost',
            port: 8080,
            path: '/notes',
            headers: {'Authorization': this.props.token}
        };
        var req = http.request(options, this.setNotes);
        req.end();
    }

    setNotes(result) {    
        result.setEncoding('utf8');
        result.pipe(bl(this.collectResponse));
        result.on('error', console.error);
    }

    collectResponse(err, data) {
        var string = data.toString();
        var object = JSON.parse(string);
        this.setState({title: object.title, notes: object.notes});
    }
}

When the state changes, the render methods are called again to update the UI. But there is a small detail in here I need to point out. We are making a GET call to a /notes endpoint on localhost. I did this because the call is made from the client, and the client will complain if we try to access data on some different server, so for this exercise I chose to pull data from an Amazon API through the server. And I’ll show you how I do that in the next and final section.

Making a call to some service

We need to add a new mapping for our express server in the program.js folder:

app.use('/bundle.js', function (req, res) {
    // handle bundle
});

app.use('/notes', function(request, response) {
    var authorization = request.headers.authorization;
    if (authorization) {
        var options = {
            method: 'GET',
            host: 'amazon-notes-api-url',
            path: '/staging/notes',
            headers: {'Authorization': authorization}
        };
        var internalRequest = https.request(options, function(result) {
            result.setEncoding('utf8');
            result.pipe(bl(onceCollected));
            result.on('error', console.error);

            function onceCollected(err, data) {
                var string = data.toString();
                console.log(string);
                response.end(string);
            }
        });
        internalRequest.end();
    } else {
        response.end();
    }
});

app.use('/', function (req, res) {
    // handle root
});

And now we finally have a working example of logging in with a Cognito identity, obtaining a user identity token and using that token to pull data from a secured Amazon API, all through a React UI. This example ties in with the previous post. Run node program.js and test it out.