├── .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 | You need to enable JavaScript to run this app.
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 |
117 |
125 |
{session}
126 |
Logout
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 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Hey there!
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | This is your
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | Confirmation Code:
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | ${codeParameter}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
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 |
--------------------------------------------------------------------------------