├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── client ├── .eslintrc ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── aws-exports.js │ ├── index.js │ └── serviceWorker.js └── yarn.lock ├── lib ├── .eslintrc ├── functions │ └── cognitoEvents.js └── index.ts ├── package.json ├── sample ├── cdk.json └── index.ts ├── test ├── __snapshots__ │ └── passwordless.test.js.snap └── passwordless.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "plugins": ["jsx-a11y", "react"], 5 | "env": { 6 | "browser": true, 7 | "commonjs": true, 8 | "es6": true, 9 | "jest": true, 10 | "node": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "generators": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | "settings": { 22 | "import/ignore": [ 23 | "node_modules", 24 | "\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$" 25 | ], 26 | "import/extensions": [".js"], 27 | "import/resolver": { 28 | "node": { 29 | "extensions": [".js", ".json"] 30 | } 31 | } 32 | }, 33 | "rules": { 34 | "array-callback-return": "warn", 35 | "camelcase": "warn", 36 | "curly": "warn", 37 | "default-case": [ 38 | "warn", 39 | { 40 | "commentPattern": "^no default$" 41 | } 42 | ], 43 | "dot-location": ["warn", "property"], 44 | "eol-last": "warn", 45 | "eqeqeq": ["warn", "always"], 46 | "indent": [ 47 | "warn", 48 | 2, 49 | { 50 | "SwitchCase": 1 51 | } 52 | ], 53 | "guard-for-in": "warn", 54 | "keyword-spacing": "warn", 55 | "new-parens": "warn", 56 | "no-array-constructor": "warn", 57 | "no-caller": "warn", 58 | "no-cond-assign": ["warn", "always"], 59 | "no-const-assign": "warn", 60 | "no-control-regex": "warn", 61 | "no-delete-var": "warn", 62 | "no-dupe-args": "warn", 63 | "no-dupe-class-members": "warn", 64 | "no-dupe-keys": "warn", 65 | "no-duplicate-case": "warn", 66 | "no-empty-character-class": "warn", 67 | "no-empty-pattern": "warn", 68 | "no-eval": "warn", 69 | "no-ex-assign": "warn", 70 | "no-extend-native": "warn", 71 | "no-extra-bind": "warn", 72 | "no-extra-label": "warn", 73 | "no-fallthrough": "warn", 74 | "no-func-assign": "warn", 75 | "no-global-assign": "warn", 76 | "no-implied-eval": "warn", 77 | "no-invalid-regexp": "warn", 78 | "no-iterator": "warn", 79 | "no-label-var": "warn", 80 | "no-labels": [ 81 | "warn", 82 | { 83 | "allowLoop": false, 84 | "allowSwitch": false 85 | } 86 | ], 87 | "no-lone-blocks": "warn", 88 | "no-loop-func": "warn", 89 | "no-mixed-operators": [ 90 | "warn", 91 | { 92 | "groups": [ 93 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 94 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 95 | ["&&", "||"], 96 | ["in", "instanceof"] 97 | ], 98 | "allowSamePrecedence": false 99 | } 100 | ], 101 | "no-multi-str": "warn", 102 | "no-new-func": "warn", 103 | "no-new-object": "warn", 104 | "no-new-symbol": "warn", 105 | "no-new-wrappers": "warn", 106 | "no-obj-calls": "warn", 107 | "no-octal": "warn", 108 | "no-octal-escape": "warn", 109 | "no-redeclare": "warn", 110 | "no-regex-spaces": "warn", 111 | "no-restricted-syntax": ["warn", "LabeledStatement", "WithStatement"], 112 | "no-script-url": "warn", 113 | "no-self-assign": "warn", 114 | "no-self-compare": "warn", 115 | "no-sequences": "warn", 116 | "no-shadow-restricted-names": "warn", 117 | "no-sparse-arrays": "warn", 118 | "no-template-curly-in-string": "warn", 119 | "no-this-before-super": "warn", 120 | "no-throw-literal": "warn", 121 | "no-undef": "warn", 122 | "no-unexpected-multiline": "warn", 123 | "no-unreachable": "warn", 124 | "no-unsafe-negation": "warn", 125 | "no-unused-expressions": "warn", 126 | "no-unused-labels": "warn", 127 | "no-unused-vars": [ 128 | "warn", 129 | { 130 | "vars": "local", 131 | "args": "none" 132 | } 133 | ], 134 | "no-use-before-define": ["warn", "nofunc"], 135 | "no-useless-computed-key": "warn", 136 | "no-useless-concat": "warn", 137 | "no-useless-constructor": "warn", 138 | "no-useless-escape": "warn", 139 | "no-useless-rename": [ 140 | "warn", 141 | { 142 | "ignoreDestructuring": false, 143 | "ignoreImport": false, 144 | "ignoreExport": false 145 | } 146 | ], 147 | "no-with": "warn", 148 | "no-whitespace-before-property": "warn", 149 | "object-curly-spacing": ["warn", "always"], 150 | "operator-assignment": ["warn", "always"], 151 | "radix": "warn", 152 | "require-yield": "warn", 153 | "rest-spread-spacing": ["warn", "never"], 154 | "semi": "warn", 155 | "strict": ["warn", "never"], 156 | "unicode-bom": ["warn", "never"], 157 | "use-isnan": "warn", 158 | "valid-typeof": "warn", 159 | "react/jsx-boolean-value": "warn", 160 | "react/jsx-closing-bracket-location": "warn", 161 | "react/jsx-curly-spacing": "warn", 162 | "react/jsx-equals-spacing": ["warn", "never"], 163 | "react/jsx-first-prop-new-line": ["warn", "multiline"], 164 | "react/jsx-handler-names": "warn", 165 | "react/jsx-indent": ["warn", 2], 166 | "react/jsx-indent-props": ["warn", 2], 167 | "react/jsx-key": "warn", 168 | "react/jsx-max-props-per-line": "warn", 169 | "react/jsx-no-bind": [ 170 | "warn", 171 | { 172 | "allowArrowFunctions": true 173 | } 174 | ], 175 | "react/jsx-no-comment-textnodes": "warn", 176 | "react/jsx-no-duplicate-props": [ 177 | "warn", 178 | { 179 | "ignoreCase": true 180 | } 181 | ], 182 | "react/jsx-no-undef": "warn", 183 | "react/jsx-pascal-case": [ 184 | "warn", 185 | { 186 | "allowAllCaps": true, 187 | "ignore": [] 188 | } 189 | ], 190 | "react/jsx-tag-spacing": "warn", 191 | "react/jsx-uses-react": "warn", 192 | "react/jsx-uses-vars": "warn", 193 | "react/jsx-wrap-multilines": "warn", 194 | "react/no-deprecated": "warn", 195 | "react/no-did-mount-set-state": "warn", 196 | "react/no-did-update-set-state": "warn", 197 | "react/no-direct-mutation-state": "warn", 198 | "react/no-is-mounted": "warn", 199 | "react/no-unused-prop-types": "warn", 200 | "react/prefer-es6-class": "warn", 201 | "react/prefer-stateless-function": "warn", 202 | "react/prop-types": "warn", 203 | "react/react-in-jsx-scope": "warn", 204 | "react/require-render-return": "warn", 205 | "react/self-closing-comp": "warn", 206 | "react/style-prop-object": "warn", 207 | "react/void-dom-elements-no-children": "warn", 208 | "jsx-a11y/aria-role": "warn", 209 | "jsx-a11y/img-redundant-alt": "warn", 210 | "jsx-a11y/no-access-key": "warn" 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !cognitoEvents.js 3 | !client/*/** 4 | *.d.ts 5 | node_modules 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | !*.js 4 | 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | client/* 10 | sample/* 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS CDK PasswordLess Construct 2 | 3 | An AWS CDK construct for creating passwordless authentication resources on AWS. 4 | This Construct will create following resources with their configuration: 5 | 6 | - Cognito User Pool 7 | - Cognito Pool Client 8 | - Cognito Lambda Trigger 9 | 10 | # Usage 11 | 12 | ```bash 13 | yarn add aws-cdk-passwordless 14 | ``` 15 | 16 | ```js 17 | import { CdkPasswordless } from "aws-cdk-passwordless"; 18 | 19 | 20 | new CdkPasswordless(this, "myPasswordLess", { 21 | mailSubject: "myStack - signIn", // subject of the mail arriving with code to confirm 22 | userPoolClientName: "myClientName", 23 | verifiedDomains: ["gmail.com"], // emails with the domains that are allow to signup 24 | postConfirmationLambda: lambda.Function(...) // passing a lambda which will be triggered after code confirmation 25 | }); 26 | ``` 27 | 28 | # note 29 | 30 | There is a sample folder showing how to deploy a stack using this construct. 31 | Additionally, There is a very simple Web Demo Client which shows how passwordless authentication can be done on the client side. It uses AWS Amplify. 32 | 33 | # License 34 | 35 | MIT 36 | 37 | # Useful commands 38 | 39 | - `npm run build` compile typescript to js 40 | - `npm run watch` watch for changes and compile 41 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farminf/aws-cdk-passwordless/58c449c0bb0de0aff18fda0acd77839e17da73b2/client/.eslintrc -------------------------------------------------------------------------------- /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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-amplify": "^1.1.36", 7 | "aws-amplify-react": "^2.3.12", 8 | "aws-sdk": "^2.513.0", 9 | "generate-password": "^1.4.2", 10 | "jest": "24.8.0", 11 | "react": "^16.9.0", 12 | "react-dom": "^16.9.0", 13 | "react-scripts": "3.1.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/farminf/aws-cdk-passwordless/58c449c0bb0de0aff18fda0acd77839e17da73b2/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | PasswordLess App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farminf/aws-cdk-passwordless/58c449c0bb0de0aff18fda0acd77839e17da73b2/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farminf/aws-cdk-passwordless/58c449c0bb0de0aff18fda0acd77839e17da73b2/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 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React , { useState , useEffect } from 'react'; 2 | import Amplify,{ Auth } from 'aws-amplify'; 3 | import generator from "generate-password"; 4 | import awsExport from './aws-exports'; 5 | import { async } from 'rxjs/internal/scheduler/async'; 6 | Amplify.configure(awsExport); 7 | 8 | function App() { 9 | 10 | const [email,setEmail] = useState(""); 11 | const [code,setCode] = useState(""); 12 | const [session,setSession] = useState("Not loggedIn"); 13 | 14 | 15 | 16 | useEffect(()=>{ 17 | async function fetchAuth() { 18 | const auth = await Auth.currentSession(); 19 | if (auth) { 20 | setSession(auth.getAccessToken().getJwtToken()); 21 | } 22 | } 23 | fetchAuth(); 24 | 25 | },[session]); 26 | 27 | const awsConfirmCode = async () => { 28 | const password = generator.generate({ 29 | length: 25, 30 | numbers: true, 31 | symbols: true, 32 | strict: true, 33 | }); 34 | try { 35 | await Auth.forgotPasswordSubmit(email, code, password); 36 | await Auth.signIn(email, password); 37 | setSession("logging in..."); 38 | 39 | } catch (e) { 40 | console.error("Error in confirmForgotPassword: ", e); 41 | return false; 42 | } 43 | }; 44 | 45 | const resetPassword = async (email) => { 46 | try { 47 | await Auth.forgotPassword(email); 48 | } catch (e) { 49 | console.error("Error in resetPassword: ", e); 50 | return false; 51 | } 52 | 53 | return true; 54 | }; 55 | 56 | const awsLogin = async () => { 57 | const password = generator.generate({ 58 | length: 25, 59 | numbers: true, 60 | symbols: true, 61 | strict: true, 62 | }); 63 | try { 64 | 65 | const auth = await Auth.signUp({ 66 | password, 67 | username: email, 68 | attributes:{ 69 | name: email 70 | } 71 | }); 72 | console.log("auth", auth); 73 | } catch (err){ 74 | 75 | if (err.code && err.code === "UsernameExistsException") { 76 | const success = await resetPassword(email); 77 | console.log("success",success); 78 | } 79 | 80 | if (err.code && err.code === "UserLambdaValidationException") { 81 | console.log("error",err); 82 | } 83 | } 84 | }; 85 | 86 | const onLogin = async (e) => { 87 | e.preventDefault(); 88 | console.log("submit"); 89 | await awsLogin(); 90 | }; 91 | 92 | const onConfirmCode = async (e) => { 93 | e.preventDefault(); 94 | console.log("submit code"); 95 | await awsConfirmCode(); 96 | }; 97 | 98 | const awsLogout = async (e) =>{ 99 | try { 100 | await Auth.signOut(); 101 | setSession("Logged Out"); 102 | } catch (e){ 103 | console.log("error signing out",e); 104 | } 105 | }; 106 | 107 | return ( 108 |
109 |
110 | setEmail(evt.target.value)} 114 | /> 115 | 116 |
117 |
118 | setCode(evt.target.value)} 122 | /> 123 | 124 |
125 |

{session}

126 | 130 |
131 | ); 132 | } 133 | 134 | export default App; 135 | -------------------------------------------------------------------------------- /client/src/aws-exports.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const awsExports = { 4 | Auth: { 5 | userPoolId: "xxx", 6 | userPoolWebClientId: "xxx", 7 | region: "eu-west-1", 8 | }, 9 | }; 10 | 11 | export default awsExports; 12 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /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.1/8 is 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 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/.eslintrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farminf/aws-cdk-passwordless/58c449c0bb0de0aff18fda0acd77839e17da73b2/lib/.eslintrc -------------------------------------------------------------------------------- /lib/functions/cognitoEvents.js: -------------------------------------------------------------------------------- 1 | const codeConfirmationEmailBody = ({ 2 | codeParameter, 3 | }) => ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 |
16 | 17 | 18 | 83 | 84 |
19 | 20 | 21 | 22 | 79 | 80 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
 
31 | Hey there! 32 |
 
 
 
44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 |
48 | This is your 49 |
 
56 | Confirmation Code: 57 |
 
64 | ${codeParameter} 65 |
 
71 | 72 | 73 | 74 | 75 | 76 |
 
77 | 78 |
81 | 82 |
85 |
86 | 87 | `; 88 | 89 | module.exports.handler = async (event, context) => { 90 | console.log("event", JSON.stringify(event, null, 2)); 91 | 92 | const verifiedDomains = process.env.VERIFIED_DOMAINS && JSON.parse(process.env.VERIFIED_DOMAINS); 93 | const signInSubject = process.env.SIGNINSUBJECT ; 94 | 95 | 96 | if (event.triggerSource === "PreSignUp_SignUp") { 97 | const { email } = event.request.userAttributes; 98 | 99 | // domain verification 100 | const domain = email.replace(/.*@/, ""); 101 | console.log("domain of email is", domain); 102 | if (verifiedDomains && verifiedDomains.indexOf(domain) < 0) { 103 | throw new Error(`Email address has unacceptable domain to be registered`); 104 | } 105 | 106 | event.response.autoConfirmUser = true; 107 | event.response.autoVerifyEmail = true; 108 | } 109 | 110 | if (event.triggerSource === "CustomMessage_ForgotPassword") { 111 | const { codeParameter } = event.request; 112 | 113 | event.response.emailSubject = `${signInSubject}` || "Passwordless App – Sign In"; 114 | event.response.emailMessage = codeConfirmationEmailBody({ 115 | codeParameter, 116 | }); 117 | } 118 | 119 | console.log("response", event); 120 | return event; 121 | }; 122 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import cdk = require("@aws-cdk/core"); 3 | import cognito = require("@aws-cdk/aws-cognito"); 4 | import iam = require("@aws-cdk/aws-iam"); 5 | import lambda = require("@aws-cdk/aws-lambda"); 6 | 7 | export interface CdkPasswordlessProps { 8 | /** 9 | * Name of the user pool client 10 | * 11 | * @default - automatically generated name by CloudFormation at deploy time 12 | */ 13 | userPoolClientName?: string; 14 | /** 15 | * list of domains which are allowed to sign up account with, if don't specified, all domains will be allowed 16 | * 17 | * @default - [] 18 | */ 19 | verifiedDomains?: Array; 20 | /** 21 | * Subject of the code confirmation mail 22 | * 23 | * @default - "Passwordless App – Sign In" 24 | */ 25 | mailSubject?: string; 26 | /** 27 | * Lambda Function which will be passed as postConfirmation trigger for cognito pool 28 | * 29 | * @default - null 30 | */ 31 | postConfirmationLambda?: lambda.Function; 32 | } 33 | 34 | /** 35 | * A construct for creating resources for doing passwordless authentication 36 | */ 37 | export class CdkPasswordless extends cdk.Construct { 38 | /** @returns the UserPool Resource */ 39 | public readonly userPool: cognito.UserPool; 40 | /** @returns the UserPool Client Resource */ 41 | public readonly userPoolClient: cognito.UserPoolClient; 42 | 43 | constructor( 44 | scope: cdk.Construct, 45 | id: string, 46 | props: CdkPasswordlessProps = {} 47 | ) { 48 | super(scope, id); 49 | 50 | const { 51 | userPoolClientName, 52 | verifiedDomains, 53 | mailSubject, 54 | postConfirmationLambda 55 | } = props; 56 | 57 | const lambdaRole = new iam.Role(this, "lambdaRole", { 58 | assumedBy: new iam.CompositePrincipal( 59 | new iam.ServicePrincipal("lambda.amazonaws.com"), 60 | new iam.ServicePrincipal("cognito-idp.amazonaws.com") 61 | ), 62 | managedPolicies: [ 63 | iam.ManagedPolicy.fromAwsManagedPolicyName( 64 | "service-role/AWSLambdaBasicExecutionRole" 65 | ) 66 | ] 67 | }); 68 | 69 | const cognitoEventsLambda = new lambda.Function( 70 | this, 71 | "cognitoEventsLambda", 72 | { 73 | code: lambda.Code.asset(path.resolve(__dirname, "functions")), 74 | description: 75 | "This function auto-confirms users and their email addresses during sign-up", 76 | handler: "cognitoEvents.handler", 77 | runtime: lambda.Runtime.NODEJS_10_X, 78 | role: lambdaRole, 79 | environment: { 80 | VERIFIED_DOMAINS: JSON.stringify(verifiedDomains) || undefined, 81 | SIGNINSUBJECT: mailSubject || undefined 82 | } 83 | } 84 | ); 85 | 86 | const userPool = new cognito.UserPool(this, "userPool", { 87 | lambdaTriggers: { 88 | preSignUp: cognitoEventsLambda, 89 | customMessage: cognitoEventsLambda, 90 | ...(postConfirmationLambda && { 91 | postConfirmation: postConfirmationLambda 92 | }) 93 | }, 94 | autoVerifiedAttributes: [cognito.UserPoolAttribute.EMAIL], 95 | signInType: cognito.SignInType.EMAIL 96 | }); 97 | 98 | const userPoolClient = new cognito.UserPoolClient(this, "userPoolClient", { 99 | userPoolClientName: userPoolClientName || "passwordless", 100 | generateSecret: false, 101 | userPool: userPool 102 | }); 103 | 104 | this.userPool = userPool; 105 | this.userPoolClient = userPoolClient; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cdk-passwordless", 3 | "version": "0.2.2", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "test": "nodeunit test/test.*.js" 10 | }, 11 | "devDependencies": { 12 | "@types/nodeunit": "^0.0.30", 13 | "babel-eslint": "^10.0.2", 14 | "eslint": "^6.2.0", 15 | "eslint-plugin-jsx-a11y": "^6.2.3", 16 | "eslint-plugin-react": "^7.14.3", 17 | "nodeunit": "^0.11.2", 18 | "typescript": "^3.3.3333" 19 | }, 20 | "peerDependencies": { 21 | "@aws-cdk/core": "^1.4.0" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-cognito": "1.4.0", 25 | "@aws-cdk/aws-iam": "1.4.0", 26 | "@aws-cdk/aws-lambda": "1.4.0", 27 | "@aws-cdk/aws-ssm": "1.4.0", 28 | "@aws-cdk/core": "1.4.0", 29 | "@types/jest": "^24.0.17", 30 | "@types/node": "^12.6.2", 31 | "jest": "24.8.0" 32 | }, 33 | "jest": { 34 | "moduleFileExtensions": [ 35 | "js" 36 | ] 37 | }, 38 | "author": { 39 | "email": "farmin.f@gmail.com", 40 | "name": "Farmin Farzin", 41 | "url": "https://github.com/farminf" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/cdk.json: -------------------------------------------------------------------------------- 1 | { "app": "node index.js" } 2 | -------------------------------------------------------------------------------- /sample/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Construct, App, Duration } from "@aws-cdk/core"; 2 | import { StringParameter } from "@aws-cdk/aws-ssm"; 3 | import { CdkPasswordless } from "../lib/index"; 4 | import { Function, Runtime, Code } from "@aws-cdk/aws-lambda"; 5 | 6 | class myStack extends Stack { 7 | constructor(scope: Construct, id: string, props: StackProps = {}) { 8 | super(scope, id, props); 9 | const postConfirmation = new Function(this, "postConfirmation", { 10 | runtime: Runtime.NODEJS_10_X, 11 | code: Code.fromAsset("./functions"), 12 | handler: "postConfirm.handler" 13 | }); 14 | const pless = new CdkPasswordless(this, "myPasswordLess", { 15 | mailSubject: "myStack - signIn", 16 | userPoolClientName: "myClientName", 17 | verifiedDomains: ["gmail.com"], 18 | postConfirmationLambda: postConfirmation 19 | }); 20 | 21 | new StringParameter(this, "userPoolIdParam", { 22 | parameterName: "/cognito/userPoolId", 23 | stringValue: pless.userPool.userPoolId 24 | }); 25 | new StringParameter(this, "userPoolClientIdParam", { 26 | parameterName: "/cognito/userPoolClientId", 27 | stringValue: pless.userPoolClient.userPoolClientId 28 | }); 29 | } 30 | } 31 | 32 | new myStack(new App(), "my-stack"); 33 | -------------------------------------------------------------------------------- /test/__snapshots__/passwordless.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`testing passwordless resources 1`] = ` 4 | Object { 5 | "Outputs": Object { 6 | "myPasswordlessTestuserPoolArnOutput04B8D94D": Object { 7 | "Export": Object { 8 | "Name": "passwordLessUserPoolArn", 9 | }, 10 | "Value": Object { 11 | "Fn::GetAtt": Array [ 12 | "myPasswordlessTestuserPool18816AD8", 13 | "Arn", 14 | ], 15 | }, 16 | }, 17 | "myPasswordlessTestuserPoolClientIdOutput76E5E879": Object { 18 | "Export": Object { 19 | "Name": "passwordLessUserPoolClientId", 20 | }, 21 | "Value": Object { 22 | "Ref": "myPasswordlessTestuserPoolClient442FD4CD", 23 | }, 24 | }, 25 | "myPasswordlessTestuserPoolIdOutputD2D99BBE": Object { 26 | "Export": Object { 27 | "Name": "passwordLessUserPoolId", 28 | }, 29 | "Value": Object { 30 | "Ref": "myPasswordlessTestuserPool18816AD8", 31 | }, 32 | }, 33 | }, 34 | "Parameters": Object { 35 | "myPasswordlessTestcognitoEventsLambdaCodeArtifactHash8198C4D7": Object { 36 | "Description": "Artifact hash for asset \\"test/myPasswordlessTest/cognitoEventsLambda/Code\\"", 37 | "Type": "String", 38 | }, 39 | "myPasswordlessTestcognitoEventsLambdaCodeS3Bucket4E0CA048": Object { 40 | "Description": "S3 bucket for asset \\"test/myPasswordlessTest/cognitoEventsLambda/Code\\"", 41 | "Type": "String", 42 | }, 43 | "myPasswordlessTestcognitoEventsLambdaCodeS3VersionKeyB7C1D3F9": Object { 44 | "Description": "S3 key for asset version \\"test/myPasswordlessTest/cognitoEventsLambda/Code\\"", 45 | "Type": "String", 46 | }, 47 | }, 48 | "Resources": Object { 49 | "myPasswordlessTestcognitoEventsLambdaC85C55A5": Object { 50 | "DependsOn": Array [ 51 | "myPasswordlessTestlambdaRole51C60D45", 52 | ], 53 | "Properties": Object { 54 | "Code": Object { 55 | "S3Bucket": Object { 56 | "Ref": "myPasswordlessTestcognitoEventsLambdaCodeS3Bucket4E0CA048", 57 | }, 58 | "S3Key": Object { 59 | "Fn::Join": Array [ 60 | "", 61 | Array [ 62 | Object { 63 | "Fn::Select": Array [ 64 | 0, 65 | Object { 66 | "Fn::Split": Array [ 67 | "||", 68 | Object { 69 | "Ref": "myPasswordlessTestcognitoEventsLambdaCodeS3VersionKeyB7C1D3F9", 70 | }, 71 | ], 72 | }, 73 | ], 74 | }, 75 | Object { 76 | "Fn::Select": Array [ 77 | 1, 78 | Object { 79 | "Fn::Split": Array [ 80 | "||", 81 | Object { 82 | "Ref": "myPasswordlessTestcognitoEventsLambdaCodeS3VersionKeyB7C1D3F9", 83 | }, 84 | ], 85 | }, 86 | ], 87 | }, 88 | ], 89 | ], 90 | }, 91 | }, 92 | "Description": "This function auto-confirms users and their email addresses during sign-up", 93 | "Environment": Object { 94 | "Variables": Object {}, 95 | }, 96 | "Handler": "cognitoEvents.handler", 97 | "Role": Object { 98 | "Fn::GetAtt": Array [ 99 | "myPasswordlessTestlambdaRole51C60D45", 100 | "Arn", 101 | ], 102 | }, 103 | "Runtime": "nodejs10.x", 104 | }, 105 | "Type": "AWS::Lambda::Function", 106 | }, 107 | "myPasswordlessTestcognitoEventsLambdaCustomMessageCognito774653EB": Object { 108 | "Properties": Object { 109 | "Action": "lambda:InvokeFunction", 110 | "FunctionName": Object { 111 | "Fn::GetAtt": Array [ 112 | "myPasswordlessTestcognitoEventsLambdaC85C55A5", 113 | "Arn", 114 | ], 115 | }, 116 | "Principal": "cognito-idp.amazonaws.com", 117 | }, 118 | "Type": "AWS::Lambda::Permission", 119 | }, 120 | "myPasswordlessTestcognitoEventsLambdaPreSignUpCognito67096823": Object { 121 | "Properties": Object { 122 | "Action": "lambda:InvokeFunction", 123 | "FunctionName": Object { 124 | "Fn::GetAtt": Array [ 125 | "myPasswordlessTestcognitoEventsLambdaC85C55A5", 126 | "Arn", 127 | ], 128 | }, 129 | "Principal": "cognito-idp.amazonaws.com", 130 | }, 131 | "Type": "AWS::Lambda::Permission", 132 | }, 133 | "myPasswordlessTestlambdaRole51C60D45": Object { 134 | "Properties": Object { 135 | "AssumeRolePolicyDocument": Object { 136 | "Statement": Array [ 137 | Object { 138 | "Action": "sts:AssumeRole", 139 | "Effect": "Allow", 140 | "Principal": Object { 141 | "Service": Array [ 142 | Object { 143 | "Fn::Join": Array [ 144 | "", 145 | Array [ 146 | "lambda.", 147 | Object { 148 | "Ref": "AWS::URLSuffix", 149 | }, 150 | ], 151 | ], 152 | }, 153 | Object { 154 | "Fn::Join": Array [ 155 | "", 156 | Array [ 157 | "cognito-idp.", 158 | Object { 159 | "Ref": "AWS::URLSuffix", 160 | }, 161 | ], 162 | ], 163 | }, 164 | ], 165 | }, 166 | }, 167 | ], 168 | "Version": "2012-10-17", 169 | }, 170 | "ManagedPolicyArns": Array [ 171 | Object { 172 | "Fn::Join": Array [ 173 | "", 174 | Array [ 175 | "arn:", 176 | Object { 177 | "Ref": "AWS::Partition", 178 | }, 179 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 180 | ], 181 | ], 182 | }, 183 | ], 184 | }, 185 | "Type": "AWS::IAM::Role", 186 | }, 187 | "myPasswordlessTestuserPool18816AD8": Object { 188 | "Properties": Object { 189 | "AutoVerifiedAttributes": Array [ 190 | "email", 191 | ], 192 | "LambdaConfig": Object { 193 | "CustomMessage": Object { 194 | "Fn::GetAtt": Array [ 195 | "myPasswordlessTestcognitoEventsLambdaC85C55A5", 196 | "Arn", 197 | ], 198 | }, 199 | "PreSignUp": Object { 200 | "Fn::GetAtt": Array [ 201 | "myPasswordlessTestcognitoEventsLambdaC85C55A5", 202 | "Arn", 203 | ], 204 | }, 205 | }, 206 | "UsernameAttributes": Array [ 207 | "email", 208 | ], 209 | }, 210 | "Type": "AWS::Cognito::UserPool", 211 | }, 212 | "myPasswordlessTestuserPoolClient442FD4CD": Object { 213 | "Properties": Object { 214 | "ClientName": "passwordless", 215 | "GenerateSecret": false, 216 | "UserPoolId": Object { 217 | "Ref": "myPasswordlessTestuserPool18816AD8", 218 | }, 219 | }, 220 | "Type": "AWS::Cognito::UserPoolClient", 221 | }, 222 | }, 223 | } 224 | `; 225 | -------------------------------------------------------------------------------- /test/passwordless.test.ts: -------------------------------------------------------------------------------- 1 | import { Stack, App } from "@aws-cdk/core"; 2 | import { CdkPasswordless } from "../lib/index"; 3 | 4 | test("testing passwordless resources", () => { 5 | const app = new App(); 6 | const stack = new Stack(app, "test"); 7 | const myPasswordlessTest = new CdkPasswordless( 8 | stack, 9 | "myPasswordlessTest", 10 | {} 11 | ); 12 | 13 | expect(app.synth().getStack("test").template).toMatchSnapshot(); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false 20 | }, 21 | "exclude": ["cdk.out"] 22 | } 23 | 24 | --------------------------------------------------------------------------------