Add Bearer JWT auth middleware for notification routes
Mirror image-api’s DID JWT verification (src/vc + requireAuth) so /notifications/* require a valid Authorization header while /health stays public. Attach req.did, req.jwt, and req.auth for downstream use.
This commit is contained in:
327
package-lock.json
generated
327
package-lock.json
generated
@@ -8,6 +8,11 @@
|
||||
"name": "notification-wakeup-service",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-ecc": "^2.3.8",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"cbor-x": "^1.5.9",
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"firebase-admin": "^13.9.0"
|
||||
},
|
||||
@@ -18,6 +23,84 @@
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.2.tgz",
|
||||
"integrity": "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.2.tgz",
|
||||
"integrity": "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.2.tgz",
|
||||
"integrity": "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.2.tgz",
|
||||
"integrity": "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.2.tgz",
|
||||
"integrity": "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.2.tgz",
|
||||
"integrity": "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
@@ -765,6 +848,48 @@
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@multiformats/base-x": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz",
|
||||
"integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.4.1.tgz",
|
||||
"integrity": "sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
|
||||
@@ -788,6 +913,50 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-ecc": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz",
|
||||
"integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
|
||||
"integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz",
|
||||
"integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
|
||||
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -862,6 +1031,15 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
|
||||
@@ -1090,6 +1268,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
|
||||
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-retry": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
||||
@@ -1204,6 +1396,46 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/canonicalize": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz",
|
||||
"integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"canonicalize": "bin/canonicalize.js"
|
||||
}
|
||||
},
|
||||
"node_modules/cbor-extract": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.2.tgz",
|
||||
"integrity": "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"download-cbor-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cbor-extract/cbor-extract-darwin-arm64": "2.2.2",
|
||||
"@cbor-extract/cbor-extract-darwin-x64": "2.2.2",
|
||||
"@cbor-extract/cbor-extract-linux-arm": "2.2.2",
|
||||
"@cbor-extract/cbor-extract-linux-arm64": "2.2.2",
|
||||
"@cbor-extract/cbor-extract-linux-x64": "2.2.2",
|
||||
"@cbor-extract/cbor-extract-win32-x64": "2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cbor-x": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.4.tgz",
|
||||
"integrity": "sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"cbor-extract": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -1337,6 +1569,39 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/did-jwt": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-7.4.7.tgz",
|
||||
"integrity": "sha512-Apz7nIfIHSKWIMaEP5L/K8xkwByvjezjTG0xiqwKdnNj1x8M0+Yasury5Dm/KPltxi2PlGfRPf3IejRKZrT8mQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.4.0",
|
||||
"@noble/curves": "^1.0.0",
|
||||
"@noble/hashes": "^1.3.0",
|
||||
"@scure/base": "^1.1.3",
|
||||
"canonicalize": "^2.0.0",
|
||||
"did-resolver": "^4.1.0",
|
||||
"multibase": "^4.0.6",
|
||||
"multiformats": "^9.6.2",
|
||||
"uint8arrays": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/did-resolver": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz",
|
||||
"integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2542,6 +2807,26 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multibase": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/multibase/-/multibase-4.0.6.tgz",
|
||||
"integrity": "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==",
|
||||
"deprecated": "This module has been superseded by the multiformats module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@multiformats/base-x": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multiformats": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
|
||||
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
|
||||
"license": "(Apache-2.0 AND MIT)"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
@@ -2601,6 +2886,21 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
|
||||
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"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/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
@@ -2746,6 +3046,24 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pvtsutils": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
|
||||
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
@@ -3235,6 +3553,15 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8arrays": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz",
|
||||
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-ecc": "^2.3.8",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"cbor-x": "^1.5.9",
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"firebase-admin": "^13.9.0"
|
||||
},
|
||||
|
||||
84
src/middleware/auth.ts
Normal file
84
src/middleware/auth.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { decodeAndVerifyJwt } from "../vc/index.js";
|
||||
|
||||
export type AuthContext = {
|
||||
did: string;
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
type ClientErrorBody = {
|
||||
clientError?: {
|
||||
message?: string;
|
||||
code?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function clientErrorMessage(err: unknown): string | undefined {
|
||||
if (err && typeof err === "object" && "clientError" in err) {
|
||||
const message = (err as ClientErrorBody).clientError?.message;
|
||||
if (typeof message === "string" && message.length > 0) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware mirroring image-api decodeJwt: Bearer JWT, DID verification,
|
||||
* attaches req.did / req.jwt / req.auth on success.
|
||||
*/
|
||||
export async function requireAuth(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
console.log("[Auth] Authentication failed");
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: 'Missing "Bearer JWT" in Authorization header.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring("Bearer ".length);
|
||||
|
||||
try {
|
||||
const verified = await decodeAndVerifyJwt(token);
|
||||
if (!verified.verified) {
|
||||
const errorTime = new Date().toISOString();
|
||||
console.log("[Auth] Authentication failed");
|
||||
console.error(
|
||||
errorTime,
|
||||
"Got invalid JWT in Authorization header:",
|
||||
verified
|
||||
);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message:
|
||||
"Got invalid JWT in Authorization header. See server logs at " +
|
||||
errorTime,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const did = verified.issuer;
|
||||
req.did = did;
|
||||
req.jwt = token;
|
||||
req.auth = { did, jwt: token };
|
||||
console.log("[Auth] Authenticated user:", did);
|
||||
next();
|
||||
} catch (err) {
|
||||
const errorTime = new Date().toISOString();
|
||||
console.log("[Auth] Authentication failed");
|
||||
console.error(errorTime, "Got invalid JWT in Authorization header:", err);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message:
|
||||
clientErrorMessage(err) ??
|
||||
"Got invalid JWT in Authorization header. See server logs at " +
|
||||
errorTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import { db } from "../db/fcmTokens.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
export const notificationsRouter = Router();
|
||||
|
||||
notificationsRouter.use(requireAuth);
|
||||
|
||||
notificationsRouter.get("/", (_req, res) => {
|
||||
res.json({ ok: true, resource: "notifications" });
|
||||
});
|
||||
|
||||
14
src/types/express.d.ts
vendored
Normal file
14
src/types/express.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
/** Authenticated user DID (issuer from verified JWT). */
|
||||
did?: string;
|
||||
/** Raw Bearer JWT from the Authorization header. */
|
||||
jwt?: string;
|
||||
/** Verified auth context (did + jwt). */
|
||||
auth?: { did: string; jwt: string };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
46
src/vc/did-eth-local-resolver.ts
Normal file
46
src/vc/did-eth-local-resolver.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { DIDResolutionResult } from "did-resolver";
|
||||
|
||||
/**
|
||||
* This did:ethr resolver instructs the did-jwt machinery to use the
|
||||
* EcdsaSecp256k1RecoveryMethod2020Uses verification method which adds the recovery bit to the
|
||||
* signature to recover the DID's public key from a signature.
|
||||
*
|
||||
* Similar code resides in image-api, crowd-funder-for-time-pwa, and endorser-ch.
|
||||
*/
|
||||
export const didEthLocalResolver = async (
|
||||
did: string
|
||||
): Promise<DIDResolutionResult> => {
|
||||
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
|
||||
const match = did.match(didRegex);
|
||||
|
||||
if (match) {
|
||||
const address = match[1];
|
||||
const publicKeyHex = address;
|
||||
|
||||
return {
|
||||
didDocumentMetadata: {},
|
||||
didResolutionMetadata: {
|
||||
contentType: "application/did+ld+json",
|
||||
},
|
||||
didDocument: {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
|
||||
],
|
||||
id: did,
|
||||
verificationMethod: [
|
||||
{
|
||||
id: `${did}#controller`,
|
||||
type: "EcdsaSecp256k1RecoveryMethod2020",
|
||||
controller: did,
|
||||
blockchainAccountId: "eip155:1:" + publicKeyHex,
|
||||
},
|
||||
],
|
||||
authentication: [`${did}#controller`],
|
||||
assertionMethod: [`${did}#controller`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported DID format: ${did}`);
|
||||
};
|
||||
104
src/vc/didPeer.ts
Normal file
104
src/vc/didPeer.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { AsnParser } from "@peculiar/asn1-schema";
|
||||
import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
||||
import crypto from "crypto";
|
||||
import { decode as cborDecode } from "cbor-x";
|
||||
|
||||
/**
|
||||
* similar code is in image-api and crowd-funder-for-time-pwa
|
||||
*/
|
||||
export async function verifyPeerSignature(
|
||||
payloadBytes: Uint8Array,
|
||||
publicKeyBytes: Uint8Array,
|
||||
signatureBytes: Uint8Array
|
||||
) {
|
||||
const finalSignatureBuffer = unwrapEC2Signature(signatureBytes);
|
||||
const verifyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
hash: { name: "SHA-256" },
|
||||
};
|
||||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
|
||||
const keyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
namedCurve: publicKeyJwk.crv,
|
||||
};
|
||||
const publicKeyCryptoKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
publicKeyJwk,
|
||||
keyAlgorithm,
|
||||
false,
|
||||
["verify"]
|
||||
);
|
||||
const verified = await crypto.subtle.verify(
|
||||
verifyAlgorithm,
|
||||
publicKeyCryptoKey,
|
||||
finalSignatureBuffer,
|
||||
payloadBytes
|
||||
);
|
||||
return verified;
|
||||
}
|
||||
|
||||
function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||
const jwkObj = cborDecode(publicKeyBytes) as Record<number, unknown>;
|
||||
if (
|
||||
jwkObj[1] != 2 ||
|
||||
jwkObj[3] != -7 ||
|
||||
jwkObj[-1] != 1 ||
|
||||
!Array.isArray(jwkObj[-2]) ||
|
||||
(jwkObj[-2] as Uint8Array).length != 32 ||
|
||||
!Array.isArray(jwkObj[-3]) ||
|
||||
(jwkObj[-3] as Uint8Array).length != 32
|
||||
) {
|
||||
throw new Error("Unable to extract key.");
|
||||
}
|
||||
const publicKeyJwk = {
|
||||
alg: "ES256",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
x: arrayToBase64Url(jwkObj[-2] as Uint8Array),
|
||||
y: arrayToBase64Url(jwkObj[-3] as Uint8Array),
|
||||
};
|
||||
const publicKeyBuffer = Buffer.concat([
|
||||
Buffer.from(jwkObj[-2] as Uint8Array),
|
||||
Buffer.from(jwkObj[-3] as Uint8Array),
|
||||
]);
|
||||
return { publicKeyJwk, publicKeyBuffer };
|
||||
}
|
||||
|
||||
function toBase64Url(anythingB64: string) {
|
||||
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function arrayToBase64Url(anything: Uint8Array) {
|
||||
return toBase64Url(Buffer.from(anything).toString("base64"));
|
||||
}
|
||||
|
||||
function unwrapEC2Signature(signature: Uint8Array) {
|
||||
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
|
||||
let rBytes = new Uint8Array(parsedSignature.r);
|
||||
let sBytes = new Uint8Array(parsedSignature.s);
|
||||
|
||||
if (shouldRemoveLeadingZero(rBytes)) {
|
||||
rBytes = rBytes.slice(1);
|
||||
}
|
||||
|
||||
if (shouldRemoveLeadingZero(sBytes)) {
|
||||
sBytes = sBytes.slice(1);
|
||||
}
|
||||
|
||||
return isoUint8ArrayConcat([rBytes, sBytes]);
|
||||
}
|
||||
|
||||
function shouldRemoveLeadingZero(bytes: Uint8Array) {
|
||||
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
|
||||
}
|
||||
|
||||
function isoUint8ArrayConcat(arrays: Uint8Array[]) {
|
||||
let pointer = 0;
|
||||
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
|
||||
const toReturn = new Uint8Array(totalLength);
|
||||
arrays.forEach((arr) => {
|
||||
toReturn.set(arr, pointer);
|
||||
pointer += arr.length;
|
||||
});
|
||||
return toReturn;
|
||||
}
|
||||
100
src/vc/index.ts
Normal file
100
src/vc/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Verifiable Credential & DID functions (shared pattern with image-api, endorser-ch).
|
||||
*/
|
||||
|
||||
import { verifyJWT } from "did-jwt";
|
||||
import { Resolver } from "did-resolver";
|
||||
|
||||
import { didEthLocalResolver } from "./did-eth-local-resolver.js";
|
||||
import { verifyJwt as peerVerifyJwt } from "./passkeyDidPeer.js";
|
||||
|
||||
export const TEST_BYPASS_ENV_VALUE = "test-local";
|
||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||
export const PEER_DID_PREFIX = "did:peer:";
|
||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED_CODE";
|
||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||
|
||||
const resolver = new Resolver({
|
||||
ethr: didEthLocalResolver,
|
||||
});
|
||||
|
||||
export type VerifiedJwt = {
|
||||
issuer: string;
|
||||
payload: Record<string, unknown>;
|
||||
verified: boolean;
|
||||
};
|
||||
|
||||
export async function decodeAndVerifyJwt(jwt: string): Promise<VerifiedJwt> {
|
||||
const pieces = jwt.split(".");
|
||||
const header = JSON.parse(
|
||||
Buffer.from(pieces[0], "base64url").toString("utf8")
|
||||
) as Record<string, unknown>;
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(pieces[1], "base64url").toString("utf8")
|
||||
) as Record<string, unknown>;
|
||||
const issuerDid = payload.iss;
|
||||
if (!issuerDid || typeof issuerDid !== "string") {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `Missing "iss" field in JWT.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
issuerDid.startsWith(ETHR_DID_PREFIX) &&
|
||||
process.env.NODE_ENV === TEST_BYPASS_ENV_VALUE
|
||||
) {
|
||||
const nowEpoch = Math.floor(new Date().getTime() / 1000);
|
||||
if (typeof payload.exp === "number" && payload.exp < nowEpoch) {
|
||||
console.log(
|
||||
"JWT with exp " +
|
||||
payload.exp +
|
||||
" has expired but we're in test mode so we'll use a new time."
|
||||
);
|
||||
payload.exp = nowEpoch + 100;
|
||||
}
|
||||
return { issuer: issuerDid, payload, verified: true };
|
||||
}
|
||||
|
||||
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||
try {
|
||||
const verified = await verifyJWT(jwt, { resolver });
|
||||
return verified as VerifiedJwt;
|
||||
} catch (e) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT failed verification: ` + e,
|
||||
code: JWT_VERIFY_FAILED_CODE,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
|
||||
const { claimPayload, verified } = await peerVerifyJwt(
|
||||
payload,
|
||||
issuerDid,
|
||||
pieces[2]
|
||||
);
|
||||
return {
|
||||
issuer: issuerDid,
|
||||
payload: claimPayload as Record<string, unknown>,
|
||||
verified,
|
||||
};
|
||||
}
|
||||
|
||||
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `Unsupported DID method ${issuerDid}`,
|
||||
code: UNSUPPORTED_DID_METHOD_CODE,
|
||||
},
|
||||
});
|
||||
}
|
||||
119
src/vc/passkeyDidPeer.ts
Normal file
119
src/vc/passkeyDidPeer.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import crypto from "crypto";
|
||||
import { multibaseToBytes } from "did-jwt";
|
||||
|
||||
import { PEER_DID_PREFIX, TEST_BYPASS_ENV_VALUE } from "./index.js";
|
||||
import { verifyPeerSignature } from "./didPeer.js";
|
||||
|
||||
export async function verifyJwt(
|
||||
payload: Record<string, unknown>,
|
||||
issuerDid: string,
|
||||
signatureString: string
|
||||
) {
|
||||
if (!payload.iss) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT is missing an "iss" field.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const nowEpoch = Math.floor(new Date().getTime() / 1000);
|
||||
if (!payload.exp) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT with is missing an "exp" field.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof payload.exp === "number" &&
|
||||
payload.exp < nowEpoch &&
|
||||
process.env.NODE_ENV !== TEST_BYPASS_ENV_VALUE
|
||||
) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT with exp ${payload.exp} has expired.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const authData = payload.AuthenticationDataB64URL;
|
||||
const clientData = payload.ClientDataJSONB64URL;
|
||||
if (typeof authData !== "string" || typeof clientData !== "string") {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT with typ == JWANT requires AuthenticationData and ClientDataJSON.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const decodedAuthDataBuff = Buffer.from(authData, "base64url");
|
||||
const decodedClientData = Buffer.from(clientData, "base64url");
|
||||
|
||||
let claimPayload = JSON.parse(decodedClientData.toString()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if (claimPayload.challenge) {
|
||||
claimPayload = JSON.parse(
|
||||
Buffer.from(claimPayload.challenge as string, "base64url").toString()
|
||||
) as Record<string, unknown>;
|
||||
if (!claimPayload.exp) {
|
||||
claimPayload.exp = payload.exp;
|
||||
}
|
||||
if (!claimPayload.iat) {
|
||||
claimPayload.iat = payload.iat;
|
||||
}
|
||||
if (!claimPayload.iss) {
|
||||
claimPayload.iss = payload.iss;
|
||||
}
|
||||
}
|
||||
if (!claimPayload.exp) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT client data challenge is missing an "exp" field.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof claimPayload.exp === "number" &&
|
||||
claimPayload.exp < nowEpoch &&
|
||||
process.env.NODE_ENV !== TEST_BYPASS_ENV_VALUE
|
||||
) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT client data challenge exp time is past.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (claimPayload.exp !== payload.exp) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT client data challenge "exp" field doesn't match the outside payload "exp".`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (claimPayload.iss !== payload.iss) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT client data challenge "iss" field doesn't match the outside payload "iss".`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const hashedClientDataBuff = crypto
|
||||
.createHash("sha256")
|
||||
.update(decodedClientData)
|
||||
.digest();
|
||||
const preimage = new Uint8Array(
|
||||
Buffer.concat([decodedAuthDataBuff, hashedClientDataBuff])
|
||||
);
|
||||
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
||||
const publicKey = multibaseToBytes(
|
||||
issuerDid.substring(PEER_DID_MULTIBASE_PREFIX.length)
|
||||
);
|
||||
const signature = new Uint8Array(
|
||||
Buffer.from(signatureString, "base64url")
|
||||
);
|
||||
const verified = await verifyPeerSignature(preimage, publicKey, signature);
|
||||
return { claimPayload, verified };
|
||||
}
|
||||
Reference in New Issue
Block a user