initial oidc support

This commit is contained in:
Wyatt Johnson
2018-06-29 16:11:32 -06:00
parent ab8fd935e7
commit 1236946312
22 changed files with 968 additions and 141 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"exec": "npm run compile:server:types",
"ext": "graphql",
"watch": [
"./src/core/server/graph"
]
}
+247 -99
View File
@@ -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",
+10 -1
View File
@@ -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"
}
}
}
+5 -1
View File
@@ -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<Express> {
// 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);
@@ -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);
@@ -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<string, StrategyItem>;
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 });
}
}
@@ -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;
}
}
+27 -13
View File
@@ -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",
+13
View File
@@ -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;
}
@@ -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;
@@ -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<Auth> = {
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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+8 -2
View File
@@ -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;
}
@@ -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!
}
################################################################################
+97 -1
View File
@@ -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<Readonly<Tenant>>("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<EmailDomainRuleCondition> {
// 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.
+25 -17
View File
@@ -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<Readonly<User>>("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<T> {
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<User, CreateUserInput> = {
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 },
+19
View File
@@ -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;
}
+16
View File
@@ -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;
}