Use React Native to post to secure AWS API Gateway endpoint

I am setting up a React Native application that will interface with an authenticated API hosted by AWS API Gateway. Here is how I set up my API to be secured through authentication. I am not sure that this will be used in production, but it is working well for testing.

This post will go over the following:

  1. Setting up a very simple React Native application
  2. Adding a simple button that will later be used to get data from an endpoint
  3. Using the react-native-dotenv module for environment set up
  4. Using the react-native-aws-signature module for authorization
  5. Debugging with react-native-aws-signature

Here is the code for this example on githhub

Setting up a very simple React Native application

Start with a brand new react-native application. To set one up, run:

[~] $ react-native init SampleProject
[~] $ cd SampleProject
[~/SampleProject] $ react-native run-ios

You should get something in the simulator that looks like this:

Adding a simple button that will later be used to get data from an endpoint

In the index.ios.js file, add Button to the imports:

import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  Button
} from 'react-native';

Replace the existing SampleProject Component with this:

export default class SampleProject extends Component {
  constructor(props){
    super(props)

    this.state = {
      textToDisplay: 'no text yet' //state value that will display API response
    }
  }

  // Action that is called when button is pressed
  retrieveData() {
    this.setState({textToDisplay: "button pressed"})
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>

        <Button
          onPress={() => this.retrieveData()}
          title="API request"
          color="#841584"
        />

        <Text>
          {this.state.textToDisplay}
        </Text>

      </View>
    );
  }
}

Reloading in the simulator should give you something like this:

If you press the ‘API Request’ link, you should get this:

Using the react-native-dotenv module for environment set up

In a production mobile application, you don’t want to save secret API keys anywhere in the code because it can be reverse engineered. There is a SO post here about it.

That being said, if you are only installing the app on your phone during the testing phase, it is probably fine.

The official react-native-dotenv instructions are here, but this is what I did to set it up.

First, install the module

npm install react-native-dotenv --save-dev

Add the react-native-dotenv preset to your .babelrc file at the project root.

{
  "presets": ["react-native", "react-native-dotenv"]
}

Create a .env file in your project root directory with your AWS credentials and the host.

# DO NOT use secret keys anywhere in your compiled code, even in .env files.
# You should use another method of authorization when this product goes to production
AWS_KEY=your key here
AWS_SECRET_KEY=your secret key here
AWS_REGION=us-west-2
API_STAGE=your api stage name here, mine is test
HOST=your host here, do not include the protocol (http:// or https://)

Now, let’s set up a really simple class that we will use to interface with our API. This should be at the same level as index.ios.js, and mine is called called SampleApi.js.

import { AWS_KEY, AWS_SECRET_KEY, HOST, AWS_REGION, API_STAGE} from 'react-native-dotenv'

class sampleApi {
  static get() {
    // Just return the host value to make sure our .env is working
    return HOST
  }
}

export default sampleApi

Then, somewhere near the top of index.ios.js, import the new class:

import sampleApi from "./SampleApi"

Replace the retrieveData function with:

retrieveData() {
  this.setState({textToDisplay: sampleApi.get()})
}

Our full index.ios.js should now look like:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  Button
} from 'react-native';

import sampleApi from "./SampleApi"

export default class SampleProject extends Component {
  constructor(props){
    super(props)

    this.state = {
      textToDisplay: "not set" // state value that will display API response
    }
  }

