├── 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 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markpking2/aws-cognito-node-react/0d1b07b462e2be17d23ba243b7657e2eea1f20eb/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markpking2/aws-cognito-node-react/0d1b07b462e2be17d23ba243b7657e2eea1f20eb/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AuthHub from "./components/authHub"; 3 | import GetUsers from "./components/getUsers"; 4 | import "./App.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/auth/changePassword.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { changePassword } from "../../utils/cognitoAuth"; 3 | 4 | export default function ChangePassword() { 5 | const [oldPassword, setOldPassword] = useState(""); 6 | const [newPassword, setNewPassword] = useState(""); 7 | 8 | function handleChangePassword(e) { 9 | e.preventDefault(); 10 | changePassword(oldPassword, newPassword) 11 | .then((res) => { 12 | console.log(res); 13 | }) 14 | .catch((err) => { 15 | console.log(err); 16 | }); 17 | } 18 | return ( 19 | <> 20 |

Change password

21 |
22 | setOldPassword(e.target.value)} 27 | /> 28 | setNewPassword(e.target.value)} 33 | /> 34 | 35 |
36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/auth/forgotPassword.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { forgotPassword } from "../../utils/cognitoAuth"; 3 | 4 | export default function ForgotPassword() { 5 | const [resetPasswordEmail, setResetPasswordEmail] = useState(""); 6 | 7 | function handleResetPassword(e) { 8 | e.preventDefault(); 9 | forgotPassword(resetPasswordEmail) 10 | .then((res) => console.log(res)) 11 | .catch((err) => console.log(err)); 12 | } 13 | 14 | return ( 15 | <> 16 |

Forgot password

17 |
18 | setResetPasswordEmail(e.target.value)} 23 | /> 24 | 25 |
26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/auth/googleSignIn.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { federatedSignIn } from "../../utils/cognitoAuth"; 3 | 4 | export default function GoogleSignIn() { 5 | return ( 6 | <> 7 |

Google Sign In

8 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/auth/index.js: -------------------------------------------------------------------------------- 1 | import ChangePassword from "./changePassword"; 2 | import ForgotPassword from "./forgotPassword"; 3 | import GoogleSignIn from "./googleSignIn"; 4 | import LogIn from "./login"; 5 | import Register from "./register"; 6 | import ResendVerificationCode from "./resendVerificationCode"; 7 | import SignOut from "./signOut"; 8 | import VerifyEmailWithCode from "./verifyEmailWithCode"; 9 | import VerifyForgotPassword from "./verifyForgotPassword"; 10 | 11 | export { 12 | ChangePassword, 13 | ForgotPassword, 14 | GoogleSignIn, 15 | LogIn, 16 | Register, 17 | ResendVerificationCode, 18 | SignOut, 19 | VerifyEmailWithCode, 20 | VerifyForgotPassword, 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/components/auth/login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { signIn } from "../../utils/cognitoAuth"; 3 | 4 | export default function LogIn() { 5 | const [loginEmail, setLoginEmail] = useState(""); 6 | const [loginPassword, setLoginPassword] = useState(""); 7 | 8 | function handleLogin(e) { 9 | e.preventDefault(); 10 | signIn(loginEmail, loginPassword).catch(() => {}); 11 | } 12 | return ( 13 | <> 14 |

Login

15 |
16 | setLoginEmail(e.target.value)} 19 | value={loginEmail} 20 | /> 21 | setLoginPassword(e.target.value)} 24 | value={loginPassword} 25 | placeholder="password" 26 | /> 27 | 28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /client/src/components/auth/register.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { signUp } from "../../utils/cognitoAuth"; 3 | 4 | export default function Register() { 5 | const [registerEmail, setRegisterEmail] = useState(""); 6 | const [registerPassword, setRegisterPassword] = useState(""); 7 | 8 | function handleRegister(e) { 9 | e.preventDefault(); 10 | signUp(registerEmail, registerPassword).catch((err) => 11 | console.log(err) 12 | ); 13 | } 14 | return ( 15 | <> 16 |

Register

17 |
18 | setRegisterEmail(e.target.value)} 21 | value={registerEmail} 22 | /> 23 | setRegisterPassword(e.target.value)} 26 | value={registerPassword} 27 | placeholder="password" 28 | /> 29 | 30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/auth/resendVerificationCode.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { resendConfirmationCode } from "../../utils/cognitoAuth"; 3 | 4 | export default function ResendVerificationCode() { 5 | const [resendEmail, setResendEmail] = useState(""); 6 | function handleResendVerification(e) { 7 | e.preventDefault(); 8 | resendConfirmationCode(resendEmail) 9 | .then((res) => console.log(res)) 10 | .catch((err) => { 11 | console.log(err); 12 | }); 13 | } 14 | return ( 15 | <> 16 |

Resend Confirmation Code

17 |
18 | setResendEmail(e.target.value)} 23 | /> 24 | 25 |
26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/auth/signOut.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { signOut } from "../../utils/cognitoAuth"; 3 | 4 | export default function SignOut() { 5 | return ( 6 | <> 7 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/components/auth/verifyEmailWithCode.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { confirmSignUp } from "../../utils/cognitoAuth"; 3 | 4 | export default function VerifyEmailWithCode() { 5 | const [verifyEmail, setVerifyEmail] = useState(""); 6 | const [verificationCode, setVerificationCode] = useState(""); 7 | 8 | function handleVerification(e) { 9 | e.preventDefault(); 10 | confirmSignUp(verifyEmail, verificationCode) 11 | .then((res) => console.log(res)) 12 | .catch((err) => console.log(err)); 13 | } 14 | 15 | return ( 16 | <> 17 |

Verify Email With Code

18 |
19 | setVerifyEmail(e.target.value)} 22 | value={verifyEmail} 23 | /> 24 | setVerificationCode(e.target.value)} 26 | value={verificationCode} 27 | placeholder="verification code" 28 | /> 29 | 30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/auth/verifyForgotPassword.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { forgotPasswordSubmit } from "../../utils/cognitoAuth"; 3 | 4 | export default function VerifyForgotPassword() { 5 | const [resetPasswordCode, setResetPasswordCode] = useState(""); 6 | const [resetNewPassword, setResetNewPassword] = useState(""); 7 | const [resetPasswordEmail, setResetPasswordEmail] = useState(""); 8 | 9 | function handleForgotPasswordSubmit(e) { 10 | e.preventDefault(); 11 | forgotPasswordSubmit( 12 | resetPasswordEmail, 13 | resetPasswordCode, 14 | resetNewPassword 15 | ) 16 | .then((res) => console.log(res)) 17 | .catch((err) => console.log(err)); 18 | } 19 | return ( 20 | <> 21 |

Verify Forgot Password

22 |
23 | setResetPasswordEmail(e.target.value)} 28 | /> 29 | setResetPasswordCode(e.target.value)} 34 | /> 35 | setResetNewPassword(e.target.value)} 40 | /> 41 | 42 |
43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/authHub.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { getCurrentUser } from "../utils/cognitoAuth"; 3 | import { Hub } from "aws-amplify"; 4 | 5 | import { 6 | ChangePassword, 7 | ForgotPassword, 8 | GoogleSignIn, 9 | LogIn, 10 | Register, 11 | ResendVerificationCode, 12 | SignOut, 13 | VerifyEmailWithCode, 14 | VerifyForgotPassword, 15 | } from "./auth"; 16 | 17 | export default function AuthHub() { 18 | const [currentUser, setCurrentUser] = useState(null); 19 | 20 | useEffect(() => { 21 | Hub.listen("auth", ({ payload: { event, data } }) => { 22 | switch (event) { 23 | case "signUp": 24 | console.log("User registered"); 25 | break; 26 | case "cognitoHostedUI": 27 | case "signIn": 28 | getCurrentUser() 29 | .then((userData) => { 30 | setCurrentUser(userData); 31 | }) 32 | .catch((err) => { 33 | console.log(err); 34 | }); 35 | break; 36 | case "signOut": 37 | setCurrentUser(null); 38 | break; 39 | case "signIn_failure": 40 | case "cognitoHostedUI_failure": 41 | console.log("Sign in failure", data); 42 | break; 43 | default: 44 | } 45 | }); 46 | 47 | getCurrentUser() 48 | .then((userData) => setCurrentUser(userData)) 49 | .catch((err) => console.log(err)); 50 | }, []); 51 | 52 | return ( 53 |
54 |

Auth Hub

55 | {!currentUser && ( 56 | <> 57 |

You are not signed in.

58 | 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | {currentUser && ( 66 | <> 67 |

Hello {currentUser["email"]}

68 | 69 | 70 | )} 71 | 72 | {(!currentUser || !currentUser.email_verified) && ( 73 | <> 74 | 75 | 76 | 77 | )} 78 | 79 | {currentUser && !currentUser.identities && ( 80 | <> 81 | 82 | 83 | )} 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /client/src/components/getUsers.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { axiosWithAuth } from "../utils/axiosWithAuth"; 3 | 4 | export default function GetUsers() { 5 | const [users, setUsers] = useState(null); 6 | const [error, setError] = useState(null); 7 | 8 | function getUsers() { 9 | setError(null); 10 | axiosWithAuth("get", "/users") 11 | .then(({ data }) => { 12 | setUsers(data); 13 | }) 14 | .catch((err) => { 15 | console.log(err); 16 | if (users) { 17 | setUsers(null); 18 | } 19 | setError(err); 20 | }); 21 | } 22 | return ( 23 | <> 24 |

Get Users:

25 | 28 | {error &&

Error: {error.message}

} 29 | {users &&

{JSON.stringify(users)}

} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /client/src/config/cognitoConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "71cs1vs1bkc1b6ai9q0olocjas", 3 | "userPoolUri": "reactnodeguide.auth.us-east-1.amazoncognito.com", 4 | "userPool": "us-east-1_M3Gx3MjTH", 5 | "region": "us-east-1", 6 | "callbackUri": "http://localhost:3000/", 7 | "signoutUri": "http://localhost:3000/", 8 | "tokenScopes": [ 9 | "openid", 10 | "email", 11 | "profile", 12 | "aws.cognito.signin.user.admin" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/utils/axiosWithAuth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getIdToken } from "./cognitoAuth"; 3 | 4 | export async function axiosWithAuth(method, path) { 5 | try { 6 | const idToken = await getIdToken(); 7 | return await axios 8 | .create({ 9 | baseURL: 10 | "http://cognitonodeserver-env.eba-zg9ahd2m.us-east-1.elasticbeanstalk.com", 11 | headers: { 12 | "Content-Type": "application/json", 13 | Authorization: `Bearer ${idToken}`, 14 | }, 15 | }) 16 | [method](path); 17 | } catch (err) { 18 | throw err; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/utils/cognitoAuth.js: -------------------------------------------------------------------------------- 1 | import cognitoConfig from "../config/cognitoConfig.json"; 2 | import Amplify, { Auth } from "aws-amplify"; 3 | // import {CustomChromeStorage} from '../utils/customChromeStorage' 4 | 5 | Amplify.configure({ 6 | Auth: { 7 | userPoolId: cognitoConfig.userPool, 8 | userPoolWebClientId: cognitoConfig.clientId, 9 | region: cognitoConfig.region, 10 | oauth: { 11 | domain: cognitoConfig.userPoolUri, 12 | scope: cognitoConfig.tokenScopes, 13 | redirectSignIn: cognitoConfig.callbackUri, 14 | redirectSignOut: cognitoConfig.signoutUri, 15 | responseType: "code", 16 | }, 17 | // storage: CustomChromeStorage 18 | }, 19 | }); 20 | 21 | async function signUp(email, password) { 22 | return await Auth.signUp({ 23 | username: email, 24 | password, 25 | }); 26 | } 27 | 28 | async function signIn(email, password) { 29 | return await Auth.signIn(email, password); 30 | } 31 | 32 | async function confirmSignUp(email, code) { 33 | return await Auth.confirmSignUp(email, code); 34 | } 35 | 36 | async function resendConfirmationCode(username) { 37 | return await Auth.resendSignUp(username); 38 | } 39 | 40 | // pass in true to sign out from all devices 41 | async function signOut(global = false) { 42 | return await Auth.signOut({ global }); 43 | } 44 | 45 | async function federatedSignIn(provider) { 46 | return await Auth.federatedSignIn({ provider }); 47 | } 48 | 49 | async function forgotPassword(email) { 50 | return await Auth.forgotPassword(email); 51 | } 52 | 53 | async function forgotPasswordSubmit(email, code, newPassword) { 54 | try { 55 | await Auth.forgotPasswordSubmit(email, code, newPassword); 56 | return "Password was changed successfully."; 57 | } catch (err) { 58 | throw err; 59 | } 60 | } 61 | 62 | async function changePassword(oldPassword, newPassword) { 63 | try { 64 | const user = await Auth.currentAuthenticatedUser(); 65 | await Auth.changePassword(user, oldPassword, newPassword); 66 | return "Password was changed successfully."; 67 | } catch (err) { 68 | throw err; 69 | } 70 | } 71 | 72 | function getIdToken() { 73 | return new Promise((resolve, reject) => { 74 | Auth.currentSession() 75 | .then((data) => { 76 | const idToken = data.getIdToken(); 77 | resolve(idToken.jwtToken); 78 | }) 79 | .catch(() => { 80 | reject(Error("Not signed in.")); 81 | }); 82 | }); 83 | } 84 | 85 | function getCurrentUser() { 86 | return new Promise((resolve, reject) => { 87 | Auth.currentSession() 88 | .then((data) => { 89 | const idToken = data.getIdToken(); 90 | const user = idToken.payload; 91 | resolve(user); 92 | }) 93 | .catch(() => { 94 | reject(Error("Not signed in.")); 95 | }); 96 | }); 97 | } 98 | 99 | export { 100 | signUp, 101 | signIn, 102 | confirmSignUp, 103 | resendConfirmationCode, 104 | signOut, 105 | federatedSignIn, 106 | forgotPassword, 107 | forgotPasswordSubmit, 108 | getIdToken, 109 | changePassword, 110 | getCurrentUser, 111 | }; 112 | -------------------------------------------------------------------------------- /client/src/utils/customChromeStorage.js: -------------------------------------------------------------------------------- 1 | /*global chrome*/ 2 | 3 | const STORAGE_PREFIX = "@CustomChromeStorage"; 4 | let dataMemory = {}; 5 | 6 | class CustomChromeStorage { 7 | static syncPromise = null; 8 | 9 | static setItem(key, value) { 10 | chrome.storage.sync.set({ key: JSON.stringify(value) }, () => { 11 | console.log("item stored"); 12 | }); 13 | dataMemory[key] = value; 14 | return dataMemory[key]; 15 | } 16 | 17 | static getItem(key) { 18 | return Object.prototype.hasOwnProperty(dataMemory, key) 19 | ? dataMemory[key] 20 | : undefined; 21 | } 22 | 23 | static removeItem(key, value) { 24 | chrome.storage.sync.remove(key, () => { 25 | console.log("item removed"); 26 | }); 27 | return delete dataMemory[key]; 28 | } 29 | 30 | static clear() { 31 | chrome.storage.sync.clear(() => { 32 | console.log("storage cleared"); 33 | }); 34 | dataMemory = {}; 35 | return dataMemory; 36 | } 37 | 38 | static sync() { 39 | if (!CustomChromeStorage.syncPromise) { 40 | CustomChromeStorage.syncPromise = new Promise((resolve, reject) => { 41 | chrome.storage.sync.get(null, (items) => { 42 | const keys = Object.keys(items); 43 | const memoryKeys = keys.filter((key) => 44 | key.startsWith(STORAGE_PREFIX) 45 | ); 46 | chrome.storage.sync.get(memoryKeys, (stores) => { 47 | for (let key in stores) { 48 | const value = stores[key]; 49 | const memoryKey = key.replace(STORAGE_PREFIX, ""); 50 | dataMemory[memoryKey] = value; 51 | } 52 | resolve(); 53 | }); 54 | }); 55 | }); 56 | } 57 | } 58 | } 59 | 60 | export { CustomChromeStorage }; 61 | -------------------------------------------------------------------------------- /lambda_function/index.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | 3 | exports.handler = async (event, context) => { 4 | const pool = new Pool({ 5 | user: process.env.RDS_USERNAME, 6 | host: process.env.RDS_HOSTNAME, 7 | database: process.env.RDS_DB_NAME, 8 | password: process.env.RDS_PASSWORD, 9 | port: process.env.RDS_PORT, 10 | }); 11 | return new Promise((resolve, reject) => { 12 | const email = 13 | event && 14 | event.request && 15 | event.request.userAttributes && 16 | event.request.userAttributes.email; 17 | async () => {}; 18 | if (email) { 19 | pool.query(`SELECT email, id from USERS WHERE email='${email}'`) 20 | .then((result) => { 21 | if (result.rows.length) { 22 | event.response = { 23 | claimsOverrideDetails: { 24 | claimsToAddOrOverride: { 25 | db_user_id: result.rows[0].id, 26 | }, 27 | }, 28 | }; 29 | resolve(event); 30 | if (!pool.ended) { 31 | pool.end(); 32 | } 33 | return; 34 | } else { 35 | pool.query( 36 | `INSERT INTO users (email) VALUES ('${email}') RETURNING id` 37 | ) 38 | .then((result) => { 39 | event.response = { 40 | claimsOverrideDetails: { 41 | claimsToAddOrOverride: { 42 | db_user_id: result.rows[0].id, 43 | }, 44 | }, 45 | }; 46 | resolve(event); 47 | if (!pool.ended) { 48 | pool.end(); 49 | } 50 | return; 51 | }) 52 | .catch((err) => { 53 | console.log(err); 54 | reject(err); 55 | if (!pool.ended) { 56 | pool.end(); 57 | } 58 | return; 59 | }); 60 | } 61 | }) 62 | .catch((err) => { 63 | console.log(err); 64 | reject(err); 65 | if (!pool.ended) { 66 | pool.end(); 67 | } 68 | return; 69 | }); 70 | } else { 71 | if (!pool.ended) { 72 | pool.end(); 73 | } 74 | resolve(event); 75 | } 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /lambda_function/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pg-lambda", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "buffer-writer": { 8 | "version": "2.0.0", 9 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 10 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 11 | }, 12 | "packet-reader": { 13 | "version": "1.0.0", 14 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 15 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 16 | }, 17 | "pg": { 18 | "version": "8.3.0", 19 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.3.0.tgz", 20 | "integrity": "sha512-jQPKWHWxbI09s/Z9aUvoTbvGgoj98AU7FDCcQ7kdejupn/TcNpx56v2gaOTzXkzOajmOEJEdi9eTh9cA2RVAjQ==", 21 | "requires": { 22 | "buffer-writer": "2.0.0", 23 | "packet-reader": "1.0.0", 24 | "pg-connection-string": "^2.3.0", 25 | "pg-pool": "^3.2.1", 26 | "pg-protocol": "^1.2.5", 27 | "pg-types": "^2.1.0", 28 | "pgpass": "1.x", 29 | "semver": "4.3.2" 30 | } 31 | }, 32 | "pg-connection-string": { 33 | "version": "2.3.0", 34 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz", 35 | "integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==" 36 | }, 37 | "pg-int8": { 38 | "version": "1.0.1", 39 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 40 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 41 | }, 42 | "pg-pool": { 43 | "version": "3.2.1", 44 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.1.tgz", 45 | "integrity": "sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA==" 46 | }, 47 | "pg-protocol": { 48 | "version": "1.2.5", 49 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.2.5.tgz", 50 | "integrity": "sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg==" 51 | }, 52 | "pg-types": { 53 | "version": "2.2.0", 54 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 55 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 56 | "requires": { 57 | "pg-int8": "1.0.1", 58 | "postgres-array": "~2.0.0", 59 | "postgres-bytea": "~1.0.0", 60 | "postgres-date": "~1.0.4", 61 | "postgres-interval": "^1.1.0" 62 | } 63 | }, 64 | "pgpass": { 65 | "version": "1.0.2", 66 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 67 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 68 | "requires": { 69 | "split": "^1.0.0" 70 | } 71 | }, 72 | "postgres-array": { 73 | "version": "2.0.0", 74 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 75 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 76 | }, 77 | "postgres-bytea": { 78 | "version": "1.0.0", 79 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 80 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 81 | }, 82 | "postgres-date": { 83 | "version": "1.0.5", 84 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.5.tgz", 85 | "integrity": "sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA==" 86 | }, 87 | "postgres-interval": { 88 | "version": "1.2.0", 89 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 90 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 91 | "requires": { 92 | "xtend": "^4.0.0" 93 | } 94 | }, 95 | "semver": { 96 | "version": "4.3.2", 97 | "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", 98 | "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" 99 | }, 100 | "split": { 101 | "version": "1.0.1", 102 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 103 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 104 | "requires": { 105 | "through": "2" 106 | } 107 | }, 108 | "through": { 109 | "version": "2.3.8", 110 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 111 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 112 | }, 113 | "xtend": { 114 | "version": "4.0.2", 115 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 116 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lambda_function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pg-lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "pg": "^8.3.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AWS Cognito Authentication with React and a custom Node.js Express API 2 | 3 | In this guide I am going to show you how to use Cognito for authentication with React and a Node.js Express API. By the time we are done, our app will be able to login/register on AWS with email and Google OAuth. Users will also be able to receive a confirmation email, and reset/change their password. 4 | 5 | This guide's main focus is authenticating with Cognito but If you want more in-depth guide on how to deploy the server on **Elastic Beanstalk**, I wrote one [here](https://github.com/markpkng/node-express-rds). 6 | 7 | Our app will be deployed on Elastic Beanstalk using RDS (Amazon's Relational Database Service). It will use a custom serverless Lambda trigger function to make sure our Cognito users are synced with our server's database. In this function we will also add the user's primary database key into the identity token so our API can easily find the user's data without having to query by email. 8 | 9 | When a user authenticates through Cognito, AWS will issue the client a JWT (JSON Web Token). Our client app will send the token to our server, which will verify the token through AWS. Let's get started! 10 | 11 | ## Table of Contents 12 | 13 | [Step One: Configuring Cognito in the AWS Console and Google OAuth](#step-one) 14 | 15 | [Step Two: Creating the React application](#step-two) 16 | 17 | [Part One: Init React application and create Auth functions](#part-one) 18 | 19 | [Part Two: Creating components to utilize our Auth functions](#part-two) 20 | 21 | [Step Three: Create middleware to verify the JWTs issued by Cognito from our client](#step-three) 22 | 23 | [Step Four: Configuring the rest of our server, deploying it to Elastic Beanstalk, and connecting an RDS instance](#step-four) 24 | 25 | [Step Five: Deploying our server to Elastic Beanstalk with an RDS instance](#step-five) 26 | 27 | [Step Six: Adding a custom Lambda trigger function to our Cognito user pool](#step-six) 28 | 29 | ### 30 | 31 | ## Step One: Configuring Cognito in the AWS Console and Google OAuth 32 | 33 | First, let's create a new user pool in Cognito. Open the AWS console. In the top right, make sure you are in the correct region you want to use for your application. Navigate to **Services** > **Cognito ** > **Manage User Pools** > **Create a user pool**. 34 | 35 | Enter any name you choose for your user pool. I'll call mine cognito-react-node. We'll click **review defaults** to load a default configuration, then we'll customize our configuration as needed. On the left, click **attributes**. For this application, our users will be able to register and sign in with their email. Select the following attributes: 36 | 37 | ![attributes](https://res.cloudinary.com/markpkng/image/upload/v1594686853/cognito-react-node/attributes_buetaf.png) 38 | 39 | Click **next step**. In **policies** you can set the password requirements. We can keep the default configuration. 40 | 41 | ![policies](https://res.cloudinary.com/markpkng/image/upload/v1594687231/cognito-react-node/policies_zozn2h.png) 42 | 43 | Next we'll also keep the default **MFA and verifications** settings. 44 | 45 | ![verification](https://res.cloudinary.com/markpkng/image/upload/v1594687232/cognito-react-node/verification_v2ycig.png) 46 | 47 | Next, in **message customizations** I'm going to set the verification type to **link**, although I will also provide the code to enter a verification code later if you wish to use that method instead. We can leave the other message settings as the defaults. 48 | 49 | ![link verification](https://res.cloudinary.com/markpkng/image/upload/v1594687583/cognito-react-node/link_hfphbu.png) 50 | 51 | You can skip the **Tags** and **Devices** settings. 52 | 53 | In **App clients** click **Add an app client**. 54 | 55 | Enter a name for your app client. Since we are using React for the frontend, uncheck **Generate client secret**. Leave the other default values and click **Create app client**. 56 | 57 | ![create app client](https://res.cloudinary.com/markpkng/image/upload/v1594687900/cognito-react-node/create_app_client_p93luh.png) 58 | 59 | Next, click **Review** and then **Create pool**. 60 | 61 | After the pool is created, on the left under **App integration**, select **Domain name**. For this guide we'll use a Amazon Cognito domain. Choose a domain name, check if it's available, then click **save changes**. Make note of the domain you just created. If you click **Go to summary** you can easily copy it. We'll need this in the next step for configuring Google OAuth. 62 | 63 | We still have some more configuration to do, but before we do that lets hop over to Google's developer console and create an OAuth application to use for our user pool. 64 | 65 | Navigate to the [Google API console](https://console.developers.google.com/) and log in. Once you are in the Google API dashboard, click **CREATE PROJECT**. 66 | 67 | ![google create project](https://res.cloudinary.com/markpkng/image/upload/v1594688324/cognito-react-node/google_create_project_spc6tz.png) 68 | 69 | Enter a name for your project, leave the location as "No organization" and click **CREATE**. 70 | 71 | Next, on the left side click **OAuth consent screen**. Select **External**, then **CREATE**. Enter a name for your application, then click **Save**. 72 | 73 | On the left, click **Credentials** > **CREATE CREDENTIALS** > **OAuth client ID**. 74 | 75 | For the **Application type** select **Web application**. Enter a name for your client. Under **Authorized JavaScript origins**, add the Cognito domain URI you created earlier. 76 | 77 | Under **Authorized redirect URIs**, enter `/oauth2/idpresponse` prefixed with the same domain URI you used above. 78 | 79 | Click **CREATE**. 80 | 81 | ![google oauth client](https://res.cloudinary.com/markpkng/image/upload/v1594689277/cognito-react-node/google_oauth_client_fnjv9l.png) 82 | 83 | Once the Oauth client is created, make note of the **client ID** and **client secret**. We'll need these values to configure Google Oauth in the Cognito user pool settings. Head back to over to your Cognito user pool. On the left under Federation, click **Identity providers**. Select **Google**. 84 | 85 | Enter your Google OAuth **client ID ** and **client secret**. For the **Autorized scope** enter **profile email openid**. Click **Enable Google**. 86 | 87 | ![google client id and secret](https://res.cloudinary.com/markpkng/image/upload/v1594689708/cognito-react-node/google_client_id_and_secret_nycyto.png) 88 | 89 | Once enabled, click **Configure attribute mapping**. 90 | 91 | ![google attribute mapping](https://res.cloudinary.com/markpkng/image/upload/v1594689833/cognito-react-node/google_attribute_mapping_knxgch.png) 92 | 93 | We'll map the **email** and **email_verified** attributes to our user pool **Email** and **Email Verified** attributes, and use **Username** for the **sub** attribute. Our users will only need to verify their emails if they register without using Google OAuth. Click **Save changes**. 94 | 95 | ![google configure attribtues](https://res.cloudinary.com/markpkng/image/upload/v1594690028/cognito-react-node/google_configure_attributes_a5oey6.png) 96 | 97 | Finally, on the left under **App integration**, click **App client settings**. Under **Enabled Identity Providers** enable **Google** and **Cognito User Pool**. 98 | 99 | For the **Callback URL** and **Sign out URL** enter `http://localhost:3000/` since we'll be developing locally. If you ever deploy your React application you'll also need to add hosted URL. 100 | 101 | Under **OAuth 2.0**, select **Authorization code grant** and **Implicit grant** under **Allowed Oauth Flows**. 102 | 103 | Select **email**, **openid**, **aws.cognito.signin.user.admin**, and **profile** for **Allowed OAuth Scopes**. 104 | 105 | Click **Save changes**. 106 | 107 | Now our Cognito user pool is configured and we are ready to start coding! 108 | 109 | 110 | 111 | ## Step Two: Creating the React application 112 | 113 | ### 114 | 115 | ### Part One: Init React application and create Auth functions. 116 | 117 | I'm going to use **Create React App** to initialize our project. 118 | 119 | `npx create-react-app cognito-react` 120 | 121 | `cd cognito-react` 122 | 123 | We'll be using **axios** to send API requests to our server, and **aws-amplify** to authenticate with Cognito. 124 | 125 | `npm i axios aws-amplify` 126 | 127 | Inside the **src** folder of your project, create a folder called **config** with a file called **cognito-config.json**. We'll heed to head back to our Cognito user pool to grab a bunch of values to store in this config file. Here is an example of what mine looks like: 128 | 129 | ###### src/config/cognitoConfig.json 130 | 131 | ```json 132 | { 133 | "clientId": "71cs1vs1bkc1b6ai9q0olocjas", 134 | "userPoolUri": "reactnodeguide.auth.us-east-1.amazoncognito.com", 135 | "userPool": "us-east-1_M3Gx3MjTH", 136 | "region": "us-east-1", 137 | "callbackUri": "http://localhost:3000/", 138 | "signoutUri": "http://localhost:3000/", 139 | "tokenScopes": [ 140 | "openid", 141 | "email", 142 | "profile", 143 | "aws.cognito.signin.user.admin" 144 | ] 145 | } 146 | ``` 147 | 148 | For **clientId**, on the left of the Cognito dashboard under **App integration**, use the **ID** found in **App client settings**. 149 | 150 | ![clientId](https://res.cloudinary.com/markpkng/image/upload/v1594692705/cognito-react-node/client_id_ie1d9j.png) 151 | 152 | Use the **domain** found in **App integration** for the **userPoolUri**. Remove `https://`. 153 | 154 | ![userPoolUri](https://res.cloudinary.com/markpkng/image/upload/v1594692780/cognito-react-node/userPoolUri_sze58a.png) 155 | 156 | Use the **Pool Id** found under **General settings** for **userPool**. 157 | 158 | ![userPool](https://res.cloudinary.com/markpkng/image/upload/v1594692849/cognito-react-node/userPool_oawab2.png) 159 | 160 | Use the **region** you created your Cognito user pool in. 161 | 162 | Finally, add the **callbackUri**, **signoutUri**, and **tokenScopes**. 163 | 164 | Now lets create all the helper functions we'll use to login, register, reset password, etc. From the root directory of your project, create a folder called **utils** and inside it create a file called **cognitoAuth.js**. 165 | 166 | Import the following: 167 | 168 | ###### src/utils/cognitoAuth.js 169 | 170 | ```javascript 171 | import cognitoConfig from "../config/cognitoConfig.json"; 172 | import Amplify, { Auth } from "aws-amplify"; 173 | ``` 174 | 175 | Next we'll need to configure Amplify. 176 | 177 | ```javascript 178 | Amplify.configure({ 179 | Auth: { 180 | userPoolId: cognitoConfig.userPool, 181 | userPoolWebClientId: cognitoConfig.clientId, 182 | region: cognitoConfig.region, 183 | oauth: { 184 | domain: cognitoConfig.userPoolUri, 185 | scope: cognitoConfig.tokenScopes, 186 | redirectSignIn: cognitoConfig.callbackUri, 187 | redirectSignOut: cognitoConfig.signoutUri, 188 | responseType: "code", 189 | }, 190 | // storage: CustomChromeStorage 191 | }, 192 | }); 193 | ``` 194 | 195 | Notice the `// storage: CustomChromeStorage`. By default, Amplify Auth functions use and store information in localStorage. You can optionally create a custom storage class if you would like to store the tokens elsewhere. For example, Chrome extensions do not have access to localStorage. We would have to provide Amplfiy with a workaround. Below is an example of a custom storage class I wrote to use Chrome Storage instead of localStorage. Amplify will sync an in-memory storage with Chrome Storage. 196 | 197 | #### * Not required. Custom Chrome Storage example: 198 | 199 | You would import this class and pass it into the Amplify configuration. 200 | 201 | ###### src/utils/customChromeStorage.js 202 | 203 | ```javascript 204 | /*global chrome*/ 205 | 206 | const STORAGE_PREFIX = "@CustomChromeStorage"; 207 | let dataMemory = {}; 208 | 209 | class CustomChromeStorage { 210 | static syncPromise = null; 211 | 212 | static setItem(key, value) { 213 | chrome.storage.sync.set({ key: JSON.stringify(value) }, () => { 214 | console.log("item stored"); 215 | }); 216 | dataMemory[key] = value; 217 | return dataMemory[key]; 218 | } 219 | 220 | static getItem(key) { 221 | return Object.prototype.hasOwnProperty(dataMemory, key) 222 | ? dataMemory[key] 223 | : undefined; 224 | } 225 | 226 | static removeItem(key, value) { 227 | chrome.storage.sync.remove(key, () => { 228 | console.log("item removed"); 229 | }); 230 | return delete dataMemory[key]; 231 | } 232 | 233 | static clear() { 234 | chrome.storage.sync.clear(() => { 235 | console.log("storage cleared"); 236 | }); 237 | dataMemory = {}; 238 | return dataMemory; 239 | } 240 | 241 | static sync() { 242 | if (!CustomChromeStorage.syncPromise) { 243 | CustomChromeStorage.syncPromise = new Promise((resolve, reject) => { 244 | chrome.storage.sync.get(null, (items) => { 245 | const keys = Object.keys(items); 246 | const memoryKeys = keys.filter((key) => 247 | key.startsWith(STORAGE_PREFIX) 248 | ); 249 | chrome.storage.sync.get(memoryKeys, (stores) => { 250 | for (let key in stores) { 251 | const value = stores[key]; 252 | const memoryKey = key.replace(STORAGE_PREFIX, ""); 253 | dataMemory[memoryKey] = value; 254 | } 255 | resolve(); 256 | }); 257 | }); 258 | }); 259 | } 260 | return CustomChromeStorage.syncPromise; 261 | } 262 | } 263 | 264 | export { CustomChromeStorage }; 265 | ``` 266 | 267 | Now to create the utility functions our app will use to authenticate with Cognito. 268 | 269 | ###### src/utils/cognitoAuth.js 270 | 271 | ##### Sign Up 272 | 273 | ```javascript 274 | async function signUp(email, password) { 275 | return await Auth.signUp({ 276 | username: email, 277 | password, 278 | }); 279 | } 280 | ``` 281 | 282 | ##### Sign In 283 | 284 | ```javascript 285 | async function signIn(email, password) { 286 | return await Auth.signIn(email, password); 287 | } 288 | ``` 289 | 290 | ##### Verify Email with Confirmation Code 291 | 292 | ```javascript 293 | async function confirmSignUp(email, code) { 294 | return await Auth.confirmSignUp(email, code); 295 | } 296 | ``` 297 | 298 | ##### Resend Email Confirmation Code 299 | 300 | ```javascript 301 | async function resendConfirmationCode(username) { 302 | return await Auth.resendSignUp(username); 303 | } 304 | ``` 305 | 306 | ##### Sign Out/Global Sign Out. Removes tokens from localStorage and optionally signs out of all devices. 307 | 308 | ```javascript 309 | // pass in true to sign out from all devices 310 | async function signOut(global = false) { 311 | return await Auth.signOut({ global }); 312 | } 313 | ``` 314 | 315 | ##### Federated Sign In for OAuth 316 | 317 | ```javascript 318 | async function federatedSignIn(provider) { 319 | return await Auth.federatedSignIn({ provider }); 320 | } 321 | ``` 322 | 323 | ##### Forgot Password: Send confirmation code to reset password 324 | 325 | ```javascript 326 | async function forgotPassword(email) { 327 | return await Auth.forgotPassword(email); 328 | } 329 | ``` 330 | 331 | ##### Forgot Password Confirmation 332 | 333 | ```javascript 334 | async function forgotPasswordSubmit(email, code, newPassword) { 335 | try { 336 | await Auth.forgotPasswordSubmit(email, code, newPassword); 337 | return "Password was changed successfully."; 338 | } catch (err) { 339 | throw err; 340 | } 341 | } 342 | ``` 343 | 344 | ##### Change Password 345 | 346 | ```javascript 347 | async function changePassword(oldPassword, newPassword) { 348 | try { 349 | const user = await Auth.currentAuthenticatedUser(); 350 | await Auth.changePassword(user, oldPassword, newPassword); 351 | return "Password was changed successfully."; 352 | } catch (err) { 353 | throw err; 354 | } 355 | } 356 | ``` 357 | 358 | ##### Get idToken 359 | 360 | ```javascript 361 | function getIdToken() { 362 | return new Promise((resolve, reject) => { 363 | Auth.currentSession() 364 | .then((data) => { 365 | const idToken = data.getIdToken(); 366 | resolve(idToken.jwtToken); 367 | }) 368 | .catch(() => { 369 | reject(Error("Not signed in.")); 370 | }); 371 | }); 372 | } 373 | ``` 374 | 375 | ##### Get Current User: Get user information from localStorage 376 | 377 | ```javascript 378 | function getCurrentUser() { 379 | return new Promise((resolve, reject) => { 380 | Auth.currentSession() 381 | .then((data) => { 382 | const idToken = data.getIdToken(); 383 | const user = idToken.payload; 384 | resolve(user); 385 | }) 386 | .catch(() => { 387 | reject(Error("Not signed in.")); 388 | }); 389 | }); 390 | } 391 | ``` 392 | 393 | Export the functions so we can use them in our application: 394 | 395 | ```javascript 396 | export { 397 | signUp, 398 | signIn, 399 | confirmSignUp, 400 | resendConfirmationCode, 401 | signOut, 402 | federatedSignIn, 403 | forgotPassword, 404 | forgotPasswordSubmit, 405 | getIdToken, 406 | changePassword, 407 | getCurrentUser, 408 | }; 409 | ``` 410 | 411 | #### Axios with Auth Helper Function 412 | 413 | When we make requests to our server, we'll need to send the authenticated user's id token in the authorization headers. Let's create a helper function that returns an axios instance with our users id token already in the headers. We'll place this function inside our **utility** folder. 414 | 415 | Getting a user's id token is an asynchronous operation, but this function will allow us to use axios similarly to what we are used to without chaining requests. All we have to do is pass in the specified method and path. The base URL will be set to the URL of our API. 416 | 417 | ###### src/utils/axiosWithAuth.js 418 | 419 | ```javascript 420 | import axios from "axios"; 421 | import { getIdToken } from "./cognitoAuth"; 422 | 423 | export async function axiosWithAuth(method, path) { 424 | try { 425 | const idToken = await getIdToken(); 426 | return await axios 427 | .create({ 428 | baseURL: "http://localhost:5000", 429 | headers: { 430 | "Content-Type": "application/json", 431 | Authorization: `Bearer ${idToken}`, 432 | }, 433 | }) 434 | [method](path); 435 | } catch (err) { 436 | throw err; 437 | } 438 | } 439 | ``` 440 | 441 | Here's an example of how we'd use this function: 442 | 443 | ```javascript 444 | axiosWithAuth('get', '/users').then(res => { 445 | console.log(res) 446 | }).catch(err => { 447 | console.log(err) 448 | }) 449 | ``` 450 | 451 | This would send a **GET** request to `http://localhost:5000/users`. 452 | 453 | 454 | 455 | ### Part Two: Creating components to utilize our Auth functions. 456 | 457 | Now it's time to start creating some components so we can put all our functions into action! 458 | 459 | Create a folder called **components** in the **src** directory. Inside this folder create a file called **authHub.js** and another folder called **auth**. We will create individual components for each of the authorization operations our app will use and store them in the **auth** folder. We'll import them into our **authHub.js** component so we can test them out. The state in our **authHub** component will keep track of the current user. It will also use Amplify's local eventing system called **Hub** to listen for authentication events. 460 | 461 | First we'll create the auth components. 462 | 463 | ###### src/components/auth/changePassword.js 464 | 465 | ```javascript 466 | import React, { useState } from "react"; 467 | import { changePassword } from "../../utils/cognitoAuth"; 468 | 469 | export default function ChangePassword() { 470 | const [oldPassword, setOldPassword] = useState(""); 471 | const [newPassword, setNewPassword] = useState(""); 472 | 473 | function handleChangePassword(e) { 474 | e.preventDefault(); 475 | changePassword(oldPassword, newPassword) 476 | .then((res) => { 477 | console.log(res); 478 | }) 479 | .catch((err) => { 480 | console.log(err); 481 | }); 482 | } 483 | return ( 484 | <> 485 |

Change password

486 |
487 | setOldPassword(e.target.value)} 492 | /> 493 | setNewPassword(e.target.value)} 498 | /> 499 | 500 |
501 | 502 | ); 503 | } 504 | ``` 505 | 506 | ###### src/components/auth/forgotPassword.js 507 | 508 | ```javascript 509 | import React, { useState } from "react"; 510 | import { forgotPassword } from "../../utils/cognitoAuth"; 511 | 512 | export default function ForgotPassword() { 513 | const [resetPasswordEmail, setResetPasswordEmail] = useState(""); 514 | 515 | function handleResetPassword(e) { 516 | e.preventDefault(); 517 | forgotPassword(resetPasswordEmail) 518 | .then((res) => console.log(res)) 519 | .catch((err) => console.log(err)); 520 | } 521 | 522 | return ( 523 | <> 524 |

Forgot password

525 |
526 | setResetPasswordEmail(e.target.value)} 531 | /> 532 | 533 |
534 | 535 | ); 536 | } 537 | ``` 538 | 539 | ###### src/components/auth/googleSignIn.js 540 | 541 | ```javascript 542 | import React from "react"; 543 | import { federatedSignIn } from "../../utils/cognitoAuth"; 544 | 545 | export default function GoogleSignIn() { 546 | return ( 547 | <> 548 |

Google Sign In

549 | 552 | 553 | ); 554 | } 555 | ``` 556 | 557 | ###### src/components/auth/logIn.js 558 | 559 | ```javascript 560 | import React, { useState } from "react"; 561 | import { signIn } from "../../utils/cognitoAuth"; 562 | 563 | export default function LogIn() { 564 | const [loginEmail, setLoginEmail] = useState(""); 565 | const [loginPassword, setLoginPassword] = useState(""); 566 | 567 | function handleLogin(e) { 568 | e.preventDefault(); 569 | signIn(loginEmail, loginPassword).catch(() => {}); 570 | } 571 | return ( 572 | <> 573 |

Login

574 |
575 | setLoginEmail(e.target.value)} 578 | value={loginEmail} 579 | /> 580 | setLoginPassword(e.target.value)} 583 | value={loginPassword} 584 | placeholder="password" 585 | /> 586 | 587 |
588 | 589 | ); 590 | } 591 | ``` 592 | 593 | ###### src/components/auth/register.js 594 | 595 | ```javascript 596 | import React, { useState } from "react"; 597 | import { signUp } from "../../utils/cognitoAuth"; 598 | 599 | export default function Register() { 600 | const [registerEmail, setRegisterEmail] = useState(""); 601 | const [registerPassword, setRegisterPassword] = useState(""); 602 | 603 | function handleRegister(e) { 604 | e.preventDefault(); 605 | signUp(registerEmail, registerPassword).catch((err) => 606 | console.log(err) 607 | ); 608 | } 609 | return ( 610 | <> 611 |

Register

612 |
613 | setRegisterEmail(e.target.value)} 616 | value={registerEmail} 617 | /> 618 | setRegisterPassword(e.target.value)} 621 | value={registerPassword} 622 | placeholder="password" 623 | /> 624 | 625 |
626 | 627 | ); 628 | } 629 | ``` 630 | 631 | ###### src/components/auth/resendVerificationCode.js 632 | 633 | ```javascript 634 | import React, { useState } from "react"; 635 | import { resendConfirmationCode } from "../../utils/cognitoAuth"; 636 | 637 | export default function ResendVerificationCode() { 638 | const [resendEmail, setResendEmail] = useState(""); 639 | function handleResendVerification(e) { 640 | e.preventDefault(); 641 | resendConfirmationCode(resendEmail) 642 | .then((res) => console.log(res)) 643 | .catch((err) => { 644 | console.log(err); 645 | }); 646 | } 647 | return ( 648 | <> 649 |

Resend Confirmation Code

650 |
651 | setResendEmail(e.target.value)} 656 | /> 657 | 658 |
659 | 660 | ); 661 | } 662 | ``` 663 | 664 | ###### src/components/auth/signOut.js 665 | 666 | ```javascript 667 | import React from "react"; 668 | import { signOut } from "../../utils/cognitoAuth"; 669 | 670 | export default function SignOut() { 671 | return ( 672 | <> 673 | 680 | 681 | ); 682 | } 683 | ``` 684 | 685 | ###### src/components/auth/verifyEmailWithCode.js 686 | 687 | ```javascript 688 | import React, { useState } from "react"; 689 | import { confirmSignUp } from "../../utils/cognitoAuth"; 690 | 691 | export default function VerifyEmailWithCode() { 692 | const [verifyEmail, setVerifyEmail] = useState(""); 693 | const [verificationCode, setVerificationCode] = useState(""); 694 | 695 | function handleVerification(e) { 696 | e.preventDefault(); 697 | confirmSignUp(verifyEmail, verificationCode) 698 | .then((res) => console.log(res)) 699 | .catch((err) => console.log(err)); 700 | } 701 | 702 | return ( 703 | <> 704 |

Verify Email With Code

705 |
706 | setVerifyEmail(e.target.value)} 709 | value={verifyEmail} 710 | /> 711 | setVerificationCode(e.target.value)} 713 | value={verificationCode} 714 | placeholder="verification code" 715 | /> 716 | 717 |
718 | 719 | ); 720 | } 721 | ``` 722 | 723 | ###### src/components/auth/verifyForgotPassword.js 724 | 725 | ```javascript 726 | import React, { useState } from "react"; 727 | import { forgotPasswordSubmit } from "../../utils/cognitoAuth"; 728 | 729 | export default function VerifyForgotPassword() { 730 | const [resetPasswordCode, setResetPasswordCode] = useState(""); 731 | const [resetNewPassword, setResetNewPassword] = useState(""); 732 | const [resetPasswordEmail, setResetPasswordEmail] = useState(""); 733 | 734 | function handleForgotPasswordSubmit(e) { 735 | e.preventDefault(); 736 | forgotPasswordSubmit( 737 | resetPasswordEmail, 738 | resetPasswordCode, 739 | resetNewPassword 740 | ) 741 | .then((res) => console.log(res)) 742 | .catch((err) => console.log(err)); 743 | } 744 | return ( 745 | <> 746 |

Verify Forgot Password

747 |
748 | setResetPasswordEmail(e.target.value)} 753 | /> 754 | setResetPasswordCode(e.target.value)} 759 | /> 760 | setResetNewPassword(e.target.value)} 765 | /> 766 | 767 |
768 | 769 | ); 770 | } 771 | ``` 772 | 773 | Let's add an **index.js** file inside our **folder**. We'll export our components from there so we can import them from one location. 774 | 775 | ###### /components/auth/index.js 776 | 777 | ```javascript 778 | import ChangePassword from "./ChangePassword"; 779 | import ForgotPassword from "./ForgotPassword"; 780 | import GoogleSignIn from "./GoogleSignIn"; 781 | import LogIn from "./LogIn"; 782 | import Register from "./Register"; 783 | import ResendVerificationCode from "./ResendVerificationCode"; 784 | import SignOut from "./SignOut"; 785 | import VerifyEmailWithCode from "./VerifyEmailWithCode"; 786 | import VerifyForgotPassword from "./VerifyForgotPassword"; 787 | 788 | export { 789 | ChangePassword, 790 | ForgotPassword, 791 | GoogleSignIn, 792 | LogIn, 793 | Register, 794 | ResendVerificationCode, 795 | SignOut, 796 | VerifyEmailWithCode, 797 | VerifyForgotPassword, 798 | }; 799 | ``` 800 | 801 | Now we'll import all our components into **Auth.js** and implement our **Hub** listnener. 802 | 803 | ###### src/components/authHub.js 804 | 805 | ```javascript 806 | import React, { useEffect, useState } from "react"; 807 | import { getCurrentUser } from "../utils/cognitoAuth"; 808 | import { Hub } from "aws-amplify"; 809 | 810 | import { 811 | ChangePassword, 812 | ForgotPassword, 813 | GoogleSignIn, 814 | LogIn, 815 | Register, 816 | ResendVerificationCode, 817 | SignOut, 818 | VerifyEmailWithCode, 819 | VerifyForgotPassword, 820 | } from "./auth"; 821 | 822 | export default function AuthHub() { 823 | const [currentUser, setCurrentUser] = useState(null); 824 | 825 | useEffect(() => { 826 | Hub.listen("auth", ({ payload: { event, data } }) => { 827 | switch (event) { 828 | case "signUp": 829 | console.log("User registered"); 830 | break; 831 | case "cognitoHostedUI": 832 | case "signIn": 833 | getCurrentUser() 834 | .then((userData) => { 835 | setCurrentUser(userData); 836 | }) 837 | .catch((err) => { 838 | console.log(err); 839 | }); 840 | break; 841 | case "signOut": 842 | setCurrentUser(null); 843 | break; 844 | case "signIn_failure": 845 | case "cognitoHostedUI_failure": 846 | console.log("Sign in failure", data); 847 | break; 848 | default: 849 | } 850 | }); 851 | 852 | getCurrentUser() 853 | .then((userData) => setCurrentUser(userData)) 854 | .catch((err) => console.log(err)); 855 | }, []); 856 | ``` 857 | 858 | Our **Hub** listener will emit an **event** each time an authentication action is performed. It uses a switch statement to perform certain actions depending on the event that is emitted. 859 | 860 | When our user signs in, it will store the user object in state. 861 | 862 | Finally, we'll lay out our components so we can test them out. We'll use conditional rendering to hide or show components based on the state of our authenticated user. 863 | 864 | ###### src/components/authHub.js 865 | 866 | ```javascript 867 | return ( 868 |
869 |

Auth Hub

870 | {!currentUser && ( 871 | <> 872 |

You are not signed in.

873 | 874 | 875 | 876 | 877 | 878 | 879 | )} 880 | {currentUser && ( 881 | <> 882 |

Hello {currentUser["email"]}

883 | 884 | 885 | )} 886 | 887 | {(!currentUser || !currentUser.email_verified) && ( 888 | <> 889 | 890 | 891 | 892 | )} 893 | 894 | {currentUser && !currentUser.identities && ( 895 | <> 896 | 897 | 898 | )} 899 |
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 | ![app start](https://res.cloudinary.com/markpkng/image/upload/v1594761622/cognito-react-node/start_app_sviko8.png) 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 | ![get users not logged in](https://res.cloudinary.com/markpkng/image/upload/v1594763004/cognito-react-node/get_users_not_signed_in_qzv5il.png) 1400 | 1401 | Now let's log in try to get users. 1402 | 1403 | ![get users logged in](https://res.cloudinary.com/markpkng/image/upload/v1594763244/cognito-react-node/logged_in_get_users_flvcrz.png) 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 | ![invalid token get users](https://res.cloudinary.com/markpkng/image/upload/v1594763463/cognito-react-node/get_users_unauthorized_itlfwk.png) 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 | ![deployed server](https://res.cloudinary.com/markpkng/image/upload/v1594767499/cognito-react-node/deployed_server_zpa6fm.png) 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 | ![knex success](https://res.cloudinary.com/markpkng/image/upload/v1594768499/cognito-react-node/knex_success_qebvga.png) 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 | ![its working](https://res.cloudinary.com/markpkng/image/upload/v1594768825/cognito-react-node/its_working_dbkksz.png) 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 | ![upload zip function](https://res.cloudinary.com/markpkng/image/upload/v1594770463/cognito-react-node/upload_zip_function_q8hytm.png) 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 | ![rds env variables](https://res.cloudinary.com/markpkng/image/upload/v1594771350/cognito-react-node/rds_env_variables_ghgxhj.png) 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 | ![lambda permissions](https://res.cloudinary.com/markpkng/image/upload/v1594775186/cognito-react-node/lambda_permissions_wqudki.png) 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("

Hello from the server side!

"); 17 | }); 18 | 19 | app.listen(PORT, () => { 20 | console.log(`app listening on port ${PORT}`); 21 | }); 22 | -------------------------------------------------------------------------------- /server/config/cognitoConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "71cs1vs1bkc1b6ai9q0olocjas", 3 | "userPool": "us-east-1_M3Gx3MjTH", 4 | "region": "us-east-1", 5 | "callbackUri": "http://localhost:3000", 6 | "signoutUri": "http://localhost:3000", 7 | "tokenScopes": [ 8 | "openid", 9 | "email", 10 | "profile", 11 | "aws.cognito.signin.user.admin" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /server/data/dbConfig.js: -------------------------------------------------------------------------------- 1 | const knex = require("knex"); 2 | const config = require("../knexfile.js"); 3 | const dbEnv = process.env.DB_ENV || "development"; 4 | 5 | module.exports = knex(config[dbEnv]); 6 | -------------------------------------------------------------------------------- /server/data/migrations/20200714161856_users.js: -------------------------------------------------------------------------------- 1 | exports.up = function (knex) { 2 | return knex.schema.createTable("users", (tbl) => { 3 | tbl.increments(); 4 | tbl.varchar("email", 255).notNullable().unique(); 5 | }); 6 | }; 7 | 8 | exports.down = function (knex) { 9 | return knex.schema.dropTableIfExists("users"); 10 | }; 11 | -------------------------------------------------------------------------------- /server/data/seeds/00-cleaner.js: -------------------------------------------------------------------------------- 1 | const cleaner = require("knex-cleaner"); 2 | 3 | exports.seed = function (knex) { 4 | return cleaner.clean(knex, { 5 | mode: "truncate", 6 | ignoreTables: ["knex_migrations", "knex_migrations_lock"], 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /server/data/seeds/01-users.js: -------------------------------------------------------------------------------- 1 | exports.seed = function (knex) { 2 | return knex("users").insert( 3 | [ 4 | { email: "testuser1@test.com" }, 5 | { email: "testuser2@test.com" }, 6 | { email: "testuser3@test.com" }, 7 | { email: "testuser4@test.com" }, 8 | ], 9 | "id" 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /server/knexfile.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | client: "sqlite3", 4 | useNullAsDefault: true, 5 | connection: { 6 | filename: "./data/dev.sqlite3", 7 | }, 8 | pool: { 9 | afterCreate: (conn, done) => { 10 | conn.run("PRAGMA foreign_keys = ON", done); 11 | }, 12 | }, 13 | migrations: { 14 | directory: "./data/migrations", 15 | }, 16 | seeds: { 17 | directory: "./data/seeds", 18 | }, 19 | }, 20 | 21 | testing: { 22 | client: "sqlite3", 23 | useNullAsDefault: true, 24 | connection: { 25 | filename: "./data/testing/test.sqlite3", 26 | }, 27 | pool: { 28 | afterCreate: (conn, done) => { 29 | conn.run("PRAGMA foreign_keys = ON", done); 30 | }, 31 | }, 32 | migrations: { 33 | directory: "./data/testing/migrations", 34 | }, 35 | seeds: { 36 | directory: "./data/testing/seeds", 37 | }, 38 | }, 39 | 40 | production: { 41 | client: "pg", 42 | connection: { 43 | host: process.env.RDS_HOSTNAME, 44 | user: process.env.RDS_USERNAME, 45 | password: process.env.RDS_PASSWORD, 46 | database: process.env.RDS_DB_NAME, 47 | }, 48 | migrations: { 49 | directory: "./data/migrations", 50 | }, 51 | seeds: { 52 | directory: "./data/seeds", 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /server/middleware/cognitoAuth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const cognitoConfig = require("../config/cognitoConfig.json"); 3 | const jwkToPem = require("jwk-to-pem"); 4 | const jwt = require("jsonwebtoken"); 5 | 6 | const JWKS_URL = `https://cognito-idp.${cognitoConfig.region}.amazonaws.com/${cognitoConfig.userPool}/.well-known/jwks.json`; 7 | 8 | class AuthErr extends Error {} 9 | 10 | async function getPems() { 11 | try { 12 | const { data } = await axios.get(`${JWKS_URL}`); 13 | if (!data || !data.keys) { 14 | throw Error("Error downloading JWKs"); 15 | } 16 | const pems = {}; 17 | for (let i = 0; i < data.keys.length; i++) { 18 | pems[data.keys[i].kid] = jwkToPem(data.keys[i]); 19 | } 20 | return pems; 21 | } catch (err) { 22 | console.log(`Error getting JWKs: ${err}`); 23 | throw Error("Error occured downloading JWKs"); 24 | } 25 | } 26 | 27 | async function verify(pems, auth) { 28 | const token = auth.substring(7); // remove 'Bearer ' from auth header 29 | const unverified = jwt.decode(token, { complete: true }); 30 | 31 | if (!unverified) { 32 | console.log(`Error decoding token.`); 33 | throw new AuthErr("Invalid JWT."); 34 | } else if (!unverified.header.kid || !pems[unverified.header.kid]) { 35 | console.log("Invalid JWT. KID not found."); 36 | throw new AuthErr("Invalid JWT."); 37 | } 38 | 39 | return jwt.verify( 40 | token, 41 | pems[unverified.header.kid], 42 | { 43 | issuer: JWKS_URL.substring( 44 | 0, 45 | JWKS_URL.indexOf("/.well-known/jwks.json") 46 | ), 47 | maxAge: 60 * 60, //3600 seconds 48 | }, 49 | (err, decoded) => { 50 | if (err) { 51 | console.log(`Invalid JWT: ${err}.`); 52 | throw new AuthErr( 53 | err instanceof jwt.TokenExpiredError 54 | ? `JWT expired.` 55 | : "Invalid JWT" 56 | ); 57 | } 58 | 59 | // Verify allowed token_use 60 | if (decoded.token_use !== "access" && decoded.token_use !== "id") { 61 | console.log( 62 | `token_use ${decoded.token_use} not "access" or "id".` 63 | ); 64 | throw new AuthErr("Invalid JWT."); 65 | } 66 | 67 | // Verify aud or client_id 68 | const clientId = decoded.aud || decoded.client_id; 69 | if (clientId !== cognitoConfig.clientId) { 70 | console.log( 71 | `Invalid JWT. Client id ${clientId} is not ${cognitoConfig.clientId}.` 72 | ); 73 | throw new AuthErr("Invalid JWT."); 74 | } 75 | return decoded; 76 | } 77 | ); 78 | } 79 | 80 | exports.getCognitoMiddleware = () => (req, res, next) => { 81 | (async () => { 82 | try { 83 | const { token_use, scope, email, db_user_id } = await verify( 84 | await getPems(), 85 | req.get("Authorization") 86 | ); 87 | req.user = { token_use }; 88 | if (token_use === "access") { 89 | req.user.scope = scope.split(" "); 90 | } else if (token_use === "id") { 91 | req.user.email = email; 92 | req.user.id = db_user_id; 93 | } 94 | next(); 95 | } catch (err) { 96 | console.log(err); 97 | res.status(err instanceof AuthErr ? 401 : 500).send( 98 | err.message || err 99 | ); 100 | } 101 | })(); 102 | }; 103 | -------------------------------------------------------------------------------- /server/models/usersModel.js: -------------------------------------------------------------------------------- 1 | const db = require("../data/dbConfig"); 2 | 3 | function getUsers() { 4 | return db("users"); 5 | } 6 | 7 | module.exports = { getUsers }; 8 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.19.2", 16 | "cors": "^2.8.5", 17 | "express": "^4.17.1", 18 | "jsonwebtoken": "^8.5.1", 19 | "jwk-to-pem": "^2.0.4", 20 | "knex": "^0.21.2", 21 | "knex-cleaner": "^1.3.0", 22 | "pg": "^8.3.0" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^2.0.4", 26 | "sqlite3": "^5.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/routers/usersRouter.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { getUsers } = require("../models/usersModel"); 3 | 4 | router.get("/", async (req, res) => { 5 | try { 6 | const users = await getUsers(); 7 | res.status(200).send(users); 8 | } catch (err) { 9 | console.log(err); 10 | res.status(500).send("Error retrieving users."); 11 | } 12 | }); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /server/scripts/knex.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export $(grep -v '^#' ../../../../opt/elasticbeanstalk/deployment/env | xargs) 4 | sudo RDS_DB_NAME=${RDS_DB_NAME} \ 5 | RDS_HOSTNAME=${RDS_HOSTNAME} \ 6 | RDS_USERNAME=${RDS_USERNAME} \ 7 | RDS_PASSWORD=${RDS_PASSWORD} \ 8 | npx knex $1 --env production --------------------------------------------------------------------------------