├── client
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ ├── auth
│ │ ├── changePassword.js
│ │ ├── forgotPassword.js
│ │ ├── googleSignIn.js
│ │ ├── index.js
│ │ ├── login.js
│ │ ├── register.js
│ │ ├── resendVerificationCode.js
│ │ ├── signOut.js
│ │ ├── verifyEmailWithCode.js
│ │ └── verifyForgotPassword.js
│ ├── authHub.js
│ └── getUsers.js
│ ├── config
│ └── cognitoConfig.json
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── serviceWorker.js
│ ├── setupTests.js
│ └── utils
│ ├── axiosWithAuth.js
│ ├── cognitoAuth.js
│ └── customChromeStorage.js
├── lambda_function
├── index.js
├── package-lock.json
└── package.json
├── readme.md
└── server
├── .gitignore
├── Procfile
├── app.js
├── config
└── cognitoConfig.json
├── data
├── dbConfig.js
├── migrations
│ └── 20200714161856_users.js
└── seeds
│ ├── 00-cleaner.js
│ └── 01-users.js
├── knexfile.js
├── middleware
└── cognitoAuth.js
├── models
└── usersModel.js
├── package-lock.json
├── package.json
├── routers
└── usersRouter.js
└── scripts
└── knex.sh
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cognito-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "aws-amplify": "^3.0.20",
10 | "axios": "^0.19.2",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-scripts": "3.4.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markpking2/aws-cognito-node-react/0d1b07b462e2be17d23ba243b7657e2eea1f20eb/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
900 | );
901 | }
902 | ```
903 |
904 | Let's create a component we'll use later to test our server's authentication and get a list of users.
905 |
906 | In `./src/components` add a file **getUsers.js** and add the following:
907 |
908 | ```javascript
909 | import React, { useState } from "react";
910 | import { axiosWithAuth } from "../utils/axiosWithAuth";
911 |
912 | export default function GetUsers() {
913 | const [users, setUsers] = useState(null);
914 | const [error, setError] = useState(null);
915 |
916 | function getUsers() {
917 | setError(null);
918 | axiosWithAuth("get", "/users")
919 | .then(({ data }) => {
920 | setUsers(data);
921 | })
922 | .catch((err) => {
923 | console.log(err);
924 | if (users) {
925 | setUsers(null);
926 | }
927 | setError(err);
928 | });
929 | }
930 | return (
931 | <>
932 |
Get Users:
933 |
936 | {error &&
Error: {error.message}
}
937 | {users &&
{JSON.stringify(users)}
}
938 | >
939 | );
940 | }
941 | ```
942 |
943 | Finally we'll import our **AuthHub** component into **App.js**. Let's get rid of the code generated by Create React App and use the following:
944 |
945 | ###### src/App.js
946 |
947 | ```javascript
948 | import React from "react";
949 | import AuthHub from "./components/authHub";
950 | import GetUsers from "./components/getUsers";
951 | import "./App.css";
952 |
953 | function App() {
954 | return (
955 |
956 |
957 |
958 |
959 | );
960 | }
961 |
962 | export default App;
963 | ```
964 |
965 | In your terminal in the root directory, let's start out app with `npm start`
966 |
967 | Your app should look like this:
968 |
969 | 
970 |
971 | You should now be able login/register with email/Google and use all of the awesome functions we implemented. Now let's create the Node.js Express API to authenticate with our client/Cognito.
972 |
973 |
974 |
975 | ## Step Three: Create middleware to verify the JWTs issued by Cognito from our client
976 |
977 | Let's init our server project. Open up a terminal, CD into the new project folder and `npm init -y` .
978 |
979 | Then `npx gitignore`
980 |
981 | Add `*.sqlite3` to the **.gitignore** file
982 |
983 | Install dependencies: `npm i axios cors express jsonwebtoken jwk-to-pem knex knex-cleaner pg`
984 |
985 | Install dev dependencies: `npm i -D nodemon sqlite3`
986 |
987 | Create a new folder called **config** and add a **cognitoConfig.json** file. It's similar to the one we created for our React application, but it doesn't need all of the values. Mine looks like:
988 |
989 | ```json
990 | {
991 | "clientId": "71cs1vs1bkc1b6ai9q0olocjas",
992 | "userPool": "us-east-1_M3Gx3MjTH",
993 | "region": "us-east-1",
994 | "callbackUri": "http://localhost:3000",
995 | "signoutUri": "http://localhost:3000",
996 | "tokenScopes": [
997 | "openid",
998 | "email",
999 | "profile",
1000 | "aws.cognito.signin.user.admin"
1001 | ]
1002 | }
1003 | ```
1004 |
1005 | Next, create a folder called **middleware** and add a file called **cognitoAuth.js**. It will be the authentication middleware our server uses for all our protected endpoints.
1006 |
1007 | Import **cognitoConfig.json** and necessary dependencies.
1008 |
1009 | ```javascript
1010 | const axios = require("axios");
1011 | const cognitoConfig = require("../config/cognitoConfig.json");
1012 | const jwkToPem = require("jwk-to-pem");
1013 | const jwt = require("jsonwebtoken");
1014 | ```
1015 |
1016 | Our server will need to download the **JWKs (JSON Web Keys)** for our Cognito user pool. This is public information and can be downloaded from:
1017 |
1018 | `https://cognito-idp..amazonaws.com//.well-known/jwks.json`
1019 |
1020 | When our server first starts, we'll convert the JWKs to **PEM (Public Enhanced Mail)** format. Our middleware will need to use the information stored in these keys to authorize actions against our server. We'll send a **GET** request to the JWKs URL above and then use the library [jwk-to-pem](https://www.npmjs.com/package/jwk-to-pem) to convert the JWKs to PEMs. Then throughout our middleware function we will use the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) library to decode and verify that the JWT's sent to our server are valid and came from Cognito.
1021 |
1022 | Lets store our **JWKs** URL in a variable.
1023 |
1024 | ```javascript
1025 | const JWKS_URL = `https://cognito-idp.${cognitoConfig.region}.amazonaws.com/${cognitoConfig.userPool}/.well-known/jwks.json`;
1026 | ```
1027 |
1028 | We'll create a custom class called **AuthErr** that extends **Error**.
1029 |
1030 | ```javascript
1031 | class AuthErr extends Error {}
1032 | ```
1033 |
1034 | We'll use this class to throw Authentication errors. Whenever an Authentication error is thrown we can respond to requests with a **401 Unauthorized status code**. We can check if an error is of the type **AuthErr** by using:
1035 |
1036 | ```javascript
1037 | instanceof AuthErr
1038 | ```
1039 |
1040 | Let's create the function that gets the **JWKs** from Cognito and converts them to **PEM** format.
1041 |
1042 | ```javascript
1043 | async function getPems() {
1044 | try {
1045 | const { data } = await axios.get(`${JWKS_URL}`);
1046 | if (!data || !data.keys) {
1047 | throw Error("Error downloading JWKs");
1048 | }
1049 | const pems = {};
1050 | for (let i = 0; i < data.keys.length; i++) {
1051 | pems[data.keys[i].kid] = jwkToPem(data.keys[i]);
1052 | }
1053 | return pems;
1054 | } catch (err) {
1055 | console.log(`Error getting JWKs: ${err}`);
1056 | throw Error("Error occured downloading JWKs");
1057 | }
1058 | }
1059 | ```
1060 |
1061 | When we send a **GET** request to the the URL stored in **JWKS_URL**, we'll get back an array of **JWKs**. For each key in the array we'll convert the key to **PEM** format and add it to an object called **pems** which is what our function will return.
1062 |
1063 | Next we'll create a function that verifies the **JWTs** sent in the authorization headers.
1064 |
1065 | ```javascript
1066 | async function getPems() {
1067 | try {
1068 | const { data } = await axios.get(`${JWKS_URL}`);
1069 | if (!data || !data.keys) {
1070 | throw Error("Error downloading JWKs");
1071 | }
1072 | const pems = {};
1073 | for (let i = 0; i < data.keys.length; i++) {
1074 | pems[data.keys[i].kid] = jwkToPem(data.keys[i]);
1075 | }
1076 | return pems;
1077 | } catch (err) {
1078 | console.log(`Error getting JWKs: ${err}`);
1079 | throw Error("Error occured downloading JWKs");
1080 | }
1081 | }
1082 |
1083 | async function verify(pems, auth) {
1084 | const token = auth.substring(7); // remove 'Bearer ' from auth header
1085 | const unverified = jwt.decode(token, { complete: true });
1086 |
1087 | if (!unverified) {
1088 | console.log(`Error decoding token.`);
1089 | throw new AuthErr("Invalid JWT.");
1090 | } else if (!unverified.header.kid || !pems[unverified.header.kid]) {
1091 | console.log("Invalid JWT. KID not found.");
1092 | throw new AuthErr("Invalid JWT.");
1093 | }
1094 |
1095 | return jwt.verify(
1096 | token,
1097 | pems[unverified.header.kid],
1098 | {
1099 | issuer: JWKS_URL.substring(
1100 | 0,
1101 | JWKS_URL.indexOf("/.well-known/jwks.json")
1102 | ),
1103 | maxAge: 60 * 60, //3600 seconds
1104 | },
1105 | (err, decoded) => {
1106 | if (err) {
1107 | console.log(`Invalid JWT: ${err}.`);
1108 | throw new AuthErr(
1109 | err instanceof jwt.TokenExpiredError
1110 | ? `JWT expired.`
1111 | : "Invalid JWT"
1112 | );
1113 | }
1114 |
1115 | // Verify allowed token_use
1116 | if (decoded.token_use !== "access" && decoded.token_use !== "id") {
1117 | console.log(
1118 | `token_use ${decoded.token_use} not "access" or "id".`
1119 | );
1120 | throw new AuthErr("Invalid JWT.");
1121 | }
1122 |
1123 | // Verify aud or client_id
1124 | const clientId = decoded.aud || decoded.client_id;
1125 | if (clientId !== cognitoConfig.clientId) {
1126 | console.log(
1127 | `Invalid JWT. Client id ${clientId} is not ${cognitoConfig.clientId}.`
1128 | );
1129 | throw new AuthErr("Invalid JWT.");
1130 | }
1131 | return decoded;
1132 | }
1133 | );
1134 | }
1135 |
1136 | exports.getCognitoMiddleware = () => (req, res, next) => {
1137 | (async () => {
1138 | try {
1139 | const { token_use, scope, email, db_user_id } = await verify(
1140 | await getPems(),
1141 | req.get("Authorization")
1142 | );
1143 | req.user = { token_use };
1144 | if (token_use === "access") {
1145 | req.user.scope = scope.split(" ");
1146 | } else if (token_use === "id") {
1147 | req.user.email = email;
1148 | req.user.id = db_user_id;
1149 | }
1150 | next();
1151 | } catch (err) {
1152 | console.log(err);
1153 | res.status(err instanceof AuthErr ? 401 : 500).send(
1154 | err.message || err
1155 | );
1156 | }
1157 | })();
1158 | };
1159 |
1160 | ```
1161 |
1162 | It's common practice to prefix tokens in Authorization headers with **"Bearer"**. We'll remove this prefix to get the actual token string.
1163 |
1164 | We'll then decode the **JWT** using the **.decode()** method. Passing in the `{complete: true}` option to **.decode()** gets the token's decoded payload and header.
1165 |
1166 | A **KID (Key Id)** header is an optional header that specifies the key used to validate the signature of the **JWT**. We'll check this value against our **PEM** keys and throw an error if the **KID** isn't found.
1167 |
1168 | If the **JWT** gets decoded without errors, it means it hasn't been altered. This does not mean the token's signature is valid. To verify the token's signature, we'll use the **jsonwebtoken** library **.verify()** method.
1169 |
1170 | The **.verify()** method takes in the token as the first argument, a public key, an options object, and a callback function. The callback function is called with the decoded token or an error.
1171 |
1172 | Cognito issues three types of tokens: **access tokens**, **id tokens**, and **refresh tokens**. We'll check the decoded token's **token_use** value to make sure it's only an access token or an id token.
1173 |
1174 | Next, we'll check compare the token's **aud** or **client_id** value to our Cognito client id. The token has an **aud** or a **client_id** depending if it's an access token or an id token. The verify function will return our decoded token if it makes it through our verify function without any errors being thrown. We'll then store any useful claims in **req.user**, and then call **next()** to exit our middleware and continue our request. Finally the middleware function is exported so it can be used by the API.
1175 |
1176 |
1177 |
1178 | ## Step Four: Configuring the rest of our server, deploying it to Elastic Beanstalk, and connecting an RDS instance
1179 |
1180 | In the root of our server project, create a **Procfile** file with the following:
1181 |
1182 | ```
1183 | web: npm start
1184 | ```
1185 |
1186 | In the root of the server folder, create a folder called **data** and inside it add a **dbConfig.js** file. Add the following:
1187 |
1188 | ```javascript
1189 | const knex = require("knex");
1190 | const config = require("../knexfile.js");
1191 | const dbEnv = process.env.DB_ENV || "development";
1192 |
1193 | module.exports = knex(config[dbEnv]);
1194 | ```
1195 |
1196 | In the root of the server folder, create another folder called **scripts**. Add a file called **knex.sh** with the following:
1197 |
1198 | ```bash
1199 | #!/bin/bash
1200 |
1201 | export $(grep -v '^#' ../../../../opt/elasticbeanstalk/deployment/env | xargs)
1202 | sudo RDS_DB_NAME=${RDS_DB_NAME} \
1203 | RDS_HOSTNAME=${RDS_HOSTNAME} \
1204 | RDS_USERNAME=${RDS_USERNAME} \
1205 | RDS_PASSWORD=${RDS_PASSWORD} \
1206 | npx knex $1 --env production
1207 | ```
1208 |
1209 | In the root of the server folder, add a **knexfile.js** file. Add the following:
1210 |
1211 | ```javascript
1212 | module.exports = {
1213 | development: {
1214 | client: "sqlite3",
1215 | useNullAsDefault: true,
1216 | connection: {
1217 | filename: "./data/dev.sqlite3",
1218 | },
1219 | pool: {
1220 | afterCreate: (conn, done) => {
1221 | conn.run("PRAGMA foreign_keys = ON", done);
1222 | },
1223 | },
1224 | migrations: {
1225 | directory: "./data/migrations",
1226 | },
1227 | seeds: {
1228 | directory: "./data/seeds",
1229 | },
1230 | },
1231 |
1232 | testing: {
1233 | client: "sqlite3",
1234 | useNullAsDefault: true,
1235 | connection: {
1236 | filename: "./data/testing/test.sqlite3",
1237 | },
1238 | pool: {
1239 | afterCreate: (conn, done) => {
1240 | conn.run("PRAGMA foreign_keys = ON", done);
1241 | },
1242 | },
1243 | migrations: {
1244 | directory: "./data/testing/migrations",
1245 | },
1246 | seeds: {
1247 | directory: "./data/testing/seeds",
1248 | },
1249 | },
1250 |
1251 | production: {
1252 | client: "pg",
1253 | connection: {
1254 | host: process.env.RDS_HOSTNAME,
1255 | user: process.env.RDS_USERNAME,
1256 | password: process.env.RDS_PASSWORD,
1257 | database: process.env.RDS_DB_NAME,
1258 | },
1259 | migrations: {
1260 | directory: "./data/migrations",
1261 | },
1262 | seeds: {
1263 | directory: "./data/seeds",
1264 | },
1265 | },
1266 | };
1267 | ```
1268 |
1269 | Modify **package.json** to include the following:
1270 |
1271 | ```json
1272 | ...
1273 | "main": "app.js",
1274 | "scripts": {
1275 | "start": "node app.js",
1276 | "dev": "nodemon app.js",
1277 | "test": "echo \"Error: no test specified\" && exit 1"
1278 | },
1279 | ...
1280 | ```
1281 |
1282 | Using **knex cli** or **npx** run `knex migrate:make users`
1283 |
1284 | Update the migration file in `./data/migrations` with the following:
1285 |
1286 | ```javascript
1287 | exports.up = function (knex) {
1288 | return knex.schema.createTable("users", (tbl) => {
1289 | tbl.increments();
1290 | tbl.varchar("email", 255).notNullable().unique();
1291 | });
1292 | };
1293 |
1294 | exports.down = function (knex) {
1295 | return knex.schema.dropTableIfExists("users");
1296 | };
1297 | ```
1298 |
1299 | Run `knex seed:make 00-cleaner` and `knex seed:make 01-users`
1300 |
1301 | In `./data/seeds` update **00-cleaner.js** with the following:
1302 |
1303 | ```javascript
1304 | const cleaner = require("knex-cleaner");
1305 |
1306 | exports.seed = function (knex) {
1307 | return cleaner.clean(knex, {
1308 | mode: "truncate",
1309 | ignoreTables: ["knex_migrations", "knex_migrations_lock"],
1310 | });
1311 | };
1312 | ```
1313 |
1314 | In `./data/seeds` update **01-users.js** with the following to add initial test users to our database:
1315 |
1316 | ```javascript
1317 | exports.seed = function (knex) {
1318 | return knex("users").insert(
1319 | [
1320 | { email: "testuser1@test.com" },
1321 | { email: "testuser2@test.com" },
1322 | { email: "testuser3@test.com" },
1323 | { email: "testuser4@test.com" },
1324 | ],
1325 | "id"
1326 | );
1327 | };
1328 | ```
1329 |
1330 | Run `knex migrate:latest` and `knex seed:run` to generate our sqlite3 database and load it with test users.
1331 |
1332 | In the root of our server project, create two new folders: **routers** and **models**
1333 |
1334 | In the models folder create a **usersModel.js** file and update it with the following:
1335 |
1336 | ```javascript
1337 | const db = require("../data/dbConfig");
1338 |
1339 | function getUsers() {
1340 | return db("users");
1341 | }
1342 |
1343 | module.exports = { getUsers };
1344 | ```
1345 |
1346 | In the routers folder create a **usersRouter.js** file and update it with the following:
1347 |
1348 | ```javascript
1349 | const router = require("express").Router();
1350 | const { getUsers } = require("../models/usersModel");
1351 |
1352 | router.get("/", async (req, res) => {
1353 | try {
1354 | const users = await getUsers();
1355 | res.status(200).send(users);
1356 | } catch (err) {
1357 | console.log(err);
1358 | res.status(500).send("Error retrieving users.");
1359 | }
1360 | });
1361 |
1362 | module.exports = router;
1363 | ```
1364 |
1365 | Finally, create a filed called **app.js** in the root directory. Update it with the following:
1366 |
1367 | ```javascript
1368 | const express = require("express");
1369 | const cors = require("cors");
1370 |
1371 | const { getCognitoMiddleware } = require("./middleware/cognitoAuth");
1372 | const usersRouter = require("./routers/usersRouter");
1373 |
1374 | const app = express();
1375 | const PORT = process.env.PORT || 5000;
1376 |
1377 | app.use(cors());
1378 | app.use(express.json());
1379 |
1380 | app.use("/users", getCognitoMiddleware(), usersRouter);
1381 |
1382 | app.get("/", (req, res) => {
1383 | res.send("
Hello from the server side!
");
1384 | });
1385 |
1386 | app.listen(PORT, () => {
1387 | console.log(`app listening on port ${PORT}`);
1388 | });
1389 | ```
1390 |
1391 | Now let's start our React app and server to make sure everything works locally.
1392 |
1393 | In the root directory of the React app run `npm start`.
1394 |
1395 | In the root directory of the server run `npm run dev`
1396 |
1397 | If we try to get users without being logged in, our app will throw an error before it sends a request to our server. This is because our **axiosWithAuth** function calls **getIdToken()** from **cognitoAuth.js** which requires a user to be logged in.
1398 |
1399 | 
1400 |
1401 | Now let's log in try to get users.
1402 |
1403 | 
1404 |
1405 | Let's try one more thing to double check our server's authentication. We'll temporarily modify the authorization header in **axiosWithAuth** to send an invalid token.
1406 |
1407 | ```javascript
1408 | // Authorization: `Bearer ${idToken}`,
1409 | Authorization: 'Bearer this_should_not_work',
1410 | ```
1411 |
1412 | 
1413 |
1414 | Great! Our server responded with a **401 Unauthorized error** when we sent an invalid token. Change the **axiosWithAuth** back to what it was earlier. Now it's time to deploy our server onto **Elastic Beanstalk** and add an **RDS** instance. After that we will create a custom **Lambda** trigger function that will run whenever Cognito generates a token. The first time a user logs in, it will add the user to the server's RDS database and insert the user's database id to the identity token's payload.
1415 |
1416 |
1417 |
1418 | ## Step Five: Deploying our server to Elastic Beanstalk with an RDS instance
1419 |
1420 | Navigate to the **AWS console** and go to **Services** > **Elastic Beanstalk**. Enter a name for your server, select **Node.js** for the platform, **version 12** for the platform branch, and use the recommended platform version. For **Application code** select **Sample application**, then **Create application**.
1421 |
1422 | Click on the application that was just created then click on its environment. It might take a few minutes for the environment to launch. Once it's done initializing, on the left click on **Configuration** > **Software** > **Edit**. Add the key : value pair `DB_ENV : production` to the **Environment properties** and click **Apply**.
1423 |
1424 | After the environment is done updating, go to **Configuration** > **Database** > **Edit**. Select **postgres** for the **Engine**, **12.3** for the **Engine version**, leave the **instance class** at **db.t2.micro** and the **storage** at **5GB**. Enter a **username** and **password** and click **Apply**.
1425 |
1426 | While the **RDS** instance is being added, upload your server to **GitHub**.
1427 |
1428 | Next we'll create a **CodePipeline**. Go to **Services** > **CodePipeline** > **Create pipeline**.
1429 |
1430 | Enter a name for your pipeline and click **Next**. Select **GitHub** as the **Source provider**. Click **Connect to GitHub**, then select the repository for your server and choose the **master** branch. Leave **GitHub webhooks** selected, and click **Next** > **Skip build stage**.
1431 |
1432 | For the deploy provider select **Elastic Beanstalk**. Select the **Elastic Beanstalk** application and environment you created earlier and click **Next** > **Create pipeline**. Your server should thenautomatically deploy from **GitHub**. After it's finished you should be able to access your deployed server.
1433 |
1434 | 
1435 |
1436 |
1437 |
1438 | Now that our server is running on **Elastic Beanstalk**, let's **SSH** into it and use the **knex.sh** script to run our migration and seed files.
1439 |
1440 | First we'll need to update the security group for our server to allow us to be able to **SSH** into it. Navigate to **Services** > **EC2**. Select your running instance, and select the **security group** under **Description**. Select the security group, then click **Actions** > **Edit inbound rules**. Click **Add rule**. Select **SSH** for the type and select **Anywhere** for the source. Click **Save rules**.
1441 |
1442 | If you are unfamiliar with **SSH**'ing into an AWS instance, checkout this guide [here.](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html) You can also go to **Services** > **EC2**. Right click your running instance, and click **Connect**. Select **EC2 Instance Connect**. Use **root** as the username, and click **Connect**.
1443 |
1444 | Once you're in, type `cd ../../var/app/current/scripts`
1445 |
1446 | Enter `bash knex.sh "migrate:latest"` and then `bash knex.sh "seed:run"`
1447 |
1448 | The database should now be seeded with our test users. Awesome!
1449 |
1450 | 
1451 |
1452 | Update the **baseURL** in **axiosWithAuth** to match the URL of your **Elastic Beanstalk** server. For example:
1453 |
1454 | ```javascript
1455 | baseURL: "http://cognitonodeserver-env.eba-zg9ahd2m.us-east-1.elasticbeanstalk.com",
1456 | ```
1457 |
1458 | Now start up your React application and check to see if it can authenticate with our deployed server and get users from the database.
1459 |
1460 | 
1461 |
1462 | It's working!
1463 |
1464 |
1465 |
1466 | ## Step Six: Adding a custom Lambda trigger function to our Cognito user pool
1467 |
1468 | Now the last thing we have to do is add a custom **Lambda** trigger to our Cognito user pool so our Cognito users will be synced to our **RDS** database. This function will also add the user's database id into the payload of issued identity tokens.
1469 |
1470 | Our Node.js **Lambda** function will make use of an NPM package called [pg](https://www.npmjs.com/package/pg) to connect to our database. Because of this, we'll have to create a deployment package and upload it to **Lambda** in a **.zip** file. Let's do that now!
1471 |
1472 | Create an empty directory for our deployment package. Open up a terminal inside the directory and enter `npm init -y`.
1473 |
1474 | Then enter `npm i pg`.
1475 |
1476 | Create an **index.js** file and add the following:
1477 |
1478 | ```javascript
1479 | const { Pool } = require("pg");
1480 |
1481 | exports.handler = async (event, context) => {
1482 | const pool = new Pool({
1483 | user: process.env.RDS_USERNAME,
1484 | host: process.env.RDS_HOSTNAME,
1485 | database: process.env.RDS_DB_NAME,
1486 | password: process.env.RDS_PASSWORD,
1487 | port: process.env.RDS_PORT,
1488 | });
1489 | return new Promise((resolve, reject) => {
1490 | const email =
1491 | event &&
1492 | event.request &&
1493 | event.request.userAttributes &&
1494 | event.request.userAttributes.email;
1495 |
1496 | if (email) {
1497 | pool.query(`SELECT email, id from USERS WHERE email='${email}'`)
1498 | .then((result) => {
1499 | if (result.rows.length) {
1500 | event.response = {
1501 | claimsOverrideDetails: {
1502 | claimsToAddOrOverride: {
1503 | db_user_id: result.rows[0].id,
1504 | },
1505 | },
1506 | };
1507 | resolve(event);
1508 | if (!pool.ended) {
1509 | pool.end();
1510 | }
1511 | return;
1512 | } else {
1513 | pool.query(
1514 | `INSERT INTO users (email) VALUES ('${email}') RETURNING id`
1515 | )
1516 | .then((result) => {
1517 | event.response = {
1518 | claimsOverrideDetails: {
1519 | claimsToAddOrOverride: {
1520 | db_user_id: result.rows[0].id,
1521 | },
1522 | },
1523 | };
1524 | resolve(event);
1525 | if (!pool.ended) {
1526 | pool.end();
1527 | }
1528 | return;
1529 | })
1530 | .catch((err) => {
1531 | console.log(err);
1532 | reject(err);
1533 | if (!pool.ended) {
1534 | pool.end();
1535 | }
1536 | return;
1537 | });
1538 | }
1539 | })
1540 | .catch((err) => {
1541 | console.log(err);
1542 | reject(err);
1543 | if (!pool.ended) {
1544 | pool.end();
1545 | }
1546 | return;
1547 | });
1548 | } else {
1549 | if (!pool.ended) {
1550 | pool.end();
1551 | }
1552 | resolve(event);
1553 | }
1554 | });
1555 | };
1556 | ```
1557 |
1558 | Whenever a token is issued to a user, this function will check if that user is in our database. If they aren't it will add them. Then either way it will grab the user's id and store it in the identity token as **db_user_id**.
1559 |
1560 | Now bundle all the files in our deployment package folder into a **.zip** file called **function.zip**.
1561 |
1562 | If you are using Linux you can use:
1563 |
1564 | `sudo apt-get install zip` and then `zip -r function.zip .`
1565 |
1566 | If you are using Windows you can download and install [7-Zip](https://www.7-zip.org/). After it is installed, add it to your path with `set PATH=%PATH%;C:\Program Files\7-Zip\` then run `7z a -tzip function.zip .`
1567 |
1568 | Go back into the AWS console and go to **Services** > **Lambda**. Click **Create function**. Select **Author from scratch** and enter a function name. Select **Node.js 12.x** for the **Runtime** and click **Create function**.
1569 |
1570 | Once your function is created, open it up and in the **Function code** section, select **Actions** > **Upload a .zip file**.
1571 |
1572 | 
1573 |
1574 | Upload the **function.zip** file you created earlier then click **Save.**
1575 |
1576 | We'll need to add the environment variables for our **RDS** database. An easy way to get the values we need is to **SSH** back into our **Elastic Beanstalk** instance. Once you're in, type `cd ../../../opt/elasticbeanstalk/deployment/`.
1577 |
1578 | Then enter `cat env` to list the env variables that are loaded into our server. Make note of these then head back over to our **Lambda** function.
1579 |
1580 | 
1581 |
1582 | Underneath the **Function code** section of our **Lambda** function, select **Edit** in the **Environment variables** section. Add all **RDS** key value pairs you grabbed earlier, then click **Save**.
1583 |
1584 | We'll need to add some role policies to our Lambda function, and add it to a **VPC (Virtual Private Cloud)** and a **Security Group** so our RDS instance can allow inbound database connections from the function's security group.
1585 |
1586 | At the top of our function's dashboard, select **Permissions** next to **Configuration**.
1587 |
1588 | 
1589 |
1590 | Select the **execution role** of your Lambda function. Then click **Attach policies**. Type in RDS and then select **AmazonRDSFullAccess**, then type in AWSLambda and select **AWSLambdaVPCAccessExecutionRole**. Click **Attach policy**.
1591 |
1592 | Go to **Services** > **VPC**. On the left under **SECURITY**, click **Security Groups** > **Create security group**. Enter a name for your security group, a description, and select the default VPC. Click **Create security group**.
1593 |
1594 | Go back to your Lambda function. Scroll down and under **VPC**, click **Edit**. Select **Custom VPC**. Select the default VPC. Add at least two subnets. Then select the **security group** your created earlier. Click **Save**.
1595 |
1596 | Now go to **Services** > **RDS**. Select the **RDS** instance for your server, and click on its **Security group**. Then click **Actions** > **Edit inbound rules** > **Add rule**. Select **PostgreSQL** for the type. Select **Custom** for the source, and select the security group you placed your Lambda function into earlier. Click **Save rules**.
1597 |
1598 | Now go to **Services** > **Cognito** and select the user pool for your application. On the left, click **Triggers**. Add the **Lambda** function you created to the **Pre Token Generation** trigger, then click **Save changes**.
1599 |
1600 | Now start up your React application and log in, then get users from the server. Your email should be in the server's RDS database now! The primary key id of the user in the database will also be stored in the identity token that get's sent to the server.
1601 |
1602 | Andddd.. that's a wrap! I hope you enjoyed this guide.
1603 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 | out
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and not Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | # Stores VSCode versions used for testing VSCode extensions
109 | .vscode-test
110 |
111 | # yarn v2
112 | .yarn/cache
113 | .yarn/unplugged
114 | .yarn/build-state.yml
115 | .yarn/install-state.gz
116 | .pnp.*
117 |
118 | *.sqlite3
119 |
--------------------------------------------------------------------------------
/server/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const cors = require("cors");
3 |
4 | const { getCognitoMiddleware } = require("./middleware/cognitoAuth");
5 | const usersRouter = require("./routers/usersRouter");
6 |
7 | const app = express();
8 | const PORT = process.env.PORT || 5000;
9 |
10 | app.use(cors());
11 | app.use(express.json());
12 |
13 | app.use("/users", getCognitoMiddleware(), usersRouter);
14 |
15 | app.get("/", (req, res) => {
16 | res.send("