  // Action that is called when button is pressed
  retrieveData() {
    this.setState({textToDisplay: sampleApi.get()})
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>

        <Button
          onPress={() => this.retrieveData()}
          title="API request"
          color="#841584"
        />

        <Text>
          {this.state.textToDisplay}
        </Text>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('SampleProject', () => SampleProject);

Note: if you change the .env file only, the simulator will not recognize the change and your changes will not take affect.

Using the react-native-aws-signature module for authorization

Now, we want to actually hit the API when the button is pressed. Start by installing the react-native-aws-signature module

npm install react-native-aws-signature --save

In SampleApi.js, add the import for AWSSignature:

import AWSSignature from 'react-native-aws-signature'

Remove the contents of the get() method in SampleApi.js and start by setting up some variables based on the .env file:

static get() {
  const verb = 'get'
  // construct the url and path for our sample API
  const path = '/' + API_STAGE + '/pets'
  const url = 'https://' + HOST + path

  let credentials = {
    AccessKeyId: AWS_KEY,
    SecretKey: AWS_SECRET_KEY
  }
}

Next, set up the header and options. These will be used to generate the authorization details and they will be used in the request to the API.

let auth_date = new Date();

  let auth_header = {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'dataType': 'json',
    'X-Amz-Date': auth_date.toISOString(),
    'host': HOST
  }

  let auth_options = {
    path: path,
    method: verb,
    service: 'execute-api',
    headers: auth_header,
    region: AWS_REGION,
    body: '',
    credentials
  };

Then, create a new AWSSignature object and call setParams. This will generate the authorization header, which we retrieve in the next bit of code:

  let awsSignature = new AWSSignature();
  awsSignature.setParams(auth_options);

Now, retrieve the authorization information and append it to our header.

  const authorization = awsSignature.getAuthorizationHeader();

  // Add the authorization to the header
  auth_header['Authorization'] = authorization['Authorization']

Finally, make the request to the API using the header we just created. We are expecting json back, and I have included some basic error checking.

let options = Object.assign({
  method: verb,
  headers: auth_header
});

return fetch(url, options).then( resp => {
  let json = resp.json();
  if (resp.ok) {
    return json
  }
  return json.then(err => {throw err});
})

Here is what the SampleApi.js file should now look like:

import AWSSignature from 'react-native-aws-signature'
import { AWS_KEY, AWS_SECRET_KEY, HOST, AWS_REGION, API_STAGE} from 'react-native-dotenv'

class sampleApi {

  static get() {
    const verb = 'get'
    // construct the url and path for our sample API
    const path = '/' + API_STAGE + '/pets'
    const url = 'https://' + HOST + path

    let credentials = {
      AccessKeyId: AWS_KEY,
      SecretKey: AWS_SECRET_KEY
    }

    let auth_date = new Date();

    let auth_header = {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'dataType': 'json',
      'X-Amz-Date': auth_date.toISOString(),
      'host': HOST
    }

    let auth_options = {
      path: path,
      method: verb,
      service: 'execute-api',
      headers: auth_header,
      region: AWS_REGION,
      body: '',
      credentials
    };

    let awsSignature = new AWSSignature();
    awsSignature.setParams(auth_options);

    const authorization = awsSignature.getAuthorizationHeader();

    // Add the authorization to the header
    auth_header['Authorization'] = authorization['Authorization']

    let options = Object.assign({
      method: verb,
      headers: auth_header
    });

    return fetch(url, options).then( resp => {
      let json = resp.json();
      if (resp.ok) {
        return json
      }
      return json.then(err => {throw err});
    })
  }
}

export default sampleApi

Modify index.ios.js to set the state to include the return value of the request. Since we are getting a json array back, we have to loop through it to make a readable text block:

// Action that is called when button is pressed
retrieveData() {
  sampleApi.get().then(resp => {
    tempText = ""
    // we will get an array back, so loop through it
    resp.forEach(function(pet) {
      tempText += JSON.stringify(pet) + "\n"
    })

    // update our state to include the new text  
    this.setState({textToDisplay: tempText})
  })
}

After you refresh the simulator, you should be able to press the button and receive a screen that looks something like this:

Debugging with react-native-aws-signature

This AWS troubleshooting guide is helpful, but react-native-aws-signature does most of the work for you, so it can be difficult to determine where your mistakes are.

I got this error when I was including the https:// at the beginning of the host parameter in the header. The full error includes what AWS was expecting for the ‘canonical string’ and the ‘string to sign’.

The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method.

I figured out how to fix the issue by using the getCanonicalString() and getStringToSign() methods.

var awsSignature = new AWSSignature();

// Set up the params here as described above

console.log("canonical string")
console.log(awsSignature.getCanonicalString())
console.log("string to sign")
console.log(awsSignature.getStringToSign())

Using Amazon Web Services to create a serverless web app

Amazon Web Services (AWS) provides resources that can take the place of a traditional webserver. A huge advantage to this approach is removing the need for the developer to maintain servers, and to allow for easy scaling depending upon use.

In my project, I want to set up mobile app that will use AWS as a backend. I plan on using the following services:

  • Lambda – allows you to run arbitrary code without setting up a server
  • Relational Database Service (RDS) – hosted database. I will be using postgresql
  • Step Functions – Connect services together using visual workflows
  • API Gateway – Create API endpoints to allow your backend to be used outside of AWS

Other technology I will be using

  • Node.js – I am new to JavaScript and want to learn more about it
  • PostgreSQL
  • Bookshelf.js/Knex – I want a well supported ORM that supports PostgreSQL. Bookshelf.js seems robust and since it is built on top of Knex, I can always fall back to Knex for unsupported functionality.
  • node-lambda – Make it easier to manage AWS Lambda deployment

Here is an outline of my steps

  • Set up PostgreSQL and Bookshelf.js
  • Set up hosted database using RDS
  • Create simple lambda that reads from RDS
  • Integrate API Gateway with Lambda
  • Connect 2 Lambdas together with Step Functions
  • Integrate API Gateway with Step Functions

Set up Bookshelf.js for Node.js

Intro

I am going to walk through setting up a brand new Node.js project using Bookshelf and PostgreSQL. At the end, we will have a simple Node.js project that can insert and retrieve data from a database.

Here is what this post will cover

  • Set up a new Node.js project
  • Install necessary libraries to interface with PostgreSQL database
  • Set up process to create a database table
  • Use Bookshelf to
    • insert data
    • retrieve data

The code for this post can be found here.

Why Bookshelf.js?

Bookshelf.js is an Object Relational Mapping (ORM) for Node.js that allows developers to more easily interface with a relational database. It allows the dev to focus on higher level functions, rather than low level queries. It takes care of some basic attribute mapping to make data models simpler. It also allows for the same code to be used against different relational databases in case you want to change the underlying database.

I chose Bookshelf.js because it is well maintained, well documented, and it is built on Knex.js, which is a SQL query building framework. I can use Knex for queries that are not easily supported in Bookshelf.

Install necessary software

Node.js

I am using Node.js 4.3.2 because 4.3 is what is supported in AWS Lambda. If you don’t have node installed, follow these instructions.

[~]$ node -v
v4.3.2

PostgreSQL

Instructions on how to install PostgreSQL. I use Postico to easily interface with the PostgreSQL.

Before we go on, start the database.

[~]$ postgres -D /usr/local/var/postgres

Set up a new Node.js project

Set up the project directory and initialize the project. When you run npm init, it will prompt you to answer some questions, you can use the default values or set your own, at this point they don’t matter too much.

[~]$ mkdir aws_walkthrough
[~]$ cd aws_walkthrough
[~/aws_walkthrough]$ npm init

Install the node modules that we will need, the -save option will add the modules to the package.json file.

[~/aws_walkthrough]$ npm install knex bookshelf pg dotenv -save

Here is what each module does

  • bookshelf – the ORM we will be using
  • knex – required by bookshelf, but in the future it may not be included automatically
  • pg – the postgresql module
  • dotenv – a utility that makes dealing with environment variables easy

Here is our current package.json

{
 "name": "aws_walkthrough",
 "version": "0.1.0",
 "description": "A sample project using AWS",
 "main": "index.js",
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "Jake Sparling",
 "repository" : {
   "type" : "git",
   "url" : "https://github.com/jsparling/aws_walkthrough"
 },
 "license": "ISC",
 "dependencies": {
   "bookshelf": "^0.10.3",
   "dotenv": "^4.0.0",
   "knex": "^0.12.7",
   "pg": "^6.1.2"
 }
}

The dotenv module reads from a .env file in the project directory. This will set the environment variables. Right now we only need to set which environment we are in.

NODE_ENV=development

Hello (node) world

Let’s create a simple javascript file named index.js that will show that we have node working.

'use strict'

console.log("node is working")

Now, make sure that it works

[~/aws_walkthrough]$ node index.js
node is working
[~/aws_walkthrough]$

Create a database

Let’s create our database, and verify that it exists.

[~/aws_walkthrough] $ createdb aws_walkthrough

#verify that the db exists, use '\q' to exit psql
[~/aws_walkthrough]$ psql aws_walkthrough
psql (9.4.0)
Type "help" for help.

aws_walkthrough=#

Create table in database

Set up a knexfile.js config file for Knex. This tells Knex how to connect to the database, notice there are entries for development and production. Right now we care most about development.

// Update with your config settings.
require('dotenv').config()

module.exports = {
  development: {
    client: 'pg',
    connection: {
      database: 'aws_walkthrough'
    },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations'
    }
  },

  production: {
    client: 'pg',
    connection: {
      host: process.env.DB_HOST,
      database: 'aws_walkthrough',
      user:     process.env.DB_USER,
      password: process.env.DB_PASS
    },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations'
    }
  },
};

