diff --git a/config/nodemon/types.json b/config/nodemon/types.json new file mode 100644 index 000000000..73203fd0a --- /dev/null +++ b/config/nodemon/types.json @@ -0,0 +1,7 @@ +{ + "exec": "npm run compile:server:types", + "ext": "graphql", + "watch": [ + "./src/core/server/graph" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ff1c5e30f..f0f773f88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1413,7 +1413,6 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", - "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -1448,7 +1447,6 @@ "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", - "dev": true, "requires": { "@types/node": "*" } @@ -1471,31 +1469,45 @@ "@types/events": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, "@types/express": { "version": "4.16.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", - "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", "@types/serve-static": "*" } }, + "@types/express-jwt": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.34.tgz", + "integrity": "sha1-/b7kxq9cCiRu8qkz9VGZc8dxfwI=", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, "@types/express-serve-static-core": { "version": "4.16.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", - "dev": true, "requires": { "@types/events": "*", "@types/node": "*", "@types/range-parser": "*" } }, + "@types/express-unless": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.0.32.tgz", + "integrity": "sha512-6YpJyFNlDDnPnRjMOvJCoDYlSDDmG/OEEUsPk7yhNkL4G9hUYtgab6vi1CcWsGSSSM0CsvNlWTG+ywAGnvF03g==", + "requires": { + "@types/express": "*" + } + }, "@types/graphql": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-0.13.1.tgz", @@ -1524,6 +1536,15 @@ "integrity": "sha512-GXYdIVpwBP5ZBOlHitSYfQdH+vWXVahhkeQwalX0LkoX7Mx0D3L3tg4vXXhr6nYHkEpWlAzWuEjgWEBtcp5NZA==", "dev": true }, + "@types/jsonwebtoken": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.7.tgz", + "integrity": "sha512-lq9X76APpxGJDUe1VptL1P5GrogqhPCH+SDy94+gaBJw7Hhj6hwrVC6zuxAx2GrgktkBuwydESZBvPfrdBoOEg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.109", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", @@ -1539,8 +1560,7 @@ "@types/mime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", - "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", - "dev": true + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" }, "@types/mongodb": { "version": "3.0.19", @@ -1556,8 +1576,16 @@ "@types/node": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.1.tgz", - "integrity": "sha512-IsX9aDHDzJohkm3VCDB8tkzl5RQ34E/PFA29TQk6uDGb7Oc869ZBtmdKVDBzY3+h9GnXB8ssrRXEPVZrlIOPOw==", - "dev": true + "integrity": "sha512-IsX9aDHDzJohkm3VCDB8tkzl5RQ34E/PFA29TQk6uDGb7Oc869ZBtmdKVDBzY3+h9GnXB8ssrRXEPVZrlIOPOw==" + }, + "@types/oauth": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.0.tgz", + "integrity": "sha512-1oouefxKPGiDkb5m6lNxDkFry3PItCOJ+tlNtEn/gRvWShb2Rb3y0pccOIGwN/AwHUpwsuwlRwSpg7aoCN3bQQ==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/passport": { "version": "0.4.5", @@ -1568,6 +1596,27 @@ "@types/express": "*" } }, + "@types/passport-oauth2": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.5.tgz", + "integrity": "sha512-q/pT4RKkiHU1W20P2qUAtVmua3bVF1b8Tulag/niDISS+8CGrRthmQxvPhkIVtJb68ssCxn8ck+eagInGuKHDQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.33.tgz", + "integrity": "sha512-tmj//XbNqCWmD+PJ/KnxAouircAmMGLN9IHBO3utH5DXuHHHYN4ZG53DRrQBjlZMiS/1b5IP38U2ay1GfbcQrQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/query-string": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/query-string/-/query-string-6.1.0.tgz", @@ -1577,8 +1626,7 @@ "@types/range-parser": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", - "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==", - "dev": true + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" }, "@types/react": { "version": "16.4.2", @@ -1627,7 +1675,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", - "dev": true, "requires": { "@types/express-serve-static-core": "*", "@types/mime": "*" @@ -2341,8 +2388,7 @@ "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, "asn1.js": { "version": "4.10.1", @@ -2384,8 +2430,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -2428,8 +2473,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.1", @@ -2454,14 +2498,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" }, "b3b": { "version": "0.0.1", @@ -4305,7 +4347,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, "optional": true, "requires": { "tweetnacl": "^0.14.3" @@ -4649,6 +4690,11 @@ "isarray": "^1.0.0" } }, + "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", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", @@ -4855,8 +4901,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "ccount": { "version": "1.0.3", @@ -5210,8 +5255,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, "coa": { "version": "1.0.4", @@ -5300,7 +5344,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -5543,8 +5586,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "3.1.0", @@ -6231,7 +6273,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -6440,8 +6481,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegate": { "version": "3.2.0", @@ -7102,12 +7142,19 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, "optional": true, "requires": { "jsbn": "~0.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", + "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7639,8 +7686,7 @@ "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" }, "extend-shallow": { "version": "3.0.2", @@ -7754,8 +7800,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "2.0.1", @@ -8105,14 +8150,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "1.0.6", @@ -8255,14 +8298,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8277,20 +8318,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8407,8 +8445,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8420,7 +8457,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8435,7 +8471,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8443,14 +8478,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8469,7 +8502,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8550,8 +8582,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8563,7 +8594,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8685,7 +8715,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8797,7 +8826,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -9301,14 +9329,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, "requires": { "ajv": "^5.1.0", "har-schema": "^2.0.0" @@ -9318,7 +9344,6 @@ "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, "requires": { "co": "^4.6.0", "fast-deep-equal": "^1.0.0", @@ -9329,14 +9354,12 @@ "fast-deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" } } }, @@ -9707,7 +9730,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -10534,8 +10556,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", @@ -10630,8 +10651,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-api": { "version": "1.3.1", @@ -11265,7 +11285,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, "optional": true }, "jsdom": { @@ -11329,8 +11348,7 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", @@ -11341,8 +11359,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json3": { "version": "3.3.2", @@ -11372,11 +11389,33 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, + "jsonwebtoken": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", + "integrity": "sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag==", + "requires": { + "jws": "^3.1.5", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -11384,6 +11423,38 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", + "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.10", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.3.0.tgz", + "integrity": "sha512-9q+d5VffK/FvFAjuXoddrq7zQybFSINV4mcwJJExGKXGyjWWpTt3vsn/aX33aB0heY02LK0qSyicdtRK0gVTig==", + "requires": { + "@types/express-jwt": "0.0.34", + "debug": "^2.2.0", + "limiter": "^1.1.0", + "lru-memoizer": "^1.6.0", + "ms": "^2.0.0", + "request": "^2.73.0" + } + }, + "jws": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", + "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "requires": { + "jwa": "^1.1.5", + "safe-buffer": "^5.0.1" + } + }, "keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", @@ -11643,6 +11714,11 @@ "type-check": "~0.3.2" } }, + "limiter": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", + "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==" + }, "load-cfg": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/load-cfg/-/load-cfg-0.2.8.tgz", @@ -11768,6 +11844,11 @@ "path-exists": "^3.0.0" } }, + "lock": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/lock/-/lock-0.1.4.tgz", + "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0=" + }, "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", @@ -11843,16 +11924,41 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, "lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isobject": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -11880,6 +11986,11 @@ "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz", "integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.partial": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.partial/-/lodash.partial-4.2.1.tgz", @@ -12031,6 +12142,28 @@ "yallist": "^2.1.2" } }, + "lru-memoizer": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.12.0.tgz", + "integrity": "sha1-7+ZXBsyKnMZT+A8NWm6jitlQ41I=", + "requires": { + "lock": "~0.1.2", + "lodash": "^4.17.4", + "lru-cache": "~4.0.0", + "very-fast-args": "^1.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + } + } + }, "luxon": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.2.1.tgz", @@ -12918,11 +13051,15 @@ "integrity": "sha512-Zt6HRR6RcJkuj5/N9zeE7FN6YitRW//hK2wTOwX274IBphbY3Zf5+yn5mZ9v/SzAOTMjQNxZf9KkmPLWn0cV4g==", "dev": true }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" }, "object-assign": { "version": "4.1.1", @@ -13316,6 +13453,17 @@ "pause": "0.0.1" } }, + "passport-oauth2": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", + "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -15874,8 +16022,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.1.28", @@ -17215,7 +17362,6 @@ "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.6.0", @@ -17242,14 +17388,12 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, "requires": { "punycode": "^1.4.1" } @@ -17495,8 +17639,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { "version": "2.5.2", @@ -18067,7 +18210,6 @@ "version": "1.14.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -19325,7 +19467,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -19334,7 +19475,6 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, "optional": true }, "type-check": { @@ -19619,6 +19759,11 @@ } } }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "ulid": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", @@ -20152,13 +20297,17 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, + "very-fast-args": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/very-fast-args/-/very-fast-args-1.1.0.tgz", + "integrity": "sha1-4W0dH6+KbllqJGQh/ZCneWPQs5Y=" + }, "vfile": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", @@ -20961,8 +21110,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { "version": "11.0.0", diff --git a/package.json b/package.json index 64e1cfa9f..b4da13f68 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "watch:css-types": "tcm src/core/client/ --watch", "watch:relay-stream": "nodemon --config ./config/nodemon/relay-stream.json", "watch:server": "nodemon --config ./config/nodemon/server.json", + "watch:types": "nodemon --config ./config/nodemon/types.json", "compile:css-types": "tcm src/core/client/", "compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema ./src/core/server/graph/tenant/schema/schema.graphql --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman", + "compile:server:types": "node ./scripts/types.js", "start:development": "NODE_ENV=development ts-node -r tsconfig-paths/register src/index.ts", "lint-fix": "npm run lint:server -- --fix && npm run lint:client -- --fix && npm run lint:scripts -- --fix", "lint": "npm-run-all --parallel lint:*", @@ -43,10 +45,14 @@ "graphql-tools": "^3.0.2", "ioredis": "^3.2.2", "joi": "^13.4.0", + "jsonwebtoken": "^8.3.0", + "jwks-rsa": "^1.3.0", "lodash": "^4.17.10", "luxon": "^1.2.1", "mongodb": "^3.0.10", "passport": "^0.4.0", + "passport-oauth2": "^1.4.0", + "passport-strategy": "^1.0.0", "performance-now": "^2.1.0", "subscriptions-transport-ws": "^0.9.11", "uuid": "^3.2.1" @@ -66,11 +72,14 @@ "@types/ioredis": "^3.2.8", "@types/jest": "^23.1.1", "@types/joi": "^13.0.8", + "@types/jsonwebtoken": "^7.2.7", "@types/lodash": "^4.14.109", "@types/luxon": "^0.5.3", "@types/mongodb": "^3.0.19", "@types/node": "^10.3.1", "@types/passport": "^0.4.5", + "@types/passport-oauth2": "^1.4.5", + "@types/passport-strategy": "^0.2.33", "@types/query-string": "^6.1.0", "@types/react-dom": "^16.0.6", "@types/react-relay": "^1.3.6", @@ -140,4 +149,4 @@ "webpack-hot-client": "^4.0.3", "webpack-manifest-plugin": "^2.0.3" } -} \ No newline at end of file +} diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 3711234e6..ef65f8157 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -3,6 +3,7 @@ import http from "http"; import { Redis } from "ioredis"; import { Db } from "mongodb"; +import { createPassport } from "talk-server/app/middleware/passport"; import { Config } from "talk-server/config"; import { handleSubscriptions } from "talk-server/graph/common/subscriptions/middleware"; import { Schemas } from "talk-server/graph/schemas"; @@ -35,8 +36,11 @@ export async function createApp(options: AppOptions): Promise { // Static Files parent.use(serveStatic); + // Create some services for the router. + const passport = createPassport({ db: options.mongo }); + // Mount the router. - parent.use(await createRouter(options)); + parent.use(await createRouter(options, { passport })); // Error Handling parent.use(errorLogger); diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index a22e11144..bebc52631 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -1,13 +1,89 @@ import { Db } from "mongodb"; import passport, { Authenticator } from "passport"; +import { NextFunction, Response } from "express"; +import OIDCStrategy, { + Token, + VerifyCallback, +} from "talk-server/app/middleware/passport/oidc"; +import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; +import { Tenant } from "talk-server/models/tenant"; +import { create, retrieveWithProfile, User } from "talk-server/models/user"; +import { Request } from "talk-server/types/express"; + export interface PassportOptions { db: Db; } -export function createPassport(opts: PassportOptions): passport.Authenticator { +async function verifyOIDC( + db: Db, + tenant: Tenant, + { iss, sub, email, email_verified }: Token, + done: VerifyCallback +) { + try { + // Construct the profile that will be used to query for the user. + const profile = { + type: "oidc", + provider: iss, + id: sub, + }; + + // Try to lookup user given their id provided in the `sub` claim. + let user = await retrieveWithProfile(db, tenant.id, profile); + if (!user) { + // FIXME: implement rules. + + // Create the new user, as one didn't exist before! + user = await create(db, tenant.id, { + username: null, + role: GQLUSER_ROLE.COMMENTER, + email, + email_verified, + profiles: [profile], + }); + } + + return done(null, user); + } catch (err) { + return done(err); + } +} + +export function createPassport({ + db, +}: PassportOptions): passport.Authenticator { // Create the authenticator. const auth = new Authenticator(); + // Process the OIDC Strategy. + auth.use(new OIDCStrategy({ db }, verifyOIDC.bind(null, db))); + return auth; } + +export const authenticate = ( + authenticator: passport.Authenticator, + name: string +) => (req: Request, res: Response, next: NextFunction) => + authenticator.authenticate( + name, + { session: false }, + (err: Error | null, user: User | null) => { + if (err) { + // TODO: wrap error? + return next(err); + } + + // Set the cache control headers. + res.header( + "Cache-Control", + "private, no-cache, no-store, must-revalidate" + ); + res.header("Expires", "-1"); + res.header("Pragma", "no-cache"); + + // Send back the details! + res.json({ user }); + } + )(req, res, next); diff --git a/src/core/server/app/middleware/passport/oidc.ts b/src/core/server/app/middleware/passport/oidc.ts new file mode 100644 index 000000000..ceac0bc37 --- /dev/null +++ b/src/core/server/app/middleware/passport/oidc.ts @@ -0,0 +1,223 @@ +import jwt from "jsonwebtoken"; +import jwks, { JwksClient } from "jwks-rsa"; +import { Strategy as OAuth2Strategy } from "passport-oauth2"; +import { Strategy } from "passport-strategy"; + +import { reconstructURL } from "talk-server/app/url"; +import { OIDCAuthIntegration, Tenant } from "talk-server/models/tenant"; +import { User } from "talk-server/models/user"; +import { Request } from "talk-server/types/express"; + +export type OIDCStrategyOptions = any; + +export interface Params { + id_token?: string; +} + +export type VerifyCallback = ( + err?: Error | null, + user?: User | null, + info?: object +) => void; + +export interface Token { + iss: string; + sub: string; + email: string; + email_verified?: boolean; +} + +export type OIDCStrategyCallback = ( + tenant: Tenant, + token: Token, + done: VerifyCallback +) => void; + +export interface StrategyItem { + strategy: OAuth2Strategy; + jwksClient?: JwksClient; +} + +// FIXME: attach strategy to cache updates of the tenants + +export default class OIDCStrategy extends Strategy { + public name: string; + + private verify: OIDCStrategyCallback; + private cache: Map; + + constructor(options: OIDCStrategyOptions, verify: OIDCStrategyCallback) { + super(); + + this.name = "oidc"; + this.cache = new Map(); + this.verify = verify; + } + + private lookupJWKSClient( + req: Request, + tenantID: string, + oidc: OIDCAuthIntegration + ) { + let entry = this.cache.get(tenantID); + if (!entry) { + const strategy = this.createStrategy(req, oidc); + + // Create the entry. + entry = { + strategy, + }; + + // We don't reset the entry in the cache here because if we just created + // it, we'll be creating the jwksClient anyways, so we'll update it there. + } + + if (!entry.jwksClient) { + // Create the new JWKS client. + const jwksClient = jwks({ + jwksUri: oidc.jwksURI, + }); + + // Set the jwksClient on the entry. + entry.jwksClient = jwksClient; + + // Update the cached entry. + this.cache.set(tenantID, entry); + } + + return entry.jwksClient; + } + + private verifyCallback( + req: Request, + accessToken: string, + refreshToken: string, + params: Params, + profile: any, + done: VerifyCallback + ) { + // Try to lookup user given their id provided in the `sub` claim of the + // `id_token`. + const { id_token } = params; + if (!id_token) { + // TODO: return better error. + return done(new Error("no id_token in params")); + } + + // Grab the tenant out of the request, as we need some more details. + const { tenant } = req; + + // Grab the JWKSClient. + const client = this.lookupJWKSClient(req, tenant!.id, tenant!.auth.oidc!); + + // Verify that the id_token is valid or not. + jwt.verify( + id_token, + ({ kid }, callback) => { + if (!kid) { + // TODO: return better error. + return callback(new Error("no kid in id_token")); + } + + // Get the signing key from the jwks provider. + client.getSigningKey(kid, (err, key) => { + if (err) { + // TODO: wrap error? + return callback(err); + } + + const signingKey = key.publicKey || key.rsaPublicKey; + callback(null, signingKey); + }); + }, + { + issuer: tenant!.auth.oidc!.issuer, + }, + (err, decoded) => { + if (err) { + // TODO: wrap error? + return done(err); + } + + this.verify(tenant!, decoded as Token, done); + } + ); + } + + private createStrategy( + req: Request, + integration: OIDCAuthIntegration + ): OAuth2Strategy { + const { clientID, clientSecret, authorizationURL, tokenURL } = integration; + + // Construct the callbackURL from the request. + const callbackURL = reconstructURL(req, "/api/tenant/auth/oidc/callback"); + + // Create a new OAuth2Strategy, where we pass the verify callback bound to + // this OIDCStrategy instance. + return new OAuth2Strategy( + { + passReqToCallback: true, + clientID, + clientSecret, + authorizationURL, + tokenURL, + callbackURL, + }, + this.verifyCallback.bind(this) + ); + } + + private async lookupStrategy(req: Request) { + const { tenant } = req; + if (!tenant) { + // TODO: return a better error. + throw new Error("tenant not found"); + } + + // Get the integration from the tenant. If needed, it will be used to create + // a new strategy. + const integration = tenant.auth.oidc; + if (!integration) { + // TODO: return a better error. + throw new Error("integration not found"); + } + + // Try to get the Tenant's cached integrations. + let entry = this.cache.get(tenant.id); + if (!entry) { + // Create the strategy. + const strategy = this.createStrategy(req, integration); + + // Reset the entry. + entry = { + strategy, + }; + + // Update the cached integrations value. + this.cache.set(tenant.id, entry); + } + + return entry.strategy; + } + + public async authenticate(req: Request) { + // Lookup the strategy. + const strategy = await this.lookupStrategy(req); + if (!strategy) { + return; + } + + // Augment the strategy with the request method bindings. + strategy.error = this.error.bind(this); + strategy.fail = this.fail.bind(this); + strategy.pass = this.pass.bind(this); + strategy.redirect = this.redirect.bind(this); + strategy.success = this.success.bind(this); + + // Authenticate with the strategy, binding the current context to the method + // to provide it with the augmented passport handlers. We also request the + // 'openid' scope so we can get an id_token back. + strategy.authenticate(req, { scope: "openid email", session: false }); + } +} diff --git a/src/core/server/app/middleware/passport/sso.ts b/src/core/server/app/middleware/passport/sso.ts new file mode 100644 index 000000000..ecc883685 --- /dev/null +++ b/src/core/server/app/middleware/passport/sso.ts @@ -0,0 +1,9 @@ +import { Strategy } from "passport-strategy"; + +import { Request } from "talk-server/types/express"; + +export default class SSOStrategy extends Strategy { + public async authenticate(req: Request) { + return; + } +} diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index fd481ee5c..1ae9934b8 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -1,13 +1,15 @@ import express from "express"; +import passport from "passport"; import tenantMiddleware from "talk-server/app/middleware/tenant"; import managementGraphMiddleware from "talk-server/graph/management/middleware"; import tenantGraphMiddleware from "talk-server/graph/tenant/middleware"; +import { authenticate } from "talk-server/app/middleware/passport"; import { AppOptions } from "./index"; import playground from "./middleware/playground"; -async function createManagementRouter(opts: AppOptions) { +async function createManagementRouter(app: AppOptions, options: RouterOptions) { const router = express.Router(); // Management API @@ -15,51 +17,63 @@ async function createManagementRouter(opts: AppOptions) { "/graphql", express.json(), await managementGraphMiddleware( - opts.schemas.management, - opts.config, - opts.mongo + app.schemas.management, + app.config, + app.mongo ) ); return router; } -async function createTenantRouter(opts: AppOptions) { +async function createTenantRouter(app: AppOptions, options: RouterOptions) { const router = express.Router(); // Tenant identification middleware. - router.use(tenantMiddleware({ db: opts.mongo })); + router.use(tenantMiddleware({ db: app.mongo })); + + router.use(options.passport.initialize()); + router.use("/auth/oidc", authenticate(options.passport, "oidc")); + router.use("/auth/oidc/callback", authenticate(options.passport, "oidc")); + // router.use("/auth/google", options.passport.authenticate("google")); + // router.use("/auth/google/callback", options.passport.authenticate("google")); + // router.use("/auth/facebook", options.passport.authenticate("facebook")); + // router.use("/auth/facebook/callback", options.passport.authenticate("facebook")); // Tenant API router.use( "/graphql", express.json(), - await tenantGraphMiddleware(opts.schemas.tenant, opts.config, opts.mongo) + await tenantGraphMiddleware(app.schemas.tenant, app.config, app.mongo) ); return router; } -async function createAPIRouter(opts: AppOptions) { +async function createAPIRouter(app: AppOptions, options: RouterOptions) { // Create a router. const router = express.Router(); // Configure the tenant routes. - router.use("/tenant", await createTenantRouter(opts)); + router.use("/tenant", await createTenantRouter(app, options)); // Configure the management routes. - router.use("/management", await createManagementRouter(opts)); + router.use("/management", await createManagementRouter(app, options)); return router; } -export async function createRouter(opts: AppOptions) { +export interface RouterOptions { + passport: passport.Authenticator; +} + +export async function createRouter(app: AppOptions, options: RouterOptions) { // Create a router. const router = express.Router(); - router.use("/api", await createAPIRouter(opts)); + router.use("/api", await createAPIRouter(app, options)); - if (opts.config.get("env") === "development") { + if (app.config.get("env") === "development") { // Tenant GraphiQL router.get( "/tenant/graphiql", diff --git a/src/core/server/app/url.ts b/src/core/server/app/url.ts new file mode 100644 index 000000000..b9f533f1a --- /dev/null +++ b/src/core/server/app/url.ts @@ -0,0 +1,13 @@ +import { Request } from "talk-server/types/express"; +import { URL } from "url"; + +export function reconstructURL(req: Request, input?: string): string { + const scheme = req.secure ? "https" : "http"; + const host = req.get("host"); + const base = `${scheme}://${host}`; + const path = input || req.originalUrl; + + const url = new URL(path, base); + + return url.href; +} diff --git a/src/core/server/graph/common/directives/auth.ts b/src/core/server/graph/common/directives/auth.ts new file mode 100644 index 000000000..600f5a2c1 --- /dev/null +++ b/src/core/server/graph/common/directives/auth.ts @@ -0,0 +1,12 @@ +import { DirectiveResolverFn } from "graphql-tools"; + +const auth: DirectiveResolverFn = (next, src, args, context) => { + return next().then(str => { + if (typeof str === "string") { + return str.toUpperCase(); + } + return str; + }); +}; + +export default auth; diff --git a/src/core/server/graph/tenant/resolvers/auth_settings.ts b/src/core/server/graph/tenant/resolvers/auth_settings.ts new file mode 100644 index 000000000..1519d24c0 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/auth_settings.ts @@ -0,0 +1,14 @@ +import { GQLAuthSettingsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { Auth, AuthIntegration } from "talk-server/models/tenant"; + +const disabled: AuthIntegration = { enabled: false }; + +const AuthSettings: GQLAuthSettingsTypeResolver = { + local: auth => auth.local || disabled, + sso: auth => auth.sso || disabled, + oidc: auth => auth.oidc || disabled, + google: auth => auth.google || disabled, + facebook: auth => auth.facebook || disabled, +}; + +export default AuthSettings; diff --git a/src/core/server/graph/tenant/resolvers/facebook_auth_integration.ts b/src/core/server/graph/tenant/resolvers/facebook_auth_integration.ts new file mode 100644 index 000000000..c0a127d82 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/facebook_auth_integration.ts @@ -0,0 +1,10 @@ +import { GQLFacebookAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { FacebookAuthIntegration } from "talk-server/models/tenant"; + +const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver< + FacebookAuthIntegration +> = { + config: auth => auth, +}; + +export default FacebookAuthIntegration; diff --git a/src/core/server/graph/tenant/resolvers/google_auth_integration.ts b/src/core/server/graph/tenant/resolvers/google_auth_integration.ts new file mode 100644 index 000000000..8edbabcf6 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/google_auth_integration.ts @@ -0,0 +1,10 @@ +import { GQLGoogleAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { GoogleAuthIntegration } from "talk-server/models/tenant"; + +const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver< + GoogleAuthIntegration +> = { + config: auth => auth, +}; + +export default GoogleAuthIntegration; diff --git a/src/core/server/graph/tenant/resolvers/local_auth_integration.ts b/src/core/server/graph/tenant/resolvers/local_auth_integration.ts new file mode 100644 index 000000000..fe14c7b92 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/local_auth_integration.ts @@ -0,0 +1,8 @@ +import { GQLLocalAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { LocalAuthIntegration } from "talk-server/models/tenant"; + +const LocalAuthIntegration: GQLLocalAuthIntegrationTypeResolver< + LocalAuthIntegration +> = {}; + +export default LocalAuthIntegration; diff --git a/src/core/server/graph/tenant/resolvers/oidc_auth_integration.ts b/src/core/server/graph/tenant/resolvers/oidc_auth_integration.ts new file mode 100644 index 000000000..966001ab9 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/oidc_auth_integration.ts @@ -0,0 +1,10 @@ +import { GQLOIDCAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { OIDCAuthIntegration } from "talk-server/models/tenant"; + +const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver< + OIDCAuthIntegration +> = { + config: auth => auth, +}; + +export default OIDCAuthIntegration; diff --git a/src/core/server/graph/tenant/resolvers/sso_auth_integration.ts b/src/core/server/graph/tenant/resolvers/sso_auth_integration.ts new file mode 100644 index 000000000..562cad894 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/sso_auth_integration.ts @@ -0,0 +1,10 @@ +import { GQLSSOAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { SSOAuthIntegration } from "talk-server/models/tenant"; + +const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver< + SSOAuthIntegration +> = { + config: auth => auth, +}; + +export default SSOAuthIntegration; diff --git a/src/core/server/graph/tenant/schema/index.ts b/src/core/server/graph/tenant/schema/index.ts index 1105c358e..1a27fdb3c 100644 --- a/src/core/server/graph/tenant/schema/index.ts +++ b/src/core/server/graph/tenant/schema/index.ts @@ -1,8 +1,14 @@ -import { IResolvers } from "graphql-tools"; +import { attachDirectiveResolvers, IResolvers } from "graphql-tools"; +import auth from "talk-server/graph/common/directives/auth"; import loadSchema from "talk-server/graph/common/schema"; import resolvers from "talk-server/graph/tenant/resolvers"; export default function getTenantSchema() { - return loadSchema("tenant", resolvers as IResolvers); + const schema = loadSchema("tenant", resolvers as IResolvers); + + // Attach the directive resolvers. + attachDirectiveResolvers(schema, { auth }); + + return schema; } diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 96657351d..9f5c0fdcd 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1,3 +1,9 @@ +################################################################################ +## Custom Directives +################################################################################ + +directive @auth(roles: [USER_ROLE!]!) on FIELD_DEFINITION + ################################################################################ ## Custom Scalar Types ################################################################################ @@ -45,7 +51,90 @@ type WordlistSettings { suspect: [String!]! } -# Settings stores the global settings for a given installation. +################################################################################ +## AuthSettings +################################################################################ + +########################## +## LocalAuthIntegration +########################## + +type LocalAuthIntegration { + enabled: Boolean! +} + +########################## +## SSOAuthIntegration +########################## + +type SSOAuthIntegrationConfig { + key: String! +} + +type SSOAuthIntegration { + enabled: Boolean! + config: SSOAuthIntegrationConfig @auth(roles: [ADMIN]) +} + +########################## +## OIDCAuthIntegration +########################## + +type OIDCAuthIntegrationConfig { + clientID: String! + clientSecret: String! + authorizationURL: String! + tokenURL: String! +} + +type OIDCAuthIntegrationOptions { + name: String! +} + +type OIDCAuthIntegration { + enabled: Boolean! + options: OIDCAuthIntegrationOptions + config: SSOAuthIntegrationConfig @auth(roles: [ADMIN]) +} + +########################## +## GoogleAuthIntegration +########################## + +type GoogleAuthIntegrationConfig { + clientID: String! + clientSecret: String! +} + +type GoogleAuthIntegration { + enabled: Boolean! + config: GoogleAuthIntegrationConfig @auth(roles: [ADMIN]) +} + +########################## +## FacebookAuthIntegration +########################## + +type FacebookAuthIntegrationConfig { + clientID: String! + clientSecret: String! +} + +type FacebookAuthIntegration { + enabled: Boolean! + config: FacebookAuthIntegrationConfig @auth(roles: [ADMIN]) +} + +""" +AuthSettings contains all the settings related to authentication and authorization. +""" +type AuthSettings { + local: LocalAuthIntegration! + sso: SSOAuthIntegration! + oidc: OIDCAuthIntegration! + google: GoogleAuthIntegration! + facebook: FacebookAuthIntegration! +} ################################################################################ ## Settings @@ -58,12 +147,12 @@ type Settings { """ domain is the domain that is associated with this Tenant. """ - domain: String! + domain: String @auth(roles: [ADMIN]) """ moderation is the moderation mode for all Asset's on the site. """ - moderation: MODERATION_MODE! + moderation: MODERATION_MODE @auth(roles: [ADMIN]) """ Enables a requirement for email confirmation before a user can login. @@ -100,7 +189,7 @@ type Settings { """ premodLinksEnable will put all comments that contain links into premod. """ - premodLinksEnable: Boolean! + premodLinksEnable: Boolean @auth(roles: [ADMIN]) """ autoCloseStream when true will auto close the stream when the `closeTimeout` @@ -165,18 +254,29 @@ type Settings { """ wordlist will return a given list of words. """ - wordlist: WordlistSettings! + wordlist: WordlistSettings @auth(roles: [ADMIN]) """ domains will return a given list of whitelisted domains. """ - domains: [String!]! + domains: [String!] @auth(roles: [ADMIN]) + + """ + auth contains all the settings related to authentication and authorization. + """ + auth: AuthSettings! } ################################################################################ ## User ################################################################################ +enum USER_ROLE { + COMMENTER + MODERATOR + ADMIN +} + """ User is someone that leaves Comments, and logs in. """ @@ -190,6 +290,11 @@ type User { username is the name of the User visible to other Users. """ username: String! + + """ + role is the current role of the User. + """ + role: USER_ROLE! } ################################################################################ diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index 8e41a1bf2..123c6b261 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -1,9 +1,11 @@ import dotize from "dotize"; import { merge } from "lodash"; import { Db } from "mongodb"; -import { Sub } from "talk-common/types"; import uuid from "uuid"; +import { Sub } from "talk-common/types"; +import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; + function collection(db: Db) { return db.collection>("tenants"); } @@ -22,6 +24,92 @@ export enum Moderation { POST = "POST", } +// AuthIntegrations. + +export interface EmailDomainRuleCondition { + // emailDomain is the domain name component of the email addresses that should + // match for this condition. + emailDomain: string; + + // emailVerifiedRequired stipulates that this rule only applies when the user + // account has been marked as having their email address already verified. + emailVerifiedRequired: boolean; +} + +// RoleRule describes the role assignment for when a user logs into Talk, how +// they can have their account automatically upgraded to a specific role when +// the domain for their email matches the one provided. +export interface RoleRule extends Partial { + // role is the specific GQLUSER_ROLE that should be assigned to the newly created + // user depending on their email address. + role: GQLUSER_ROLE; +} + +export interface AuthRules { + // roles allow the configuration of automatic role assignment based on the + // user's email address. + roles?: RoleRule[]; + + // restrictTo when populated, will restrict which users can login using this + // integration. If a user successfully logs in using the OIDCStrategy, but + // does not match the following rules, the user will not be created. + restrictTo?: EmailDomainRuleCondition[]; +} + +export interface AuthIntegration { + enabled: boolean; +} + +// SSOAuthIntegration is an AuthIntegration that provides a secret to the admins +// of a tenant, where they can sign a SSO payload with it to provide to the +// embed to allow single sign on. +export interface SSOAuthIntegration extends AuthIntegration { + key: string; +} + +// OIDCAuthIntegration provides a way to store Open ID Connect credentials. This +// will be used in the admin to provide staff logins for users. +export interface OIDCAuthIntegration extends AuthIntegration { + clientID: string; + clientSecret: string; + issuer: string; + authorizationURL: string; + jwksURI: string; + tokenURL: string; +} + +export interface FacebookAuthIntegration extends AuthIntegration { + clientID: string; + clientSecret: string; +} + +export interface GoogleAuthIntegration extends AuthIntegration { + clientID: string; + clientSecret: string; +} + +export type LocalAuthIntegration = AuthIntegration; + +// Auth describes all of the possible auth integration configurations. +export interface Auth { + // local is the auth integration for the local auth. + local: LocalAuthIntegration; + + // sso is the external auth integration for the single sign on auth. + sso?: SSOAuthIntegration; + + // sso is the external auth integration for the OpenID Connect auth. + oidc?: OIDCAuthIntegration; + + // sso is the external auth integration for the Google auth. + google?: GoogleAuthIntegration; + + // sso is the external auth integration for the Facebook auth. + facebook?: FacebookAuthIntegration; +} + +// Tenant definition. + export interface Tenant { readonly id: string; @@ -57,6 +145,9 @@ export interface Tenant { // domains is the set of whitelisted domains. domains: string[]; + + // Set of configured authentication integrations. + auth: Auth; } /** @@ -99,6 +190,11 @@ export async function createTenant(db: Db, input: CreateTenantInput) { suspect: [], banned: [], }, + auth: { + local: { + enabled: true, + }, + }, }; // Create the new Tenant by merging it together with the defaults. diff --git a/src/core/server/models/user.ts b/src/core/server/models/user.ts index 6812fefce..487f7174f 100644 --- a/src/core/server/models/user.ts +++ b/src/core/server/models/user.ts @@ -1,9 +1,11 @@ import { merge } from "lodash"; import { Db } from "mongodb"; +import uuid from "uuid"; + import { Omit, Sub } from "talk-common/types"; +import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; import { ActionCounts } from "talk-server/models/actions"; import { TenantResource } from "talk-server/models/tenant"; -import uuid from "uuid"; function collection(db: Db) { return db.collection>("users"); @@ -11,6 +13,7 @@ function collection(db: Db) { export interface Profile { readonly id: string; + readonly type: string; provider: string; } @@ -45,13 +48,6 @@ export enum UserUsernameStatus { CHANGED = "CHANGED", } -export enum UserRole { - ADMIN = "ADMIN", - MODERATOR = "MODERATOR", - STAFF = "STAFF", - COMMENTER = "COMMENTER", -} - export interface UserStatusHistory { status: T; // TODO: migrate field assigned_by?: string; @@ -72,11 +68,13 @@ export interface UserStatus { export interface User extends TenantResource { readonly id: string; - username: string; + username: string | null; password?: string; + email?: string; + email_verified?: boolean; profiles: Profile[]; tokens: Token[]; - role: UserRole; + role: GQLUSER_ROLE; status: UserStatus; action_counts: ActionCounts; ignored_users: string[]; // TODO: migrate field @@ -89,7 +87,6 @@ export type CreateUserInput = Omit< | "tenant_id" | "tokens" | "status" - | "role" | "action_counts" | "ignored_users" | "created_at" @@ -98,15 +95,11 @@ export type CreateUserInput = Omit< export async function create(db: Db, tenantID: string, input: CreateUserInput) { const now = new Date(); - // // Pull out some useful properties from the input. - // const { body, status } = input; - // default are the properties set by the application when a new user is // created. const defaults: Sub = { id: uuid.v4(), tenant_id: tenantID, - role: UserRole.COMMENTER, tokens: [], action_counts: {}, ignored_users: [], @@ -120,7 +113,9 @@ export async function create(db: Db, tenantID: string, input: CreateUserInput) { history: [], }, username: { - status: UserUsernameStatus.SET, + status: input.username + ? UserUsernameStatus.SET + : UserUsernameStatus.UNSET, history: [], }, }, @@ -153,11 +148,24 @@ export async function retrieveMany(db: Db, tenantID: string, ids: string[]) { return ids.map(id => users.find(comment => comment.id === id) || null); } +export async function retrieveWithProfile( + db: Db, + tenantID: string, + profile: Profile +) { + return collection(db).findOne({ + tenant_id: tenantID, + profiles: { + $elemMatch: profile, + }, + }); +} + export async function updateRole( db: Db, tenantID: string, id: string, - role: UserRole + role: GQLUSER_ROLE ) { const result = await collection(db).findOneAndUpdate( { id, tenant_id: tenantID }, diff --git a/src/types/jsonwebtoken.d.ts b/src/types/jsonwebtoken.d.ts new file mode 100644 index 000000000..de8c9f82f --- /dev/null +++ b/src/types/jsonwebtoken.d.ts @@ -0,0 +1,19 @@ +import { VerifyOptions, VerifyCallback } from "jsonwebtoken"; + +declare module "jsonwebtoken" { + export type KeyFunctionCallback = ( + err: Error | null, + secretOrPublicKey?: string | Buffer + ) => void; + export type KeyFunction = ( + headers: { kid?: string }, + callback: KeyFunctionCallback + ) => void; + + export function verify( + token: string, + secretOrPublicKey: string | Buffer | KeyFunction, + options?: VerifyOptions, + callback?: VerifyCallback + ): void; +} diff --git a/src/types/webfinger.d.ts b/src/types/webfinger.d.ts new file mode 100644 index 000000000..a13bfb998 --- /dev/null +++ b/src/types/webfinger.d.ts @@ -0,0 +1,16 @@ +declare module "webfinger" { + export interface WebfingerOptions { + webfingerOnly?: boolean; + } + + export interface WebfingerCallback { + (err: Error, jrd: { [key: string]: any }): void; + } + + export function webfinger( + resource: string, + res: string, + options: WebfingerOptions, + callback: WebfingerCallback + ): void; +}