home / 2017.03.19 17:30 /aws /cognito /node /react
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.
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.
Logging in to Cognito using the Cognito SDK is pretty simple. The steps are:
CognitoUser
object to authenticate the user using their passwordThis 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:
onSuccess
- straight-forward, this is where we have our tokens and we can continue to use the app as a logged in useronFailure
- no explanations necessary, print the error message somewhere or add additional ways to handle and recover from itnewPasswordRequired
- this
is what we are going to use when we are logging in a user for the first
time, it is probably used in other scenarios, like when a password
expiresmfaRequired
- this is for situations when you have multi factor authentication enabledcustomChallenge
- with some Lambdas you can create your own custom challenges for CognitoWe’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.
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.
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);
}
}
username
, password
, newPassword
and confirmNewPassword
isFirstLogin
and passwordsDoNotMatch
challenge
and cognitoUser
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')
}
}
}
render
method, we are delegating most of the form building to other methodsrenderFormFieldRow
is the
most used method; it creates a form field and associated label, wrapped
in a row, with a value taken from the component and a method reference
for handling the input change eventrenderLoginButtonRow
and renderMessageRow
are similar methods, returning table rows that either show the login button or a messagerenderNewPasswordComponents
will only render something when isFirstLogin
flag is set on the component staterenderPasswordsDoNotMatchMessage
will only render a message when the passwordsDoNotMatch
flag is setDirectly 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();
}
}
completeNewPasswordChallenge
processpasswordsDoNotMatch
flagverifyNewPassword
method just compares the new password fields and makes sure they match and are not emptyNext 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()
});
}
}
getUserPool
method will just create a new CognitoUserPool
using the properties used to create the componentgetCognitoUser
will create a
new Cognito user object, if one does not already exists; we need to use
the Cognito user object that initiated the login process if the process
is interrupted by a challenge, like resetting the passwordgetAuthenticationDetails
will create an object containing the username and password from the component stateThe 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);
}
}
getCallbacks
will return
the three callbacks required in our scenario: what happens when a login
is successful, what happens when the login fails and what happens when
we receive a newPasswordRequired
challengenewPasswordRequired
challenge, the newPasswordRequired
method will modify the state of the login component by setting the isFirstLogin
flag to true and adding the challenge object received to the challenge
state variable; next, the re-rendering of the component will show the
new password fields and it is up to users to input a new valid password
and to the login
method to answer to the challenge when the login button is pressed againnotifySuccessfulLogin
method:
loginHandler
sent to us when the login component was initialized in the main application componentloginHandler
function to which we provide the username and the id token we obtained through the login processSo, in summary, what this component does is:
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);
}
}
username
and the token
used to make calls to APIs that require authenticationhandleLoggedIn
,
to the current instance of the application; we are doing this because
we will send the method to the login component, and the handler method
will be called inside the login component, but we want the value of this
inside the handler method to point to the current instance of the application classThe 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>
}
}
}
render
method will just delegate to other more specific rendering methodsrenderLoginForm
method to also provide the loginHandler
to the LoginForm
component when we initialize itrenderMainScreen
is currently just showing a div, we will return to this laterTime 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");
app
variablebrowserify
and babelify
are used to convert and bundle our components to a javascript file that can be used to run all the component code on the clientNext, 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);
});
app
variable to respond to calls to the /bundle.js
URL with a function expecting a request and a responseSo 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);
});
data
variable is serialized into the initial-data
variableapp
and two scripts, one that contains the initial data and the second one loading the bundle.js fileFinally, 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.
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();
}
}
loadNotes
which will load the dataclass 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>
)
}
}
render
method will display a hello message and delegate to the renderNotes
methodrenderNotes
will map all notes in the state to divs that contain each note’s title and contentsrenderNotes
will also show a title value, and it is dependent on the current state of the componentLastly, 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});
}
}
loadNotes
makes a GET call to an endpoint that expects an Authorization
headersetNotes
method as callbacksetNotes
method will use the bl library to read the whole response and save that response to the state of the component within the collectResponse
methodWhen 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.
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
});
/notes
URLAnd 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.