From e2bdaf6603f5976d149955c6b548279be067d887 Mon Sep 17 00:00:00 2001 From: ModZero Date: Thu, 26 Sep 2019 15:52:58 +0200 Subject: [PATCH] Basic login --- package-lock.json | 256 +++++++++++++++++++++++++++++++++++- package.json | 11 +- samples.http | 24 ++++ src/crypto.ts | 55 ++++++++ src/db/index.ts | 3 +- src/db/repos/index.ts | 4 +- src/db/repos/users.ts | 50 +++++++ src/db/sql/index.ts | 7 +- src/db/sql/users/create.sql | 1 + src/db/sql/users/login.sql | 1 + src/index.ts | 26 +++- src/routes/auth.ts | 64 +++++++++ src/routes/index.ts | 24 ++++ 13 files changed, 514 insertions(+), 12 deletions(-) create mode 100644 samples.http create mode 100644 src/crypto.ts create mode 100644 src/db/repos/users.ts create mode 100644 src/db/sql/users/create.sql create mode 100644 src/db/sql/users/login.sql create mode 100644 src/routes/auth.ts create mode 100644 src/routes/index.ts diff --git a/package-lock.json b/package-lock.json index 7b4b52f..8ba472b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,14 @@ "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", "dev": true }, + "@phc/format": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-0.5.0.tgz", + "integrity": "sha512-JWtZ5P1bfXU0bAtTzCpOLYHDXuxSVdtL/oqz4+xa97h8w9E5IlVN333wugXVFv8vZ1hbXObKQf1ptXmFFcMByg==", + "requires": { + "safe-buffer": "^5.1.2" + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -122,6 +130,15 @@ "@types/node": "*" } }, + "@types/cookie-parser": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz", + "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/cookies": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.3.tgz", @@ -198,11 +215,26 @@ "graphql": "^14.5.3" } }, + "@types/helmet": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.44.tgz", + "integrity": "sha512-MPZ7HoCGLoajTZhy3hMWHvdiOMHCZJ51U2Bve25oujn3G7KdXy0G3iRS0dUpVtKOGMNcuBF6yLDRpNdm2JH0OQ==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/http-assert": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" }, + "@types/http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha512-mzJX3tIbtadNZQIDbfA9eW+mAjww7GY7WYcfKDGB5SSXMAzI8KD+5fvyX5FqcKtns346+WGwAJJ8cPsDxMz0lw==", + "dev": true + }, "@types/keygrip": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", @@ -523,6 +555,16 @@ "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", "dev": true }, + "argon2": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.24.1.tgz", + "integrity": "sha512-2S677iO18I+SQEUONkpvyagF9BJDYdiT82KqSMPQ2zP0oIYagVIPM0Y8T5pJ/4F4CrnN9PTCGA+ye1S0KupW3g==", + "requires": { + "@phc/format": "^0.5.0", + "node-addon-api": "^1.7.1", + "node-gyp-build": "^4.1.0" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -607,8 +649,27 @@ "qs": "6.7.0", "raw-body": "2.4.0", "type-is": "~1.6.17" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } } }, + "bowser": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.6.1.tgz", + "integrity": "sha512-hySGUuLhi0KetfxPZpuJOsjM0kRvCiCgPBygBkzGzJNsq/nbJmaO8QJc6xlWfeFFnMvtd/LeKkhDJGVrmVobUA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -665,6 +726,11 @@ "map-obj": "^1.0.0" } }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -711,6 +777,11 @@ "safe-buffer": "5.1.2" } }, + "content-security-policy-builder": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", + "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -721,6 +792,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -749,6 +836,11 @@ "array-find-index": "^1.0.1" } }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, "dataloader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", @@ -821,6 +913,16 @@ "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", "dev": true }, + "dns-prefetch-control": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", + "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==" + }, + "dont-sniff-mimetype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", + "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" + }, "dotenv": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", @@ -923,6 +1025,11 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "expect-ct": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz", + "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g==" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -983,6 +1090,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, + "feature-policy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", + "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==" + }, "filewatcher": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.1.tgz", @@ -1026,6 +1138,11 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, + "frameguard": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz", + "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==" + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -1152,22 +1269,99 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, + "helmet": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.21.1.tgz", + "integrity": "sha512-IC/54Lxvvad2YiUdgLmPlNFKLhNuG++waTF5KPYq/Feo3NNhqMFbcLAlbVkai+9q0+4uxjxGPJ9bNykG+3zZNg==", + "requires": { + "depd": "2.0.0", + "dns-prefetch-control": "0.2.0", + "dont-sniff-mimetype": "1.1.0", + "expect-ct": "0.2.0", + "feature-policy": "0.3.0", + "frameguard": "3.1.0", + "helmet-crossdomain": "0.4.0", + "helmet-csp": "2.9.2", + "hide-powered-by": "1.1.0", + "hpkp": "2.0.0", + "hsts": "2.2.0", + "ienoopen": "1.1.0", + "nocache": "2.1.0", + "referrer-policy": "1.2.0", + "x-xss-protection": "1.3.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "helmet-crossdomain": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", + "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" + }, + "helmet-csp": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.9.2.tgz", + "integrity": "sha512-Lt5WqNfbNjEJ6ysD4UNpVktSyjEKfU9LVJ1LaFmPfYseg/xPealPfgHhtqdAdjPDopp5zbg/VWCyp4cluMIckw==", + "requires": { + "bowser": "^2.6.1", + "camelize": "1.0.0", + "content-security-policy-builder": "2.1.0", + "dasherize": "2.0.0" + } + }, + "hide-powered-by": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", + "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" + }, "hosted-git-info": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", "dev": true }, + "hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", + "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", + "requires": { + "depd": "2.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", "requires": { "depd": "~1.1.2", - "inherits": "2.0.3", + "inherits": "2.0.4", "setprototypeof": "1.1.1", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } } }, "iconv-lite": { @@ -1178,6 +1372,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ienoopen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz", + "integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ==" + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -1466,11 +1665,26 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nocache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", + "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" + }, + "node-addon-api": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz", + "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==" + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, + "node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==" + }, "node-notifier": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", @@ -1843,6 +2057,20 @@ "http-errors": "1.7.2", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } } }, "read-pkg": { @@ -1887,6 +2115,11 @@ "strip-indent": "^1.0.1" } }, + "referrer-policy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", + "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==" + }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", @@ -2312,6 +2545,16 @@ "tslib": "^1.8.1" } }, + "tweetnacl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", + "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==" + }, + "tweetnacl-util": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz", + "integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU=" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2395,6 +2638,11 @@ "async-limiter": "~1.0.0" } }, + "x-xss-protection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", + "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 96c076f..f949fd4 100644 --- a/package.json +++ b/package.json @@ -15,20 +15,29 @@ "license": "AGPL-3.0-or-later", "dependencies": { "apollo-server-express": "^2.9.3", + "argon2": "^0.24.1", + "cookie-parser": "^1.4.4", "dataloader": "^1.4.0", "dotenv": "^8.1.0", "express": "^4.17.1", "express-pino-logger": "^4.0.0", "graphql": "^14.5.7", + "helmet": "^3.21.1", + "http-errors": "^1.7.3", "luxon": "^1.19.2", "monet": "^0.9.0", "pg-promise": "^9.2.1", - "pino": "^5.13.3" + "pino": "^5.13.3", + "tweetnacl": "^1.0.1", + "tweetnacl-util": "^0.15.0" }, "devDependencies": { + "@types/cookie-parser": "^1.4.2", "@types/dotenv": "^6.1.1", "@types/express": "^4.17.1", "@types/express-pino-logger": "^4.0.1", + "@types/helmet": "0.0.44", + "@types/http-errors": "^1.6.2", "@types/luxon": "^1.15.2", "@types/node": "^12.7.5", "@types/pino": "^5.8.10", diff --git a/samples.http b/samples.http new file mode 100644 index 0000000..3a8a411 --- /dev/null +++ b/samples.http @@ -0,0 +1,24 @@ +# @name: login + +POST {{ baseUrl }}/auth/ HTTP/1.1 +Content-type: application/json + +{ + "email": "test@test.com", + "password": "hornyhorny" +} + +### + +GET {{ baseUrl }}/auth/bootstrap HTTP/1.1 + +### + +POST {{ baseUrl }}/auth/bootstrap HTTP/1.1 +Content-Type: application/json + +{ + "token": {{ bootstrapToken }}, + "email": "test@test.com", + "password": "hornyhorny" +} diff --git a/src/crypto.ts b/src/crypto.ts new file mode 100644 index 0000000..28b3327 --- /dev/null +++ b/src/crypto.ts @@ -0,0 +1,55 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { randomBytes, secretbox } from "tweetnacl"; +import { + decodeBase64, + decodeUTF8, + encodeBase64, + encodeUTF8 +} from "tweetnacl-util"; + +const secret = decodeBase64(process.env.SECRET); +const newNonce = () => randomBytes(secretbox.nonceLength); +const secretBoxKey = secret.slice(0, secretbox.keyLength); +if (secretBoxKey.length !== secretbox.keyLength) { + throw new Error("Secret too short to encrypt anything"); +} + +export const box = (json: any, key: Uint8Array = secretBoxKey) => { + const nonce = newNonce(); + const messageUint8 = decodeUTF8(JSON.stringify(json)); + const encrypted = secretbox(messageUint8, nonce, key); + const fullMessage = new Uint8Array(nonce.length + encrypted.length); + fullMessage.set(nonce); + fullMessage.set(encrypted, nonce.length); + + return encodeBase64(fullMessage); +}; + +export const unbox = ( + messageWithNonce: string, + key: Uint8Array = secretBoxKey +) => { + const fullMessageUint8 = decodeBase64(messageWithNonce); + const nonce = fullMessageUint8.slice(0, secretbox.nonceLength); + const message = fullMessageUint8.slice( + secretbox.nonceLength, + fullMessageUint8.length + ); + + const decrypted = secretbox.open(message, nonce, key); + return JSON.parse(encodeUTF8(decrypted)); +}; diff --git a/src/db/index.ts b/src/db/index.ts index d4cab85..557650e 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -14,13 +14,14 @@ // along with this program. If not, see . import pgPromise, { IDatabase, IInitOptions } from "pg-promise"; -import { Extensions, MigrationRepository } from "./repos"; +import { Extensions, MigrationRepository, UserRepository } from "./repos"; type ExtendedProtocol = IDatabase & Extensions; const initOptions: IInitOptions = { extend(obj: ExtendedProtocol, dc: any) { obj.migrations = new MigrationRepository(obj, pgp); + obj.users = new UserRepository(obj, pgp); } }; diff --git a/src/db/repos/index.ts b/src/db/repos/index.ts index 6eff231..522b81a 100644 --- a/src/db/repos/index.ts +++ b/src/db/repos/index.ts @@ -14,9 +14,11 @@ // along with this program. If not, see . import { MigrationRepository } from "./migrations"; +import { UserRepository } from "./users"; export interface Extensions { migrations: MigrationRepository; + users: UserRepository; } -export { MigrationRepository }; +export { MigrationRepository, UserRepository }; diff --git a/src/db/repos/users.ts b/src/db/repos/users.ts new file mode 100644 index 0000000..bcfceb0 --- /dev/null +++ b/src/db/repos/users.ts @@ -0,0 +1,50 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import argon2 from "argon2"; +import { Maybe, None, Some } from "monet"; +import { IDatabase, IMain } from "pg-promise"; +import { users as sql } from "../sql"; + +export class UserRepository { + private db: IDatabase; + + constructor(db: IDatabase, pgp: IMain) { + this.db = db; + } + + public async login(email: string, password: string): Promise> { + const { id, encryptedPassword } = await this.db + .oneOrNone(sql.login, [email]) + .then(user => ({ + encryptedPassword: user.encrypted_password, + id: +user.id + })); + if (id === null) { + return None(); + } + + return (await argon2.verify(encryptedPassword, password)) + ? Some(id) + : None(); + } + + public async create(email: string, password: string): Promise { + const encryptedPassword = await argon2.hash(password); + return this.db + .one(sql.create, [email, encryptedPassword]) + .then((user: { id: number }) => +user.id); + } +} diff --git a/src/db/sql/index.ts b/src/db/sql/index.ts index 3db15be..aab5e4b 100644 --- a/src/db/sql/index.ts +++ b/src/db/sql/index.ts @@ -33,7 +33,12 @@ const migrations = { })) }; -export { migrations }; +const users = { + create: sql("users/create.sql"), + login: sql("users/login.sql") +}; + +export { migrations, users }; /** Helper for linking to external query files */ function sql(file: string): QueryFile { diff --git a/src/db/sql/users/create.sql b/src/db/sql/users/create.sql new file mode 100644 index 0000000..b87d5b3 --- /dev/null +++ b/src/db/sql/users/create.sql @@ -0,0 +1 @@ +INSERT INTO users (email, encrypted_password) VALUES ($1, $2) RETURNING id \ No newline at end of file diff --git a/src/db/sql/users/login.sql b/src/db/sql/users/login.sql new file mode 100644 index 0000000..69e9831 --- /dev/null +++ b/src/db/sql/users/login.sql @@ -0,0 +1 @@ +SELECT id, encrypted_password FROM users WHERE email=$1 \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a00bd26..626ae25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,23 +13,41 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +import cookieParser from "cookie-parser"; import express from "express"; import pinoExpress from "express-pino-logger"; +import helmet from "helmet"; +import createHttpError from "http-errors"; import { db } from "./db"; import { server as graphqlServer } from "./graphql"; import logger from "./logger"; +import authRouter from "./routes/auth"; +import indexRouter from "./routes/index"; async function main() { - await db.migrations.create(); - await db.migrations.apply(); + await db.tx(async t => { + await t.migrations.create(); + await t.migrations.apply(); + }); const server = graphqlServer(); const app = express(); const expressPino = pinoExpress({ logger }); + app.use(helmet()); app.use(expressPino); - server.applyMiddleware({ app, path: "/graphql" }); - const port = 3000; + app.use(express.json()); + app.use(express.urlencoded({ extended: false })); + app.use(cookieParser()); + app.use("/", indexRouter); + app.use("/auth/", authRouter); + server.applyMiddleware({ app, path: "/graphql" }); + + app.use((req, res, next) => { + next(createHttpError(404)); + }); + + const port = 3000; app.listen(port, () => logger.info("Example app listening", { uri: `http://localhost:${port}${server.graphqlPath}` diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..360ba97 --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,64 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import express from "express"; +import createHttpError from "http-errors"; +import { DateTime } from "luxon"; +import { box, unbox } from "../crypto"; +import { db } from "../db"; + +const router = express.Router(); + +router.post("/", async (req, res, next) => { + const userID = await db.users.login(req.body.email, req.body.password); + if (userID.isSome()) { + res.send(`Hi, ${userID.some()}`); + } else { + res.send(`Go away.`); + } +}); + +interface Token { + expires: string; +} + +router.get("/bootstrap", async (req, res, next) => { + const token: Token = { + expires: DateTime.local() + .plus({ hours: 2 }) + .toISO() + }; + req.log.info("Token issued", { token: box(token) }); +}); + +router.post("/bootstrap", async (req, res, next) => { + const token: Token = unbox(req.body.token); + const expired = DateTime.fromISO(token.expires).diffNow(); + if (expired.as("milliseconds") < 0) { + next(createHttpError(401)); + return; + } + + const email: string = req.body.email; + const password: string = req.body.password; + + if (!email || !password || password.length < 8) { + res.send("Please provide an email and a password longer than 8 characters"); + return; + } + await db.users.create(email, password); +}); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..8dd3c7a --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,24 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import express from "express"; + +const router = express.Router(); + +router.get("/", (req, res, next) => { + res.send("Hello, world!"); +}); + +export default router;