├── .env ├── src ├── theme.js ├── UserContext.js ├── aws-exports-example.js ├── Home.js ├── Protected.js ├── setupTests.js ├── App.test.js ├── index.css ├── index.js ├── App.css ├── ProtectedRoute.js ├── App.js ├── Router.js ├── logo.svg ├── serviceWorker.js └── Profile.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── README.md ├── amplify.json ├── amplify ├── backend │ ├── backend-config.json │ └── auth │ │ └── customauthenticationd831e6d5 │ │ ├── parameters.json │ │ └── customauthenticationd831e6d5-cloudformation-template.yml ├── .config │ └── project-config.json └── team-provider-info.json ├── .gitignore └── package.json /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | primaryColor: '#007bff' 3 | } 4 | 5 | export default theme -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/full-stack-serverless/react-custom-authentication-aws/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/full-stack-serverless/react-custom-authentication-aws/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/full-stack-serverless/react-custom-authentication-aws/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/UserContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const AuthContext = React.createContext('user'); 3 | export default AuthContext; -------------------------------------------------------------------------------- /src/aws-exports-example.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | Auth: { 3 | region: "", 4 | userPoolId: "", 5 | userPoolWebClientId: "" 6 | } 7 | } 8 | 9 | export default config; -------------------------------------------------------------------------------- /src/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Hello from Home component

