From f8d64dab3968f2ef12cacbcbfd29aec139315f2d Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 23 Jun 2020 17:30:47 -0400 Subject: [PATCH] Start on Grant / oAUTH flow --- example.env | 31 +++- package-lock.json | 219 ++++++++++++++++++++++++++++ package.json | 5 + src/routes/auth.ts | 18 +++ src/types/grant.d.ts | 4 + src/utils/get-email-from-profile.ts | 24 +++ src/utils/get-grant-config.ts | 38 +++++ 7 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 src/types/grant.d.ts create mode 100644 src/utils/get-email-from-profile.ts create mode 100644 src/utils/get-grant-config.ts diff --git a/example.env b/example.env index 123102faa9..486ab87256 100644 --- a/example.env +++ b/example.env @@ -1,12 +1,12 @@ +#################################################################################################### # General + PORT=3000 +PUBLIC_URL="http://localhost:3000" -# Auth -SECRET="abcdef" -ACCESS_TOKEN_EXPIRY_TIME="15m" -REFRESH_TOKEN_EXPIRY_TIME="7d" - +#################################################################################################### # Database + DB_CLIENT="pg" DB_HOST="localhost" DB_PORT=5432 @@ -14,10 +14,31 @@ DB_NAME="directus" DB_USER="postgres" DB_PASSWORD="psql1234" +#################################################################################################### +# Auth + +SECRET="abcdef" +ACCESS_TOKEN_EXPIRY_TIME="15m" +REFRESH_TOKEN_EXPIRY_TIME="7d" + +#################################################################################################### +# SSO (oAuth) Providers + +OAUTH_PROVIDERS="github,facebook" + +OAUTH_GITHUB_KEY="abcdef" +OAUTH_GITHUB_SECRET="ghijkl" +OAUTH_FACEBOOK_KEY="abcdef" +OAUTH_FACEBOOK_SECRET="ghijkl" + +#################################################################################################### # Extensions + EXTENSIONS_PATH="./extensions" +#################################################################################################### # Email + EMAIL_TRANSPORT="sendmail" ## Email (Sendmail Transport) diff --git a/package-lock.json b/package-lock.json index 933fecaecf..8594531200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,6 +157,16 @@ "@types/range-parser": "*" } }, + "@types/express-session": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.0.tgz", + "integrity": "sha512-OQEHeBFE1UhChVIBhRh9qElHUvTp4BzKKHxMDkGHT7WuYk5eL93hPG7D8YAIkoBSbhNEY0RjreF15zn+U0eLjA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/node": "*" + } + }, "@types/hapi__joi": { "version": "17.1.2", "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-17.1.2.tgz", @@ -172,6 +182,12 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.156", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.156.tgz", + "integrity": "sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ==", + "dev": true + }, "@types/mime": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", @@ -461,6 +477,18 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "optional": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -608,6 +636,12 @@ } } }, + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "optional": true + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -657,6 +691,12 @@ "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "optional": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1154,6 +1194,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "optional": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1541,6 +1596,46 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.1.4.tgz", "integrity": "sha512-HdmbVF4V4w1q/iz++RV7bUxIeepTukWewiJGkoCKQMtvPF11MLTa7It9PRc/reysXXZSEyD4Pthchju+IUbMiQ==" }, + "express-session": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", + "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.0", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2165,6 +2260,46 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, + "grant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/grant/-/grant-5.2.0.tgz", + "integrity": "sha512-XLB6H5CYp/A4+fw7CFBYLA6Q+ayHsZHgUO7+SD+VIgSeQI4wdpW5ZA+vWd2bwlKTccuCWNuEOJBneFuQd/xOUg==", + "requires": { + "jwk-to-pem": "^2.0.3", + "jws": "^4.0.0", + "qs": "^6.9.4", + "request-compose": "^2.1.0", + "request-oauth": "^1.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } + } + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -2242,6 +2377,27 @@ } } }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "optional": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -2720,6 +2876,17 @@ "safe-buffer": "^5.0.1" } }, + "jwk-to-pem": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.4.tgz", + "integrity": "sha512-4CCK9UBHNWjWtfSHdyu3I6rA8vlN5cWqnVuwY0cOMyXtw6M1tP+yrM8GZpwk+P932Dc3cLag4d35B6CqyIf89A==", + "optional": true, + "requires": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.3", + "safe-buffer": "^5.0.1" + } + }, "jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -3262,6 +3429,18 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "optional": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "optional": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -3649,6 +3828,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4127,6 +4311,11 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz", "integrity": "sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4330,6 +4519,28 @@ } } }, + "request-compose": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/request-compose/-/request-compose-2.1.0.tgz", + "integrity": "sha512-mIWvU9HA2whb/fHcqeQ0LQXAImCGISqUPyjuFF2rALhym2Fu4ebZGv7wxFA78rsJO/fn2OeEaK54TSjwSwRAFw==" + }, + "request-oauth": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/request-oauth/-/request-oauth-1.0.0.tgz", + "integrity": "sha512-wsDzIq1Qu2itLDlcpFph8xh5Q+FVyUj4os5zdQTlZL/JvZYF/qOyaawVPsxxhDG4QwCB3tzSFprj6dkjqR+e8w==", + "requires": { + "oauth-sign": "^0.9.0", + "qs": "^6.9.3", + "uuid": "^3.4.0" + }, + "dependencies": { + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5320,6 +5531,14 @@ "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", "dev": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index 9ef069c8bd..c733bb0051 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,10 @@ "devDependencies": { "@types/atob": "^2.1.2", "@types/express": "^4.17.6", + "@types/express-session": "^1.17.0", "@types/hapi__joi": "^17.1.2", "@types/jsonwebtoken": "^8.5.0", + "@types/lodash": "^4.14.156", "@types/nodemailer": "^6.4.0", "@types/pino": "^6.3.0", "copyfiles": "^2.3.0", @@ -63,10 +65,13 @@ "dotenv": "^8.2.0", "express": "^4.17.1", "express-async-handler": "^1.1.4", + "express-session": "^1.17.1", "get-port": "^5.1.1", + "grant": "^5.2.0", "jsonwebtoken": "^8.5.1", "knex": "^0.21.1", "liquidjs": "^9.12.0", + "lodash": "^4.17.15", "mssql": "^6.2.0", "mysql": "^2.18.1", "nodemailer": "^6.4.10", diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 8d3e818629..55a36e0320 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,7 +1,10 @@ import { Router } from 'express'; +import session from 'express-session'; import asyncHandler from 'express-async-handler'; import Joi from '@hapi/joi'; import * as AuthService from '../services/auth'; +import grant from 'grant'; +import getGrantConfig from '../utils/get-grant-config'; const router = Router(); @@ -24,4 +27,19 @@ router.post( }) ); +router.use('/sso', session({ secret: process.env.SECRET, saveUninitialized: true, resave: false })); + +router.use(grant.express()(getGrantConfig())); + +router.get('/sso/:provider/callback', (req, res) => { + console.log(req.session.grant); + + /** + * @TODO + * + */ + + res.send(req.session.grant); +}); + export default router; diff --git a/src/types/grant.d.ts b/src/types/grant.d.ts new file mode 100644 index 0000000000..d91e1ffcb7 --- /dev/null +++ b/src/types/grant.d.ts @@ -0,0 +1,4 @@ +declare module 'grant' { + const grant: any; + export default grant; +} diff --git a/src/utils/get-email-from-profile.ts b/src/utils/get-email-from-profile.ts new file mode 100644 index 0000000000..b67eee5f9a --- /dev/null +++ b/src/utils/get-email-from-profile.ts @@ -0,0 +1,24 @@ +import { get } from 'lodash'; + +// The path in JSON to fetch the email address from the profile. +const profileMap = { + github: 'email', +}; + +/** + * Extract the email address from a given user profile coming from a providers API + * + * Falls back to OAUTH__PROFILE_EMAIL if we don't have it preconfigured yet + * + * This is used in the SSO flow to extract the users + */ +export default function getEmailFromProfile(provider: string, profile: Record) { + const path = + profileMap[provider] || process.env[`OAUTH_${provider.toUpperCase()}_PROFILE_EMAIL`]; + + if (!path) { + throw new Error('Path to email in profile object is unknown.'); + } + + return get(profile, path); +} diff --git a/src/utils/get-grant-config.ts b/src/utils/get-grant-config.ts new file mode 100644 index 0000000000..2d3ece030b --- /dev/null +++ b/src/utils/get-grant-config.ts @@ -0,0 +1,38 @@ +/** + * Reads the environment variables to construct the configuration object required by Grant + */ +export default function getGrantConfig() { + const enabledProviders = process.env.OAUTH_PROVIDERS.split(',').map((provider) => + provider.trim() + ); + + const config: any = { + defaults: { + origin: process.env.PUBLIC_URL, + transport: 'session', + prefix: '/auth/sso', + response: ['tokens', 'profile'], + }, + }; + + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('OAUTH') === false) continue; + + const parts = key.split('_'); + const provider = parts[1].toLowerCase(); + + if (enabledProviders.includes(provider) === false) continue; + + // OAUTH SETTING = VALUE + parts.splice(0, 2); + + const configKey = parts.join('_').toLowerCase(); + + config[provider] = { + ...(config[provider] || {}), + [configKey]: value, + }; + } + + return config; +}