Now create a migration that will define our table. The resulting XXXXX_create_table.js file is just a blank template that we will fill out. Yours will have a different date and time in the name.

[~/aws_walkthrough] $ knex migrate:make create_table
Using environment: development
Created Migration: /Users/jake/aws_walkthrough/migrations/20170219092242_create_table.js
[~/aws_walkthrough] $

Later, we will be using the test jsonAPI here. To keep it simple, let’s just create a table that replicates the post resource, which has data like this:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat providen",
  "body": "quia et suscipit suscipit recusandae"
}

Edit the XXXXX_create_table.js file to include the columns we want our table to have. Mine looks like this

exports.up = function(knex, Promise) {
  return Promise.all([
    knex.schema.createTable('articles', function(table) {
      table.increments('id').primary();
      table.integer('remote_id').unique();
      table.text('title');
      table.text('body');
      table.timestamps();
    })
  ])
};

exports.down = function(knex, Promise) {
  return Promise.all([
    knex.schema.dropTable('articles')
  ])
};

This will create a table called ‘articles’.  We have an id field that will automatically increment when we insert new records. The remote_id is the id from the test service, we want to be sure that we don’t have duplicates, so we use the .unique() method. The table.timestamps(); command will add a created_at and updated_at column that are automatically populated when a record is added or updated. I have excluded the user_id that appears in the test API to keep the table simple.