7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /src/Protected.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Protected() { 4 | return ( 5 |
6 |

Hello from Protected component

7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom authentication with Amazon Cognito, React, React Router, and Amplify JS 2 | 3 | This application shows how to build a custom authentication flow complete with routing and protected routes. 4 | -------------------------------------------------------------------------------- /amplify.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": 3 | { 4 | "graphqltransformer": 5 | { 6 | "transformerversion": 5 7 | }, 8 | "keytransformer": 9 | { 10 | "defaultquery": true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "customauthenticationd831e6d5": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "customauthentication", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 15 | * { 16 | font-family: 'Raleway', sans-serif; 17 | } 18 | 19 | input:-webkit-autofill, 20 | input:-webkit-autofill:hover, 21 | input:-webkit-autofill:focus, 22 | input:-webkit-autofill:active { 23 | -webkit-box-shadow: 0 0 0 30px white inset !important; 24 | } -------------------------------------------------------------------------------- /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 | import config from './aws-exports'; 8 | import Amplify from 'aws-amplify'; 9 | Amplify.configure(config); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 25 | #amplify 26 | amplify/\#current-cloud-backend 27 | amplify/.config/local-* 28 | amplify/mock-data 29 | amplify/backend/amplify-meta.json 30 | amplify/backend/awscloudformation 31 | build/ 32 | dist/ 33 | node_modules/ 34 | aws-exports.js 35 | awsconfiguration.json 36 | amplifyconfiguration.json 37 | amplify-build-config.json 38 | amplify-gradle-config.json 39 | amplifytools.xcconfig -------------------------------------------------------------------------------- /src/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { 3 | useLocation, 4 | Route, 5 | Redirect, 6 | } from 'react-router-dom' 7 | 8 | import UserContext from './UserContext'; 9 | 10 | export default function PrivateRoute({ 11 | component: Component, ...rest 12 | }) { 13 | const context = useContext(UserContext); 14 | 15 | let location = useLocation(); 16 | useEffect(() => { 17 | context.updateCurrentUser() 18 | }, [location]); 19 | 20 | const isAuthenticated = context.user && context.user.username ? true : false 21 | const isLoaded = context.isLoaded 22 | if (!isLoaded) return null 23 | 24 | return ( 25 | { 28 | return isAuthenticated ? ( 29 | 30 | ) : ( 31 | 36 | ) 37 | }} 38 | /> 39 | ) 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-authentication", 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.23", 10 | "emotion": "^10.0.27", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "3.4.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-customauthentication-dev-215319-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::557458351015:role/amplify-customauthentication-dev-215319-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::557458351015:role/amplify-customauthentication-dev-215319-authRole", 7 | "Region": "us-east-1", 8 | "DeploymentBucketName": "amplify-customauthentication-dev-215319-deployment", 9 | "UnauthRoleName": "amplify-customauthentication-dev-215319-unauthRole", 10 | "StackName": "amplify-customauthentication-dev-215319", 11 | "StackId": "arn:aws:cloudformation:us-east-1:557458351015:stack/amplify-customauthentication-dev-215319/1ac8eb00-d9e3-11ea-a24d-0a91287fb5a1", 12 | "AmplifyAppId": "d260cj5ptorxkp" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "customauthenticationd831e6d5": {} 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Auth, Hub } from 'aws-amplify' 3 | import './App.css'; 4 | 5 | import UserContext from './UserContext'; 6 | import Router from './Router'; 7 | 8 | function App() { 9 | const [currentUser, setCurrentUser] = useState({}); 10 | const [isLoaded, setIsLoaded] = useState({}); 11 | useEffect(() => { 12 | updateCurrentUser(); 13 | listen(); 14 | }, []) 15 | function listen() { 16 | Hub.listen('auth', (data) => { 17 | if (data.payload.event === 'signIn') { 18 | updateCurrentUser(); 19 | } 20 | if (data.payload.event === 'signOut') { 21 | updateCurrentUser(); 22 | } 23 | }); 24 | } 25 | async function updateCurrentUser(user = null) { 26 | if (user) { 27 | setCurrentUser(user); 28 | return 29 | } 30 | try { 31 | const user = await Auth.currentAuthenticatedUser() 32 | setCurrentUser(user); 33 | setIsLoaded(true); 34 | } catch (err) { 35 | setCurrentUser(null); 36 | setIsLoaded(true); 37 | } 38 | } 39 | return ( 40 | 45 | 46 | 47 | ); 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /amplify/backend/auth/customauthenticationd831e6d5/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "customauthenticationd831e6d5_identitypool_d831e6d5", 3 | "allowUnauthenticatedIdentities": false, 4 | "resourceNameTruncated": "customd831e6d5", 5 | "userPoolName": "customauthenticationd831e6d5_userpool_d831e6d5", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "customd831e6d5_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "sharedId": "d831e6d5", 34 | "resourceName": "customauthenticationd831e6d5", 35 | "authSelections": "identityPoolAndUserPool", 36 | "authRoleArn": { 37 | "Fn::GetAtt": [ 38 | "AuthRole", 39 | "Arn" 40 | ] 41 | }, 42 | "unauthRoleArn": { 43 | "Fn::GetAtt": [ 44 | "UnauthRole", 45 | "Arn" 46 | ] 47 | }, 48 | "useDefault": "default", 49 | "userPoolGroupList": [], 50 | "dependsOn": [] 51 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 29 | React App 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter, Route, Link, Switch 4 | } from 'react-router-dom'; 5 | import { css } from 'emotion'; 6 | 7 | import Home from './Home'; 8 | import Profile from './Profile'; 9 | import Protected from './Protected'; 10 | import ProtectedRoute from './ProtectedRoute'; 11 | 12 | import theme from './theme'; 13 | 14 | const { primaryColor } = theme; 15 | 16 | export default function Router() { 17 | return ( 18 | 19 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | { /* For a protected route, use the ProtectedRoute component */ } 44 | 49 | 50 |
51 |
52 | ) 53 | } 54 | 55 | const linkTitleStyle = css` 56 | text-decoration: none; 57 | ` 58 | 59 | const containerStyle = css` 60 | padding: 0px 30px; 61 | ` 62 | 63 | const linkStyle = css` 64 | color: white; 65 | text-decoration: none; 66 | margin-left: 10px; 67 | font-size: 18px; 68 | &:hover { 69 | color: #ddd; 70 | } 71 | ` 72 | 73 | const headingStyle = css` 74 | background-color: ${primaryColor}; 75 | padding: 30px; 76 | display: flex; 77 | align-items: center; 78 | ` 79 | 80 | const titleStyle = css` 81 | color: white; 82 | margin: 0; 83 | font-size: 32px; 84 | margin-right: 50px; 85 | ` -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { css } from 'emotion'; 3 | import theme from './theme'; 4 | import { Auth } from 'aws-amplify'; 5 | 6 | const intitialFormState = { 7 | username: '', 8 | email: '', 9 | password: '', 10 | authCode: '', 11 | formType: 'signUp' 12 | } 13 | 14 | const { primaryColor } = theme; 15 | 16 | export default function Profile() { 17 | const [formState, setFormState] = useState(intitialFormState); 18 | const [loading, setLoading] = useState(true); 19 | const { username, email, password, authCode, formType } = formState; 20 | const [user, setUser] = useState(null); 21 | useEffect(() => { 22 | checkUser(); 23 | }, []); 24 | 25 | async function checkUser() { 26 | try { 27 | const userData = await Auth.currentAuthenticatedUser(); 28 | setUser(userData); 29 | setFormState(() => ({ ...formState, formType: 'signedIn' })); 30 | setLoading(false); 31 | } catch (err) { 32 | setUser(null); 33 | setLoading(false); 34 | } 35 | } 36 | 37 | function onChange(e) { 38 | e.persist(); 39 | setFormState(() => ({ ...formState, [e.target.name]: e.target.value })); 40 | } 41 | 42 | function toggleFormType(formType) { 43 | setFormState(state => ({ ...state, formType })); 44 | } 45 | 46 | async function signUp() { 47 | if (!username || !email || !password) return; 48 | try { 49 | await Auth.signUp({ 50 | username, password, attributes: { email } 51 | }); 52 | setFormState(() => ({ ...formState, formType: 'confirmSignUp' })); 53 | } catch (err) { 54 | console.log('error signing up: ', err); 55 | } 56 | } 57 | async function signIn() { 58 | if (!username || !password) return; 59 | try { 60 | const user = await Auth.signIn(username, password); 61 | setUser(user); 62 | setFormState(() => ({ ...formState, formType: 'signedIn' })); 63 | } catch (err) { 64 | console.log('error signing in: ', err); 65 | } 66 | } 67 | async function confirmSignUp() { 68 | if (!authCode) return; 69 | try { 70 | await Auth.confirmSignUp(username, authCode); 71 | await signIn(); 72 | } catch (err) { 73 | console.log('error confirming signing up: ', err); 74 | } 75 | } 76 | async function signOut() { 77 | await Auth.signOut(); 78 | setFormState(() => ({ ...formState, formType: 'signUp' })); 79 | setUser(null); 80 | } 81 | if (loading) return null 82 | return ( 83 |
84 | { 85 | formType === 'signUp' && ( 86 |
87 | 94 | 102 | 109 | 112 |

toggleFormType('signIn')} className={stateToggleStyle}> 113 | Already signed up? Sign in. 114 |

115 |
116 | ) 117 | } 118 | { 119 | formType === 'confirmSignUp' && ( 120 |
121 | 128 | 131 |
132 | ) 133 | } 134 | { 135 | formType === 'signIn' && ( 136 |
137 | 144 | 152 | 155 |

toggleFormType('signUp')} className={stateToggleStyle}> 156 | Need an account? Sign up. 157 |

158 |
159 | ) 160 | } 161 | { 162 | formType === 'signedIn' && ( 163 |
164 |

Hello, {user.username}

165 | 168 |
169 | ) 170 | } 171 |
172 | ) 173 | } 174 | 175 | const profileContainerStyle = css` 176 | display: flex; 177 | flex-direction: column; 178 | ` 179 | 180 | const stateToggleStyle = css` 181 | color: ${primaryColor}; 182 | cursor: pointer; 183 | text-align: center; 184 | margin-top: 25px; 185 | &:hover { 186 | opacity: .8; 187 | } 188 | ` 189 | 190 | const formContainerStyle = css` 191 | display: flex; 192 | flex-direction: column; 193 | width: 400px; 194 | margin: 0 auto; 195 | margin-top: 100px; 196 | ` 197 | 198 | const inputStyle = css` 199 | outline: none; 200 | border: none; 201 | border-bottom: 2px solid ${primaryColor}; 202 | margin: 4px 0px 0px; 203 | height: 40px; 204 | font-size: 20px; 205 | ` 206 | 207 | const buttonStyle = css` 208 | outline: none; 209 | border: none; 210 | cursor: pointer; 211 | font-size: 18px; 212 | margin-top: 20px; 213 | background-color: ${primaryColor}; 214 | color: white; 215 | height: 50px; 216 | text-shadow: 1px 1px 1px 1px rgba(0, 0, 0, .2); 217 | box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, .2); 218 | &:hover { 219 | opacity: .85; 220 | } 221 | ` -------------------------------------------------------------------------------- /amplify/backend/auth/customauthenticationd831e6d5/customauthenticationd831e6d5-cloudformation-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | env: 5 | Type: String 6 | authRoleArn: 7 | Type: String 8 | unauthRoleArn: 9 | Type: String 10 | 11 | 12 | 13 | 14 | identityPoolName: 15 | Type: String 16 | 17 | allowUnauthenticatedIdentities: 18 | Type: String 19 | 20 | resourceNameTruncated: 21 | Type: String 22 | 23 | userPoolName: 24 | Type: String 25 | 26 | autoVerifiedAttributes: 27 | Type: CommaDelimitedList 28 | 29 | mfaConfiguration: 30 | Type: String 31 | 32 | mfaTypes: 33 | Type: CommaDelimitedList 34 | 35 | smsAuthenticationMessage: 36 | Type: String 37 | 38 | smsVerificationMessage: 39 | Type: String 40 | 41 | emailVerificationSubject: 42 | Type: String 43 | 44 | emailVerificationMessage: 45 | Type: String 46 | 47 | defaultPasswordPolicy: 48 | Type: String 49 | 50 | passwordPolicyMinLength: 51 | Type: Number 52 | 53 | passwordPolicyCharacters: 54 | Type: CommaDelimitedList 55 | 56 | requiredAttributes: 57 | Type: CommaDelimitedList 58 | 59 | userpoolClientGenerateSecret: 60 | Type: String 61 | 62 | userpoolClientRefreshTokenValidity: 63 | Type: Number 64 | 65 | userpoolClientWriteAttributes: 66 | Type: CommaDelimitedList 67 | 68 | userpoolClientReadAttributes: 69 | Type: CommaDelimitedList 70 | 71 | userpoolClientLambdaRole: 72 | Type: String 73 | 74 | userpoolClientSetAttributes: 75 | Type: String 76 | 77 | sharedId: 78 | Type: String 79 | 80 | resourceName: 81 | Type: String 82 | 83 | authSelections: 84 | Type: String 85 | 86 | useDefault: 87 | Type: String 88 | 89 | userPoolGroupList: 90 | Type: CommaDelimitedList 91 | 92 | dependsOn: 93 | Type: CommaDelimitedList 94 | 95 | Conditions: 96 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] 97 | 98 | Resources: 99 | 100 | 101 | # BEGIN SNS ROLE RESOURCE 102 | SNSRole: 103 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process 104 | Type: AWS::IAM::Role 105 | Properties: 106 | RoleName: !If [ShouldNotCreateEnvResources, 'customd831e6d5_sns-role', !Join ['',[ 'sns', 'd831e6d5', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 107 | AssumeRolePolicyDocument: 108 | Version: "2012-10-17" 109 | Statement: 110 | - Sid: "" 111 | Effect: "Allow" 112 | Principal: 113 | Service: "cognito-idp.amazonaws.com" 114 | Action: 115 | - "sts:AssumeRole" 116 | Condition: 117 | StringEquals: 118 | sts:ExternalId: customd831e6d5_role_external_id 119 | Policies: 120 | - 121 | PolicyName: customd831e6d5-sns-policy 122 | PolicyDocument: 123 | Version: "2012-10-17" 124 | Statement: 125 | - 126 | Effect: "Allow" 127 | Action: 128 | - "sns:Publish" 129 | Resource: "*" 130 | # BEGIN USER POOL RESOURCES 131 | UserPool: 132 | # Created upon user selection 133 | # Depends on SNS Role for Arn if MFA is enabled 134 | Type: AWS::Cognito::UserPool 135 | UpdateReplacePolicy: Retain 136 | Properties: 137 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]] 138 | 139 | Schema: 140 | 141 | - 142 | Name: email 143 | Required: true 144 | Mutable: true 145 | 146 | 147 | 148 | 149 | AutoVerifiedAttributes: !Ref autoVerifiedAttributes 150 | 151 | 152 | EmailVerificationMessage: !Ref emailVerificationMessage 153 | EmailVerificationSubject: !Ref emailVerificationSubject 154 | 155 | Policies: 156 | PasswordPolicy: 157 | MinimumLength: !Ref passwordPolicyMinLength 158 | RequireLowercase: false 159 | RequireNumbers: false 160 | RequireSymbols: false 161 | RequireUppercase: false 162 | 163 | MfaConfiguration: !Ref mfaConfiguration 164 | SmsVerificationMessage: !Ref smsVerificationMessage 165 | SmsConfiguration: 166 | SnsCallerArn: !GetAtt SNSRole.Arn 167 | ExternalId: customd831e6d5_role_external_id 168 | 169 | 170 | UserPoolClientWeb: 171 | # Created provide application access to user pool 172 | # Depends on UserPool for ID reference 173 | Type: "AWS::Cognito::UserPoolClient" 174 | Properties: 175 | ClientName: customd831e6d5_app_clientWeb 176 | 177 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 178 | UserPoolId: !Ref UserPool 179 | DependsOn: UserPool 180 | UserPoolClient: 181 | # Created provide application access to user pool 182 | # Depends on UserPool for ID reference 183 | Type: "AWS::Cognito::UserPoolClient" 184 | Properties: 185 | ClientName: customd831e6d5_app_client 186 | 187 | GenerateSecret: !Ref userpoolClientGenerateSecret 188 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 189 | UserPoolId: !Ref UserPool 190 | DependsOn: UserPool 191 | # BEGIN USER POOL LAMBDA RESOURCES 192 | UserPoolClientRole: 193 | # Created to execute Lambda which gets userpool app client config values 194 | Type: 'AWS::IAM::Role' 195 | Properties: 196 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',['upClientLambdaRole', 'd831e6d5', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 197 | AssumeRolePolicyDocument: 198 | Version: '2012-10-17' 199 | Statement: 200 | - Effect: Allow 201 | Principal: 202 | Service: 203 | - lambda.amazonaws.com 204 | Action: 205 | - 'sts:AssumeRole' 206 | DependsOn: UserPoolClient 207 | UserPoolClientLambda: 208 | # Lambda which gets userpool app client config values 209 | # Depends on UserPool for id 210 | # Depends on UserPoolClientRole for role ARN 211 | Type: 'AWS::Lambda::Function' 212 | Properties: 213 | Code: 214 | ZipFile: !Join 215 | - |+ 216 | - - 'const response = require(''cfn-response'');' 217 | - 'const aws = require(''aws-sdk'');' 218 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 219 | - 'exports.handler = (event, context, callback) => {' 220 | - ' if (event.RequestType == ''Delete'') { ' 221 | - ' response.send(event, context, response.SUCCESS, {})' 222 | - ' }' 223 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 224 | - ' const params = {' 225 | - ' ClientId: event.ResourceProperties.clientId,' 226 | - ' UserPoolId: event.ResourceProperties.userpoolId' 227 | - ' };' 228 | - ' identity.describeUserPoolClient(params).promise()' 229 | - ' .then((res) => {' 230 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});' 231 | - ' })' 232 | - ' .catch((err) => {' 233 | - ' response.send(event, context, response.FAILED, {err});' 234 | - ' });' 235 | - ' }' 236 | - '};' 237 | Handler: index.handler 238 | Runtime: nodejs10.x 239 | Timeout: '300' 240 | Role: !GetAtt 241 | - UserPoolClientRole 242 | - Arn 243 | DependsOn: UserPoolClientRole 244 | UserPoolClientLambdaPolicy: 245 | # Sets userpool policy for the role that executes the Userpool Client Lambda 246 | # Depends on UserPool for Arn 247 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing 248 | Type: 'AWS::IAM::Policy' 249 | Properties: 250 | PolicyName: customd831e6d5_userpoolclient_lambda_iam_policy 251 | Roles: 252 | - !Ref UserPoolClientRole 253 | PolicyDocument: 254 | Version: '2012-10-17' 255 | Statement: 256 | - Effect: Allow 257 | Action: 258 | - 'cognito-idp:DescribeUserPoolClient' 259 | Resource: !GetAtt UserPool.Arn 260 | DependsOn: UserPoolClientLambda 261 | UserPoolClientLogPolicy: 262 | # Sets log policy for the role that executes the Userpool Client Lambda 263 | # Depends on UserPool for Arn 264 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 265 | Type: 'AWS::IAM::Policy' 266 | Properties: 267 | PolicyName: customd831e6d5_userpoolclient_lambda_log_policy 268 | Roles: 269 | - !Ref UserPoolClientRole 270 | PolicyDocument: 271 | Version: 2012-10-17 272 | Statement: 273 | - Effect: Allow 274 | Action: 275 | - 'logs:CreateLogGroup' 276 | - 'logs:CreateLogStream' 277 | - 'logs:PutLogEvents' 278 | Resource: !Sub 279 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 280 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda} 281 | DependsOn: UserPoolClientLambdaPolicy 282 | UserPoolClientInputs: 283 | # Values passed to Userpool client Lambda 284 | # Depends on UserPool for Id 285 | # Depends on UserPoolClient for Id 286 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 287 | Type: 'Custom::LambdaCallout' 288 | Properties: 289 | ServiceToken: !GetAtt UserPoolClientLambda.Arn 290 | clientId: !Ref UserPoolClient 291 | userpoolId: !Ref UserPool 292 | DependsOn: UserPoolClientLogPolicy 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | # BEGIN IDENTITY POOL RESOURCES 301 | 302 | 303 | IdentityPool: 304 | # Always created 305 | Type: AWS::Cognito::IdentityPool 306 | Properties: 307 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'customauthenticationd831e6d5_identitypool_d831e6d5', !Join ['',['customauthenticationd831e6d5_identitypool_d831e6d5', '__', !Ref env]]] 308 | 309 | CognitoIdentityProviders: 310 | - ClientId: !Ref UserPoolClient 311 | ProviderName: !Sub 312 | - cognito-idp.${region}.amazonaws.com/${client} 313 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 314 | - ClientId: !Ref UserPoolClientWeb 315 | ProviderName: !Sub 316 | - cognito-idp.${region}.amazonaws.com/${client} 317 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 318 | 319 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities 320 | 321 | 322 | DependsOn: UserPoolClientInputs 323 | 324 | 325 | IdentityPoolRoleMap: 326 | # Created to map Auth and Unauth roles to the identity pool 327 | # Depends on Identity Pool for ID ref 328 | Type: AWS::Cognito::IdentityPoolRoleAttachment 329 | Properties: 330 | IdentityPoolId: !Ref IdentityPool 331 | Roles: 332 | unauthenticated: !Ref unauthRoleArn 333 | authenticated: !Ref authRoleArn 334 | DependsOn: IdentityPool 335 | 336 | 337 | Outputs : 338 | 339 | IdentityPoolId: 340 | Value: !Ref 'IdentityPool' 341 | Description: Id for the identity pool 342 | IdentityPoolName: 343 | Value: !GetAtt IdentityPool.Name 344 | 345 | 346 | 347 | 348 | UserPoolId: 349 | Value: !Ref 'UserPool' 350 | Description: Id for the user pool 351 | UserPoolName: 352 | Value: !Ref userPoolName 353 | AppClientIDWeb: 354 | Value: !Ref 'UserPoolClientWeb' 355 | Description: The user pool app client id for web 356 | AppClientID: 357 | Value: !Ref 'UserPoolClient' 358 | Description: The user pool app client id 359 | AppClientSecret: 360 | Value: !GetAtt UserPoolClientInputs.appSecret 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | --------------------------------------------------------------------------------