├── images ├── 00.jpeg ├── 01.jpeg ├── 02.jpeg └── 03.jpeg ├── package.json ├── .env.example ├── cognito.js ├── index.js ├── .gitignore └── README.md /images/00.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oieduardorabelo/node-amazon-cognito-oauth/HEAD/images/00.jpeg -------------------------------------------------------------------------------- /images/01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oieduardorabelo/node-amazon-cognito-oauth/HEAD/images/01.jpeg -------------------------------------------------------------------------------- /images/02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oieduardorabelo/node-amazon-cognito-oauth/HEAD/images/02.jpeg -------------------------------------------------------------------------------- /images/03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oieduardorabelo/node-amazon-cognito-oauth/HEAD/images/03.jpeg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "nodemon ./index.js" 5 | }, 6 | "dependencies": { 7 | "axios": "0.21.1", 8 | "cookie-session": "1.4.0", 9 | "dotenv": "8.2.0", 10 | "express": "4.17.1", 11 | "qs": "6.7.0" 12 | }, 13 | "devDependencies": { 14 | "nodemon": "2.0.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COGNITO_CLIENT_ID=xxxxxxx 2 | COGNITO_DOMAIN_NAME_URL=https://xxxxxxxx.auth.xxxxxxxx.amazoncognito.com 3 | COGNITO_LOGIN_GRANT_TYPE=authorization_code 4 | COGNITO_LOGIN_REDIRECT_URL=http://localhost:4200/oauth/cognito 5 | COGNITO_LOGIN_RESPONSE_TYPE=code 6 | COGNITO_LOGIN_SCOPE=email+openid 7 | COGNITO_LOGOUT_REDIRECT_URL=http://localhost:4200/oauth/cognito/logout 8 | COOKIE_SESSION_NAME=nodejs-cognito-oauth 9 | COOKIE_SESSION_SECRET=xxxxxx 10 | HOSTNAME=:: 11 | PORT=4200 12 | -------------------------------------------------------------------------------- /cognito.js: -------------------------------------------------------------------------------- 1 | let axios = require("axios"); 2 | let qs = require("qs"); 3 | 4 | let { 5 | COGNITO_CLIENT_ID, 6 | COGNITO_DOMAIN_NAME_URL, 7 | COGNITO_LOGIN_GRANT_TYPE, 8 | COGNITO_LOGIN_REDIRECT_URL, 9 | COGNITO_LOGIN_RESPONSE_TYPE, 10 | COGNITO_LOGIN_SCOPE, 11 | COGNITO_LOGOUT_REDIRECT_URL, 12 | } = process.env; 13 | 14 | let getUserInfo = async ({ accessToken }) => { 15 | let url = `${COGNITO_DOMAIN_NAME_URL}/oauth2/userInfo`; 16 | let { data } = await axios({ 17 | url, 18 | method: "get", 19 | headers: { 20 | Authorization: `Bearer ${accessToken}`, 21 | }, 22 | }); 23 | 24 | // "data" looks like: 25 | // { 26 | // sub: '6010f967-5ed...', 27 | // email_verified: 'true', 28 | // email: 'example@example.com', 29 | // username: '6010f967-5ed...' 30 | // } 31 | console.log("/oauth2/userInfo", data); 32 | return data; 33 | }; 34 | 35 | let postToken = async ({ code }) => { 36 | console.log("code", code); 37 | let url = `${COGNITO_DOMAIN_NAME_URL}/oauth2/token`; 38 | let params = { 39 | grant_type: COGNITO_LOGIN_GRANT_TYPE, 40 | client_id: COGNITO_CLIENT_ID, 41 | redirect_uri: COGNITO_LOGIN_REDIRECT_URL, 42 | code, 43 | }; 44 | let { data } = await axios({ 45 | url, 46 | method: "post", 47 | data: qs.stringify(params), 48 | }); 49 | 50 | // "data" looks like: 51 | // { 52 | // id_token: "eyJra...", 53 | // access_token: "eyJra...", 54 | // refresh_token: "eyJjd...", 55 | // expires_in: 3600, 56 | // token_type: "Bearer", 57 | // }; 58 | console.log("/oauth2/token", data); 59 | return data; 60 | }; 61 | 62 | let getLogin = async () => { 63 | let params = { 64 | client_id: COGNITO_CLIENT_ID, 65 | response_type: COGNITO_LOGIN_RESPONSE_TYPE, 66 | scope: COGNITO_LOGIN_SCOPE, 67 | redirect_uri: COGNITO_LOGIN_REDIRECT_URL, 68 | } 69 | let url = `${COGNITO_DOMAIN_NAME_URL}/login?${qs.stringify(params, { encode: false })}`; 70 | return url; 71 | }; 72 | 73 | let getLogout = async () => { 74 | let params = { 75 | client_id: COGNITO_CLIENT_ID, 76 | logout_uri: COGNITO_LOGOUT_REDIRECT_URL, 77 | } 78 | let url = `${COGNITO_DOMAIN_NAME_URL}/logout?${qs.stringify(params, { encode: false })}`; 79 | return url; 80 | }; 81 | 82 | module.exports = { getUserInfo, postToken, getLogin, getLogout }; 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | let express = require("express"); 4 | let session = require("cookie-session"); 5 | 6 | let { uptime, env } = process; 7 | let { PORT, HOSTNAME, COOKIE_SESSION_SECRET, COOKIE_SESSION_NAME } = env; 8 | 9 | let { getUserInfo, postToken, getLogin, getLogout } = require("./cognito"); 10 | 11 | let app = express(); 12 | 13 | app.use( 14 | session({ 15 | secret: COOKIE_SESSION_SECRET, 16 | name: COOKIE_SESSION_NAME, 17 | cookie: { 18 | secure: false, 19 | httpOnly: true, 20 | }, 21 | }) 22 | ); 23 | 24 | app.get("/", (req, res) => { 25 | if (req.session.user) { 26 | res.redirect(`/users/${req.session.user.id}`); 27 | } else { 28 | getLogin() 29 | .then((url) => { 30 | res.status(200).send({ ok: true, uptime: uptime(), login: url }); 31 | }) 32 | .catch((err) => { 33 | console.log(err); 34 | res.status(400).send({ ok: false, error: err.message }); 35 | }); 36 | } 37 | }); 38 | 39 | app.get("/oauth/cognito", (req, res) => { 40 | let { code } = req.query; 41 | 42 | postToken({ code }) 43 | .then((tokens) => { 44 | getUserInfo({ accessToken: tokens.access_token }) 45 | .then((userInfo) => { 46 | req.session.user = { 47 | accessToken: tokens.access_token, 48 | email: userInfo.email, 49 | emailVefired: userInfo.email_verified, 50 | id: userInfo.username, 51 | }; 52 | res.redirect(302, `/users/${userInfo.username}`); 53 | }) 54 | .catch((err) => { 55 | console.log(err); 56 | res.status(400).send({ ok: false, error: err.message }); 57 | }); 58 | }) 59 | .catch((err) => { 60 | console.log(err); 61 | res.status(400).send({ ok: false, error: err.message }); 62 | }); 63 | }); 64 | 65 | app.get("/oauth/cognito/logout", (req, res) => { 66 | if (req.session.user) { 67 | req.session = {}; 68 | res.redirect("/"); 69 | } else { 70 | res.redirect("/"); 71 | } 72 | }); 73 | 74 | app.get("/users/:id", (req, res) => { 75 | if (!req.session.user) { 76 | res.redirect("/"); 77 | } else { 78 | let { id } = req.query; 79 | let { accessToken, email, emailVefired } = req.session.user; 80 | getLogout() 81 | .then((url) => { 82 | res.status(200).send({ 83 | ok: true, 84 | id, 85 | accessToken, 86 | email, 87 | emailVefired, 88 | logout: url, 89 | }); 90 | }) 91 | .catch((err) => { 92 | console.log(err); 93 | res.status(400).send({ ok: false, error: err.message }); 94 | }); 95 | } 96 | }); 97 | 98 | app.listen(Number(PORT), HOSTNAME, () => { 99 | console.log(`Running at [${HOSTNAME}]:${PORT}`); 100 | }); 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | 43 | # Diagnostic reports (https://nodejs.org/api/report.html) 44 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 45 | 46 | # Runtime data 47 | pids 48 | *.pid 49 | *.seed 50 | *.pid.lock 51 | 52 | # Directory for instrumented libs generated by jscoverage/JSCover 53 | lib-cov 54 | 55 | # Coverage directory used by tools like istanbul 56 | coverage 57 | *.lcov 58 | 59 | # nyc test coverage 60 | .nyc_output 61 | 62 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 63 | .grunt 64 | 65 | # Bower dependency directory (https://bower.io/) 66 | bower_components 67 | 68 | # node-waf configuration 69 | .lock-wscript 70 | 71 | # Compiled binary addons (https://nodejs.org/api/addons.html) 72 | build/Release 73 | 74 | # Dependency directories 75 | node_modules/ 76 | jspm_packages/ 77 | 78 | # TypeScript v1 declaration files 79 | typings/ 80 | 81 | # TypeScript cache 82 | *.tsbuildinfo 83 | 84 | # Optional npm cache directory 85 | .npm 86 | 87 | # Optional eslint cache 88 | .eslintcache 89 | 90 | # Optional stylelint cache 91 | .stylelintcache 92 | 93 | # Microbundle cache 94 | .rpt2_cache/ 95 | .rts2_cache_cjs/ 96 | .rts2_cache_es/ 97 | .rts2_cache_umd/ 98 | 99 | # Optional REPL history 100 | .node_repl_history 101 | 102 | # Output of 'npm pack' 103 | *.tgz 104 | 105 | # Yarn Integrity file 106 | .yarn-integrity 107 | 108 | # dotenv environment variables file 109 | .env 110 | .env.test 111 | .env*.local 112 | 113 | # parcel-bundler cache (https://parceljs.org/) 114 | .cache 115 | .parcel-cache 116 | 117 | # Next.js build output 118 | .next 119 | 120 | # Nuxt.js build / generate output 121 | .nuxt 122 | dist 123 | 124 | # Gatsby files 125 | .cache/ 126 | # Comment in the public line in if your project uses Gatsby and not Next.js 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | # public 129 | 130 | # vuepress build output 131 | .vuepress/dist 132 | 133 | # Serverless directories 134 | .serverless/ 135 | 136 | # FuseBox cache 137 | .fusebox/ 138 | 139 | # DynamoDB Local files 140 | .dynamodb/ 141 | 142 | # TernJS port file 143 | .tern-port 144 | 145 | # Stores VSCode versions used for testing VSCode extensions 146 | .vscode-test 147 | 148 | # End of https://www.toptal.com/developers/gitignore/api/node,macos 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using Amazon Cognito OAuth Server instead of AWS-SDK with Node.js 2 | 3 | Amazon Cognito Hosted UI provides you an OAuth 2.0 compliant authorization server. In this repository you can find a working example using [Amazon Cognito User Pools Auth API Reference](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html). 4 | 5 | We take advantage of Amazon Cognito OAuth Domain Name to exchange tokens and access user information in our Amazon Cognito User Pool. 6 | 7 | **Note:** If you want to update the user attributes in the user pool, you still need to build / provite a CRUD API using [AWS-SDK](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html). This is out of the scope of this article. 8 | 9 | # User Pool Configuration 10 | 11 | The example above assumes a few things: 12 | 13 | - There's NO MFA CONFIGURED in the Cognito User Pool 14 | - There's EMAIL VERIFICATION by the Cognito User Pool (e.g. you need a valid email in the sign up process) 15 | - Password policy is "Minimum length: 6" only (e.g. numbers, special character verification are turned off) 16 | 17 | ## General settings 18 | 19 | - Pool Id - REDACTED (Unique to you) 20 | - Poolr ARN - REDACTED (Unique to you) 21 | - Required attributes - none 22 | - Alias attributes - none 23 | - Username attributes - email 24 | - Enable case insensitivity? - Yes 25 | - Custom Attributes - none 26 | - Minimum password length - 6 27 | - Password policy - no requirements 28 | - User sign ups allowed? - Users can sign themselves up 29 | - FROM email address - Default 30 | - Email Delivery through Amazon SES - No 31 | - MFA - Disabled 32 | - Verifications - Email 33 | - Advanced security - none 34 | - Tags - none 35 | - App clients - client-localhost 36 | - Triggers - none 37 | 38 | ![](./images/00.jpeg) 39 | 40 | ## General settings > App clients 41 | 42 | We configured **ONE app client to work with localhost urls**. 43 | 44 | You can create multiple app clients to work with different environments (e.g. web-client, mobile-client, qa-client etc). 45 | 46 | Configuration used when creating the app client: 47 | 48 | - App client name - client-localhost 49 | - Refresh token expiration - I kept default values (30 days and 0 minutes) 50 | - Access token expiration - I kept default values (0 days and 60 minutes) 51 | - ID token expiration - I kept default values (0 days and 60 minutes) 52 | - **IMPORTANT** Generate client secret - Uncheck/Turn off this checkbox 53 | - Auth Flows Configuration - I kept default values (values selected are: `ALLOW_CUSTOM_AUTH`, `ALLOW_USER_SRP_AUTH`, `ALLOW_REFRESH_TOKEN_AUTH`) 54 | - Security configuration 55 | - > Prevent User Existence Errors - I kept default values (checkbox for "Enabled (Recommended)" checked/turned on) 56 | - Click "Create app client" 57 | 58 | After the app client is created copy the unique **App client id** into `.env` on `COGNITO_CLIENT_ID=` variable. 59 | 60 | ![](./images/01.jpeg) 61 | 62 | ## App integration > Domain name 63 | 64 | Navigate to `App integration > Domain name` and check the availability of a Amazon Cognito domain. It will generate our OAuth 2.0 compliant authorization server. 65 | 66 | You can also use your own domain for the authorization server, check the [documentation for more instructions](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). 67 | 68 | - Domain prefix - REDACTED (Unique to you) 69 | 70 | ![](./images/02.jpeg) 71 | 72 | ## App integration > App client settings 73 | 74 | Now we need to define OAuth flows and scopes for the App Client we created before (e.g. `client-localhost`). You can [learn more about App Client Settings Terminology in the documentation.](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html#cognito-user-pools-app-idp-settings-about) 75 | 76 | - ID - REDACTED (Unique to you) 77 | - Enabled Identity Providers - Check/turn on "Select all" checkbox 78 | - Sign in and sign out URLs 79 | - > Callback URL(s) - http://localhost:4200/oauth/cognito 80 | - > Sign out URL(s) - http://localhost:4200/oauth/cognito/logout 81 | - OAuth 2.0 82 | - > Allowed OAuth Flows - Check/turn on "Authorization code grant" and "Implicit grant" checkboxes 83 | - > Allowed OAuth Scopes - Check/turn on "email" and "openid" checkboxes 84 | 85 | ![](./images/03.jpeg) 86 | 87 | # To the moon 🚀 88 | 89 | You are ready to run this example, make sure to: 90 | 91 | 1. Copy `.env.example` to `.env` and udpate ALL variables values 92 | 2. Run `npm install` 93 | 3. Start the example with `npm start` 94 | 95 | If you have any questions you can reach out via [twitter/oieduardorabelo](https://twitter.com/oieduardorabelo). 96 | --------------------------------------------------------------------------------