The exports.down function defines what should happen if we rollback the migration, in general this should do the reverse of what the exports.up function does.

Now, run the migration with knex migrate:latest, and make sure the table exists.

~/aws_walkthrough] $ knex migrate:latest
Using environment: development
Batch 1 run: 1 migrations
/Users/jake/aws_walkthrough/migrations/20170219092242_create_table.js
[~/aws_walkthrough] $


[~/aws_walkthrough] $ psql aws_walkthrough
psql (9.4.0)
Type "help" for help.

aws_walkthrough=# \d articles
                                    Table "public.articles"
   Column   |           Type           |                       Modifiers
------------+--------------------------+-------------------------------------------------------
 id         | integer                  | not null default nextval('articles_id_seq'::regclass)
 remote_id  | integer                  |
 title      | text                     |
 body       | text                     |
 created_at | timestamp with time zone |
 updated_at | timestamp with time zone |
Indexes:
    "articles_pkey" PRIMARY KEY, btree (id)
    "articles_remote_id_unique" UNIQUE CONSTRAINT, btree (remote_id)

aws_walkthrough=#

Set up Bookshelf

Start with a bookshelf.js file that will allow us to use bookshelf in other files.

'use strict'

// Set up knex using the config file for the environment
var knex = require('knex')(require('./knexfile')[process.env.NODE_ENV])

