mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-22 10:22:39 +01:00
Implement WebAuthn (#967)
* implement webauthn * code review --------- Co-authored-by: Madeline <46743919+MaddyUnderStars@users.noreply.github.com>
This commit is contained in:
parent
e98cdfbce0
commit
709dc7280e
@ -903,11 +903,37 @@
|
||||
"payload_json": {
|
||||
"type": "string"
|
||||
},
|
||||
"file": {},
|
||||
"file": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"filename"
|
||||
]
|
||||
},
|
||||
"attachments": {
|
||||
"description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion",
|
||||
"type": "array",
|
||||
"items": {}
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"filename",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"sticker_ids": {
|
||||
"type": "array",
|
||||
@ -962,6 +988,9 @@
|
||||
"BanCreateSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"delete_message_seconds": {
|
||||
"type": "string"
|
||||
},
|
||||
"delete_message_days": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -1237,7 +1266,9 @@
|
||||
"client_build_number": {
|
||||
"type": "integer"
|
||||
},
|
||||
"client_event_source": {},
|
||||
"client_event_source": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_version": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -2284,6 +2315,129 @@
|
||||
"required": [
|
||||
"days"
|
||||
]
|
||||
},
|
||||
"TransportMakeRequestResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"statusCode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"x-sentry-rate-limits": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"retry-after": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"retry-after",
|
||||
"x-sentry-rate-limits"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Partial<GenerateWebAuthnCredentialsSchema>": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Partial<CreateWebAuthnCredentialSchema>": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credential": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserDeleteSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"GenerateWebAuthnCredentialsSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password"
|
||||
]
|
||||
},
|
||||
"CreateWebAuthnCredentialSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credential": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"credential",
|
||||
"name",
|
||||
"ticket"
|
||||
]
|
||||
},
|
||||
"WebAuthnPostSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Partial<GenerateWebAuthnCredentialsSchema>"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Partial<CreateWebAuthnCredentialSchema>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebAuthnTotpSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"ticket"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -2377,6 +2531,9 @@
|
||||
},
|
||||
{
|
||||
"name": "-"
|
||||
},
|
||||
{
|
||||
"name": "read-states"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@ -4912,7 +5069,8 @@
|
||||
],
|
||||
"tags": [
|
||||
"guilds"
|
||||
]
|
||||
],
|
||||
"x-permission-required": "MANAGE_GUILD"
|
||||
}
|
||||
},
|
||||
"/guilds/{guild_id}/emojis/": {
|
||||
@ -6850,6 +7008,28 @@
|
||||
"users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/read-states/ack-bulk/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"bearer": true
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AckBulkSchema"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"read-states"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34298
assets/schemas.json
34298
assets/schemas.json
File diff suppressed because it is too large
Load Diff
259
package-lock.json
generated
259
package-lock.json
generated
@ -24,6 +24,7 @@
|
||||
"dotenv": "^16.0.2",
|
||||
"exif-be-gone": "^1.3.1",
|
||||
"fast-zlib": "^2.0.1",
|
||||
"fido2-lib": "^3.3.5",
|
||||
"file-type": "16.5",
|
||||
"form-data": "^4.0.0",
|
||||
"i18next": "^21.9.2",
|
||||
@ -1409,6 +1410,78 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz",
|
||||
"integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz",
|
||||
"integrity": "sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz",
|
||||
"integrity": "sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz",
|
||||
"integrity": "sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz",
|
||||
"integrity": "sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz",
|
||||
"integrity": "sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@ -1489,6 +1562,11 @@
|
||||
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@hexagon/base64": {
|
||||
"version": "1.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.25.tgz",
|
||||
"integrity": "sha512-BaG1ep08FpbHB11ck2aH4bvXvoFUn0GPireHCa92Sl1f8JCQnIboBEAJ4FmonIx67S00Mf3h7P8nJqeznFWGcQ=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
@ -1634,6 +1712,42 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz",
|
||||
"integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==",
|
||||
"dependencies": {
|
||||
"asn1js": "^3.0.5",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/json-schema": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
|
||||
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/webcrypto": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz",
|
||||
"integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.0",
|
||||
"@peculiar/json-schema": "^1.1.12",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"tslib": "^2.4.1",
|
||||
"webcrypto-core": "^1.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.28.1.tgz",
|
||||
@ -2503,6 +2617,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
|
||||
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||
@ -2704,6 +2831,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytestreamjs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
|
||||
"integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache": {
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
|
||||
@ -2784,6 +2919,35 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cbor-extract": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.1.1.tgz",
|
||||
"integrity": "sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"download-cbor-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cbor-extract/cbor-extract-darwin-arm64": "2.1.1",
|
||||
"@cbor-extract/cbor-extract-darwin-x64": "2.1.1",
|
||||
"@cbor-extract/cbor-extract-linux-arm": "2.1.1",
|
||||
"@cbor-extract/cbor-extract-linux-arm64": "2.1.1",
|
||||
"@cbor-extract/cbor-extract-linux-x64": "2.1.1",
|
||||
"@cbor-extract/cbor-extract-win32-x64": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cbor-x": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.4.1.tgz",
|
||||
"integrity": "sha512-qp6nM61RaamDJWsDGHzMIQ4+XBtg7/QIoBi5Lra4IDU65eP8lHcgkkJ9t2yIU8EvRThBfFCh6+S1Qkrmq93J3Q==",
|
||||
"optionalDependencies": {
|
||||
"cbor-extract": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@ -4031,6 +4195,23 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fido2-lib": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/fido2-lib/-/fido2-lib-3.3.5.tgz",
|
||||
"integrity": "sha512-u+2RITFHew1tYFtzde/+FX1fyh1mVGB7QLiU7gyHwq7g8W02FvOvhv4oJqDh7J90TyLFbEqPdP4W/tFNEKiHMw==",
|
||||
"dependencies": {
|
||||
"@hexagon/base64": "~1.1.23",
|
||||
"@peculiar/webcrypto": "~1.4.0",
|
||||
"asn1js": "~3.0.2",
|
||||
"cbor-x": "~1.4.0",
|
||||
"jose": "^4.10.0",
|
||||
"pkijs": "~3.0.8",
|
||||
"tldts": "~5.7.91"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -4750,6 +4931,14 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz",
|
||||
"integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
|
||||
@ -5505,6 +5694,17 @@
|
||||
"node": ">= 10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz",
|
||||
"integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/are-we-there-yet": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
|
||||
@ -5925,6 +6125,21 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkijs": {
|
||||
"version": "3.0.13",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.0.13.tgz",
|
||||
"integrity": "sha512-a4uShsMDMZf0UpiNeedpARIN2TChjFn4xze7HE+Dm3lsX+o2MHcSm8Lf2Tt+f1le8FHbBevdWlcLO5boSW/9NQ==",
|
||||
"dependencies": {
|
||||
"asn1js": "^3.0.5",
|
||||
"bytestreamjs": "^2.0.0",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
@ -6071,6 +6286,22 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pvtsutils": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz",
|
||||
"integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
|
||||
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
@ -6775,6 +7006,22 @@
|
||||
"node": ">=0.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "5.7.104",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-5.7.104.tgz",
|
||||
"integrity": "sha512-PlziEIVPH/ogbqOhS35K6MOeD09rd9U5g2NHO5n9NZeMC1PGpXgsjQpoJ1KiRnjhZsWDkzN8EoX3xQZuz5ZyFQ==",
|
||||
"dependencies": {
|
||||
"tldts-core": "^5.7.104"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "5.7.104",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-5.7.104.tgz",
|
||||
"integrity": "sha512-8vhSgc2nzPNT0J7XyCqcOtQ6+ySBn+gsPmj5h95YytIZ7L2Xl40paUmj0T6Uko42HegHGQxXieunHIQuABWSmQ=="
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@ -7264,6 +7511,18 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webcrypto-core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz",
|
||||
"integrity": "sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.1.6",
|
||||
"@peculiar/json-schema": "^1.1.12",
|
||||
"asn1js": "^3.0.1",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
@ -78,6 +78,7 @@
|
||||
"dotenv": "^16.0.2",
|
||||
"exif-be-gone": "^1.3.1",
|
||||
"fast-zlib": "^2.0.1",
|
||||
"fido2-lib": "^3.3.5",
|
||||
"file-type": "16.5",
|
||||
"form-data": "^4.0.0",
|
||||
"i18next": "^21.9.2",
|
||||
|
@ -19,7 +19,13 @@
|
||||
import "missing-native-js-functions";
|
||||
import { Server, ServerOptions } from "lambert-server";
|
||||
import { Authentication, CORS } from "./middlewares/";
|
||||
import { Config, initDatabase, initEvent, Sentry } from "@fosscord/util";
|
||||
import {
|
||||
Config,
|
||||
initDatabase,
|
||||
initEvent,
|
||||
Sentry,
|
||||
WebAuthn,
|
||||
} from "@fosscord/util";
|
||||
import { ErrorHandler } from "./middlewares/ErrorHandler";
|
||||
import { BodyParser } from "./middlewares/BodyParser";
|
||||
import { Router, Request, Response } from "express";
|
||||
@ -58,6 +64,7 @@ export class FosscordServer extends Server {
|
||||
await initEvent();
|
||||
await initInstance();
|
||||
await Sentry.init(this.app);
|
||||
WebAuthn.init();
|
||||
|
||||
const logRequests = process.env["LOG_REQUESTS"] != undefined;
|
||||
if (logRequests) {
|
||||
|
@ -27,6 +27,7 @@ export const NO_AUTHORIZATION_ROUTES = [
|
||||
"/auth/register",
|
||||
"/auth/location-metadata",
|
||||
"/auth/mfa/totp",
|
||||
"/auth/mfa/webauthn",
|
||||
// Routes with a seperate auth system
|
||||
"/webhooks/",
|
||||
// Public information endpoints
|
||||
|
@ -16,18 +16,20 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route, getIpAdress, verifyCaptcha } from "@fosscord/api";
|
||||
import bcrypt from "bcrypt";
|
||||
import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
|
||||
import {
|
||||
Config,
|
||||
User,
|
||||
generateToken,
|
||||
adjustEmail,
|
||||
Config,
|
||||
FieldErrors,
|
||||
generateToken,
|
||||
generateWebAuthnTicket,
|
||||
LoginSchema,
|
||||
User,
|
||||
WebAuthn,
|
||||
} from "@fosscord/util";
|
||||
import bcrypt from "bcrypt";
|
||||
import crypto from "crypto";
|
||||
import { Request, Response, Router } from "express";
|
||||
|
||||
const router: Router = Router();
|
||||
export default router;
|
||||
@ -73,7 +75,10 @@ router.post(
|
||||
"settings",
|
||||
"totp_secret",
|
||||
"mfa_enabled",
|
||||
"webauthn_enabled",
|
||||
"security_keys",
|
||||
],
|
||||
relations: ["security_keys"],
|
||||
}).catch(() => {
|
||||
throw FieldErrors({
|
||||
login: {
|
||||
@ -116,7 +121,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (user.mfa_enabled) {
|
||||
if (user.mfa_enabled && !user.webauthn_enabled) {
|
||||
// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
|
||||
const ticket = crypto.randomBytes(40).toString("hex");
|
||||
|
||||
@ -130,6 +135,40 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (user.mfa_enabled && user.webauthn_enabled) {
|
||||
if (!WebAuthn.fido2) {
|
||||
// TODO: I did this for typescript and I can't use !
|
||||
throw new Error("WebAuthn not enabled");
|
||||
}
|
||||
|
||||
const options = await WebAuthn.fido2.assertionOptions();
|
||||
const challenge = JSON.stringify({
|
||||
publicKey: {
|
||||
...options,
|
||||
challenge: Buffer.from(options.challenge).toString(
|
||||
"base64",
|
||||
),
|
||||
allowCredentials: user.security_keys.map((x) => ({
|
||||
id: x.key_id,
|
||||
type: "public-key",
|
||||
})),
|
||||
transports: ["usb", "ble", "nfc"],
|
||||
timeout: 60000,
|
||||
},
|
||||
});
|
||||
|
||||
const ticket = await generateWebAuthnTicket(challenge);
|
||||
await User.update({ id: user.id }, { totp_last_ticket: ticket });
|
||||
|
||||
return res.json({
|
||||
ticket: ticket,
|
||||
mfa: true,
|
||||
sms: false, // TODO
|
||||
token: null,
|
||||
webauthn: challenge,
|
||||
});
|
||||
}
|
||||
|
||||
const token = await generateToken(user.id);
|
||||
|
||||
// Notice this will have a different token structure, than discord
|
||||
@ -147,6 +186,9 @@ router.post(
|
||||
* MFA required:
|
||||
* @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
|
||||
|
||||
* WebAuthn MFA required:
|
||||
* @returns {"token": null, "mfa": true, "webauthn": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
|
||||
|
||||
* Captcha required:
|
||||
* @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}
|
||||
|
||||
|
112
src/api/routes/auth/mfa/webauthn.ts
Normal file
112
src/api/routes/auth/mfa/webauthn.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { route } from "@fosscord/api";
|
||||
import {
|
||||
generateToken,
|
||||
SecurityKey,
|
||||
User,
|
||||
verifyWebAuthnToken,
|
||||
WebAuthn,
|
||||
WebAuthnTotpSchema,
|
||||
} from "@fosscord/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { ExpectedAssertionResult } from "fido2-lib";
|
||||
import { HTTPError } from "lambert-server";
|
||||
const router = Router();
|
||||
|
||||
function toArrayBuffer(buf: Buffer) {
|
||||
const ab = new ArrayBuffer(buf.length);
|
||||
const view = new Uint8Array(ab);
|
||||
for (let i = 0; i < buf.length; ++i) {
|
||||
view[i] = buf[i];
|
||||
}
|
||||
return ab;
|
||||
}
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
route({ body: "WebAuthnTotpSchema" }),
|
||||
async (req: Request, res: Response) => {
|
||||
if (!WebAuthn.fido2) {
|
||||
// TODO: I did this for typescript and I can't use !
|
||||
throw new Error("WebAuthn not enabled");
|
||||
}
|
||||
|
||||
const { code, ticket } = req.body as WebAuthnTotpSchema;
|
||||
|
||||
const user = await User.findOneOrFail({
|
||||
where: {
|
||||
totp_last_ticket: ticket,
|
||||
},
|
||||
select: ["id", "settings"],
|
||||
});
|
||||
|
||||
const ret = await verifyWebAuthnToken(ticket);
|
||||
if (!ret)
|
||||
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
|
||||
|
||||
await User.update({ id: user.id }, { totp_last_ticket: "" });
|
||||
|
||||
const clientAttestationResponse = JSON.parse(code);
|
||||
const securityKey = await SecurityKey.findOneOrFail({
|
||||
where: {
|
||||
user_id: req.user_id,
|
||||
key_id: clientAttestationResponse.rawId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!clientAttestationResponse.rawId)
|
||||
throw new HTTPError("Missing rawId", 400);
|
||||
|
||||
clientAttestationResponse.rawId = toArrayBuffer(
|
||||
Buffer.from(clientAttestationResponse.rawId, "base64"),
|
||||
);
|
||||
|
||||
const assertionExpectations: ExpectedAssertionResult = JSON.parse(
|
||||
Buffer.from(
|
||||
clientAttestationResponse.response.clientDataJSON,
|
||||
"base64",
|
||||
).toString(),
|
||||
);
|
||||
|
||||
const authnResult = await WebAuthn.fido2.assertionResult(
|
||||
clientAttestationResponse,
|
||||
{
|
||||
...assertionExpectations,
|
||||
factor: "second",
|
||||
publicKey: securityKey.public_key,
|
||||
prevCounter: securityKey.counter,
|
||||
userHandle: securityKey.key_id,
|
||||
},
|
||||
);
|
||||
|
||||
const counter = authnResult.authnrData.get("counter");
|
||||
|
||||
securityKey.counter = counter;
|
||||
|
||||
await securityKey.save();
|
||||
|
||||
return res.json({
|
||||
token: await generateToken(user.id),
|
||||
user_settings: user.settings,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { route } from "@fosscord/api";
|
||||
import { SecurityKey } from "@fosscord/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
router.delete("/", route({}), async (req: Request, res: Response) => {
|
||||
const { key_id } = req.params;
|
||||
|
||||
await SecurityKey.delete({
|
||||
id: key_id,
|
||||
user_id: req.user_id,
|
||||
});
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
196
src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
Normal file
196
src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { route } from "@fosscord/api";
|
||||
import {
|
||||
CreateWebAuthnCredentialSchema,
|
||||
DiscordApiErrors,
|
||||
FieldErrors,
|
||||
GenerateWebAuthnCredentialsSchema,
|
||||
generateWebAuthnTicket,
|
||||
SecurityKey,
|
||||
User,
|
||||
verifyWebAuthnToken,
|
||||
WebAuthn,
|
||||
WebAuthnPostSchema,
|
||||
} from "@fosscord/util";
|
||||
import bcrypt from "bcrypt";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { ExpectedAttestationResult } from "fido2-lib";
|
||||
import { HTTPError } from "lambert-server";
|
||||
const router = Router();
|
||||
|
||||
const isGenerateSchema = (
|
||||
body: WebAuthnPostSchema,
|
||||
): body is GenerateWebAuthnCredentialsSchema => {
|
||||
return "password" in body;
|
||||
};
|
||||
|
||||
const isCreateSchema = (
|
||||
body: WebAuthnPostSchema,
|
||||
): body is CreateWebAuthnCredentialSchema => {
|
||||
return "credential" in body;
|
||||
};
|
||||
|
||||
function toArrayBuffer(buf: Buffer) {
|
||||
const ab = new ArrayBuffer(buf.length);
|
||||
const view = new Uint8Array(ab);
|
||||
for (let i = 0; i < buf.length; ++i) {
|
||||
view[i] = buf[i];
|
||||
}
|
||||
return ab;
|
||||
}
|
||||
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
const securityKeys = await SecurityKey.find({
|
||||
where: {
|
||||
user_id: req.user_id,
|
||||
},
|
||||
});
|
||||
|
||||
return res.json(
|
||||
securityKeys.map((key) => ({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
route({ body: "WebAuthnPostSchema" }),
|
||||
async (req: Request, res: Response) => {
|
||||
if (!WebAuthn.fido2) {
|
||||
// TODO: I did this for typescript and I can't use !
|
||||
throw new Error("WebAuthn not enabled");
|
||||
}
|
||||
|
||||
const user = await User.findOneOrFail({
|
||||
where: {
|
||||
id: req.user_id,
|
||||
},
|
||||
select: [
|
||||
"data",
|
||||
"id",
|
||||
"disabled",
|
||||
"deleted",
|
||||
"settings",
|
||||
"totp_secret",
|
||||
"mfa_enabled",
|
||||
"username",
|
||||
],
|
||||
});
|
||||
|
||||
if (isGenerateSchema(req.body)) {
|
||||
const { password } = req.body;
|
||||
const same_password = await bcrypt.compare(
|
||||
password,
|
||||
user.data.hash || "",
|
||||
);
|
||||
if (!same_password) {
|
||||
throw FieldErrors({
|
||||
password: {
|
||||
message: req.t("auth:login.INVALID_PASSWORD"),
|
||||
code: "INVALID_PASSWORD",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const registrationOptions =
|
||||
await WebAuthn.fido2.attestationOptions();
|
||||
const challenge = JSON.stringify({
|
||||
publicKey: {
|
||||
...registrationOptions,
|
||||
challenge: Buffer.from(
|
||||
registrationOptions.challenge,
|
||||
).toString("base64"),
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
displayName: user.username,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ticket = await generateWebAuthnTicket(challenge);
|
||||
|
||||
return res.json({
|
||||
ticket: ticket,
|
||||
challenge,
|
||||
});
|
||||
} else if (isCreateSchema(req.body)) {
|
||||
const { credential, name, ticket } = req.body;
|
||||
|
||||
const verified = await verifyWebAuthnToken(ticket);
|
||||
if (!verified) throw new HTTPError("Invalid ticket", 400);
|
||||
|
||||
const clientAttestationResponse = JSON.parse(credential);
|
||||
|
||||
if (!clientAttestationResponse.rawId)
|
||||
throw new HTTPError("Missing rawId", 400);
|
||||
|
||||
const rawIdBuffer = Buffer.from(
|
||||
clientAttestationResponse.rawId,
|
||||
"base64",
|
||||
);
|
||||
clientAttestationResponse.rawId = toArrayBuffer(rawIdBuffer);
|
||||
|
||||
const attestationExpectations: ExpectedAttestationResult =
|
||||
JSON.parse(
|
||||
Buffer.from(
|
||||
clientAttestationResponse.response.clientDataJSON,
|
||||
"base64",
|
||||
).toString(),
|
||||
);
|
||||
|
||||
const regResult = await WebAuthn.fido2.attestationResult(
|
||||
clientAttestationResponse,
|
||||
{
|
||||
...attestationExpectations,
|
||||
factor: "second",
|
||||
},
|
||||
);
|
||||
|
||||
const authnrData = regResult.authnrData;
|
||||
const keyId = Buffer.from(authnrData.get("credId")).toString(
|
||||
"base64",
|
||||
);
|
||||
const counter = authnrData.get("counter");
|
||||
const publicKey = authnrData.get("credentialPublicKeyPem");
|
||||
|
||||
const securityKey = SecurityKey.create({
|
||||
name,
|
||||
counter,
|
||||
public_key: publicKey,
|
||||
user_id: req.user_id,
|
||||
key_id: keyId,
|
||||
});
|
||||
|
||||
await securityKey.save();
|
||||
|
||||
return res.json({
|
||||
name,
|
||||
id: securityKey.id,
|
||||
});
|
||||
} else {
|
||||
throw DiscordApiErrors.INVALID_AUTHENTICATION_TOKEN;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
46
src/util/entities/SecurityKey.ts
Normal file
46
src/util/entities/SecurityKey.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("security_keys")
|
||||
export class SecurityKey extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((key: SecurityKey) => key.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
key_id: string;
|
||||
|
||||
@Column()
|
||||
public_key: string;
|
||||
|
||||
@Column()
|
||||
counter: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
}
|
@ -33,6 +33,7 @@ import { UserSettings } from "./UserSettings";
|
||||
import { Session } from "./Session";
|
||||
import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from "..";
|
||||
import { Request } from "express";
|
||||
import { SecurityKey } from "./SecurityKey";
|
||||
|
||||
export enum PublicUserEnum {
|
||||
username,
|
||||
@ -138,6 +139,9 @@ export class User extends BaseClass {
|
||||
@Column({ select: false })
|
||||
mfa_enabled: boolean = false; // if multi factor authentication is enabled
|
||||
|
||||
@Column({ select: false, default: false })
|
||||
webauthn_enabled: boolean = false; // if webauthn multi factor authentication is enabled
|
||||
|
||||
@Column({ select: false, nullable: true })
|
||||
totp_secret?: string = "";
|
||||
|
||||
@ -223,6 +227,9 @@ export class User extends BaseClass {
|
||||
@Column({ type: "simple-json", select: false })
|
||||
extended_settings: string = "{}";
|
||||
|
||||
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
|
||||
security_keys: SecurityKey[];
|
||||
|
||||
// TODO: I don't like this method?
|
||||
validate() {
|
||||
if (this.email) {
|
||||
|
@ -23,8 +23,8 @@ export * from "./BackupCodes";
|
||||
export * from "./Ban";
|
||||
export * from "./BaseClass";
|
||||
export * from "./Categories";
|
||||
export * from "./ClientRelease";
|
||||
export * from "./Channel";
|
||||
export * from "./ClientRelease";
|
||||
export * from "./Config";
|
||||
export * from "./ConnectedAccount";
|
||||
export * from "./EmbedCache";
|
||||
@ -41,6 +41,7 @@ export * from "./ReadState";
|
||||
export * from "./Recipient";
|
||||
export * from "./Relationship";
|
||||
export * from "./Role";
|
||||
export * from "./SecurityKey";
|
||||
export * from "./Session";
|
||||
export * from "./Sticker";
|
||||
export * from "./StickerPack";
|
||||
|
27
src/util/migration/mariadb/1675045120206-webauthn.ts
Normal file
27
src/util/migration/mariadb/1675045120206-webauthn.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class webauthn1675045120206 implements MigrationInterface {
|
||||
name = "webauthn1675045120206";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE \`security_keys\` (\`id\` varchar(255) NOT NULL, \`user_id\` varchar(255) NULL, \`key_id\` varchar(255) NOT NULL, \`public_key\` varchar(255) NOT NULL, \`counter\` int NOT NULL, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`users\` ADD \`webauthn_enabled\` tinyint NOT NULL DEFAULT 0`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`security_keys\` ADD CONSTRAINT \`FK_24c97d0771cafedce6d7163eaad\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`security_keys\` DROP FOREIGN KEY \`FK_24c97d0771cafedce6d7163eaad\``,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`users\` DROP COLUMN \`webauthn_enabled\``,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE \`security_keys\``);
|
||||
}
|
||||
}
|
27
src/util/migration/mysql/1675045120206-webauthn.ts
Normal file
27
src/util/migration/mysql/1675045120206-webauthn.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class webauthn1675045120206 implements MigrationInterface {
|
||||
name = "webauthn1675045120206";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE \`security_keys\` (\`id\` varchar(255) NOT NULL, \`user_id\` varchar(255) NULL, \`key_id\` varchar(255) NOT NULL, \`public_key\` varchar(255) NOT NULL, \`counter\` int NOT NULL, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`users\` ADD \`webauthn_enabled\` tinyint NOT NULL DEFAULT 0`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`security_keys\` ADD CONSTRAINT \`FK_24c97d0771cafedce6d7163eaad\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`security_keys\` DROP FOREIGN KEY \`FK_24c97d0771cafedce6d7163eaad\``,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`users\` DROP COLUMN \`webauthn_enabled\``,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE \`security_keys\``);
|
||||
}
|
||||
}
|
27
src/util/migration/postgresql/1675044825710-webauthn.ts
Normal file
27
src/util/migration/postgresql/1675044825710-webauthn.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class webauthn1675044825710 implements MigrationInterface {
|
||||
name = "webauthn1675044825710";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "security_keys" ("id" character varying NOT NULL, "user_id" character varying, "key_id" character varying NOT NULL, "public_key" character varying NOT NULL, "counter" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_6e95cdd91779e7cca06d1fff89c" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD "webauthn_enabled" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "security_keys" ADD CONSTRAINT "FK_24c97d0771cafedce6d7163eaad" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "security_keys" DROP CONSTRAINT "FK_24c97d0771cafedce6d7163eaad"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" DROP COLUMN "webauthn_enabled"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "security_keys"`);
|
||||
}
|
||||
}
|
38
src/util/schemas/WebAuthnSchema.ts
Normal file
38
src/util/schemas/WebAuthnSchema.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// FIXME: better naming
|
||||
export interface GenerateWebAuthnCredentialsSchema {
|
||||
password: string;
|
||||
}
|
||||
|
||||
// FIXME: better naming
|
||||
export interface CreateWebAuthnCredentialSchema {
|
||||
credential: string;
|
||||
name: string;
|
||||
ticket: string;
|
||||
}
|
||||
|
||||
export type WebAuthnPostSchema = Partial<
|
||||
GenerateWebAuthnCredentialsSchema | CreateWebAuthnCredentialSchema
|
||||
>;
|
||||
|
||||
export interface WebAuthnTotpSchema {
|
||||
code: string;
|
||||
ticket: string;
|
||||
}
|
@ -16,66 +16,59 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./Validator";
|
||||
export * from "./SelectProtocolSchema";
|
||||
export * from "./LoginSchema";
|
||||
export * from "./RegisterSchema";
|
||||
export * from "./TotpSchema";
|
||||
export * from "./ActivitySchema";
|
||||
export * from "./ApplicationAuthorizeSchema";
|
||||
export * from "./ApplicationCreateSchema";
|
||||
export * from "./ApplicationModifySchema";
|
||||
export * from "./BackupCodesChallengeSchema";
|
||||
export * from "./ChannelModifySchema";
|
||||
export * from "./InviteCreateSchema";
|
||||
export * from "./PurgeSchema";
|
||||
export * from "./WebhookCreateSchema";
|
||||
export * from "./MessageCreateSchema";
|
||||
export * from "./MessageAcknowledgeSchema";
|
||||
export * from "./GuildCreateSchema";
|
||||
export * from "./BanCreateSchema";
|
||||
export * from "./BanModeratorSchema";
|
||||
export * from "./BanRegistrySchema";
|
||||
export * from "./BotModifySchema";
|
||||
export * from "./ChannelModifySchema";
|
||||
export * from "./ChannelPermissionOverwriteSchema";
|
||||
export * from "./ChannelReorderSchema";
|
||||
export * from "./CodesVerificationSchema";
|
||||
export * from "./DmChannelCreateSchema";
|
||||
export * from "./EmojiCreateSchema";
|
||||
export * from "./EmojiModifySchema";
|
||||
export * from "./ModifyGuildStickerSchema";
|
||||
export * from "./TemplateCreateSchema";
|
||||
export * from "./TemplateModifySchema";
|
||||
export * from "./VanityUrlSchema";
|
||||
export * from "./GatewayPayloadSchema";
|
||||
export * from "./GuildCreateSchema";
|
||||
export * from "./GuildTemplateCreateSchema";
|
||||
export * from "./GuildUpdateSchema";
|
||||
export * from "./GuildUpdateWelcomeScreenSchema";
|
||||
export * from "./WidgetModifySchema";
|
||||
export * from "./IdentifySchema";
|
||||
export * from "./InviteCreateSchema";
|
||||
export * from "./LazyRequestSchema";
|
||||
export * from "./LoginSchema";
|
||||
export * from "./MemberChangeProfileSchema";
|
||||
export * from "./MemberChangeSchema";
|
||||
export * from "./RoleModifySchema";
|
||||
export * from "./GuildTemplateCreateSchema";
|
||||
export * from "./DmChannelCreateSchema";
|
||||
export * from "./UserModifySchema";
|
||||
export * from "./MessageAcknowledgeSchema";
|
||||
export * from "./MessageCreateSchema";
|
||||
export * from "./MfaCodesSchema";
|
||||
export * from "./ModifyGuildStickerSchema";
|
||||
export * from "./PurgeSchema";
|
||||
export * from "./RegisterSchema";
|
||||
export * from "./RelationshipPostSchema";
|
||||
export * from "./RelationshipPutSchema";
|
||||
export * from "./CodesVerificationSchema";
|
||||
export * from "./MfaCodesSchema";
|
||||
export * from "./RoleModifySchema";
|
||||
export * from "./RolePositionUpdateSchema";
|
||||
export * from "./SelectProtocolSchema";
|
||||
export * from "./TemplateCreateSchema";
|
||||
export * from "./TemplateModifySchema";
|
||||
export * from "./TotpDisableSchema";
|
||||
export * from "./TotpEnableSchema";
|
||||
export * from "./VoiceIdentifySchema";
|
||||
export * from "./TotpSchema";
|
||||
export * from "./UserDeleteSchema";
|
||||
export * from "./UserGuildSettingsSchema";
|
||||
export * from "./UserModifySchema";
|
||||
export * from "./UserProfileModifySchema";
|
||||
export * from "./UserSettingsSchema";
|
||||
export * from "./Validator";
|
||||
export * from "./VanityUrlSchema";
|
||||
export * from "./VoiceIdentifySchema";
|
||||
export * from "./VoiceStateUpdateSchema";
|
||||
export * from "./VoiceVideoSchema";
|
||||
export * from "./IdentifySchema";
|
||||
export * from "./ActivitySchema";
|
||||
export * from "./LazyRequestSchema";
|
||||
export * from "./GuildUpdateSchema";
|
||||
export * from "./ChannelPermissionOverwriteSchema";
|
||||
export * from "./UserGuildSettingsSchema";
|
||||
export * from "./GatewayPayloadSchema";
|
||||
export * from "./RolePositionUpdateSchema";
|
||||
export * from "./ChannelReorderSchema";
|
||||
export * from "./UserSettingsSchema";
|
||||
export * from "./BotModifySchema";
|
||||
export * from "./ApplicationModifySchema";
|
||||
export * from "./ApplicationCreateSchema";
|
||||
export * from "./ApplicationAuthorizeSchema";
|
||||
export * from "./WebAuthnSchema";
|
||||
export * from "./WebhookCreateSchema";
|
||||
export * from "./WidgetModifySchema";
|
||||
|
68
src/util/util/WebAuthn.ts
Normal file
68
src/util/util/WebAuthn.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Fosscord and Fosscord Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Fido2Lib } from "fido2-lib";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Config } from "./Config";
|
||||
|
||||
const JWTOptions: jwt.SignOptions = {
|
||||
algorithm: "HS256",
|
||||
expiresIn: "5m",
|
||||
};
|
||||
|
||||
export const WebAuthn: {
|
||||
fido2: Fido2Lib | null;
|
||||
init: () => void;
|
||||
} = {
|
||||
fido2: null,
|
||||
init: function () {
|
||||
this.fido2 = new Fido2Lib({
|
||||
challengeSize: 128,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateWebAuthnTicket(
|
||||
challenge: string,
|
||||
): Promise<string> {
|
||||
return new Promise((res, rej) => {
|
||||
jwt.sign(
|
||||
{ challenge },
|
||||
Config.get().security.jwtSecret,
|
||||
JWTOptions,
|
||||
(err, token) => {
|
||||
if (err || !token) return rej(err || "no token");
|
||||
return res(token);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyWebAuthnToken(token: string) {
|
||||
return new Promise((res, rej) => {
|
||||
jwt.verify(
|
||||
token,
|
||||
Config.get().security.jwtSecret,
|
||||
JWTOptions,
|
||||
async (err, decoded) => {
|
||||
if (err) return rej(err);
|
||||
return res(decoded);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -39,3 +39,4 @@ export * from "./Array";
|
||||
export * from "./TraverseDirectory";
|
||||
export * from "./InvisibleCharacters";
|
||||
export * from "./Sentry";
|
||||
export * from "./WebAuthn";
|
||||
|
Loading…
Reference in New Issue
Block a user