// set up bookshelf using the knex setup we created above
var bookshelf = require('bookshelf')(knex)

// make sure bookshelf is available when importing this file
module.exports = bookshelf

Create a model class for the articles table that we can use to interface with the table. By extending the bookshelf.Model, we get functionality that allows us to access the attributes of the table as methods on the Article variable.

'use strict'

var bookshelf = require('./../bookshelf')

// create the Article model, it will include all of the attributes of the table.
// the hasTimestamps: true command will automatically populate the created_at and updated_at columns
var Article = bookshelf.Model.extend({
  tableName: 'articles',
  hasTimestamps: true
})

module.exports = Article

Insert data into the table

Edit index.js to create a new entry in the articles table

'use strict'

var bookshelf = require('./bookshelf')
var Article = require("./models/article")

var insertArticle = (callback) =>{
 // create a new entry in articles database
 new Article({
   title: "Sample title",
   body: "Sample body"
 }).save()
 .then(function(saved) {
   console.log(saved)
   const insertedId = saved.attributes.id

   callback(insertedId)
 })
}

// insert the article, and when we are done, destroy connection and get the inserted article
insertArticle(function(id){
  bookshelf.knex.destroy()
  console.log("inserted article with id: " + id)
})

Run index.js and make sure that it adds data to the table.

[~/aws_walkthrough] $ node index.js
ModelBase {
  attributes:
   { title: 'Sample title',
     body: 'Sample body',
     updated_at: Sun Feb 19 2017 10:34:22 GMT-0800 (PST),
     created_at: Sun Feb 19 2017 10:34:22 GMT-0800 (PST),
     id: 6 },
  _previousAttributes:
   { title: 'Sample title',
     body: 'Sample body',
     updated_at: Sun Feb 19 2017 10:34:22 GMT-0800 (PST),
     created_at: Sun Feb 19 2017 10:34:22 GMT-0800 (PST),
     id: 1 },
  changed: {},
  relations: {},
  cid: 'c1',
  _knex: null,
  id: 1 }
inserted article with id: 1


[~/aws_walkthrough] $ psql aws_walkthrough
psql (9.4.0)
Type "help" for help.

aws_walkthrough=# select * from articles;
 id | remote_id |    title     |    body     |         created_at         |         updated_at
----+-----------+--------------+-------------+----------------------------+----------------------------
  1 |           | Sample title | Sample body | 2017-02-19 10:34:22.967-08 | 2017-02-19 10:34:22.967-08
(1 row)

aws_walkthrough=#

Retrieve data from the database

Now, modify index.js to also query the database for the article we just inserted. Below we have added the getInsertedArticle function, commented out the code that just did the insert, and added some new code to insert and then retrieve the record.

'use strict'

var bookshelf = require('./bookshelf')
var Article = require("./models/article")

var getInsertedArticle = (id, callback) =>{
  console.log("\nNow get the article from the db\n")
  Article.where('id', id).fetch().then(function(article) {
    callback(article)
  })
}

var insertArticle = (callback) =>{
  // create a new entry in articles database
  new Article({
    title: "Sample title",
    body: "Sample body"
  }).save()
  .then(function(saved) {
    console.log(saved)
    const insertedId = saved.attributes.id

    callback(insertedId)
  })
}

// insert the article, and when we are done, destroy connection and get the inserted article
// insertArticle(function(id){
//   bookshelf.knex.destroy()
//   console.log("inserted article with id: " + id)
// })

insertArticle(function(id) {
  getInsertedArticle(id, function(article) {
    bookshelf.knex.destroy()
    console.log(article)
  })
})

Done

That is it. There are many more things we can do with a database, like create relationships, do complex joins, etc., but this is a good starting point.

Here is a link to the code from this post.