@verii/server-credentialagent 1.0.0-pre.1752076816

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/.localdev.e2e.env +40 -0
  2. package/.localdev.env +41 -0
  3. package/.standalone.env +5 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +1 -0
  6. package/README.md +19 -0
  7. package/docker/compose.yml +33 -0
  8. package/e2e/README.md +12 -0
  9. package/e2e/org-registration-and-issuing.e2e.test.js +624 -0
  10. package/jest.config.js +20 -0
  11. package/migrate-mongo.config.js +36 -0
  12. package/migrations/20210317133137-add-index-to-offers-repo.js +57 -0
  13. package/migrations/20210416145639-add-index-to-revocation-list.js +27 -0
  14. package/migrations/20210719120225-add_unique_did_index_to_tenant.js +45 -0
  15. package/migrations/20230524053029-add-vendorUserIdMappings-index.js +32 -0
  16. package/migrations/20230616111907-add-configuration-type-index.js +32 -0
  17. package/package.json +108 -0
  18. package/src/assets/public/favicon.ico +0 -0
  19. package/src/assets/public/logo192.png +0 -0
  20. package/src/assets/public/logo512.png +0 -0
  21. package/src/assets/public/manifest.json +28 -0
  22. package/src/assets/templates/app-redirect.hbs +16 -0
  23. package/src/config/config.js +44 -0
  24. package/src/config/core-config.js +143 -0
  25. package/src/config/holder-config.js +104 -0
  26. package/src/config/index.js +22 -0
  27. package/src/config/operator-config.js +64 -0
  28. package/src/controllers/autoload-holder-api-controllers.js +30 -0
  29. package/src/controllers/autoload-operator-api-controllers.js +31 -0
  30. package/src/controllers/autoload-root-api-controller.js +30 -0
  31. package/src/controllers/autoload-saasoperator-api-controllers.js +31 -0
  32. package/src/controllers/holder/autohooks.js +55 -0
  33. package/src/controllers/holder/get-exchange-progress/autohooks.js +27 -0
  34. package/src/controllers/holder/get-exchange-progress/controller.js +50 -0
  35. package/src/controllers/holder/inspect/autohooks.js +35 -0
  36. package/src/controllers/holder/inspect/get-presentation-request/controller.js +100 -0
  37. package/src/controllers/holder/inspect/schemas/holder-disclosure.schema.json +73 -0
  38. package/src/controllers/holder/inspect/schemas/index.js +33 -0
  39. package/src/controllers/holder/inspect/schemas/presentation-definition.v1.schema.json +461 -0
  40. package/src/controllers/holder/inspect/schemas/presentation-request.schema.json +279 -0
  41. package/src/controllers/holder/inspect/schemas/presentation-submission.schema.json +41 -0
  42. package/src/controllers/holder/inspect/schemas/siop-presentation-submission.schema.json +74 -0
  43. package/src/controllers/holder/inspect/schemas/velocity-presentation-submission.response.200.schema.json +36 -0
  44. package/src/controllers/holder/inspect/schemas/velocity-presentation-submission.schema.json +34 -0
  45. package/src/controllers/holder/inspect/submit-presentation/controller.js +89 -0
  46. package/src/controllers/holder/issue/autohooks.js +23 -0
  47. package/src/controllers/holder/issue/get-credential-manifest/controller.js +193 -0
  48. package/src/controllers/holder/issue/offers/autohooks.js +35 -0
  49. package/src/controllers/holder/issue/offers/controller.js +164 -0
  50. package/src/controllers/holder/issue/offers/credential-offers/controller.js +460 -0
  51. package/src/controllers/holder/issue/submit-identification/autohooks.js +37 -0
  52. package/src/controllers/holder/issue/submit-identification/controller.js +63 -0
  53. package/src/controllers/holder/oauth/autohooks.js +19 -0
  54. package/src/controllers/holder/oauth/controller.js +140 -0
  55. package/src/controllers/index.js +22 -0
  56. package/src/controllers/operator/tenants/_tenantId/autohooks.js +40 -0
  57. package/src/controllers/operator/tenants/_tenantId/check-credentials/autohooks.js +24 -0
  58. package/src/controllers/operator/tenants/_tenantId/check-credentials/controller-v0.8.js +200 -0
  59. package/src/controllers/operator/tenants/_tenantId/check-credentials/schemas/index.js +19 -0
  60. package/src/controllers/operator/tenants/_tenantId/check-credentials/schemas/vendor-credential.schema.json +244 -0
  61. package/src/controllers/operator/tenants/_tenantId/controller-v0.8.js +221 -0
  62. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/autohooks.js +30 -0
  63. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/controller-v0.8.js +271 -0
  64. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/autohooks.js +45 -0
  65. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/controller-v0.8.js +199 -0
  66. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/add-feed.schema.js +14 -0
  67. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/feed.schema.json +27 -0
  68. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/index.js +25 -0
  69. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/modify-feed-update-body.schema.js +18 -0
  70. package/src/controllers/operator/tenants/_tenantId/disclosures/_id/feeds/schemas/modify-feed.schema.json +19 -0
  71. package/src/controllers/operator/tenants/_tenantId/disclosures/autohooks.js +34 -0
  72. package/src/controllers/operator/tenants/_tenantId/disclosures/controller-v0.8.js +100 -0
  73. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/agent-disclosure-presentation-definition.schema.json +404 -0
  74. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/agent-disclosure.schema.js +24 -0
  75. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/index.js +29 -0
  76. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/new-agent-disclosure.schema.json +166 -0
  77. package/src/controllers/operator/tenants/_tenantId/disclosures/schemas/update-agent-disclosure.schema.js +20 -0
  78. package/src/controllers/operator/tenants/_tenantId/exchanges/_exchangeId/autohooks.js +30 -0
  79. package/src/controllers/operator/tenants/_tenantId/exchanges/_exchangeId/controller-v0.8.js +73 -0
  80. package/src/controllers/operator/tenants/_tenantId/exchanges/autohooks.js +19 -0
  81. package/src/controllers/operator/tenants/_tenantId/exchanges/controller-v0.8.js +150 -0
  82. package/src/controllers/operator/tenants/_tenantId/exchanges/schemas/get-exchange.response.body.json +147 -0
  83. package/src/controllers/operator/tenants/_tenantId/exchanges/schemas/index.js +21 -0
  84. package/src/controllers/operator/tenants/_tenantId/issued-credentials/autohooks.js +27 -0
  85. package/src/controllers/operator/tenants/_tenantId/issued-credentials/controller-v0.8.js +303 -0
  86. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/index.js +23 -0
  87. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/issued-credential.schema.json +115 -0
  88. package/src/controllers/operator/tenants/_tenantId/issued-credentials/schemas/revoke-credentials.schema.json +18 -0
  89. package/src/controllers/operator/tenants/_tenantId/keys/controller-v0.8.js +168 -0
  90. package/src/controllers/operator/tenants/_tenantId/offer-data/controller-v0.8.js +78 -0
  91. package/src/controllers/operator/tenants/_tenantId/offers/autohooks.js +34 -0
  92. package/src/controllers/operator/tenants/_tenantId/offers/controller-v0.8.js +253 -0
  93. package/src/controllers/operator/tenants/_tenantId/offers/schemas/index.js +23 -0
  94. package/src/controllers/operator/tenants/_tenantId/offers/schemas/new-vendor-offer.schema.js +47 -0
  95. package/src/controllers/operator/tenants/_tenantId/offers/schemas/vendor-offer.schema.json +56 -0
  96. package/src/controllers/operator/tenants/_tenantId/users/autohooks.js +24 -0
  97. package/src/controllers/operator/tenants/_tenantId/users/controller-v0.8.js +92 -0
  98. package/src/controllers/operator/tenants/_tenantId/users/schemas/index.js +23 -0
  99. package/src/controllers/operator/tenants/_tenantId/users/schemas/new-user.schema.json +13 -0
  100. package/src/controllers/operator/tenants/_tenantId/users/schemas/user.schema.json +16 -0
  101. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/autohooks.js +34 -0
  102. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/controller-v0.8.js +110 -0
  103. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/Credential.schema.js +18 -0
  104. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialOptions.schema.json +42 -0
  105. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialRequest.schema.json +13 -0
  106. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/IssueCredentialResponse.schema.json +19 -0
  107. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/LinkedDataProof.schema.json +43 -0
  108. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/VerifiableCredential.schema.js +16 -0
  109. package/src/controllers/operator/tenants/_tenantId/vc-api/credentials/schemas/index.js +31 -0
  110. package/src/controllers/operator/tenants/autohooks.js +65 -0
  111. package/src/controllers/operator/tenants/controller-v0.8.js +167 -0
  112. package/src/controllers/operator/tenants/schemas/index.js +41 -0
  113. package/src/controllers/operator/tenants/schemas/modify-secret.schema.json +11 -0
  114. package/src/controllers/operator/tenants/schemas/modify-tenant-v0.8.schema.json +44 -0
  115. package/src/controllers/operator/tenants/schemas/new-tenant-v0.8.schema.json +19 -0
  116. package/src/controllers/operator/tenants/schemas/new-tenant.response.200.schema.json +7 -0
  117. package/src/controllers/operator/tenants/schemas/secret-key-metadata.schema.json +31 -0
  118. package/src/controllers/operator/tenants/schemas/secret-key.schema.json +29 -0
  119. package/src/controllers/operator/tenants/schemas/secret-kid.schema.json +13 -0
  120. package/src/controllers/operator/tenants/schemas/secret-new-tenant-v0.8.schema.json +28 -0
  121. package/src/controllers/operator/tenants/schemas/secret-tenant-key-v0.8.schema.json +13 -0
  122. package/src/controllers/operator/tenants/schemas/tenant-key-v0.8.schema.json +14 -0
  123. package/src/controllers/operator/tenants/schemas/tenant-v0.8.schema.json +62 -0
  124. package/src/controllers/root/autohooks.js +23 -0
  125. package/src/controllers/root/controller.js +173 -0
  126. package/src/controllers/saasoperator/groups/_id/autohooks.js +9 -0
  127. package/src/controllers/saasoperator/groups/_id/controller.js +121 -0
  128. package/src/controllers/saasoperator/groups/autohooks.js +19 -0
  129. package/src/controllers/saasoperator/groups/controller.js +65 -0
  130. package/src/controllers/saasoperator/groups/schemas/group.schema.js +17 -0
  131. package/src/controllers/saasoperator/groups/schemas/index.js +4 -0
  132. package/src/controllers/saasoperator/groups/schemas/new-group.schema.js +13 -0
  133. package/src/entities/common/domains/get-json-at-path.js +28 -0
  134. package/src/entities/common/domains/index.js +17 -0
  135. package/src/entities/common/index.js +17 -0
  136. package/src/entities/credentials/domains/credential-format.js +22 -0
  137. package/src/entities/credentials/domains/index.js +19 -0
  138. package/src/entities/credentials/index.js +17 -0
  139. package/src/entities/deep-links/domains/extract-did.js +11 -0
  140. package/src/entities/deep-links/domains/index.js +20 -0
  141. package/src/entities/deep-links/domains/velocity-protocol-uri-to-http-uri.js +32 -0
  142. package/src/entities/deep-links/index.js +19 -0
  143. package/src/entities/disclosures/domains/assert-disclosure-active.js +21 -0
  144. package/src/entities/disclosures/domains/compute-disclosure-configuration-type.js +29 -0
  145. package/src/entities/disclosures/domains/constants.js +61 -0
  146. package/src/entities/disclosures/domains/errors.js +34 -0
  147. package/src/entities/disclosures/domains/get-disclosure-configuration-type.js +60 -0
  148. package/src/entities/disclosures/domains/index.js +32 -0
  149. package/src/entities/disclosures/domains/is-issuing-disclosure.js +23 -0
  150. package/src/entities/disclosures/domains/parse-body-to-disclosure.js +17 -0
  151. package/src/entities/disclosures/domains/validate-by-identification-method.js +69 -0
  152. package/src/entities/disclosures/domains/validate-commercial-entity.js +26 -0
  153. package/src/entities/disclosures/domains/validate-disclosure-by-configuration-type.js +47 -0
  154. package/src/entities/disclosures/domains/validate-disclosure-default-issuing.js +77 -0
  155. package/src/entities/disclosures/domains/validate-disclosure.js +37 -0
  156. package/src/entities/disclosures/domains/validate-feed.js +16 -0
  157. package/src/entities/disclosures/domains/validate-presentation-definition.js +54 -0
  158. package/src/entities/disclosures/domains/validate-vendor-endpoint.js +22 -0
  159. package/src/entities/disclosures/domains/validate-vendor-webhook.js +18 -0
  160. package/src/entities/disclosures/factories/disclosure-factory.js +94 -0
  161. package/src/entities/disclosures/factories/index.js +19 -0
  162. package/src/entities/disclosures/index.js +22 -0
  163. package/src/entities/disclosures/orchestrators/get-disclosure.js +18 -0
  164. package/src/entities/disclosures/orchestrators/index.js +20 -0
  165. package/src/entities/disclosures/orchestrators/update-disclosure-configuration-type.js +32 -0
  166. package/src/entities/disclosures/repos/index.js +20 -0
  167. package/src/entities/disclosures/repos/repo.js +118 -0
  168. package/src/entities/disclosures/repos/set-configuration-type.js +33 -0
  169. package/src/entities/exchanges/adapters/index.js +17 -0
  170. package/src/entities/exchanges/adapters/sign-exchange-response.js +45 -0
  171. package/src/entities/exchanges/domains/build-exchange-progress.js +56 -0
  172. package/src/entities/exchanges/domains/constants.js +24 -0
  173. package/src/entities/exchanges/domains/ensure-exchange-state-valid.js +35 -0
  174. package/src/entities/exchanges/domains/errors.js +33 -0
  175. package/src/entities/exchanges/domains/index.js +25 -0
  176. package/src/entities/exchanges/domains/states.js +43 -0
  177. package/src/entities/exchanges/domains/types.js +31 -0
  178. package/src/entities/exchanges/factories/disclosure-exchange-factory.js +46 -0
  179. package/src/entities/exchanges/factories/index.js +20 -0
  180. package/src/entities/exchanges/factories/offer-exchange-factory.js +48 -0
  181. package/src/entities/exchanges/index.js +23 -0
  182. package/src/entities/exchanges/orchestrators/build-exchange-request-deep-link.js +50 -0
  183. package/src/entities/exchanges/orchestrators/index.js +19 -0
  184. package/src/entities/exchanges/repos/exchange-repo-projections.js +45 -0
  185. package/src/entities/exchanges/repos/exchange-state-repo-extension.js +76 -0
  186. package/src/entities/exchanges/repos/index.js +20 -0
  187. package/src/entities/exchanges/repos/repo.js +44 -0
  188. package/src/entities/feeds/factories/feed-factory.js +47 -0
  189. package/src/entities/feeds/factories/index.js +19 -0
  190. package/src/entities/feeds/index.js +20 -0
  191. package/src/entities/feeds/repos/index.js +19 -0
  192. package/src/entities/feeds/repos/repo.js +95 -0
  193. package/src/entities/groups/domains/format-group.js +11 -0
  194. package/src/entities/groups/domains/index.js +3 -0
  195. package/src/entities/groups/factories/group-factory.js +40 -0
  196. package/src/entities/groups/factories/index.js +19 -0
  197. package/src/entities/groups/index.js +22 -0
  198. package/src/entities/groups/orchestrators/find-group-or-error.js +16 -0
  199. package/src/entities/groups/orchestrators/index.js +6 -0
  200. package/src/entities/groups/orchestrators/validate-did.js +24 -0
  201. package/src/entities/groups/orchestrators/validate-group-by-user.js +16 -0
  202. package/src/entities/groups/orchestrators/validate-group.js +39 -0
  203. package/src/entities/groups/repos/delete-tenant-extension.js +13 -0
  204. package/src/entities/groups/repos/index.js +19 -0
  205. package/src/entities/groups/repos/repo.js +38 -0
  206. package/src/entities/groups/repos/update-or-error-extension.js +46 -0
  207. package/src/entities/index.js +37 -0
  208. package/src/entities/keys/domains/constants.js +37 -0
  209. package/src/entities/keys/domains/index.js +21 -0
  210. package/src/entities/keys/domains/is-matching-private-key-kid.js +41 -0
  211. package/src/entities/keys/domains/validate-key.js +62 -0
  212. package/src/entities/keys/factories/index.js +19 -0
  213. package/src/entities/keys/factories/key-factory.js +56 -0
  214. package/src/entities/keys/index.js +22 -0
  215. package/src/entities/keys/orchestrators/index.js +3 -0
  216. package/src/entities/keys/orchestrators/validate-did-doc-keys.js +69 -0
  217. package/src/entities/metadata-list-allocations/index.js +19 -0
  218. package/src/entities/metadata-list-allocations/repos/index.js +19 -0
  219. package/src/entities/metadata-list-allocations/repos/repo.js +40 -0
  220. package/src/entities/notifications/domains/index.js +19 -0
  221. package/src/entities/notifications/domains/notification-types.js +25 -0
  222. package/src/entities/notifications/index.js +19 -0
  223. package/src/entities/offers/domains/build-clean-pii-filter.js +35 -0
  224. package/src/entities/offers/domains/build-deeplink-url.js +120 -0
  225. package/src/entities/offers/domains/build-offer.js +88 -0
  226. package/src/entities/offers/domains/build-qr-code-url.js +37 -0
  227. package/src/entities/offers/domains/constants.js +32 -0
  228. package/src/entities/offers/domains/filter-object-ids.js +34 -0
  229. package/src/entities/offers/domains/generate-issuing-challenge.js +26 -0
  230. package/src/entities/offers/domains/generate-link-code.js +35 -0
  231. package/src/entities/offers/domains/index.js +31 -0
  232. package/src/entities/offers/domains/post-validation-offers-handler.js +31 -0
  233. package/src/entities/offers/domains/prepare-linked-credentials-for-holder.js +36 -0
  234. package/src/entities/offers/domains/resolve-subject.js +142 -0
  235. package/src/entities/offers/domains/validate-offer-commercial-entity.js +24 -0
  236. package/src/entities/offers/domains/validate-offer.js +90 -0
  237. package/src/entities/offers/factories/index.js +19 -0
  238. package/src/entities/offers/factories/offer-factory.js +119 -0
  239. package/src/entities/offers/index.js +22 -0
  240. package/src/entities/offers/orchestrators/create-verifiable-credentials.js +131 -0
  241. package/src/entities/offers/orchestrators/finalize-exchange.js +44 -0
  242. package/src/entities/offers/orchestrators/index.js +23 -0
  243. package/src/entities/offers/orchestrators/load-credential-refs.js +57 -0
  244. package/src/entities/offers/orchestrators/load-credential-types-map.js +44 -0
  245. package/src/entities/offers/orchestrators/prepare-offers.js +35 -0
  246. package/src/entities/offers/orchestrators/trigger-issued-credentials-webhook.js +63 -0
  247. package/src/entities/offers/repos/clean-pii-extension.js +85 -0
  248. package/src/entities/offers/repos/index.js +20 -0
  249. package/src/entities/offers/repos/issued-credential-projection.js +44 -0
  250. package/src/entities/offers/repos/repo.js +177 -0
  251. package/src/entities/presentations/domains/build-identity-doc.js +120 -0
  252. package/src/entities/presentations/domains/build-request-response-schema.js +46 -0
  253. package/src/entities/presentations/domains/build-vendor-data.js +31 -0
  254. package/src/entities/presentations/domains/check-payment-requirement.js +30 -0
  255. package/src/entities/presentations/domains/errors.js +28 -0
  256. package/src/entities/presentations/domains/extract-fields-from-id-credential.js +35 -0
  257. package/src/entities/presentations/domains/index.js +26 -0
  258. package/src/entities/presentations/domains/merge-credential-check-results.js +24 -0
  259. package/src/entities/presentations/domains/validate-presentation.js +128 -0
  260. package/src/entities/presentations/index.js +20 -0
  261. package/src/entities/presentations/orchestrators/create-presentation-request.js +148 -0
  262. package/src/entities/presentations/orchestrators/deduplicate-disclosure-exchange.js +52 -0
  263. package/src/entities/presentations/orchestrators/handle-presentation-submission.js +47 -0
  264. package/src/entities/presentations/orchestrators/index.js +20 -0
  265. package/src/entities/presentations/orchestrators/match-identity-on-exchange.js +114 -0
  266. package/src/entities/presentations/orchestrators/share-identification-credentials.js +110 -0
  267. package/src/entities/presentations/orchestrators/share-presentation.js +234 -0
  268. package/src/entities/push-delegate/get-push-delegate.js +37 -0
  269. package/src/entities/push-delegate/index.js +17 -0
  270. package/src/entities/redirect/index.js +3 -0
  271. package/src/entities/redirect/orchestrators/index.js +3 -0
  272. package/src/entities/redirect/orchestrators/load-org-info.js +40 -0
  273. package/src/entities/revocation-list-allocations/index.js +19 -0
  274. package/src/entities/revocation-list-allocations/repos/index.js +19 -0
  275. package/src/entities/revocation-list-allocations/repos/repo.js +40 -0
  276. package/src/entities/schemas/index.js +19 -0
  277. package/src/entities/schemas/orchestrators/index.js +19 -0
  278. package/src/entities/schemas/orchestrators/load-schema-validation.js +73 -0
  279. package/src/entities/tenants/domains/build-service-ids.js +27 -0
  280. package/src/entities/tenants/domains/extract-service.js +27 -0
  281. package/src/entities/tenants/domains/index.js +21 -0
  282. package/src/entities/tenants/domains/validate-service-ids.js +35 -0
  283. package/src/entities/tenants/factories/index.js +19 -0
  284. package/src/entities/tenants/factories/tenant-factory.js +37 -0
  285. package/src/entities/tenants/index.js +22 -0
  286. package/src/entities/tenants/orchestrators/add-primary-address-to-tenant.js +47 -0
  287. package/src/entities/tenants/orchestrators/create-tenant.js +91 -0
  288. package/src/entities/tenants/orchestrators/index.js +22 -0
  289. package/src/entities/tenants/orchestrators/refresh-tenant-dids.js +146 -0
  290. package/src/entities/tenants/orchestrators/set-tenant-default-issuing-disclosure.js +31 -0
  291. package/src/entities/tenants/repos/index.js +20 -0
  292. package/src/entities/tenants/repos/insert-tenant-extension.js +33 -0
  293. package/src/entities/tenants/repos/repo.js +52 -0
  294. package/src/entities/tenants/repos/tenant-default-projection.js +33 -0
  295. package/src/entities/tokens/adapters/access-token.js +49 -0
  296. package/src/entities/tokens/adapters/index.js +19 -0
  297. package/src/entities/tokens/index.js +19 -0
  298. package/src/entities/users/factories/index.js +19 -0
  299. package/src/entities/users/factories/user-factory.js +36 -0
  300. package/src/entities/users/index.js +20 -0
  301. package/src/entities/users/repos/add-anonymous-user-repo-extension.js +23 -0
  302. package/src/entities/users/repos/find-or-insert-vendor-user-repo-extension.js +30 -0
  303. package/src/entities/users/repos/index.js +19 -0
  304. package/src/entities/users/repos/repo.js +50 -0
  305. package/src/fetchers/index.js +20 -0
  306. package/src/fetchers/operator/identify-fetcher.js +36 -0
  307. package/src/fetchers/operator/index.js +21 -0
  308. package/src/fetchers/operator/inspection-fetcher.js +35 -0
  309. package/src/fetchers/operator/issuing-fetcher.js +50 -0
  310. package/src/fetchers/operator/webhook-auth-header.js +45 -0
  311. package/src/fetchers/push-gateway/generate-push-gateway-token.js +40 -0
  312. package/src/fetchers/push-gateway/index.js +19 -0
  313. package/src/fetchers/push-gateway/push-fetcher.js +39 -0
  314. package/src/index.js +19 -0
  315. package/src/init-holder-server.js +108 -0
  316. package/src/init-operator-server.js +101 -0
  317. package/src/init-server.js +120 -0
  318. package/src/main-holder.js +18 -0
  319. package/src/main-operator.js +19 -0
  320. package/src/main.js +18 -0
  321. package/src/plugins/autoload-repos.js +28 -0
  322. package/src/plugins/disclosure-loader-plugin.js +56 -0
  323. package/src/plugins/ensure-disclosure-active-plugin.js +30 -0
  324. package/src/plugins/ensure-disclosure-configuration-type-plugin.js +29 -0
  325. package/src/plugins/ensure-tenant-default-issuing-disclosure-id-plugin.js +60 -0
  326. package/src/plugins/ensure-tenant-primary-address-plugin.js +44 -0
  327. package/src/plugins/exchange-error-handler-plugin.js +51 -0
  328. package/src/plugins/exchange-loader-plugin.js +50 -0
  329. package/src/plugins/group-loader-plugin.js +51 -0
  330. package/src/plugins/index.js +32 -0
  331. package/src/plugins/kms-plugin.js +57 -0
  332. package/src/plugins/tenant-loader-plugin.js +91 -0
  333. package/src/plugins/validate-cao-plugin.js +81 -0
  334. package/src/plugins/vendor-routes-auth-plugin.js +24 -0
  335. package/src/plugins/verify-access-token-plugin.js +88 -0
  336. package/src/standalone.js +24 -0
  337. package/src/start-app-server.js +38 -0
  338. package/test/combined/app-redirect.test.js +199 -0
  339. package/test/combined/helpers/credentialagent-build-fastify.js +29 -0
  340. package/test/combined/helpers/index.js +22 -0
  341. package/test/combined/helpers/nock-registrar-app-schema-name.js +50 -0
  342. package/test/combined/helpers/nock-registrar-get-organization-diddoc.js +26 -0
  343. package/test/combined/helpers/nock-registrar-get-organization-verified-profile.js +33 -0
  344. package/test/combined/manifest.json.test.js +55 -0
  345. package/test/combined/root-controller.test.js +42 -0
  346. package/test/combined/schemas/education-degree.schema.json +166 -0
  347. package/test/combined/schemas/employment-current-v1.1.schema.json +253 -0
  348. package/test/combined/schemas/open-badge-credential.schema.json +1285 -0
  349. package/test/combined/schemas/past-employment-position-with-uri-id.schema.js +22 -0
  350. package/test/combined/schemas/past-employment-position.schema.json +148 -0
  351. package/test/combined/schemas/will-always-validate.json +10 -0
  352. package/test/combined/validate-cao-plugin.test.js +155 -0
  353. package/test/get-push-delegate.test.js +54 -0
  354. package/test/helpers/jwt-vc-expectation.js +109 -0
  355. package/test/holder/build-request-response-schema.test.js +55 -0
  356. package/test/holder/credential-manifest-controller.test.js +3192 -0
  357. package/test/holder/e2e-issuing-controller.test.js +425 -0
  358. package/test/holder/get-exchange-progress-controller.test.js +521 -0
  359. package/test/holder/get-presentation-request.test.js +906 -0
  360. package/test/holder/helpers/credential-type-metadata.js +98 -0
  361. package/test/holder/helpers/credentialagent-holder-build-fastify.js +32 -0
  362. package/test/holder/helpers/generate-presentation.js +441 -0
  363. package/test/holder/helpers/generate-test-access-token.js +54 -0
  364. package/test/holder/helpers/jwt-access-token-expectation.js +32 -0
  365. package/test/holder/helpers/jwt-vc-expectation.js +115 -0
  366. package/test/holder/issuing-controller.test.js +7076 -0
  367. package/test/holder/oauth-token-controller.test.js +412 -0
  368. package/test/holder/presentation-submission.test.js +2365 -0
  369. package/test/holder/submit-identification.test.js +4815 -0
  370. package/test/operator/check-credentials-controller-v0.8.test.js +832 -0
  371. package/test/operator/credentials-revoke.test.js +536 -0
  372. package/test/operator/disclosures-controller-v0.8.test.js +4157 -0
  373. package/test/operator/exchanges-controller-v0.8.test.js +414 -0
  374. package/test/operator/exchanges-id-controller-v0.8.test.js +162 -0
  375. package/test/operator/feeds-controller-v0.8.test.js +659 -0
  376. package/test/operator/generate-push-gateway-token.test.js +116 -0
  377. package/test/operator/groups-controller.test.js +145 -0
  378. package/test/operator/groups-id-controller.test.js +287 -0
  379. package/test/operator/helpers/create-test-org-doc.js +60 -0
  380. package/test/operator/helpers/credentialagent-operator-build-fastify.js +32 -0
  381. package/test/operator/helpers/find-kms-key.js +31 -0
  382. package/test/operator/helpers/generate-primary-and-add-operator-to-primary.js +63 -0
  383. package/test/operator/helpers/init-agent-kms.js +22 -0
  384. package/test/operator/issued-credentials-controller-v0.8.test.js +398 -0
  385. package/test/operator/keys-controller-v0.8.test.js +1130 -0
  386. package/test/operator/offer-data-controller-v0.8.test.js +253 -0
  387. package/test/operator/offers-controller-v0.8.test.js +3026 -0
  388. package/test/operator/set-configuration-type-modifier.test.js +75 -0
  389. package/test/operator/swagger.test.js +37 -0
  390. package/test/operator/tenant-controller-v0.8.test.js +730 -0
  391. package/test/operator/tenant-loader-plugin.test.js +96 -0
  392. package/test/operator/tenants-controller-v0.8.test.js +2093 -0
  393. package/test/operator/users-controller-v0.8.test.js +137 -0
  394. package/test/operator/vc-api-credentials.test.js +963 -0
  395. package/verification.env +28 -0
@@ -0,0 +1,4815 @@
1
+ /**
2
+ * Copyright 2023 Velocity Team
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // eslint-disable-next-line import/order
18
+ const buildFastify = require('./helpers/credentialagent-holder-build-fastify');
19
+ const { generateKeyPair } = require('@verii/crypto');
20
+ const { VnfProtocolVersions } = require('@verii/vc-checks');
21
+ const { mongoDb } = require('@spencejs/spence-mongo-repos');
22
+ const { ObjectId } = require('mongodb');
23
+ const { decodeJwt } = require('jose');
24
+ const { map, omitBy } = require('lodash/fp');
25
+ const nock = require('nock');
26
+ const { ISO_DATETIME_FORMAT } = require('@verii/test-regexes');
27
+ const { mongoify, errorResponseMatcher } = require('@verii/tests-helpers');
28
+ const metadataRegistration = require('@verii/metadata-registration');
29
+
30
+ const DEFAULT_CREDENTIAL_CHECKS = {
31
+ TRUSTED_HOLDER: 'PASS',
32
+ TRUSTED_ISSUER: 'PASS',
33
+ UNEXPIRED: 'PASS',
34
+ UNTAMPERED: 'PASS',
35
+ UNREVOKED: 'PASS',
36
+ };
37
+
38
+ jest.mock('@verii/metadata-registration');
39
+
40
+ const mockVerifyCredentials = jest.fn();
41
+ jest.mock('@verii/verifiable-credentials', () => ({
42
+ ...jest.requireActual('@verii/verifiable-credentials'),
43
+ verifyCredentials: (...args) => mockVerifyCredentials(...args),
44
+ }));
45
+
46
+ const { CredentialCheckResultValue } = require('@verii/verifiable-credentials');
47
+ const { getDidUriFromJwk } = require('@verii/did-doc');
48
+ const { holderConfig } = require('../../src/config/holder-config');
49
+ const {
50
+ initTenantFactory,
51
+ initKeysFactory,
52
+ initUserFactory,
53
+ initOfferExchangeFactory,
54
+ initDisclosureFactory,
55
+ ExchangeStates,
56
+ ExchangeTypes,
57
+ VendorEndpoint,
58
+ } = require('../../src/entities');
59
+ const {
60
+ generateKYCPresentation,
61
+ emailPayload,
62
+ phonePayload,
63
+ idDocPayload,
64
+ legacyIdDocPayload,
65
+ verificationIdentifierPayload,
66
+ } = require('./helpers/generate-presentation');
67
+
68
+ const setMockVerifyCredentials = (checkResults = DEFAULT_CREDENTIAL_CHECKS) => {
69
+ const { decodeCredentialJwt } = require('@verii/jwt');
70
+ // mockVerifyCredentials.reset();
71
+ mockVerifyCredentials.mockImplementation(async ({ credentials }) =>
72
+ Promise.resolve(
73
+ credentials.map((credential) => ({
74
+ credential: decodeCredentialJwt(credential),
75
+ credentialChecks: checkResults,
76
+ }))
77
+ )
78
+ );
79
+ };
80
+
81
+ const credentialTypesObject = { credentialTypes: ['PastEmploymentPosition'] };
82
+
83
+ describe('submit identification disclosure', () => {
84
+ let fastify;
85
+ let tenant;
86
+ let persistOfferExchange;
87
+ let persistTenant;
88
+ let persistKey;
89
+ let persistDisclosure;
90
+ let newVendorUserIdMapping;
91
+ let persistVendorUserIdMapping;
92
+
93
+ let holderKeys;
94
+ let holderDid;
95
+ let holderKid;
96
+
97
+ const idUrl = ({ did }) =>
98
+ `/api/holder/v0.6/org/${did}/issue/submit-identification`;
99
+
100
+ const mockVendorUrl = 'http://mockvendor.localhost.test';
101
+
102
+ const identifyUserOnVendorEndpoint = '/issuing/identify';
103
+
104
+ beforeAll(async () => {
105
+ fastify = buildFastify(holderConfig);
106
+ await fastify.ready();
107
+ ({ persistOfferExchange } = initOfferExchangeFactory(fastify));
108
+ ({ persistTenant } = initTenantFactory(fastify));
109
+ ({ persistKey } = initKeysFactory(fastify));
110
+ ({ newVendorUserIdMapping, persistVendorUserIdMapping } =
111
+ initUserFactory(fastify));
112
+ ({ persistDisclosure } = initDisclosureFactory(fastify));
113
+ });
114
+
115
+ beforeEach(async () => {
116
+ fastify.resetOverrides();
117
+ nock.cleanAll();
118
+ jest.resetAllMocks();
119
+ setMockVerifyCredentials();
120
+
121
+ await mongoDb().collection('tenants').deleteMany({});
122
+ await mongoDb().collection('vendorUserIdMappings').deleteMany({});
123
+ await mongoDb().collection('exchanges').deleteMany({});
124
+ await mongoDb().collection('disclosures').deleteMany({});
125
+ tenant = await persistTenant();
126
+ const keyPair = generateKeyPair({ format: 'jwk' });
127
+ await persistKey({
128
+ tenant,
129
+ kidFragment: '#ID1',
130
+ keyPair,
131
+ });
132
+
133
+ holderKeys = generateKeyPair({ format: 'jwk' });
134
+ holderDid = getDidUriFromJwk(holderKeys.publicKey);
135
+ holderKid = `${holderDid}#0`;
136
+
137
+ nock('http://oracle.localhost.test')
138
+ .get('/api/v0.6/credential-types', () => {
139
+ return true;
140
+ })
141
+ .reply(
142
+ 200,
143
+ [
144
+ {
145
+ credentialType: 'Passport',
146
+ issuerCategory: 'ContactIssuer',
147
+ },
148
+ ],
149
+ { 'cache-control': 'max-age=3600' }
150
+ );
151
+
152
+ metadataRegistration.initVerificationCoupon.mockImplementation(() => ({
153
+ getCoupon: () => Promise.resolve(42),
154
+ }));
155
+ metadataRegistration.initRevocationRegistry.mockImplementation(() => ({
156
+ getRevokedStatus: () => Promise.resolve(0),
157
+ }));
158
+ });
159
+
160
+ afterAll(async () => {
161
+ await fastify.close();
162
+ nock.cleanAll();
163
+ nock.restore();
164
+ });
165
+
166
+ describe('using integrated identification', () => {
167
+ it('should fail if the json path doesnt exist', async () => {
168
+ const disclosure = await persistDisclosure({
169
+ tenant,
170
+ description: 'Credential Issuance disclosure',
171
+ types: [{ type: 'EmailV1.0' }],
172
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
173
+ purpose: 'Identification',
174
+ duration: '6y',
175
+ identityMatchers: {
176
+ rules: [
177
+ {
178
+ valueIndex: 0, // used for identifying the value
179
+ path: ['$.doesntexist'], // jsonPath within the credential
180
+ rule: 'pick', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
181
+ },
182
+ ],
183
+ vendorUserIdIndex: 0,
184
+ },
185
+ });
186
+ const exchange = await persistOfferExchange({
187
+ tenant,
188
+ disclosure,
189
+ identityMatcherValues: ['adam.smith@example.com'],
190
+ });
191
+
192
+ const presentationEmail = await generateKYCPresentation(
193
+ exchange,
194
+ 'email'
195
+ );
196
+
197
+ const response = await fastify.injectJson({
198
+ method: 'POST',
199
+ url: idUrl(tenant),
200
+ payload: {
201
+ jwt_vp: await presentationEmail.selfSign(),
202
+ exchange_id: exchange._id,
203
+ },
204
+ });
205
+
206
+ expect(response.statusCode).toEqual(400);
207
+ expect(response.json).toEqual(
208
+ errorResponseMatcher({
209
+ error: 'Bad Request',
210
+ message: 'Presentation doesnt contain value at $.doesntexist',
211
+ statusCode: 400,
212
+ errorCode: 'presentation_credential_jsonpath_empty',
213
+ })
214
+ );
215
+
216
+ const exchangeDBResult = await mongoDb()
217
+ .collection('exchanges')
218
+ .findOne({ _id: new ObjectId(exchange._id) });
219
+ expect(exchangeDBResult).toEqual(
220
+ mongoify({
221
+ ...exchange,
222
+ tenantId: tenant._id,
223
+ presentationId: presentationEmail.presentation.id,
224
+ disclosureConsentedAt: expect.any(Date),
225
+ err: 'Presentation doesnt contain value at $.doesntexist',
226
+ events: [
227
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
228
+ {
229
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
230
+ timestamp: expect.any(Date),
231
+ },
232
+ {
233
+ state: ExchangeStates.DISCLOSURE_CHECKED,
234
+ timestamp: expect.any(Date),
235
+ },
236
+ {
237
+ state: ExchangeStates.UNEXPECTED_ERROR,
238
+ timestamp: expect.any(Date),
239
+ },
240
+ ],
241
+ offerHashes: [],
242
+ ...credentialTypesObject,
243
+ createdAt: expect.any(Date),
244
+ updatedAt: expect.any(Date),
245
+ })
246
+ );
247
+ });
248
+
249
+ it('should error if rule isnt "all", "pick" or "equal"', async () => {
250
+ const disclosure = await persistDisclosure({
251
+ tenant,
252
+ description: 'Credential Issuance disclosure',
253
+ types: [{ type: 'EmailV1.0' }],
254
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
255
+ purpose: 'Identification',
256
+ duration: '6y',
257
+ identityMatchers: {
258
+ rules: [
259
+ {
260
+ valueIndex: 0, // used for identifying the value
261
+ path: ['$.emails'], // jsonPath within the credential
262
+ rule: 'notARule', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
263
+ },
264
+ ],
265
+ vendorUserIdIndex: 0,
266
+ },
267
+ });
268
+
269
+ const exchange = await persistOfferExchange({
270
+ tenant,
271
+ disclosure,
272
+ identityMatcherValues: ['adam.smith@example.com'],
273
+ });
274
+
275
+ const presentationEmail = await generateKYCPresentation(
276
+ exchange,
277
+ 'email'
278
+ );
279
+
280
+ const response = await fastify.injectJson({
281
+ method: 'POST',
282
+ url: idUrl(tenant),
283
+ payload: {
284
+ jwt_vp: await presentationEmail.selfSign(),
285
+ exchange_id: exchange._id,
286
+ },
287
+ });
288
+ expect(response.statusCode).toEqual(500);
289
+ expect(response.json).toEqual(
290
+ errorResponseMatcher({
291
+ error: 'Internal Server Error',
292
+ message:
293
+ 'Credential Agent only supports "pick" or "all" for "identityMatchers.rule"',
294
+ statusCode: 500,
295
+ errorCode: 'missing_error_code',
296
+ })
297
+ );
298
+ });
299
+
300
+ describe('"pick" rule matching using email', () => {
301
+ let disclosure;
302
+
303
+ beforeEach(async () => {
304
+ disclosure = await persistDisclosure({
305
+ tenant,
306
+ description: 'Credential Issuance disclosure',
307
+ types: [{ type: 'EmailV1.0' }],
308
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
309
+ purpose: 'Identification',
310
+ duration: '6y',
311
+ identityMatchers: {
312
+ rules: [
313
+ {
314
+ valueIndex: 0, // used for identifying the value
315
+ path: ['$.emails'], // jsonPath within the credential
316
+ rule: 'pick', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
317
+ },
318
+ ],
319
+ vendorUserIdIndex: 0,
320
+ },
321
+ });
322
+ });
323
+
324
+ it('email based matching should match an existent user', async () => {
325
+ const exchange = await persistOfferExchange({
326
+ tenant,
327
+ disclosure,
328
+ identityMatcherValues: ['Adam.smith@example.com'],
329
+ });
330
+ const vendorUserId = exchange.identityMatcherValues[0];
331
+
332
+ const presentationEmail = await generateKYCPresentation(
333
+ exchange,
334
+ 'email'
335
+ );
336
+
337
+ const response = await fastify.injectJson({
338
+ method: 'POST',
339
+ url: idUrl(tenant),
340
+ payload: {
341
+ jwt_vp: await presentationEmail.selfSign(),
342
+ exchange_id: exchange._id,
343
+ },
344
+ });
345
+ expect(response.statusCode).toEqual(200);
346
+ expect(response.json).toEqual({
347
+ token: expect.any(String),
348
+ exchange: {
349
+ disclosureComplete: true,
350
+ exchangeComplete: false,
351
+ id: exchange._id,
352
+ type: ExchangeTypes.ISSUING,
353
+ },
354
+ });
355
+
356
+ const dbResult = await mongoDb()
357
+ .collection('vendorUserIdMappings')
358
+ .findOne({ vendorUserId });
359
+ const { sub } = decodeJwt(response.json.token);
360
+ expect(dbResult).toEqual({
361
+ _id: new ObjectId(sub),
362
+ vendorUserId,
363
+ tenantId: new ObjectId(tenant._id),
364
+ createdAt: expect.any(Date),
365
+ updatedAt: expect.any(Date),
366
+ });
367
+ const exchangeDBResult = await mongoDb()
368
+ .collection('exchanges')
369
+ .findOne({ _id: new ObjectId(exchange._id) });
370
+ expect(exchangeDBResult).toEqual(
371
+ mongoify({
372
+ ...exchange,
373
+ tenantId: tenant._id,
374
+ presentationId: presentationEmail.presentation.id,
375
+ disclosureConsentedAt: expect.any(Date),
376
+ events: [
377
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
378
+ {
379
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
380
+ timestamp: expect.any(Date),
381
+ },
382
+ {
383
+ state: ExchangeStates.DISCLOSURE_CHECKED,
384
+ timestamp: expect.any(Date),
385
+ },
386
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
387
+ ],
388
+ offerHashes: [],
389
+ ...credentialTypesObject,
390
+ createdAt: expect.any(Date),
391
+ updatedAt: expect.any(Date),
392
+ })
393
+ );
394
+ });
395
+
396
+ it('email based matching should succeed if holder check is NOT_APPLICABLE', async () => {
397
+ setMockVerifyCredentials({
398
+ ...DEFAULT_CREDENTIAL_CHECKS,
399
+ TRUSTED_HOLDER: CredentialCheckResultValue.NOT_APPLICABLE,
400
+ });
401
+ const exchange = await persistOfferExchange({
402
+ tenant,
403
+ disclosure,
404
+ identityMatcherValues: ['Adam.smith@example.com'],
405
+ });
406
+ const vendorUserId = exchange.identityMatcherValues[0];
407
+
408
+ const presentationEmail = await generateKYCPresentation(
409
+ exchange,
410
+ 'email'
411
+ );
412
+
413
+ const response = await fastify.injectJson({
414
+ method: 'POST',
415
+ url: idUrl(tenant),
416
+ payload: {
417
+ jwt_vp: await presentationEmail.selfSign(),
418
+ exchange_id: exchange._id,
419
+ },
420
+ });
421
+ expect(response.statusCode).toEqual(200);
422
+ expect(response.json).toEqual({
423
+ token: expect.any(String),
424
+ exchange: {
425
+ disclosureComplete: true,
426
+ exchangeComplete: false,
427
+ id: exchange._id,
428
+ type: ExchangeTypes.ISSUING,
429
+ },
430
+ });
431
+
432
+ const dbResult = await mongoDb()
433
+ .collection('vendorUserIdMappings')
434
+ .findOne({ vendorUserId });
435
+ const { sub } = decodeJwt(response.json.token);
436
+ expect(dbResult).toEqual({
437
+ _id: new ObjectId(sub),
438
+ vendorUserId,
439
+ tenantId: new ObjectId(tenant._id),
440
+ createdAt: expect.any(Date),
441
+ updatedAt: expect.any(Date),
442
+ });
443
+ const exchangeDBResult = await mongoDb()
444
+ .collection('exchanges')
445
+ .findOne({ _id: new ObjectId(exchange._id) });
446
+ expect(exchangeDBResult).toEqual(
447
+ mongoify({
448
+ ...exchange,
449
+ tenantId: tenant._id,
450
+ presentationId: presentationEmail.presentation.id,
451
+ disclosureConsentedAt: expect.any(Date),
452
+ events: [
453
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
454
+ {
455
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
456
+ timestamp: expect.any(Date),
457
+ },
458
+ {
459
+ state: ExchangeStates.DISCLOSURE_CHECKED,
460
+ timestamp: expect.any(Date),
461
+ },
462
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
463
+ ],
464
+ offerHashes: [],
465
+ ...credentialTypesObject,
466
+ createdAt: expect.any(Date),
467
+ updatedAt: expect.any(Date),
468
+ })
469
+ );
470
+ });
471
+
472
+ it('email based matching should 401 if user not found', async () => {
473
+ const exchange = await persistOfferExchange({
474
+ tenant,
475
+ disclosure,
476
+ identityMatcherValues: ['adam.nomatch@example.com'],
477
+ });
478
+
479
+ const presentationEmail = await generateKYCPresentation(
480
+ exchange,
481
+ 'email'
482
+ );
483
+
484
+ const response = await fastify.injectJson({
485
+ method: 'POST',
486
+ url: idUrl(tenant),
487
+ payload: {
488
+ jwt_vp: await presentationEmail.selfSign(),
489
+ exchange_id: exchange._id,
490
+ },
491
+ });
492
+ expect(response.statusCode).toEqual(401);
493
+ expect(response.json).toEqual(
494
+ errorResponseMatcher({
495
+ error: 'Unauthorized',
496
+ errorCode: 'integrated_identification_user_not_found',
497
+ message: 'User Not Found',
498
+ statusCode: 401,
499
+ })
500
+ );
501
+
502
+ const exchangeDBResult = await mongoDb()
503
+ .collection('exchanges')
504
+ .findOne({ _id: new ObjectId(exchange._id) });
505
+ expect(exchangeDBResult).toEqual(
506
+ mongoify({
507
+ ...exchange,
508
+ tenantId: tenant._id,
509
+ presentationId: presentationEmail.presentation.id,
510
+ disclosureConsentedAt: expect.any(Date),
511
+ err: 'User Not Found',
512
+ events: [
513
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
514
+ {
515
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
516
+ timestamp: expect.any(Date),
517
+ },
518
+ {
519
+ state: ExchangeStates.DISCLOSURE_CHECKED,
520
+ timestamp: expect.any(Date),
521
+ },
522
+ {
523
+ state: ExchangeStates.NOT_IDENTIFIED,
524
+ timestamp: expect.any(Date),
525
+ },
526
+ ],
527
+ offerHashes: [],
528
+ ...credentialTypesObject,
529
+ createdAt: expect.any(Date),
530
+ updatedAt: expect.any(Date),
531
+ })
532
+ );
533
+ });
534
+
535
+ it('email based matching should 401 if credential tampered', async () => {
536
+ setMockVerifyCredentials({
537
+ ...DEFAULT_CREDENTIAL_CHECKS,
538
+ UNTAMPERED: CredentialCheckResultValue.FAIL,
539
+ });
540
+ const exchange = await persistOfferExchange({
541
+ tenant,
542
+ disclosure,
543
+ identityMatcherValues: ['adam.smith@example.com'],
544
+ });
545
+
546
+ const presentationEmail = await generateKYCPresentation(
547
+ exchange,
548
+ 'email'
549
+ );
550
+
551
+ const response = await fastify.injectJson({
552
+ method: 'POST',
553
+ url: idUrl(tenant),
554
+ payload: {
555
+ jwt_vp: await presentationEmail.selfSign(),
556
+ exchange_id: exchange._id,
557
+ },
558
+ });
559
+ expect(response.statusCode).toEqual(401);
560
+
561
+ const exchangeDBResult = await mongoDb()
562
+ .collection('exchanges')
563
+ .findOne({ _id: new ObjectId(exchange._id) });
564
+ expect(exchangeDBResult).toEqual(
565
+ mongoify({
566
+ ...exchange,
567
+ tenantId: tenant._id,
568
+ presentationId: presentationEmail.presentation.id,
569
+ disclosureConsentedAt: expect.any(Date),
570
+ err: 'presentation_credential_tampered',
571
+ events: [
572
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
573
+ {
574
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
575
+ timestamp: expect.any(Date),
576
+ },
577
+ {
578
+ state: ExchangeStates.DISCLOSURE_CHECKED,
579
+ timestamp: expect.any(Date),
580
+ },
581
+ {
582
+ state: ExchangeStates.NOT_IDENTIFIED,
583
+ timestamp: expect.any(Date),
584
+ },
585
+ ],
586
+ offerHashes: [],
587
+ ...credentialTypesObject,
588
+ createdAt: expect.any(Date),
589
+ updatedAt: expect.any(Date),
590
+ })
591
+ );
592
+ });
593
+
594
+ it('email based matching should 401 if credential not from a trusted issuer', async () => {
595
+ setMockVerifyCredentials({
596
+ ...DEFAULT_CREDENTIAL_CHECKS,
597
+ TRUSTED_ISSUER: CredentialCheckResultValue.FAIL,
598
+ });
599
+ const exchange = await persistOfferExchange({
600
+ tenant,
601
+ disclosure,
602
+ identityMatcherValues: ['adam.smith@example.com'],
603
+ });
604
+
605
+ const presentationEmail = await generateKYCPresentation(
606
+ exchange,
607
+ 'email'
608
+ );
609
+
610
+ const response = await fastify.injectJson({
611
+ method: 'POST',
612
+ url: idUrl(tenant),
613
+ payload: {
614
+ jwt_vp: await presentationEmail.selfSign(),
615
+ exchange_id: exchange._id,
616
+ },
617
+ });
618
+ expect(response.statusCode).toEqual(401);
619
+ expect(response.json).toEqual(
620
+ errorResponseMatcher({
621
+ error: 'Unauthorized',
622
+ errorCode: 'presentation_credential_bad_issuer',
623
+ message: 'presentation_credential_bad_issuer',
624
+ statusCode: 401,
625
+ })
626
+ );
627
+
628
+ const exchangeDBResult = await mongoDb()
629
+ .collection('exchanges')
630
+ .findOne({ _id: new ObjectId(exchange._id) });
631
+ expect(exchangeDBResult).toEqual(
632
+ mongoify({
633
+ ...exchange,
634
+ tenantId: tenant._id,
635
+ presentationId: presentationEmail.presentation.id,
636
+ disclosureConsentedAt: expect.any(Date),
637
+ err: 'presentation_credential_bad_issuer',
638
+ events: [
639
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
640
+ {
641
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
642
+ timestamp: expect.any(Date),
643
+ },
644
+ {
645
+ state: ExchangeStates.DISCLOSURE_CHECKED,
646
+ timestamp: expect.any(Date),
647
+ },
648
+ {
649
+ state: ExchangeStates.NOT_IDENTIFIED,
650
+ timestamp: expect.any(Date),
651
+ },
652
+ ],
653
+ offerHashes: [],
654
+ ...credentialTypesObject,
655
+ createdAt: expect.any(Date),
656
+ updatedAt: expect.any(Date),
657
+ })
658
+ );
659
+ });
660
+
661
+ it('email based matching should 401 if credential not from a trusted holder', async () => {
662
+ setMockVerifyCredentials({
663
+ ...DEFAULT_CREDENTIAL_CHECKS,
664
+ TRUSTED_HOLDER: CredentialCheckResultValue.FAIL,
665
+ });
666
+ const exchange = await persistOfferExchange({
667
+ tenant,
668
+ disclosure,
669
+ identityMatcherValues: ['adam.smith@example.com'],
670
+ });
671
+
672
+ const presentationEmail = await generateKYCPresentation(
673
+ exchange,
674
+ 'email'
675
+ );
676
+
677
+ const response = await fastify.injectJson({
678
+ method: 'POST',
679
+ url: idUrl(tenant),
680
+ payload: {
681
+ jwt_vp: await presentationEmail.selfSign(),
682
+ exchange_id: exchange._id,
683
+ },
684
+ });
685
+ expect(response.statusCode).toEqual(401);
686
+
687
+ const exchangeDBResult = await mongoDb()
688
+ .collection('exchanges')
689
+ .findOne({ _id: new ObjectId(exchange._id) });
690
+ expect(exchangeDBResult).toEqual(
691
+ mongoify({
692
+ ...exchange,
693
+ tenantId: tenant._id,
694
+ presentationId: presentationEmail.presentation.id,
695
+ disclosureConsentedAt: expect.any(Date),
696
+ err: 'presentation_credential_bad_holder',
697
+ events: [
698
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
699
+ {
700
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
701
+ timestamp: expect.any(Date),
702
+ },
703
+ {
704
+ state: ExchangeStates.DISCLOSURE_CHECKED,
705
+ timestamp: expect.any(Date),
706
+ },
707
+ {
708
+ state: ExchangeStates.NOT_IDENTIFIED,
709
+ timestamp: expect.any(Date),
710
+ },
711
+ ],
712
+ offerHashes: [],
713
+ ...credentialTypesObject,
714
+ createdAt: expect.any(Date),
715
+ updatedAt: expect.any(Date),
716
+ })
717
+ );
718
+ });
719
+
720
+ it('email based matching should 401 if credential revoked', async () => {
721
+ setMockVerifyCredentials({
722
+ ...DEFAULT_CREDENTIAL_CHECKS,
723
+ UNREVOKED: CredentialCheckResultValue.FAIL,
724
+ });
725
+ const exchange = await persistOfferExchange({
726
+ tenant,
727
+ disclosure,
728
+ identityMatcherValues: ['adam.smith@example.com'],
729
+ });
730
+
731
+ const presentationEmail = await generateKYCPresentation(
732
+ exchange,
733
+ 'email'
734
+ );
735
+
736
+ const response = await fastify.injectJson({
737
+ method: 'POST',
738
+ url: idUrl(tenant),
739
+ payload: {
740
+ jwt_vp: await presentationEmail.selfSign(),
741
+ exchange_id: exchange._id,
742
+ },
743
+ });
744
+ expect(response.statusCode).toEqual(401);
745
+
746
+ const exchangeDBResult = await mongoDb()
747
+ .collection('exchanges')
748
+ .findOne({ _id: new ObjectId(exchange._id) });
749
+ expect(exchangeDBResult).toEqual(
750
+ mongoify({
751
+ ...exchange,
752
+ tenantId: tenant._id,
753
+ presentationId: presentationEmail.presentation.id,
754
+ disclosureConsentedAt: expect.any(Date),
755
+ err: 'presentation_credential_revoked',
756
+ events: [
757
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
758
+ {
759
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
760
+ timestamp: expect.any(Date),
761
+ },
762
+ {
763
+ state: ExchangeStates.DISCLOSURE_CHECKED,
764
+ timestamp: expect.any(Date),
765
+ },
766
+ {
767
+ state: ExchangeStates.NOT_IDENTIFIED,
768
+ timestamp: expect.any(Date),
769
+ },
770
+ ],
771
+ offerHashes: [],
772
+ ...credentialTypesObject,
773
+ createdAt: expect.any(Date),
774
+ updatedAt: expect.any(Date),
775
+ })
776
+ );
777
+ });
778
+
779
+ it('email based matching should 401 if credential expired', async () => {
780
+ setMockVerifyCredentials({
781
+ ...DEFAULT_CREDENTIAL_CHECKS,
782
+ UNEXPIRED: CredentialCheckResultValue.FAIL,
783
+ });
784
+ const exchange = await persistOfferExchange({
785
+ tenant,
786
+ disclosure,
787
+ identityMatcherValues: ['adam.smith@example.com'],
788
+ });
789
+
790
+ const presentationEmail = await generateKYCPresentation(
791
+ exchange,
792
+ 'email'
793
+ );
794
+
795
+ const response = await fastify.injectJson({
796
+ method: 'POST',
797
+ url: idUrl(tenant),
798
+ payload: {
799
+ jwt_vp: await presentationEmail.selfSign(),
800
+ exchange_id: exchange._id,
801
+ },
802
+ });
803
+ expect(response.statusCode).toEqual(401);
804
+
805
+ const exchangeDBResult = await mongoDb()
806
+ .collection('exchanges')
807
+ .findOne({ _id: new ObjectId(exchange._id) });
808
+ expect(exchangeDBResult).toEqual(
809
+ mongoify({
810
+ ...exchange,
811
+ tenantId: tenant._id,
812
+ presentationId: presentationEmail.presentation.id,
813
+ disclosureConsentedAt: expect.any(Date),
814
+ err: 'presentation_credential_expired',
815
+ events: [
816
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
817
+ {
818
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
819
+ timestamp: expect.any(Date),
820
+ },
821
+ {
822
+ state: ExchangeStates.DISCLOSURE_CHECKED,
823
+ timestamp: expect.any(Date),
824
+ },
825
+ {
826
+ state: ExchangeStates.NOT_IDENTIFIED,
827
+ timestamp: expect.any(Date),
828
+ },
829
+ ],
830
+ offerHashes: [],
831
+ ...credentialTypesObject,
832
+ createdAt: expect.any(Date),
833
+ updatedAt: expect.any(Date),
834
+ })
835
+ );
836
+ });
837
+
838
+ it('email based matching should match an existent user on second try', async () => {
839
+ const exchange = await persistOfferExchange({
840
+ tenant,
841
+ disclosure,
842
+ identityMatcherValues: ['adam.smith@example.com'],
843
+ events: [
844
+ { state: ExchangeStates.NEW, timestamp: new Date() },
845
+ {
846
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
847
+ timestamp: new Date(),
848
+ },
849
+ {
850
+ state: ExchangeStates.DISCLOSURE_CHECKED,
851
+ timestamp: new Date(),
852
+ },
853
+ {
854
+ state: ExchangeStates.NOT_IDENTIFIED,
855
+ timestamp: new Date(),
856
+ },
857
+ ],
858
+ });
859
+ const vendorUserId = exchange.identityMatcherValues[0];
860
+
861
+ const presentationEmail = await generateKYCPresentation(
862
+ exchange,
863
+ 'email'
864
+ );
865
+
866
+ const response = await fastify.injectJson({
867
+ method: 'POST',
868
+ url: idUrl(tenant),
869
+ payload: {
870
+ jwt_vp: await presentationEmail.selfSign(),
871
+ exchange_id: exchange._id,
872
+ },
873
+ });
874
+ expect(response.statusCode).toEqual(200);
875
+ expect(response.json).toEqual({
876
+ token: expect.any(String),
877
+ exchange: {
878
+ disclosureComplete: true,
879
+ exchangeComplete: false,
880
+ id: exchange._id,
881
+ type: ExchangeTypes.ISSUING,
882
+ },
883
+ });
884
+
885
+ const dbResult = await mongoDb()
886
+ .collection('vendorUserIdMappings')
887
+ .findOne({ vendorUserId });
888
+ const { sub } = decodeJwt(response.json.token);
889
+ expect(dbResult).toEqual({
890
+ _id: new ObjectId(sub),
891
+ vendorUserId,
892
+ tenantId: new ObjectId(tenant._id),
893
+ createdAt: expect.any(Date),
894
+ updatedAt: expect.any(Date),
895
+ });
896
+ const exchangeDBResult = await mongoDb()
897
+ .collection('exchanges')
898
+ .findOne({ _id: new ObjectId(exchange._id) });
899
+ expect(exchangeDBResult).toEqual(
900
+ mongoify({
901
+ ...exchange,
902
+ tenantId: tenant._id,
903
+ presentationId: presentationEmail.presentation.id,
904
+ disclosureConsentedAt: expect.any(Date),
905
+ events: [
906
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
907
+ {
908
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
909
+ timestamp: expect.any(Date),
910
+ },
911
+ {
912
+ state: ExchangeStates.DISCLOSURE_CHECKED,
913
+ timestamp: expect.any(Date),
914
+ },
915
+ {
916
+ state: ExchangeStates.NOT_IDENTIFIED,
917
+ timestamp: expect.any(Date),
918
+ },
919
+ {
920
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
921
+ timestamp: expect.any(Date),
922
+ },
923
+ {
924
+ state: ExchangeStates.DISCLOSURE_CHECKED,
925
+ timestamp: expect.any(Date),
926
+ },
927
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
928
+ ],
929
+ offerHashes: [],
930
+ ...credentialTypesObject,
931
+ createdAt: expect.any(Date),
932
+ updatedAt: expect.any(Date),
933
+ })
934
+ );
935
+ });
936
+ });
937
+
938
+ describe('"pick" rule matching using id credential', () => {
939
+ let disclosure;
940
+
941
+ beforeEach(async () => {
942
+ disclosure = await persistDisclosure({
943
+ tenant,
944
+ description: 'Credential Issuance disclosure',
945
+ types: [{ type: 'DriversLicenseV1.0' }],
946
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
947
+ purpose: 'Identification',
948
+ duration: '6y',
949
+ identityMatchers: {
950
+ rules: [
951
+ {
952
+ valueIndex: 0, // used for identifying the value
953
+ path: [
954
+ '$.idDocumentCredentials[*].credentialSubject.identifier',
955
+ ], // jsonPath within the credential
956
+ rule: 'pick', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
957
+ },
958
+ ],
959
+ vendorUserIdIndex: 0,
960
+ },
961
+ });
962
+ });
963
+
964
+ it('identifier based matching should match an existent user', async () => {
965
+ const exchange = await persistOfferExchange({
966
+ tenant,
967
+ disclosure,
968
+ identityMatcherValues: ['2200221100'],
969
+ });
970
+ const vendorUserId = exchange.identityMatcherValues[0];
971
+
972
+ const presentationEmail = await generateKYCPresentation(
973
+ exchange,
974
+ 'driversLicense'
975
+ );
976
+
977
+ const response = await fastify.injectJson({
978
+ method: 'POST',
979
+ url: idUrl(tenant),
980
+ payload: {
981
+ jwt_vp: await presentationEmail.selfSign(),
982
+ exchange_id: exchange._id,
983
+ },
984
+ });
985
+ expect(response.statusCode).toEqual(200);
986
+ expect(response.json).toEqual({
987
+ token: expect.any(String),
988
+ exchange: {
989
+ disclosureComplete: true,
990
+ exchangeComplete: false,
991
+ id: exchange._id,
992
+ type: ExchangeTypes.ISSUING,
993
+ },
994
+ });
995
+
996
+ const dbResult = await mongoDb()
997
+ .collection('vendorUserIdMappings')
998
+ .findOne({ vendorUserId });
999
+ const { sub } = decodeJwt(response.json.token);
1000
+ expect(dbResult).toEqual({
1001
+ _id: new ObjectId(sub),
1002
+ vendorUserId,
1003
+ tenantId: new ObjectId(tenant._id),
1004
+ createdAt: expect.any(Date),
1005
+ updatedAt: expect.any(Date),
1006
+ });
1007
+ const exchangeDBResult = await mongoDb()
1008
+ .collection('exchanges')
1009
+ .findOne({ _id: new ObjectId(exchange._id) });
1010
+ expect(exchangeDBResult).toEqual(
1011
+ mongoify({
1012
+ ...exchange,
1013
+ tenantId: tenant._id,
1014
+ presentationId: presentationEmail.presentation.id,
1015
+ disclosureConsentedAt: expect.any(Date),
1016
+ events: [
1017
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1018
+ {
1019
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1020
+ timestamp: expect.any(Date),
1021
+ },
1022
+ {
1023
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1024
+ timestamp: expect.any(Date),
1025
+ },
1026
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
1027
+ ],
1028
+ offerHashes: [],
1029
+ ...credentialTypesObject,
1030
+ createdAt: expect.any(Date),
1031
+ updatedAt: expect.any(Date),
1032
+ })
1033
+ );
1034
+ });
1035
+ it('identifier based matching should 401 if user not found', async () => {
1036
+ const exchange = await persistOfferExchange({
1037
+ tenant,
1038
+ disclosure,
1039
+ identityMatcherValues: ['foo'],
1040
+ });
1041
+
1042
+ const presentationEmail = await generateKYCPresentation(
1043
+ exchange,
1044
+ 'driversLicense'
1045
+ );
1046
+
1047
+ const response = await fastify.injectJson({
1048
+ method: 'POST',
1049
+ url: idUrl(tenant),
1050
+ payload: {
1051
+ jwt_vp: await presentationEmail.selfSign(),
1052
+ exchange_id: exchange._id,
1053
+ },
1054
+ });
1055
+ expect(response.statusCode).toEqual(401);
1056
+ expect(response.json).toEqual(
1057
+ errorResponseMatcher({
1058
+ error: 'Unauthorized',
1059
+ errorCode: 'integrated_identification_user_not_found',
1060
+ message: 'User Not Found',
1061
+ statusCode: 401,
1062
+ })
1063
+ );
1064
+
1065
+ const exchangeDBResult = await mongoDb()
1066
+ .collection('exchanges')
1067
+ .findOne({ _id: new ObjectId(exchange._id) });
1068
+ expect(exchangeDBResult).toEqual(
1069
+ mongoify({
1070
+ ...exchange,
1071
+ tenantId: tenant._id,
1072
+ presentationId: presentationEmail.presentation.id,
1073
+ disclosureConsentedAt: expect.any(Date),
1074
+ err: 'User Not Found',
1075
+ events: [
1076
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1077
+ {
1078
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1079
+ timestamp: expect.any(Date),
1080
+ },
1081
+ {
1082
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1083
+ timestamp: expect.any(Date),
1084
+ },
1085
+ {
1086
+ state: ExchangeStates.NOT_IDENTIFIED,
1087
+ timestamp: expect.any(Date),
1088
+ },
1089
+ ],
1090
+ offerHashes: [],
1091
+ ...credentialTypesObject,
1092
+ createdAt: expect.any(Date),
1093
+ updatedAt: expect.any(Date),
1094
+ })
1095
+ );
1096
+ });
1097
+ });
1098
+
1099
+ describe('"all" rule for givenName matching', () => {
1100
+ let disclosure;
1101
+ let exchange;
1102
+ let vendorUserId;
1103
+
1104
+ beforeEach(async () => {
1105
+ disclosure = await persistDisclosure({
1106
+ tenant,
1107
+ description: 'Credential Issuance disclosure',
1108
+ types: [{ type: 'EmailV1.0' }],
1109
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
1110
+ purpose: 'Identification',
1111
+ duration: '6y',
1112
+ identityMatchers: {
1113
+ rules: [
1114
+ {
1115
+ valueIndex: 0, // used for identifying the value
1116
+ path: [
1117
+ '$.idDocumentCredentials[*].credentialSubject.person.givenName',
1118
+ ], // jsonPath within the credential
1119
+ rule: 'all', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
1120
+ },
1121
+ ],
1122
+ vendorUserIdIndex: 0,
1123
+ },
1124
+ });
1125
+ exchange = await persistOfferExchange({
1126
+ tenant,
1127
+ disclosure,
1128
+ identityMatcherValues: ['sam'],
1129
+ });
1130
+ vendorUserId = exchange.identityMatcherValues[0];
1131
+ });
1132
+
1133
+ it('given name matching should 200 if user found', async () => {
1134
+ const presentationEmail = await generateKYCPresentation(
1135
+ exchange,
1136
+ 'idDocument'
1137
+ );
1138
+
1139
+ const response = await fastify.injectJson({
1140
+ method: 'POST',
1141
+ url: idUrl(tenant),
1142
+ headers: {
1143
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1144
+ },
1145
+ payload: {
1146
+ jwt_vp: await presentationEmail.sign(
1147
+ holderKid,
1148
+ holderKeys.privateKey,
1149
+ holderDid
1150
+ ),
1151
+ exchange_id: exchange._id,
1152
+ },
1153
+ });
1154
+ expect(response.statusCode).toEqual(200);
1155
+ expect(response.json).toEqual({
1156
+ token: expect.any(String),
1157
+ exchange: {
1158
+ disclosureComplete: true,
1159
+ exchangeComplete: false,
1160
+ id: exchange._id,
1161
+ type: ExchangeTypes.ISSUING,
1162
+ },
1163
+ });
1164
+
1165
+ const dbResult = await mongoDb()
1166
+ .collection('vendorUserIdMappings')
1167
+ .findOne({ vendorUserId });
1168
+ const { sub } = decodeJwt(response.json.token);
1169
+ expect(dbResult).toEqual({
1170
+ _id: new ObjectId(sub),
1171
+ vendorUserId,
1172
+ tenantId: new ObjectId(tenant._id),
1173
+ createdAt: expect.any(Date),
1174
+ updatedAt: expect.any(Date),
1175
+ });
1176
+ const exchangeDBResult = await mongoDb()
1177
+ .collection('exchanges')
1178
+ .findOne({ _id: new ObjectId(exchange._id) });
1179
+ expect(exchangeDBResult).toEqual(
1180
+ mongoify({
1181
+ ...exchange,
1182
+ tenantId: tenant._id,
1183
+ presentationId: presentationEmail.presentation.id,
1184
+ disclosureConsentedAt: expect.any(Date),
1185
+ events: [
1186
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1187
+ {
1188
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1189
+ timestamp: expect.any(Date),
1190
+ },
1191
+ {
1192
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1193
+ timestamp: expect.any(Date),
1194
+ },
1195
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
1196
+ ],
1197
+ offerHashes: [],
1198
+ ...credentialTypesObject,
1199
+ createdAt: expect.any(Date),
1200
+ updatedAt: expect.any(Date),
1201
+ })
1202
+ );
1203
+ });
1204
+ it('given name matching on all docs should 200 if user found', async () => {
1205
+ const presentationEmail = await generateKYCPresentation(exchange, [
1206
+ 'idDocument',
1207
+ 'driversLicense',
1208
+ ]);
1209
+
1210
+ const response = await fastify.injectJson({
1211
+ method: 'POST',
1212
+ url: idUrl(tenant),
1213
+ headers: {
1214
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1215
+ },
1216
+ payload: {
1217
+ jwt_vp: await presentationEmail.sign(
1218
+ holderKid,
1219
+ holderKeys.privateKey,
1220
+ holderDid
1221
+ ),
1222
+ exchange_id: exchange._id,
1223
+ },
1224
+ });
1225
+ expect(response.statusCode).toEqual(200);
1226
+ expect(response.json).toEqual({
1227
+ token: expect.any(String),
1228
+ exchange: {
1229
+ disclosureComplete: true,
1230
+ exchangeComplete: false,
1231
+ id: exchange._id,
1232
+ type: ExchangeTypes.ISSUING,
1233
+ },
1234
+ });
1235
+
1236
+ const dbResult = await mongoDb()
1237
+ .collection('vendorUserIdMappings')
1238
+ .findOne({ vendorUserId });
1239
+ const { sub } = decodeJwt(response.json.token);
1240
+ expect(dbResult).toEqual({
1241
+ _id: new ObjectId(sub),
1242
+ vendorUserId,
1243
+ tenantId: new ObjectId(tenant._id),
1244
+ createdAt: expect.any(Date),
1245
+ updatedAt: expect.any(Date),
1246
+ });
1247
+ const exchangeDBResult = await mongoDb()
1248
+ .collection('exchanges')
1249
+ .findOne({ _id: new ObjectId(exchange._id) });
1250
+ expect(exchangeDBResult).toEqual(
1251
+ mongoify({
1252
+ ...exchange,
1253
+ tenantId: tenant._id,
1254
+ presentationId: presentationEmail.presentation.id,
1255
+ disclosureConsentedAt: expect.any(Date),
1256
+ events: [
1257
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1258
+ {
1259
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1260
+ timestamp: expect.any(Date),
1261
+ },
1262
+ {
1263
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1264
+ timestamp: expect.any(Date),
1265
+ },
1266
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
1267
+ ],
1268
+ offerHashes: [],
1269
+ ...credentialTypesObject,
1270
+ createdAt: expect.any(Date),
1271
+ updatedAt: expect.any(Date),
1272
+ })
1273
+ );
1274
+ });
1275
+ it('given name matching should 401 if user not found', async () => {
1276
+ const noMatchExchange = await persistOfferExchange({
1277
+ tenant,
1278
+ disclosure,
1279
+ identityMatcherValues: ['NotMatchingName'],
1280
+ });
1281
+
1282
+ const presentationEmail = await generateKYCPresentation(
1283
+ noMatchExchange,
1284
+ 'idDocument'
1285
+ );
1286
+
1287
+ const response = await fastify.injectJson({
1288
+ method: 'POST',
1289
+ headers: {
1290
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1291
+ },
1292
+ url: idUrl(tenant),
1293
+ payload: {
1294
+ jwt_vp: await presentationEmail.sign(
1295
+ holderKid,
1296
+ holderKeys.privateKey,
1297
+ holderDid
1298
+ ),
1299
+ exchange_id: noMatchExchange._id,
1300
+ },
1301
+ });
1302
+ expect(response.statusCode).toEqual(401);
1303
+ expect(response.json).toEqual(
1304
+ errorResponseMatcher({
1305
+ error: 'Unauthorized',
1306
+ errorCode: 'integrated_identification_user_not_found',
1307
+ message: 'User Not Found',
1308
+ statusCode: 401,
1309
+ })
1310
+ );
1311
+
1312
+ const exchangeDBResult = await mongoDb()
1313
+ .collection('exchanges')
1314
+ .findOne({ _id: new ObjectId(noMatchExchange._id) });
1315
+ expect(exchangeDBResult).toEqual(
1316
+ mongoify({
1317
+ ...noMatchExchange,
1318
+ tenantId: tenant._id,
1319
+ presentationId: presentationEmail.presentation.id,
1320
+ disclosureConsentedAt: expect.any(Date),
1321
+ err: 'User Not Found',
1322
+ events: [
1323
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1324
+ {
1325
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1326
+ timestamp: expect.any(Date),
1327
+ },
1328
+ {
1329
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1330
+ timestamp: expect.any(Date),
1331
+ },
1332
+ {
1333
+ state: ExchangeStates.NOT_IDENTIFIED,
1334
+ timestamp: expect.any(Date),
1335
+ },
1336
+ ],
1337
+ offerHashes: [],
1338
+ ...credentialTypesObject,
1339
+ createdAt: expect.any(Date),
1340
+ updatedAt: expect.any(Date),
1341
+ })
1342
+ );
1343
+ });
1344
+ });
1345
+
1346
+ describe('associated exchanges test suite', () => {
1347
+ it('should error if rule isnt "all", "pick" or "equal"', async () => {
1348
+ const disclosure = await persistDisclosure({
1349
+ tenant,
1350
+ description: 'Credential Issuance disclosure',
1351
+ types: [{ type: 'EmailV1.0' }],
1352
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
1353
+ purpose: 'Identification',
1354
+ duration: '6y',
1355
+ identityMatchers: {
1356
+ rules: [
1357
+ {
1358
+ valueIndex: 0, // used for identifying the value
1359
+ path: ['$.emails'], // jsonPath within the credential
1360
+ rule: 'notARule', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
1361
+ },
1362
+ ],
1363
+ vendorUserIdIndex: 0,
1364
+ },
1365
+ });
1366
+
1367
+ const exchange = await persistOfferExchange({
1368
+ tenant,
1369
+ disclosure,
1370
+ });
1371
+ await persistOfferExchange({
1372
+ tenant,
1373
+ disclosure,
1374
+ identityMatcherValues: ['adam.smith@example.com'],
1375
+ });
1376
+
1377
+ const presentationEmail = await generateKYCPresentation(
1378
+ exchange,
1379
+ 'email'
1380
+ );
1381
+
1382
+ const response = await fastify.injectJson({
1383
+ method: 'POST',
1384
+ headers: {
1385
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1386
+ },
1387
+ url: idUrl(tenant),
1388
+ payload: {
1389
+ jwt_vp: await presentationEmail.sign(
1390
+ holderKid,
1391
+ holderKeys.privateKey,
1392
+ holderDid
1393
+ ),
1394
+ exchange_id: exchange._id,
1395
+ },
1396
+ });
1397
+ expect(response.statusCode).toEqual(500);
1398
+ expect(response.json).toEqual(
1399
+ errorResponseMatcher({
1400
+ error: 'Internal Server Error',
1401
+ message:
1402
+ 'Credential Agent only supports "pick" or "all" for "identityMatchers.rule"',
1403
+ statusCode: 500,
1404
+ errorCode: 'missing_error_code',
1405
+ })
1406
+ );
1407
+ });
1408
+
1409
+ it('should match with email based matching and "pick" rule', async () => {
1410
+ const disclosure = await persistDisclosure({
1411
+ tenant,
1412
+ description: 'Credential Issuance disclosure',
1413
+ types: [{ type: 'EmailV1.0' }],
1414
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
1415
+ purpose: 'Identification',
1416
+ duration: '6y',
1417
+ identityMatchers: {
1418
+ rules: [
1419
+ {
1420
+ valueIndex: 0, // used for identifying the value
1421
+ path: ['$.emails'], // jsonPath within the credential
1422
+ rule: 'pick', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
1423
+ },
1424
+ ],
1425
+ vendorUserIdIndex: 0,
1426
+ },
1427
+ });
1428
+ const exchangeWithoutIdentityMatcherValues = await persistOfferExchange(
1429
+ {
1430
+ tenant,
1431
+ disclosure,
1432
+ }
1433
+ );
1434
+
1435
+ await persistOfferExchange({
1436
+ tenant,
1437
+ disclosure,
1438
+ identityMatcherValues: ['not-match.olivia@example.com'],
1439
+ });
1440
+ await persistOfferExchange({
1441
+ tenant,
1442
+ disclosure,
1443
+ identityMatcherValues: ['Adam.smith@example.com'],
1444
+ });
1445
+ const vendorUserId = 'Adam.smith@example.com';
1446
+
1447
+ const presentationEmail = await generateKYCPresentation(
1448
+ exchangeWithoutIdentityMatcherValues,
1449
+ 'email'
1450
+ );
1451
+
1452
+ const response = await fastify.injectJson({
1453
+ method: 'POST',
1454
+ headers: {
1455
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1456
+ },
1457
+ url: idUrl(tenant),
1458
+ payload: {
1459
+ jwt_vp: await presentationEmail.sign(
1460
+ holderKid,
1461
+ holderKeys.privateKey,
1462
+ holderDid
1463
+ ),
1464
+ exchange_id: exchangeWithoutIdentityMatcherValues._id,
1465
+ },
1466
+ });
1467
+ expect(response.statusCode).toEqual(200);
1468
+ expect(response.json).toEqual({
1469
+ token: expect.any(String),
1470
+ exchange: {
1471
+ disclosureComplete: true,
1472
+ exchangeComplete: false,
1473
+ id: exchangeWithoutIdentityMatcherValues._id,
1474
+ type: ExchangeTypes.ISSUING,
1475
+ },
1476
+ });
1477
+
1478
+ const dbResult = await mongoDb()
1479
+ .collection('vendorUserIdMappings')
1480
+ .findOne({ vendorUserId });
1481
+ const { sub } = decodeJwt(response.json.token);
1482
+ expect(dbResult).toEqual({
1483
+ _id: new ObjectId(sub),
1484
+ vendorUserId,
1485
+ tenantId: new ObjectId(tenant._id),
1486
+ createdAt: expect.any(Date),
1487
+ updatedAt: expect.any(Date),
1488
+ });
1489
+ const exchangeDBResult = await mongoDb()
1490
+ .collection('exchanges')
1491
+ .findOne({
1492
+ _id: new ObjectId(exchangeWithoutIdentityMatcherValues._id),
1493
+ });
1494
+ expect(exchangeDBResult).toEqual(
1495
+ mongoify({
1496
+ ...exchangeWithoutIdentityMatcherValues,
1497
+ tenantId: tenant._id,
1498
+ presentationId: presentationEmail.presentation.id,
1499
+ disclosureConsentedAt: expect.any(Date),
1500
+ events: [
1501
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
1502
+ {
1503
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
1504
+ timestamp: expect.any(Date),
1505
+ },
1506
+ {
1507
+ state: ExchangeStates.DISCLOSURE_CHECKED,
1508
+ timestamp: expect.any(Date),
1509
+ },
1510
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
1511
+ ],
1512
+ offerHashes: [],
1513
+ ...credentialTypesObject,
1514
+ createdAt: expect.any(Date),
1515
+ updatedAt: expect.any(Date),
1516
+ })
1517
+ );
1518
+ });
1519
+ });
1520
+ });
1521
+
1522
+ describe('using the webhook', () => {
1523
+ let exchange;
1524
+ let disclosure;
1525
+ let vendorUserId;
1526
+
1527
+ beforeEach(async () => {
1528
+ fastify.resetOverrides();
1529
+ disclosure = await persistDisclosure({
1530
+ tenant,
1531
+ description: 'Credential Issuance disclosure',
1532
+ types: [{ type: 'EmailV1.0' }],
1533
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
1534
+ purpose: 'Identification',
1535
+ duration: '6y',
1536
+ });
1537
+ ({ vendorUserId } = await newVendorUserIdMapping());
1538
+ exchange = await persistOfferExchange({ tenant, disclosure });
1539
+ });
1540
+
1541
+ it('should 200 when the presentation is empty but the vendorOriginContext is sent', async () => {
1542
+ const preauthDisclosure = await persistDisclosure({
1543
+ tenant,
1544
+ description: 'Credential Issuance disclosure',
1545
+ identificationMethods: ['preauth'],
1546
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
1547
+ purpose: 'Identification',
1548
+ duration: '6y',
1549
+ });
1550
+ const emptyDisclosureExchange = await persistOfferExchange({
1551
+ tenant,
1552
+ disclosure: preauthDisclosure,
1553
+ });
1554
+ const presentationIdDocument = await generateKYCPresentation(
1555
+ emptyDisclosureExchange,
1556
+ 'nothing'
1557
+ );
1558
+ presentationIdDocument.presentation.vendorOriginContext = '123';
1559
+
1560
+ let identifyWebhookPayload;
1561
+ nock(mockVendorUrl)
1562
+ .post(identifyUserOnVendorEndpoint)
1563
+ .reply(200, (uri, body) => {
1564
+ identifyWebhookPayload = body;
1565
+ return {
1566
+ vendorUserId,
1567
+ };
1568
+ });
1569
+
1570
+ const response = await fastify.injectJson({
1571
+ method: 'POST',
1572
+ url: idUrl(tenant),
1573
+ headers: {
1574
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1575
+ },
1576
+ payload: {
1577
+ exchange_id: emptyDisclosureExchange._id,
1578
+ jwt_vp: await presentationIdDocument.sign(
1579
+ holderKid,
1580
+ holderKeys.privateKey,
1581
+ holderDid
1582
+ ),
1583
+ },
1584
+ });
1585
+
1586
+ expect(response.statusCode).toEqual(200);
1587
+ expect(response.json).toEqual({
1588
+ token: expect.any(String),
1589
+ exchange: {
1590
+ disclosureComplete: true,
1591
+ exchangeComplete: false,
1592
+ id: emptyDisclosureExchange._id,
1593
+ type: ExchangeTypes.ISSUING,
1594
+ },
1595
+ });
1596
+ const dbResult = await mongoDb()
1597
+ .collection('vendorUserIdMappings')
1598
+ .findOne({ vendorUserId });
1599
+ const { sub } = decodeJwt(response.json.token);
1600
+ expect(dbResult).toEqual({
1601
+ _id: new ObjectId(sub),
1602
+ vendorUserId,
1603
+ tenantId: new ObjectId(tenant._id),
1604
+ createdAt: expect.any(Date),
1605
+ updatedAt: expect.any(Date),
1606
+ });
1607
+ expect(
1608
+ await mongoDb()
1609
+ .collection('exchanges')
1610
+ .findOne({ _id: new ObjectId(emptyDisclosureExchange._id) })
1611
+ ).toEqual(
1612
+ expectedExchange(
1613
+ tenant,
1614
+ emptyDisclosureExchange,
1615
+ presentationIdDocument,
1616
+ [
1617
+ ExchangeStates.NEW,
1618
+ ExchangeStates.DISCLOSURE_RECEIVED,
1619
+ ExchangeStates.DISCLOSURE_CHECKED,
1620
+ ExchangeStates.IDENTIFIED,
1621
+ ]
1622
+ )
1623
+ );
1624
+ expect(identifyWebhookPayload).toEqual({
1625
+ emailCredentials: [],
1626
+ emails: [],
1627
+ exchangeId: emptyDisclosureExchange._id,
1628
+ idDocumentCredentials: [],
1629
+ phoneCredentials: [],
1630
+ phones: [],
1631
+ vendorOriginContext: '123',
1632
+ tenantDID: tenant.did,
1633
+ tenantId: tenant._id,
1634
+ });
1635
+ });
1636
+
1637
+ it('should 200 when sub is new', async () => {
1638
+ const presentationIdDocument = await generateKYCPresentation(
1639
+ exchange,
1640
+ 'idDocument'
1641
+ );
1642
+
1643
+ let identifyWebhookPayload;
1644
+ nock(mockVendorUrl)
1645
+ .post(identifyUserOnVendorEndpoint)
1646
+ .reply(200, (uri, body) => {
1647
+ identifyWebhookPayload = body;
1648
+ return {
1649
+ vendorUserId,
1650
+ };
1651
+ });
1652
+
1653
+ const response = await fastify.injectJson({
1654
+ method: 'POST',
1655
+ url: idUrl(tenant),
1656
+ headers: {
1657
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1658
+ },
1659
+ payload: {
1660
+ exchange_id: exchange._id,
1661
+ jwt_vp: await presentationIdDocument.sign(
1662
+ holderKid,
1663
+ holderKeys.privateKey,
1664
+ holderDid
1665
+ ),
1666
+ },
1667
+ });
1668
+
1669
+ expect(response.statusCode).toEqual(200);
1670
+ expect(response.json).toEqual({
1671
+ token: expect.any(String),
1672
+ exchange: {
1673
+ disclosureComplete: true,
1674
+ exchangeComplete: false,
1675
+ id: exchange._id,
1676
+ type: ExchangeTypes.ISSUING,
1677
+ },
1678
+ });
1679
+ const dbResult = await mongoDb()
1680
+ .collection('vendorUserIdMappings')
1681
+ .findOne({ vendorUserId });
1682
+ const { sub } = decodeJwt(response.json.token);
1683
+ expect(dbResult).toEqual({
1684
+ _id: new ObjectId(sub),
1685
+ vendorUserId,
1686
+ tenantId: new ObjectId(tenant._id),
1687
+ createdAt: expect.any(Date),
1688
+ updatedAt: expect.any(Date),
1689
+ });
1690
+ expect(
1691
+ await mongoDb()
1692
+ .collection('exchanges')
1693
+ .findOne({ _id: new ObjectId(exchange._id) })
1694
+ ).toEqual(
1695
+ expectedExchange(tenant, exchange, presentationIdDocument, [
1696
+ ExchangeStates.NEW,
1697
+ ExchangeStates.DISCLOSURE_RECEIVED,
1698
+ ExchangeStates.DISCLOSURE_CHECKED,
1699
+ ExchangeStates.IDENTIFIED,
1700
+ ])
1701
+ );
1702
+ expect(identifyWebhookPayload).toEqual({
1703
+ emailCredentials: [],
1704
+ emails: [],
1705
+ exchangeId: exchange._id,
1706
+ idDocumentCredentials: [
1707
+ {
1708
+ id: idDocPayload.vc.id,
1709
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
1710
+ credentialSubject: idDocPayload.vc.credentialSubject,
1711
+ credentialType: 'IdDocumentV1.0',
1712
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
1713
+ issuer: {
1714
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
1715
+ },
1716
+ validFrom: '2017-09-01',
1717
+ validUntil: '2021-09-01',
1718
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
1719
+ },
1720
+ ],
1721
+ phoneCredentials: [],
1722
+ phones: [],
1723
+ tenantDID: tenant.did,
1724
+ tenantId: tenant._id,
1725
+ });
1726
+ });
1727
+
1728
+ it('should 200 when tenant has a custom webhook', async () => {
1729
+ const webhookUrl = 'http://cutomUrl.com';
1730
+ const customTenant = await persistTenant({
1731
+ webhookUrl,
1732
+ });
1733
+
1734
+ const keyPair = generateKeyPair({ format: 'jwk' });
1735
+
1736
+ await persistKey({
1737
+ tenant: customTenant,
1738
+ kidFragment: '#ID1',
1739
+ keyPair,
1740
+ });
1741
+
1742
+ const customDisclosure = await persistDisclosure({
1743
+ tenant: customTenant,
1744
+ description: 'Credential Issuance disclosure',
1745
+ types: [{ type: 'EmailV1.0' }],
1746
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
1747
+ purpose: 'Identification',
1748
+ duration: '6y',
1749
+ });
1750
+
1751
+ const customExchange = await persistOfferExchange({
1752
+ tenant: customTenant,
1753
+ disclosure: customDisclosure,
1754
+ });
1755
+
1756
+ const presentationIdDocument = await generateKYCPresentation(
1757
+ customExchange,
1758
+ 'idDocument'
1759
+ );
1760
+
1761
+ let identifyWebhookPayload;
1762
+ const nockWebhook = await nock(webhookUrl)
1763
+ .post(identifyUserOnVendorEndpoint)
1764
+ .reply(200, (uri, body) => {
1765
+ identifyWebhookPayload = body;
1766
+ return {
1767
+ vendorUserId,
1768
+ };
1769
+ });
1770
+
1771
+ const response = await fastify.injectJson({
1772
+ method: 'POST',
1773
+ url: idUrl(customTenant),
1774
+ headers: {
1775
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1776
+ },
1777
+ payload: {
1778
+ exchange_id: customExchange._id,
1779
+ jwt_vp: await presentationIdDocument.sign(
1780
+ holderKid,
1781
+ holderKeys.privateKey,
1782
+ holderDid
1783
+ ),
1784
+ },
1785
+ });
1786
+
1787
+ expect(response.statusCode).toEqual(200);
1788
+ expect(response.json).toEqual({
1789
+ token: expect.any(String),
1790
+ exchange: {
1791
+ disclosureComplete: true,
1792
+ exchangeComplete: false,
1793
+ id: customExchange._id,
1794
+ type: ExchangeTypes.ISSUING,
1795
+ },
1796
+ });
1797
+ const dbResult = await mongoDb()
1798
+ .collection('vendorUserIdMappings')
1799
+ .findOne({ vendorUserId });
1800
+ const { sub } = decodeJwt(response.json.token);
1801
+ expect(dbResult).toEqual({
1802
+ _id: new ObjectId(sub),
1803
+ vendorUserId,
1804
+ tenantId: new ObjectId(customTenant._id),
1805
+ createdAt: expect.any(Date),
1806
+ updatedAt: expect.any(Date),
1807
+ });
1808
+ expect(
1809
+ await mongoDb()
1810
+ .collection('exchanges')
1811
+ .findOne({ _id: new ObjectId(customExchange._id) })
1812
+ ).toEqual(
1813
+ expectedExchange(customTenant, customExchange, presentationIdDocument, [
1814
+ ExchangeStates.NEW,
1815
+ ExchangeStates.DISCLOSURE_RECEIVED,
1816
+ ExchangeStates.DISCLOSURE_CHECKED,
1817
+ ExchangeStates.IDENTIFIED,
1818
+ ])
1819
+ );
1820
+ expect(nockWebhook.isDone()).toEqual(true);
1821
+ expect(identifyWebhookPayload).toEqual({
1822
+ emailCredentials: [],
1823
+ emails: [],
1824
+ exchangeId: customExchange._id,
1825
+ idDocumentCredentials: [
1826
+ {
1827
+ id: idDocPayload.vc.id,
1828
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
1829
+ credentialSubject: idDocPayload.vc.credentialSubject,
1830
+ credentialType: 'IdDocumentV1.0',
1831
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
1832
+ issuer: {
1833
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
1834
+ },
1835
+ validFrom: '2017-09-01',
1836
+ validUntil: '2021-09-01',
1837
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
1838
+ },
1839
+ ],
1840
+ phoneCredentials: [],
1841
+ phones: [],
1842
+ tenantDID: customTenant.did,
1843
+ tenantId: customTenant._id,
1844
+ });
1845
+ });
1846
+
1847
+ it('should 200 when tenant has a custom webhook with custom webhookAuth', async () => {
1848
+ const webhookUrl = 'http://cutomUrl.com';
1849
+ const customTenant = await persistTenant({
1850
+ webhookUrl,
1851
+ webhookAuth: {
1852
+ type: 'bearer',
1853
+ bearerToken: 'secret',
1854
+ },
1855
+ });
1856
+
1857
+ const keyPair = generateKeyPair({ format: 'jwk' });
1858
+
1859
+ await persistKey({
1860
+ tenant: customTenant,
1861
+ kidFragment: '#ID1',
1862
+ keyPair,
1863
+ });
1864
+
1865
+ const customDisclosure = await persistDisclosure({
1866
+ tenant: customTenant,
1867
+ description: 'Credential Issuance disclosure',
1868
+ types: [{ type: 'EmailV1.0' }],
1869
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
1870
+ purpose: 'Identification',
1871
+ duration: '6y',
1872
+ });
1873
+
1874
+ const customExchange = await persistOfferExchange({
1875
+ tenant: customTenant,
1876
+ disclosure: customDisclosure,
1877
+ });
1878
+
1879
+ const presentationIdDocument = await generateKYCPresentation(
1880
+ customExchange,
1881
+ 'idDocument'
1882
+ );
1883
+
1884
+ let identifyWebhookPayload;
1885
+ let identityWebhookHeaders;
1886
+ const nockWebhook = await nock(webhookUrl)
1887
+ .post(identifyUserOnVendorEndpoint)
1888
+ // eslint-disable-next-line prefer-arrow-functions/prefer-arrow-functions
1889
+ .reply(200, function namedFn(uri, body) {
1890
+ identityWebhookHeaders = this.req.headers;
1891
+ identifyWebhookPayload = body;
1892
+ return {
1893
+ vendorUserId,
1894
+ };
1895
+ });
1896
+
1897
+ const response = await fastify.injectJson({
1898
+ method: 'POST',
1899
+ url: idUrl(customTenant),
1900
+ headers: {
1901
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1902
+ },
1903
+ payload: {
1904
+ exchange_id: customExchange._id,
1905
+ jwt_vp: await presentationIdDocument.sign(
1906
+ holderKid,
1907
+ holderKeys.privateKey,
1908
+ holderDid
1909
+ ),
1910
+ },
1911
+ });
1912
+
1913
+ expect(response.statusCode).toEqual(200);
1914
+
1915
+ expect(nockWebhook.isDone()).toEqual(true);
1916
+ expect(identifyWebhookPayload).toEqual({
1917
+ emailCredentials: [],
1918
+ emails: [],
1919
+ exchangeId: customExchange._id,
1920
+ idDocumentCredentials: [
1921
+ {
1922
+ id: idDocPayload.vc.id,
1923
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
1924
+ credentialSubject: idDocPayload.vc.credentialSubject,
1925
+ credentialType: 'IdDocumentV1.0',
1926
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
1927
+ issuer: {
1928
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
1929
+ },
1930
+ validFrom: '2017-09-01',
1931
+ validUntil: '2021-09-01',
1932
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
1933
+ },
1934
+ ],
1935
+ phoneCredentials: [],
1936
+ phones: [],
1937
+ tenantDID: customTenant.did,
1938
+ tenantId: customTenant._id,
1939
+ });
1940
+ expect(identityWebhookHeaders.authorization).toEqual('Bearer secret');
1941
+ });
1942
+
1943
+ it('should 200 when identifying email and a coupon was not provided', async () => {
1944
+ const presentationEmail = await generateKYCPresentation(
1945
+ exchange,
1946
+ 'email'
1947
+ );
1948
+ metadataRegistration.initVerificationCoupon.mockImplementation(() => ({
1949
+ getCoupon: () => Promise.resolve(null),
1950
+ }));
1951
+
1952
+ let identifyWebhookPayload;
1953
+ nock(mockVendorUrl)
1954
+ .post(identifyUserOnVendorEndpoint)
1955
+ .reply(200, (uri, body) => {
1956
+ identifyWebhookPayload = body;
1957
+ return {
1958
+ vendorUserId,
1959
+ };
1960
+ });
1961
+
1962
+ const response = await fastify.injectJson({
1963
+ method: 'POST',
1964
+ url: idUrl(tenant),
1965
+ headers: {
1966
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
1967
+ },
1968
+ payload: {
1969
+ jwt_vp: await presentationEmail.sign(
1970
+ holderKid,
1971
+ holderKeys.privateKey,
1972
+ holderDid
1973
+ ),
1974
+ exchange_id: exchange._id,
1975
+ },
1976
+ });
1977
+
1978
+ expect(response.statusCode).toEqual(200);
1979
+ expect(response.json).toEqual({
1980
+ token: expect.any(String),
1981
+ exchange: {
1982
+ disclosureComplete: true,
1983
+ exchangeComplete: false,
1984
+ id: exchange._id,
1985
+ type: ExchangeTypes.ISSUING,
1986
+ },
1987
+ });
1988
+ const dbResult = await mongoDb()
1989
+ .collection('vendorUserIdMappings')
1990
+ .findOne({ vendorUserId });
1991
+ const { sub } = decodeJwt(response.json.token);
1992
+ expect(dbResult).toEqual({
1993
+ _id: new ObjectId(sub),
1994
+ vendorUserId,
1995
+ tenantId: new ObjectId(tenant._id),
1996
+ createdAt: expect.any(Date),
1997
+ updatedAt: expect.any(Date),
1998
+ });
1999
+ expect(
2000
+ await mongoDb()
2001
+ .collection('exchanges')
2002
+ .findOne({ _id: new ObjectId(exchange._id) })
2003
+ ).toEqual(
2004
+ expectedExchange(tenant, exchange, presentationEmail, [
2005
+ ExchangeStates.NEW,
2006
+ ExchangeStates.DISCLOSURE_RECEIVED,
2007
+ ExchangeStates.DISCLOSURE_CHECKED,
2008
+ ExchangeStates.IDENTIFIED,
2009
+ ])
2010
+ );
2011
+ expect(identifyWebhookPayload).toEqual({
2012
+ emailCredentials: [
2013
+ {
2014
+ id: emailPayload.vc.id,
2015
+ type: ['EmailV1.0', 'VerifiableCredential'],
2016
+ credentialSubject: emailPayload.vc.credentialSubject,
2017
+ credentialType: 'EmailV1.0',
2018
+ issuer: {
2019
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2020
+ },
2021
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2022
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2023
+ },
2024
+ ],
2025
+ idDocumentCredentials: [],
2026
+ phoneCredentials: [],
2027
+ emails: [emailPayload.vc.credentialSubject.email],
2028
+ phones: [],
2029
+ vendorOrganizationId: tenant.vendorOrganizationId,
2030
+ tenantDID: tenant.did,
2031
+ tenantId: tenant._id,
2032
+ exchangeId: exchange._id,
2033
+ });
2034
+ metadataRegistration.initVerificationCoupon.mockImplementation(() => ({
2035
+ getCoupon: () => Promise.resolve(42),
2036
+ }));
2037
+ });
2038
+ it('should 200 when identifying email', async () => {
2039
+ const presentationEmail = await generateKYCPresentation(
2040
+ exchange,
2041
+ 'email'
2042
+ );
2043
+
2044
+ let identifyWebhookPayload;
2045
+ nock(mockVendorUrl)
2046
+ .post(identifyUserOnVendorEndpoint)
2047
+ .reply(200, (uri, body) => {
2048
+ identifyWebhookPayload = body;
2049
+ return {
2050
+ vendorUserId,
2051
+ };
2052
+ });
2053
+
2054
+ const response = await fastify.injectJson({
2055
+ method: 'POST',
2056
+ url: idUrl(tenant),
2057
+ headers: {
2058
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2059
+ },
2060
+ payload: {
2061
+ jwt_vp: await presentationEmail.sign(
2062
+ holderKid,
2063
+ holderKeys.privateKey,
2064
+ holderDid
2065
+ ),
2066
+ exchange_id: exchange._id,
2067
+ },
2068
+ });
2069
+
2070
+ expect(response.statusCode).toEqual(200);
2071
+ expect(response.json).toEqual({
2072
+ token: expect.any(String),
2073
+ exchange: {
2074
+ disclosureComplete: true,
2075
+ exchangeComplete: false,
2076
+ id: exchange._id,
2077
+ type: ExchangeTypes.ISSUING,
2078
+ },
2079
+ });
2080
+ const dbResult = await mongoDb()
2081
+ .collection('vendorUserIdMappings')
2082
+ .findOne({ vendorUserId });
2083
+ const { sub } = decodeJwt(response.json.token);
2084
+ expect(dbResult).toEqual({
2085
+ _id: new ObjectId(sub),
2086
+ vendorUserId,
2087
+ tenantId: new ObjectId(tenant._id),
2088
+ createdAt: expect.any(Date),
2089
+ updatedAt: expect.any(Date),
2090
+ });
2091
+ expect(
2092
+ await mongoDb()
2093
+ .collection('exchanges')
2094
+ .findOne({ _id: new ObjectId(exchange._id) })
2095
+ ).toEqual(
2096
+ expectedExchange(tenant, exchange, presentationEmail, [
2097
+ ExchangeStates.NEW,
2098
+ ExchangeStates.DISCLOSURE_RECEIVED,
2099
+ ExchangeStates.DISCLOSURE_CHECKED,
2100
+ ExchangeStates.IDENTIFIED,
2101
+ ])
2102
+ );
2103
+ expect(identifyWebhookPayload).toEqual({
2104
+ emailCredentials: [
2105
+ {
2106
+ id: emailPayload.vc.id,
2107
+ type: ['EmailV1.0', 'VerifiableCredential'],
2108
+ credentialSubject: emailPayload.vc.credentialSubject,
2109
+ credentialType: 'EmailV1.0',
2110
+ issuer: {
2111
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2112
+ },
2113
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2114
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2115
+ },
2116
+ ],
2117
+ idDocumentCredentials: [],
2118
+ phoneCredentials: [],
2119
+ emails: [emailPayload.vc.credentialSubject.email],
2120
+ phones: [],
2121
+ tenantDID: tenant.did,
2122
+ tenantId: tenant._id,
2123
+ exchangeId: exchange._id,
2124
+ });
2125
+ });
2126
+
2127
+ it('should 200 when identifying whatever with logo and email', async () => {
2128
+ const presentationEmail = await generateKYCPresentation(
2129
+ exchange,
2130
+ 'whateverWithLogoName'
2131
+ );
2132
+
2133
+ let identifyWebhookPayload;
2134
+ nock(mockVendorUrl)
2135
+ .post(identifyUserOnVendorEndpoint)
2136
+ .reply(200, (uri, body) => {
2137
+ identifyWebhookPayload = body;
2138
+ return {
2139
+ vendorUserId,
2140
+ };
2141
+ });
2142
+
2143
+ const response = await fastify.injectJson({
2144
+ method: 'POST',
2145
+ url: idUrl(tenant),
2146
+ headers: {
2147
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2148
+ },
2149
+ payload: {
2150
+ jwt_vp: await presentationEmail.sign(
2151
+ holderKid,
2152
+ holderKeys.privateKey,
2153
+ holderDid
2154
+ ),
2155
+ exchange_id: exchange._id,
2156
+ },
2157
+ });
2158
+
2159
+ expect(response.statusCode).toEqual(200);
2160
+ expect(response.json).toEqual({
2161
+ token: expect.any(String),
2162
+ exchange: {
2163
+ disclosureComplete: true,
2164
+ exchangeComplete: false,
2165
+ id: exchange._id,
2166
+ type: ExchangeTypes.ISSUING,
2167
+ },
2168
+ });
2169
+ const dbResult = await mongoDb()
2170
+ .collection('vendorUserIdMappings')
2171
+ .findOne({ vendorUserId });
2172
+ const { sub } = decodeJwt(response.json.token);
2173
+ expect(dbResult).toEqual({
2174
+ _id: new ObjectId(sub),
2175
+ vendorUserId,
2176
+ tenantId: new ObjectId(tenant._id),
2177
+ createdAt: expect.any(Date),
2178
+ updatedAt: expect.any(Date),
2179
+ });
2180
+ expect(
2181
+ await mongoDb()
2182
+ .collection('exchanges')
2183
+ .findOne({ _id: new ObjectId(exchange._id) })
2184
+ ).toEqual(
2185
+ expectedExchange(tenant, exchange, presentationEmail, [
2186
+ ExchangeStates.NEW,
2187
+ ExchangeStates.DISCLOSURE_RECEIVED,
2188
+ ExchangeStates.DISCLOSURE_CHECKED,
2189
+ ExchangeStates.IDENTIFIED,
2190
+ ])
2191
+ );
2192
+ expect(identifyWebhookPayload).toEqual({
2193
+ emailCredentials: [
2194
+ {
2195
+ id: emailPayload.vc.id,
2196
+ type: ['EmailV1.0', 'VerifiableCredential'],
2197
+ credentialSubject: emailPayload.vc.credentialSubject,
2198
+ credentialType: 'EmailV1.0',
2199
+ issuer: {
2200
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2201
+ image: 'https://example.com/image.png',
2202
+ name: 'Whatever',
2203
+ },
2204
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2205
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2206
+ },
2207
+ ],
2208
+ idDocumentCredentials: [],
2209
+ phoneCredentials: [],
2210
+ emails: [emailPayload.vc.credentialSubject.email],
2211
+ phones: [],
2212
+ tenantDID: tenant.did,
2213
+ tenantId: tenant._id,
2214
+ exchangeId: exchange._id,
2215
+ });
2216
+ });
2217
+
2218
+ it('should 200 when identifying legacy email', async () => {
2219
+ const presentationEmail = await generateKYCPresentation(
2220
+ exchange,
2221
+ 'legacyEmail'
2222
+ );
2223
+
2224
+ let identifyWebhookPayload;
2225
+ nock(mockVendorUrl)
2226
+ .post(identifyUserOnVendorEndpoint)
2227
+ .reply(200, (uri, body) => {
2228
+ identifyWebhookPayload = body;
2229
+ return {
2230
+ vendorUserId,
2231
+ };
2232
+ });
2233
+
2234
+ const response = await fastify.injectJson({
2235
+ method: 'POST',
2236
+ url: idUrl(tenant),
2237
+ headers: {
2238
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2239
+ },
2240
+ payload: {
2241
+ jwt_vp: await presentationEmail.sign(
2242
+ holderKid,
2243
+ holderKeys.privateKey,
2244
+ holderDid
2245
+ ),
2246
+ exchange_id: exchange._id,
2247
+ },
2248
+ });
2249
+
2250
+ expect(response.statusCode).toEqual(200);
2251
+ expect(response.json).toEqual({
2252
+ token: expect.any(String),
2253
+ exchange: {
2254
+ disclosureComplete: true,
2255
+ exchangeComplete: false,
2256
+ id: exchange._id,
2257
+ type: ExchangeTypes.ISSUING,
2258
+ },
2259
+ });
2260
+ const dbResult = await mongoDb()
2261
+ .collection('vendorUserIdMappings')
2262
+ .findOne({ vendorUserId });
2263
+ const { sub } = decodeJwt(response.json.token);
2264
+ expect(dbResult).toEqual({
2265
+ _id: new ObjectId(sub),
2266
+ vendorUserId,
2267
+ tenantId: new ObjectId(tenant._id),
2268
+ createdAt: expect.any(Date),
2269
+ updatedAt: expect.any(Date),
2270
+ });
2271
+ expect(
2272
+ await mongoDb()
2273
+ .collection('exchanges')
2274
+ .findOne({ _id: new ObjectId(exchange._id) })
2275
+ ).toEqual(
2276
+ expectedExchange(tenant, exchange, presentationEmail, [
2277
+ ExchangeStates.NEW,
2278
+ ExchangeStates.DISCLOSURE_RECEIVED,
2279
+ ExchangeStates.DISCLOSURE_CHECKED,
2280
+ ExchangeStates.IDENTIFIED,
2281
+ ])
2282
+ );
2283
+ expect(identifyWebhookPayload).toEqual({
2284
+ emailCredentials: [
2285
+ {
2286
+ id: emailPayload.vc.id,
2287
+ type: ['Email', 'VerifiableCredential'],
2288
+ credentialSubject: emailPayload.vc.credentialSubject,
2289
+ credentialType: 'Email',
2290
+ issuer: {
2291
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2292
+ },
2293
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2294
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2295
+ },
2296
+ ],
2297
+ idDocumentCredentials: [],
2298
+ phoneCredentials: [],
2299
+ emails: [emailPayload.vc.credentialSubject.email],
2300
+ phones: [],
2301
+ tenantDID: tenant.did,
2302
+ tenantId: tenant._id,
2303
+ exchangeId: exchange._id,
2304
+ });
2305
+ });
2306
+
2307
+ it('should 200 when identifying phone', async () => {
2308
+ const presentationPhone = await generateKYCPresentation(
2309
+ exchange,
2310
+ 'phone'
2311
+ );
2312
+
2313
+ let identifyWebhookPayload;
2314
+ nock(mockVendorUrl)
2315
+ .post(identifyUserOnVendorEndpoint)
2316
+ .reply(200, (uri, body) => {
2317
+ identifyWebhookPayload = body;
2318
+ return {
2319
+ vendorUserId,
2320
+ };
2321
+ });
2322
+
2323
+ const response = await fastify.injectJson({
2324
+ method: 'POST',
2325
+ url: idUrl(tenant),
2326
+ headers: {
2327
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2328
+ },
2329
+ payload: {
2330
+ jwt_vp: await presentationPhone.sign(
2331
+ holderKid,
2332
+ holderKeys.privateKey,
2333
+ holderDid
2334
+ ),
2335
+ exchange_id: exchange._id,
2336
+ },
2337
+ });
2338
+
2339
+ expect(response.statusCode).toEqual(200);
2340
+ expect(response.json).toEqual({
2341
+ token: expect.any(String),
2342
+ exchange: {
2343
+ disclosureComplete: true,
2344
+ exchangeComplete: false,
2345
+ id: exchange._id,
2346
+ type: ExchangeTypes.ISSUING,
2347
+ },
2348
+ });
2349
+ const dbResult = await mongoDb()
2350
+ .collection('vendorUserIdMappings')
2351
+ .findOne({ vendorUserId });
2352
+ const { sub } = decodeJwt(response.json.token);
2353
+ expect(dbResult).toEqual({
2354
+ _id: new ObjectId(sub),
2355
+ vendorUserId,
2356
+ tenantId: new ObjectId(tenant._id),
2357
+ createdAt: expect.any(Date),
2358
+ updatedAt: expect.any(Date),
2359
+ });
2360
+ expect(
2361
+ await mongoDb()
2362
+ .collection('exchanges')
2363
+ .findOne({ _id: new ObjectId(exchange._id) })
2364
+ ).toEqual(
2365
+ expectedExchange(tenant, exchange, presentationPhone, [
2366
+ ExchangeStates.NEW,
2367
+ ExchangeStates.DISCLOSURE_RECEIVED,
2368
+ ExchangeStates.DISCLOSURE_CHECKED,
2369
+ ExchangeStates.IDENTIFIED,
2370
+ ])
2371
+ );
2372
+ expect(identifyWebhookPayload).toEqual({
2373
+ phoneCredentials: [
2374
+ {
2375
+ id: phonePayload.vc.id,
2376
+ type: ['PhoneV1.0', 'VerifiableCredential'],
2377
+ credentialSubject: phonePayload.vc.credentialSubject,
2378
+ credentialType: 'PhoneV1.0',
2379
+ issuer: {
2380
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2381
+ },
2382
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2383
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2384
+ },
2385
+ ],
2386
+ idDocumentCredentials: [],
2387
+ emailCredentials: [],
2388
+ emails: [],
2389
+ phones: [phonePayload.vc.credentialSubject.phone],
2390
+ tenantDID: tenant.did,
2391
+ tenantId: tenant._id,
2392
+ exchangeId: exchange._id,
2393
+ });
2394
+ });
2395
+
2396
+ it('should 200 when identifying VerificationIdentifier', async () => {
2397
+ const presentationVerificationIdentifier = await generateKYCPresentation(
2398
+ exchange,
2399
+ 'verificationIdentifier'
2400
+ );
2401
+
2402
+ let identifyWebhookPayload;
2403
+ nock(mockVendorUrl)
2404
+ .post(identifyUserOnVendorEndpoint)
2405
+ .reply(200, (uri, body) => {
2406
+ identifyWebhookPayload = body;
2407
+ return {
2408
+ vendorUserId,
2409
+ };
2410
+ });
2411
+
2412
+ const response = await fastify.injectJson({
2413
+ method: 'POST',
2414
+ url: idUrl(tenant),
2415
+ headers: {
2416
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2417
+ },
2418
+ payload: {
2419
+ jwt_vp: await presentationVerificationIdentifier.sign(
2420
+ holderKid,
2421
+ holderKeys.privateKey,
2422
+ holderDid
2423
+ ),
2424
+ exchange_id: exchange._id,
2425
+ },
2426
+ });
2427
+
2428
+ expect(response.statusCode).toEqual(200);
2429
+ expect(response.json).toEqual({
2430
+ token: expect.any(String),
2431
+ exchange: {
2432
+ disclosureComplete: true,
2433
+ exchangeComplete: false,
2434
+ id: exchange._id,
2435
+ type: ExchangeTypes.ISSUING,
2436
+ },
2437
+ });
2438
+ const dbResult = await mongoDb()
2439
+ .collection('vendorUserIdMappings')
2440
+ .findOne({ vendorUserId });
2441
+ const { sub } = decodeJwt(response.json.token);
2442
+ expect(dbResult).toEqual({
2443
+ _id: new ObjectId(sub),
2444
+ vendorUserId,
2445
+ tenantId: new ObjectId(tenant._id),
2446
+ createdAt: expect.any(Date),
2447
+ updatedAt: expect.any(Date),
2448
+ });
2449
+ expect(
2450
+ await mongoDb()
2451
+ .collection('exchanges')
2452
+ .findOne({ _id: new ObjectId(exchange._id) })
2453
+ ).toEqual(
2454
+ expectedExchange(tenant, exchange, presentationVerificationIdentifier, [
2455
+ ExchangeStates.NEW,
2456
+ ExchangeStates.DISCLOSURE_RECEIVED,
2457
+ ExchangeStates.DISCLOSURE_CHECKED,
2458
+ ExchangeStates.IDENTIFIED,
2459
+ ])
2460
+ );
2461
+ expect(identifyWebhookPayload).toEqual({
2462
+ phones: [],
2463
+ idDocumentCredentials: [
2464
+ {
2465
+ id: verificationIdentifierPayload.vc.id,
2466
+ type: ['VerificationIdentifier', 'VerifiableCredential'],
2467
+ credentialSubject:
2468
+ verificationIdentifierPayload.vc.credentialSubject,
2469
+ credentialType: 'VerificationIdentifier',
2470
+ issuer: {
2471
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2472
+ },
2473
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2474
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2475
+ },
2476
+ ],
2477
+ emails: [],
2478
+ phoneCredentials: [],
2479
+ emailCredentials: [],
2480
+ tenantDID: tenant.did,
2481
+ tenantId: tenant._id,
2482
+ exchangeId: exchange._id,
2483
+ });
2484
+ });
2485
+
2486
+ // TODO Remove after December 2024
2487
+ it('should 200 when email & verificationIdentifier included for the "issued" claim', async () => {
2488
+ fastify.overrides.reqConfig = (config) => ({
2489
+ ...config,
2490
+ vendorCredentialsIncludeIssuedClaim: true,
2491
+ });
2492
+
2493
+ const presentation = await generateKYCPresentation(exchange, [
2494
+ 'email',
2495
+ 'verificationIdentifier',
2496
+ ]);
2497
+
2498
+ let identifyWebhookPayload;
2499
+ nock(mockVendorUrl)
2500
+ .post(identifyUserOnVendorEndpoint)
2501
+ .reply(200, (uri, body) => {
2502
+ identifyWebhookPayload = body;
2503
+ return {
2504
+ vendorUserId,
2505
+ };
2506
+ });
2507
+
2508
+ const response = await fastify.injectJson({
2509
+ method: 'POST',
2510
+ url: idUrl(tenant),
2511
+ headers: {
2512
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2513
+ },
2514
+ payload: {
2515
+ jwt_vp: await presentation.sign(
2516
+ holderKid,
2517
+ holderKeys.privateKey,
2518
+ holderDid
2519
+ ),
2520
+ exchange_id: exchange._id,
2521
+ },
2522
+ });
2523
+
2524
+ expect(response.statusCode).toEqual(200);
2525
+ expect(response.json).toEqual({
2526
+ token: expect.any(String),
2527
+ exchange: {
2528
+ disclosureComplete: true,
2529
+ exchangeComplete: false,
2530
+ id: exchange._id,
2531
+ type: ExchangeTypes.ISSUING,
2532
+ },
2533
+ });
2534
+ const dbResult = await mongoDb()
2535
+ .collection('vendorUserIdMappings')
2536
+ .findOne({ vendorUserId });
2537
+ const { sub } = decodeJwt(response.json.token);
2538
+ expect(dbResult).toEqual({
2539
+ _id: new ObjectId(sub),
2540
+ vendorUserId,
2541
+ tenantId: new ObjectId(tenant._id),
2542
+ createdAt: expect.any(Date),
2543
+ updatedAt: expect.any(Date),
2544
+ });
2545
+ expect(
2546
+ await mongoDb()
2547
+ .collection('exchanges')
2548
+ .findOne({ _id: new ObjectId(exchange._id) })
2549
+ ).toEqual(
2550
+ expectedExchange(tenant, exchange, presentation, [
2551
+ ExchangeStates.NEW,
2552
+ ExchangeStates.DISCLOSURE_RECEIVED,
2553
+ ExchangeStates.DISCLOSURE_CHECKED,
2554
+ ExchangeStates.IDENTIFIED,
2555
+ ])
2556
+ );
2557
+ expect(identifyWebhookPayload).toEqual({
2558
+ emailCredentials: [
2559
+ {
2560
+ id: emailPayload.vc.id,
2561
+ type: ['EmailV1.0', 'VerifiableCredential'],
2562
+ credentialSubject: emailPayload.vc.credentialSubject,
2563
+ credentialType: 'EmailV1.0',
2564
+ issuer: {
2565
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2566
+ },
2567
+ issued: expect.stringMatching(ISO_DATETIME_FORMAT),
2568
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2569
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2570
+ },
2571
+ ],
2572
+ idDocumentCredentials: [
2573
+ {
2574
+ id: verificationIdentifierPayload.vc.id,
2575
+ type: ['VerificationIdentifier', 'VerifiableCredential'],
2576
+ credentialSubject:
2577
+ verificationIdentifierPayload.vc.credentialSubject,
2578
+ credentialType: 'VerificationIdentifier',
2579
+ issuer: {
2580
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2581
+ },
2582
+ issued: expect.stringMatching(ISO_DATETIME_FORMAT),
2583
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2584
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2585
+ },
2586
+ ],
2587
+ phoneCredentials: [],
2588
+ emails: [emailPayload.vc.credentialSubject.email],
2589
+ phones: [],
2590
+ tenantDID: tenant.did,
2591
+ tenantId: tenant._id,
2592
+ exchangeId: exchange._id,
2593
+ });
2594
+ });
2595
+
2596
+ it('should 200 when identifying unrecognized credential type', async () => {
2597
+ let bodyReceived;
2598
+ nock(mockVendorUrl)
2599
+ .post(identifyUserOnVendorEndpoint, (body) => {
2600
+ bodyReceived = body;
2601
+ return body;
2602
+ })
2603
+ .reply(200, {
2604
+ vendorUserId,
2605
+ });
2606
+
2607
+ const presentationWhatever = await generateKYCPresentation(
2608
+ exchange,
2609
+ 'whateverIdentifier'
2610
+ );
2611
+
2612
+ const response = await fastify.injectJson({
2613
+ method: 'POST',
2614
+ url: idUrl(tenant),
2615
+ headers: {
2616
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2617
+ },
2618
+ payload: {
2619
+ jwt_vp: await presentationWhatever.sign(
2620
+ holderKid,
2621
+ holderKeys.privateKey,
2622
+ holderDid
2623
+ ),
2624
+ exchange_id: exchange._id,
2625
+ },
2626
+ });
2627
+
2628
+ expect(response.statusCode).toEqual(200);
2629
+ expect(bodyReceived).toEqual({
2630
+ phones: [],
2631
+ idDocumentCredentials: [
2632
+ {
2633
+ id: idDocPayload.vc.id,
2634
+ type: ['Whatever', 'VerifiableCredential'],
2635
+ credentialType: 'Whatever',
2636
+ credentialSubject: {
2637
+ email: 'adam.smith@example.com',
2638
+ },
2639
+ issuer: {
2640
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2641
+ },
2642
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2643
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2644
+ },
2645
+ ],
2646
+ emails: [],
2647
+ phoneCredentials: [],
2648
+ emailCredentials: [],
2649
+ vendorOrganizationId: tenant.vendorOrganizationId,
2650
+ tenantDID: tenant.did,
2651
+ tenantId: tenant._id,
2652
+ exchangeId: exchange._id,
2653
+ });
2654
+
2655
+ expect(
2656
+ await mongoDb()
2657
+ .collection('exchanges')
2658
+ .findOne({ _id: new ObjectId(exchange._id) })
2659
+ ).toEqual(
2660
+ expectedExchange(tenant, exchange, presentationWhatever, [
2661
+ ExchangeStates.NEW,
2662
+ ExchangeStates.DISCLOSURE_RECEIVED,
2663
+ ExchangeStates.DISCLOSURE_CHECKED,
2664
+ ExchangeStates.IDENTIFIED,
2665
+ ])
2666
+ );
2667
+ });
2668
+
2669
+ it('should 200 if vcs credential not exist', async () => {
2670
+ const presentationMixed = await generateKYCPresentation(
2671
+ exchange,
2672
+ ['idDocument', 'phone'],
2673
+ { isBrokeVCS: true }
2674
+ );
2675
+
2676
+ let identifyWebhookPayload;
2677
+ nock(mockVendorUrl)
2678
+ .post(identifyUserOnVendorEndpoint)
2679
+ .reply(200, (uri, body) => {
2680
+ identifyWebhookPayload = body;
2681
+ return { vendorUserId };
2682
+ });
2683
+
2684
+ const response = await fastify.injectJson({
2685
+ method: 'POST',
2686
+ url: idUrl(tenant),
2687
+ headers: {
2688
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2689
+ },
2690
+ payload: {
2691
+ jwt_vp: await presentationMixed.sign(
2692
+ holderKid,
2693
+ holderKeys.privateKey,
2694
+ holderDid
2695
+ ),
2696
+ exchange_id: exchange._id,
2697
+ },
2698
+ });
2699
+
2700
+ expect(response.statusCode).toEqual(200);
2701
+ expect(response.json).toEqual({
2702
+ token: expect.any(String),
2703
+ exchange: {
2704
+ disclosureComplete: true,
2705
+ exchangeComplete: false,
2706
+ id: exchange._id,
2707
+ type: ExchangeTypes.ISSUING,
2708
+ },
2709
+ });
2710
+ const dbResult = await mongoDb()
2711
+ .collection('vendorUserIdMappings')
2712
+ .findOne({ vendorUserId });
2713
+ const { sub } = decodeJwt(response.json.token);
2714
+ expect(dbResult).toEqual({
2715
+ _id: new ObjectId(sub),
2716
+ vendorUserId,
2717
+ tenantId: new ObjectId(tenant._id),
2718
+ createdAt: expect.any(Date),
2719
+ updatedAt: expect.any(Date),
2720
+ });
2721
+
2722
+ expect(
2723
+ await mongoDb()
2724
+ .collection('exchanges')
2725
+ .findOne({ _id: new ObjectId(exchange._id) })
2726
+ ).toEqual(
2727
+ expectedExchange(tenant, exchange, presentationMixed, [
2728
+ ExchangeStates.NEW,
2729
+ ExchangeStates.DISCLOSURE_RECEIVED,
2730
+ ExchangeStates.DISCLOSURE_CHECKED,
2731
+ ExchangeStates.IDENTIFIED,
2732
+ ])
2733
+ );
2734
+
2735
+ expect(identifyWebhookPayload).toEqual({
2736
+ phoneCredentials: [],
2737
+ phones: [],
2738
+ emails: [],
2739
+ idDocumentCredentials: [
2740
+ {
2741
+ id: idDocPayload.vc.id,
2742
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
2743
+ credentialSubject: idDocPayload.vc.credentialSubject,
2744
+ credentialType: 'IdDocumentV1.0',
2745
+ issuer: {
2746
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2747
+ },
2748
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2749
+ validFrom: '2017-09-01',
2750
+ validUntil: '2021-09-01',
2751
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2752
+ },
2753
+ ],
2754
+ emailCredentials: [],
2755
+ tenantDID: tenant.did,
2756
+ tenantId: tenant._id,
2757
+ exchangeId: exchange._id,
2758
+ });
2759
+ });
2760
+
2761
+ it('should 200 when identifying mixed ', async () => {
2762
+ const presentationMixed = await generateKYCPresentation(exchange, [
2763
+ 'idDocument',
2764
+ 'phone',
2765
+ 'email',
2766
+ ]);
2767
+
2768
+ let identifyWebhookPayload;
2769
+ nock(mockVendorUrl)
2770
+ .post(identifyUserOnVendorEndpoint)
2771
+ .reply(200, (uri, body) => {
2772
+ identifyWebhookPayload = body;
2773
+ return { vendorUserId };
2774
+ });
2775
+
2776
+ const response = await fastify.injectJson({
2777
+ method: 'POST',
2778
+ url: idUrl(tenant),
2779
+ headers: {
2780
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2781
+ },
2782
+ payload: {
2783
+ jwt_vp: await presentationMixed.sign(
2784
+ holderKid,
2785
+ holderKeys.privateKey,
2786
+ holderDid
2787
+ ),
2788
+ exchange_id: exchange._id,
2789
+ },
2790
+ });
2791
+
2792
+ expect(response.statusCode).toEqual(200);
2793
+ expect(response.json).toEqual({
2794
+ token: expect.any(String),
2795
+ exchange: {
2796
+ disclosureComplete: true,
2797
+ exchangeComplete: false,
2798
+ id: exchange._id,
2799
+ type: ExchangeTypes.ISSUING,
2800
+ },
2801
+ });
2802
+ const dbResult = await mongoDb()
2803
+ .collection('vendorUserIdMappings')
2804
+ .findOne({ vendorUserId });
2805
+ const { sub } = decodeJwt(response.json.token);
2806
+ expect(dbResult).toEqual({
2807
+ _id: new ObjectId(sub),
2808
+ vendorUserId,
2809
+ tenantId: new ObjectId(tenant._id),
2810
+ createdAt: expect.any(Date),
2811
+ updatedAt: expect.any(Date),
2812
+ });
2813
+ expect(
2814
+ await mongoDb()
2815
+ .collection('exchanges')
2816
+ .findOne({ _id: new ObjectId(exchange._id) })
2817
+ ).toEqual(
2818
+ expectedExchange(tenant, exchange, presentationMixed, [
2819
+ ExchangeStates.NEW,
2820
+ ExchangeStates.DISCLOSURE_RECEIVED,
2821
+ ExchangeStates.DISCLOSURE_CHECKED,
2822
+ ExchangeStates.IDENTIFIED,
2823
+ ])
2824
+ );
2825
+
2826
+ expect(identifyWebhookPayload).toEqual({
2827
+ phoneCredentials: [
2828
+ {
2829
+ id: phonePayload.vc.id,
2830
+ type: ['PhoneV1.0', 'VerifiableCredential'],
2831
+ credentialSubject: phonePayload.vc.credentialSubject,
2832
+ credentialType: 'PhoneV1.0',
2833
+ issuer: {
2834
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2835
+ },
2836
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2837
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2838
+ },
2839
+ ],
2840
+ phones: [phonePayload.vc.credentialSubject.phone],
2841
+ emails: [emailPayload.vc.credentialSubject.email],
2842
+ idDocumentCredentials: [
2843
+ {
2844
+ id: idDocPayload.vc.id,
2845
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
2846
+ credentialSubject: idDocPayload.vc.credentialSubject,
2847
+ credentialType: 'IdDocumentV1.0',
2848
+ issuer: {
2849
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2850
+ },
2851
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2852
+ validFrom: '2017-09-01',
2853
+ validUntil: '2021-09-01',
2854
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2855
+ },
2856
+ ],
2857
+ emailCredentials: [
2858
+ {
2859
+ id: emailPayload.vc.id,
2860
+ type: ['EmailV1.0', 'VerifiableCredential'],
2861
+ credentialSubject: emailPayload.vc.credentialSubject,
2862
+ credentialType: 'EmailV1.0',
2863
+ issuer: {
2864
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2865
+ },
2866
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2867
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2868
+ },
2869
+ ],
2870
+ tenantDID: tenant.did,
2871
+ tenantId: tenant._id,
2872
+ exchangeId: exchange._id,
2873
+ });
2874
+ });
2875
+
2876
+ it('should 200 when identifying mixed of schemas legacy versions of docs', async () => {
2877
+ const presentationMixed = await generateKYCPresentation(exchange, [
2878
+ 'legacyIdDocument',
2879
+ 'phone',
2880
+ 'legacyEmail',
2881
+ ]);
2882
+
2883
+ let identifyWebhookPayload;
2884
+ nock(mockVendorUrl)
2885
+ .post(identifyUserOnVendorEndpoint)
2886
+ .reply(200, (uri, body) => {
2887
+ identifyWebhookPayload = body;
2888
+ return { vendorUserId };
2889
+ });
2890
+
2891
+ const response = await fastify.injectJson({
2892
+ method: 'POST',
2893
+ url: idUrl(tenant),
2894
+ headers: {
2895
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
2896
+ },
2897
+ payload: {
2898
+ jwt_vp: await presentationMixed.sign(
2899
+ holderKid,
2900
+ holderKeys.privateKey,
2901
+ holderDid
2902
+ ),
2903
+ exchange_id: exchange._id,
2904
+ },
2905
+ });
2906
+
2907
+ expect(response.statusCode).toEqual(200);
2908
+ expect(response.json).toEqual({
2909
+ token: expect.any(String),
2910
+ exchange: {
2911
+ disclosureComplete: true,
2912
+ exchangeComplete: false,
2913
+ id: exchange._id,
2914
+ type: ExchangeTypes.ISSUING,
2915
+ },
2916
+ });
2917
+ const dbResult = await mongoDb()
2918
+ .collection('vendorUserIdMappings')
2919
+ .findOne({ vendorUserId });
2920
+ const { sub } = decodeJwt(response.json.token);
2921
+ expect(dbResult).toEqual({
2922
+ _id: new ObjectId(sub),
2923
+ vendorUserId,
2924
+ tenantId: new ObjectId(tenant._id),
2925
+ createdAt: expect.any(Date),
2926
+ updatedAt: expect.any(Date),
2927
+ });
2928
+
2929
+ const exchangeDBResult = await mongoDb()
2930
+ .collection('exchanges')
2931
+ .findOne({ _id: new ObjectId(exchange._id) });
2932
+ expect(exchangeDBResult).toEqual(
2933
+ expectedExchange(tenant, exchange, presentationMixed, [
2934
+ ExchangeStates.NEW,
2935
+ ExchangeStates.DISCLOSURE_RECEIVED,
2936
+ ExchangeStates.DISCLOSURE_CHECKED,
2937
+ ExchangeStates.IDENTIFIED,
2938
+ ])
2939
+ );
2940
+
2941
+ expect(identifyWebhookPayload).toEqual({
2942
+ firstName: 'Sam',
2943
+ lastName: 'Smith',
2944
+ phoneCredentials: [
2945
+ {
2946
+ id: phonePayload.vc.id,
2947
+ type: ['PhoneV1.0', 'VerifiableCredential'],
2948
+ credentialSubject: phonePayload.vc.credentialSubject,
2949
+ credentialType: 'PhoneV1.0',
2950
+ issuer: {
2951
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2952
+ },
2953
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2954
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2955
+ },
2956
+ ],
2957
+ phones: [phonePayload.vc.credentialSubject.phone],
2958
+ emails: [emailPayload.vc.credentialSubject.email],
2959
+ idDocumentCredentials: [
2960
+ {
2961
+ id: legacyIdDocPayload.vc.id,
2962
+ type: ['IdDocument', 'VerifiableCredential'],
2963
+ credentialSubject: legacyIdDocPayload.vc.credentialSubject,
2964
+ credentialType: 'IdDocument',
2965
+ issuer: {
2966
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2967
+ },
2968
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2969
+ validFrom: '2017-09-01',
2970
+ validUntil: '2021-09-01',
2971
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2972
+ },
2973
+ ],
2974
+ emailCredentials: [
2975
+ {
2976
+ id: emailPayload.vc.id,
2977
+ type: ['Email', 'VerifiableCredential'],
2978
+ credentialSubject: emailPayload.vc.credentialSubject,
2979
+ credentialType: 'Email',
2980
+ issuer: {
2981
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
2982
+ },
2983
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
2984
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
2985
+ },
2986
+ ],
2987
+ tenantDID: tenant.did,
2988
+ tenantId: tenant._id,
2989
+ exchangeId: exchange._id,
2990
+ });
2991
+ });
2992
+
2993
+ it('should 200 when sub is existing', async () => {
2994
+ const presentationIdDocument = await generateKYCPresentation(
2995
+ exchange,
2996
+ 'idDocument'
2997
+ );
2998
+
2999
+ const existingUser = await persistVendorUserIdMapping({ tenant });
3000
+ let identifyWebhookPayload;
3001
+ nock(mockVendorUrl)
3002
+ .post(identifyUserOnVendorEndpoint)
3003
+ .reply(200, (uri, body) => {
3004
+ identifyWebhookPayload = body;
3005
+ return {
3006
+ vendorUserId: existingUser.vendorUserId,
3007
+ };
3008
+ });
3009
+
3010
+ const response = await fastify.injectJson({
3011
+ method: 'POST',
3012
+ url: idUrl(tenant),
3013
+ headers: {
3014
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3015
+ },
3016
+ payload: {
3017
+ jwt_vp: await presentationIdDocument.sign(
3018
+ holderKid,
3019
+ holderKeys.privateKey,
3020
+ holderDid
3021
+ ),
3022
+ exchange_id: exchange._id,
3023
+ },
3024
+ });
3025
+
3026
+ expect(response.statusCode).toEqual(200);
3027
+ expect(response.json).toEqual({
3028
+ token: expect.any(String),
3029
+ exchange: {
3030
+ disclosureComplete: true,
3031
+ exchangeComplete: false,
3032
+ id: exchange._id,
3033
+ type: ExchangeTypes.ISSUING,
3034
+ },
3035
+ });
3036
+
3037
+ const dbResult = await mongoDb()
3038
+ .collection('vendorUserIdMappings')
3039
+ .findOne({ vendorUserId });
3040
+ const { sub } = decodeJwt(response.json.token);
3041
+ expect(sub).toEqual(existingUser._id.toString());
3042
+ expect(dbResult).toEqual({
3043
+ _id: new ObjectId(existingUser._id),
3044
+ vendorUserId,
3045
+ tenantId: new ObjectId(tenant._id),
3046
+ createdAt: expect.any(Date),
3047
+ updatedAt: expect.any(Date),
3048
+ });
3049
+
3050
+ const exchangeDBResult = await mongoDb()
3051
+ .collection('exchanges')
3052
+ .findOne({ _id: new ObjectId(exchange._id) });
3053
+ expect(exchangeDBResult).toEqual(
3054
+ expectedExchange(tenant, exchange, presentationIdDocument, [
3055
+ ExchangeStates.NEW,
3056
+ ExchangeStates.DISCLOSURE_RECEIVED,
3057
+ ExchangeStates.DISCLOSURE_CHECKED,
3058
+ ExchangeStates.IDENTIFIED,
3059
+ ])
3060
+ );
3061
+ expect(identifyWebhookPayload).toEqual({
3062
+ emailCredentials: [],
3063
+ emails: [],
3064
+ exchangeId: exchange._id,
3065
+ idDocumentCredentials: [
3066
+ {
3067
+ id: idDocPayload.vc.id,
3068
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
3069
+ credentialSubject: idDocPayload.vc.credentialSubject,
3070
+ credentialType: 'IdDocumentV1.0',
3071
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
3072
+ issuer: {
3073
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
3074
+ },
3075
+ validFrom: '2017-09-01',
3076
+ validUntil: '2021-09-01',
3077
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
3078
+ },
3079
+ ],
3080
+ phoneCredentials: [],
3081
+ phones: [],
3082
+ tenantDID: tenant.did,
3083
+ tenantId: tenant._id,
3084
+ });
3085
+ const countResult = await mongoDb()
3086
+ .collection('vendorUserIdMappings')
3087
+ .countDocuments({ vendorUserId });
3088
+ expect(countResult).toEqual(1);
3089
+ });
3090
+
3091
+ it('should use another prefix url if tenant has webhookUrl', async () => {
3092
+ const webhookUrl = 'http://cutomUrl.com';
3093
+ const customTenant = await persistTenant({
3094
+ webhookUrl,
3095
+ });
3096
+
3097
+ const keyPair = generateKeyPair({ format: 'jwk' });
3098
+
3099
+ await persistKey({
3100
+ tenant: customTenant,
3101
+ kidFragment: '#ID1',
3102
+ keyPair,
3103
+ });
3104
+
3105
+ const customDisclosure = await persistDisclosure({
3106
+ tenant: customTenant,
3107
+ description: 'Credential Issuance disclosure',
3108
+ types: [{ type: 'EmailV1.0' }],
3109
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
3110
+ purpose: 'Identification',
3111
+ duration: '6y',
3112
+ });
3113
+
3114
+ const customExchange = await persistOfferExchange({
3115
+ tenant: customTenant,
3116
+ disclosure: customDisclosure,
3117
+ });
3118
+
3119
+ const presentationIdDocument = await generateKYCPresentation(
3120
+ customExchange,
3121
+ 'idDocument'
3122
+ );
3123
+
3124
+ const existingUser = await persistVendorUserIdMapping({
3125
+ tenant: customTenant,
3126
+ });
3127
+ let identifyWebhookPayload;
3128
+ const nockWebhook = await nock(webhookUrl)
3129
+ .post(identifyUserOnVendorEndpoint)
3130
+ .reply(200, (uri, body) => {
3131
+ identifyWebhookPayload = body;
3132
+ return {
3133
+ vendorUserId: existingUser.vendorUserId,
3134
+ };
3135
+ });
3136
+
3137
+ const response = await fastify.injectJson({
3138
+ method: 'POST',
3139
+ url: idUrl(customTenant),
3140
+ headers: {
3141
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3142
+ },
3143
+ payload: {
3144
+ jwt_vp: await presentationIdDocument.sign(
3145
+ holderKid,
3146
+ holderKeys.privateKey,
3147
+ holderDid
3148
+ ),
3149
+ exchange_id: customExchange._id,
3150
+ },
3151
+ });
3152
+
3153
+ expect(nockWebhook.isDone()).toEqual(true);
3154
+ expect(response.statusCode).toEqual(200);
3155
+ expect(response.json).toEqual({
3156
+ token: expect.any(String),
3157
+ exchange: {
3158
+ disclosureComplete: true,
3159
+ exchangeComplete: false,
3160
+ id: customExchange._id,
3161
+ type: ExchangeTypes.ISSUING,
3162
+ },
3163
+ });
3164
+
3165
+ const dbResult = await mongoDb()
3166
+ .collection('vendorUserIdMappings')
3167
+ .findOne({ vendorUserId });
3168
+ const { sub } = decodeJwt(response.json.token);
3169
+ expect(sub).toEqual(existingUser._id.toString());
3170
+ expect(dbResult).toEqual({
3171
+ _id: new ObjectId(existingUser._id),
3172
+ vendorUserId,
3173
+ tenantId: new ObjectId(customTenant._id),
3174
+ createdAt: expect.any(Date),
3175
+ updatedAt: expect.any(Date),
3176
+ });
3177
+ const exchangeDBResult = await mongoDb()
3178
+ .collection('exchanges')
3179
+ .findOne({ _id: new ObjectId(customExchange._id) });
3180
+ expect(exchangeDBResult).toEqual(
3181
+ expectedExchange(customTenant, customExchange, presentationIdDocument, [
3182
+ ExchangeStates.NEW,
3183
+ ExchangeStates.DISCLOSURE_RECEIVED,
3184
+ ExchangeStates.DISCLOSURE_CHECKED,
3185
+ ExchangeStates.IDENTIFIED,
3186
+ ])
3187
+ );
3188
+ expect(identifyWebhookPayload).toEqual({
3189
+ emailCredentials: [],
3190
+ emails: [],
3191
+ exchangeId: customExchange._id,
3192
+ idDocumentCredentials: [
3193
+ {
3194
+ id: idDocPayload.vc.id,
3195
+ type: ['IdDocumentV1.0', 'VerifiableCredential'],
3196
+ credentialSubject: idDocPayload.vc.credentialSubject,
3197
+ credentialType: 'IdDocumentV1.0',
3198
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
3199
+ issuer: {
3200
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
3201
+ },
3202
+ validFrom: '2017-09-01',
3203
+ validUntil: '2021-09-01',
3204
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
3205
+ },
3206
+ ],
3207
+ phoneCredentials: [],
3208
+ phones: [],
3209
+ tenantDID: customTenant.did,
3210
+ tenantId: customTenant._id,
3211
+ });
3212
+ const countResult = await mongoDb()
3213
+ .collection('vendorUserIdMappings')
3214
+ .countDocuments({ vendorUserId });
3215
+ expect(countResult).toEqual(1);
3216
+ });
3217
+
3218
+ it('should return 400 when jwt_vp verification failed', async () => {
3219
+ const presentationIdDocument = await generateKYCPresentation(
3220
+ exchange,
3221
+ 'idDocument'
3222
+ );
3223
+
3224
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(200, {});
3225
+
3226
+ const response = await fastify.injectJson({
3227
+ method: 'POST',
3228
+ url: idUrl(tenant),
3229
+ headers: {
3230
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3231
+ },
3232
+ payload: {
3233
+ jwt_vp: await presentationIdDocument.sign(
3234
+ 'WRONG',
3235
+ holderKeys.privateKey,
3236
+ 'MISSING'
3237
+ ),
3238
+ exchange_id: exchange._id,
3239
+ },
3240
+ });
3241
+
3242
+ expect(response.statusCode).toEqual(400);
3243
+ expect(response.json).toEqual(
3244
+ errorResponseMatcher({
3245
+ error: 'Bad Request',
3246
+ errorCode: 'presentation_malformed',
3247
+ message: 'Malformed jwt_vp property: must_be_did',
3248
+ statusCode: 400,
3249
+ })
3250
+ );
3251
+ });
3252
+
3253
+ it('should return 400 when no exchange', async () => {
3254
+ const presentationIdDocument = await generateKYCPresentation(
3255
+ exchange,
3256
+ 'idDocument'
3257
+ );
3258
+ const response = await fastify.injectJson({
3259
+ method: 'POST',
3260
+ url: idUrl(tenant),
3261
+ headers: {
3262
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3263
+ },
3264
+ payload: {
3265
+ jwt_vp: await presentationIdDocument.sign(
3266
+ holderKid,
3267
+ holderKeys.privateKey,
3268
+ holderDid
3269
+ ),
3270
+ exchange_id: 'no-exchange',
3271
+ },
3272
+ });
3273
+ expect(response.statusCode).toEqual(404);
3274
+ expect(response.json).toEqual(
3275
+ errorResponseMatcher({
3276
+ error: 'Not Found',
3277
+ errorCode: 'exchange_not_found',
3278
+ message: 'Exchange no-exchange not found',
3279
+ statusCode: 404,
3280
+ })
3281
+ );
3282
+ });
3283
+
3284
+ it('should return 401 when preauth disclosure and no vendorOriginContext', async () => {
3285
+ const preauthDisclosure = await persistDisclosure({
3286
+ tenant,
3287
+ description: 'Preauth identification disclosure',
3288
+ identificationMethods: ['preauth'],
3289
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
3290
+ purpose: 'Identification',
3291
+ duration: '6y',
3292
+ });
3293
+ ({ vendorUserId } = await newVendorUserIdMapping());
3294
+ const preAuthExchange = await persistOfferExchange({
3295
+ tenant,
3296
+ disclosure: preauthDisclosure,
3297
+ });
3298
+
3299
+ const presentationIdDocument = await generateKYCPresentation(
3300
+ preAuthExchange,
3301
+ 'idDocument'
3302
+ ); // .override({ vendorOriginContext: 'foo' });
3303
+
3304
+ const identifyWebhookNock = nock(mockVendorUrl)
3305
+ .post(identifyUserOnVendorEndpoint)
3306
+ .reply(200, {});
3307
+
3308
+ const response = await fastify.injectJson({
3309
+ method: 'POST',
3310
+ url: idUrl(tenant),
3311
+ headers: {
3312
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3313
+ },
3314
+ payload: {
3315
+ jwt_vp: await presentationIdDocument.sign(
3316
+ holderKid,
3317
+ holderKeys.privateKey,
3318
+ holderDid
3319
+ ),
3320
+ exchange_id: preAuthExchange._id,
3321
+ },
3322
+ });
3323
+
3324
+ expect(response.statusCode).toEqual(401);
3325
+ expect(response.json).toEqual(
3326
+ errorResponseMatcher({
3327
+ statusCode: 401,
3328
+ error: 'Unauthorized',
3329
+ errorCode: 'presentation_request_invalid',
3330
+ message:
3331
+ 'Presentation for a preauth disclosure must contain a vendorOriginContext',
3332
+ })
3333
+ );
3334
+
3335
+ expect(
3336
+ await mongoDb()
3337
+ .collection('exchanges')
3338
+ .findOne({ _id: new ObjectId(preAuthExchange._id) })
3339
+ ).toEqual(
3340
+ expectedExchange(
3341
+ tenant,
3342
+ preAuthExchange,
3343
+ null,
3344
+ [ExchangeStates.NEW, ExchangeStates.NOT_IDENTIFIED],
3345
+ 'Presentation for a preauth disclosure must contain a vendorOriginContext'
3346
+ )
3347
+ );
3348
+ expect(identifyWebhookNock.isDone()).toEqual(false);
3349
+ });
3350
+
3351
+ it('should return 401 when vendor responds with no user', async () => {
3352
+ const presentationIdDocument = await generateKYCPresentation(
3353
+ exchange,
3354
+ 'idDocument'
3355
+ );
3356
+
3357
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(200, {});
3358
+
3359
+ const response = await fastify.injectJson({
3360
+ method: 'POST',
3361
+ url: idUrl(tenant),
3362
+ headers: {
3363
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3364
+ },
3365
+ payload: {
3366
+ jwt_vp: await presentationIdDocument.sign(
3367
+ holderKid,
3368
+ holderKeys.privateKey,
3369
+ holderDid
3370
+ ),
3371
+ exchange_id: exchange._id,
3372
+ },
3373
+ });
3374
+
3375
+ expect(response.statusCode).toEqual(401);
3376
+
3377
+ expect(
3378
+ await mongoDb()
3379
+ .collection('exchanges')
3380
+ .findOne({ _id: new ObjectId(exchange._id) })
3381
+ ).toEqual(
3382
+ expectedExchange(
3383
+ tenant,
3384
+ exchange,
3385
+ presentationIdDocument,
3386
+ [
3387
+ ExchangeStates.NEW,
3388
+ ExchangeStates.DISCLOSURE_RECEIVED,
3389
+ ExchangeStates.DISCLOSURE_CHECKED,
3390
+ ExchangeStates.NOT_IDENTIFIED,
3391
+ ],
3392
+ 'user not found - vendorUserId property should be a string value'
3393
+ )
3394
+ );
3395
+ });
3396
+
3397
+ it('should return 401 when vendor responds with no vendorUserId', async () => {
3398
+ const presentationIdDocument = await generateKYCPresentation(
3399
+ exchange,
3400
+ 'idDocument'
3401
+ );
3402
+
3403
+ nock(mockVendorUrl)
3404
+ .post(identifyUserOnVendorEndpoint)
3405
+ .reply(200, { foo: 'foo' });
3406
+
3407
+ const response = await fastify.injectJson({
3408
+ method: 'POST',
3409
+ url: idUrl(tenant),
3410
+ headers: {
3411
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3412
+ },
3413
+ payload: {
3414
+ jwt_vp: await presentationIdDocument.sign(
3415
+ holderKid,
3416
+ holderKeys.privateKey,
3417
+ holderDid
3418
+ ),
3419
+ exchange_id: exchange._id,
3420
+ },
3421
+ });
3422
+
3423
+ expect(response.statusCode).toEqual(401);
3424
+ expect(
3425
+ await mongoDb()
3426
+ .collection('exchanges')
3427
+ .findOne({ _id: new ObjectId(exchange._id) })
3428
+ ).toEqual(
3429
+ expectedExchange(
3430
+ tenant,
3431
+ exchange,
3432
+ presentationIdDocument,
3433
+ [
3434
+ ExchangeStates.NEW,
3435
+ ExchangeStates.DISCLOSURE_RECEIVED,
3436
+ ExchangeStates.DISCLOSURE_CHECKED,
3437
+ ExchangeStates.NOT_IDENTIFIED,
3438
+ ],
3439
+ 'user not found - vendorUserId property should be a string value'
3440
+ )
3441
+ );
3442
+ });
3443
+
3444
+ it('should return 401 when vendor responds with non-string vendorUserId', async () => {
3445
+ const presentationIdDocument = await generateKYCPresentation(
3446
+ exchange,
3447
+ 'idDocument'
3448
+ );
3449
+
3450
+ nock(mockVendorUrl)
3451
+ .post(identifyUserOnVendorEndpoint)
3452
+ .reply(200, { vendorUserId: 1 });
3453
+
3454
+ const response = await fastify.injectJson({
3455
+ method: 'POST',
3456
+ url: idUrl(tenant),
3457
+ headers: {
3458
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3459
+ },
3460
+ payload: {
3461
+ jwt_vp: await presentationIdDocument.sign(
3462
+ holderKid,
3463
+ holderKeys.privateKey,
3464
+ holderDid
3465
+ ),
3466
+ exchange_id: exchange._id,
3467
+ },
3468
+ });
3469
+
3470
+ expect(response.statusCode).toEqual(401);
3471
+
3472
+ expect(
3473
+ await mongoDb()
3474
+ .collection('exchanges')
3475
+ .findOne({ _id: new ObjectId(exchange._id) })
3476
+ ).toEqual(
3477
+ expectedExchange(
3478
+ tenant,
3479
+ exchange,
3480
+ presentationIdDocument,
3481
+ [
3482
+ ExchangeStates.NEW,
3483
+ ExchangeStates.DISCLOSURE_RECEIVED,
3484
+ ExchangeStates.DISCLOSURE_CHECKED,
3485
+ ExchangeStates.NOT_IDENTIFIED,
3486
+ ],
3487
+ 'user not found - vendorUserId property should be a string value'
3488
+ )
3489
+ );
3490
+ });
3491
+
3492
+ it('should 401 in case user not found', async () => {
3493
+ const presentationIdDocument = await generateKYCPresentation(
3494
+ exchange,
3495
+ 'idDocument'
3496
+ );
3497
+
3498
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(404);
3499
+
3500
+ const response = await fastify.injectJson({
3501
+ method: 'POST',
3502
+ url: idUrl(tenant),
3503
+ headers: {
3504
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3505
+ },
3506
+ payload: {
3507
+ jwt_vp: await presentationIdDocument.sign(
3508
+ holderKid,
3509
+ holderKeys.privateKey,
3510
+ holderDid
3511
+ ),
3512
+ exchange_id: exchange._id,
3513
+ },
3514
+ });
3515
+
3516
+ expect(response.statusCode).toEqual(401);
3517
+
3518
+ expect(
3519
+ await mongoDb()
3520
+ .collection('exchanges')
3521
+ .findOne({ _id: new ObjectId(exchange._id) })
3522
+ ).toEqual(
3523
+ expectedExchange(
3524
+ tenant,
3525
+ exchange,
3526
+ presentationIdDocument,
3527
+ [
3528
+ ExchangeStates.NEW,
3529
+ ExchangeStates.DISCLOSURE_RECEIVED,
3530
+ ExchangeStates.DISCLOSURE_CHECKED,
3531
+ ExchangeStates.NOT_IDENTIFIED,
3532
+ ],
3533
+ 'user not found'
3534
+ )
3535
+ );
3536
+ });
3537
+
3538
+ it('should 400 in case exchangeId not passed', async () => {
3539
+ const existingUser = await persistVendorUserIdMapping({ tenant });
3540
+ const presentationIdDocument = await generateKYCPresentation(
3541
+ exchange,
3542
+ 'idDocument'
3543
+ );
3544
+
3545
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(200, {
3546
+ vendorUserId: existingUser.vendorUserId,
3547
+ });
3548
+
3549
+ const response = await fastify.injectJson({
3550
+ method: 'POST',
3551
+ url: idUrl(tenant),
3552
+ headers: {
3553
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3554
+ },
3555
+ payload: {
3556
+ jwt_vp: await presentationIdDocument.sign(
3557
+ holderKid,
3558
+ holderKeys.privateKey,
3559
+ holderDid
3560
+ ),
3561
+ },
3562
+ });
3563
+
3564
+ expect(response.statusCode).toEqual(400);
3565
+
3566
+ expect(
3567
+ await mongoDb()
3568
+ .collection('exchanges')
3569
+ .findOne({ _id: new ObjectId(exchange._id) })
3570
+ ).toEqual(
3571
+ expectedExchange(tenant, exchange, undefined, [ExchangeStates.NEW])
3572
+ );
3573
+ });
3574
+
3575
+ it('should 500 in other cases', async () => {
3576
+ const presentationIdDocument = await generateKYCPresentation(
3577
+ exchange,
3578
+ 'idDocument'
3579
+ );
3580
+
3581
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(500);
3582
+
3583
+ const response = await fastify.injectJson({
3584
+ method: 'POST',
3585
+ url: idUrl(tenant),
3586
+ headers: {
3587
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3588
+ },
3589
+ payload: {
3590
+ jwt_vp: await presentationIdDocument.sign(
3591
+ holderKid,
3592
+ holderKeys.privateKey,
3593
+ holderDid
3594
+ ),
3595
+ exchange_id: exchange._id,
3596
+ },
3597
+ });
3598
+
3599
+ expect(response.statusCode).toEqual(502);
3600
+
3601
+ expect(
3602
+ await mongoDb()
3603
+ .collection('exchanges')
3604
+ .findOne({ _id: new ObjectId(exchange._id) })
3605
+ ).toEqual(
3606
+ expectedExchange(
3607
+ tenant,
3608
+ exchange,
3609
+ presentationIdDocument,
3610
+ [
3611
+ ExchangeStates.NEW,
3612
+ ExchangeStates.DISCLOSURE_RECEIVED,
3613
+ ExchangeStates.DISCLOSURE_CHECKED,
3614
+ ExchangeStates.UNEXPECTED_ERROR,
3615
+ ],
3616
+ 'Response code 500 (Internal Server Error)'
3617
+ )
3618
+ );
3619
+ });
3620
+
3621
+ it('should 409 if the exact same presentation twice', async () => {
3622
+ const presentationEmail = await generateKYCPresentation(
3623
+ exchange,
3624
+ 'email'
3625
+ );
3626
+
3627
+ nock(mockVendorUrl)
3628
+ .post(identifyUserOnVendorEndpoint)
3629
+ .reply(200, () => {
3630
+ return {
3631
+ vendorUserId,
3632
+ };
3633
+ });
3634
+
3635
+ const headers = {
3636
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3637
+ };
3638
+ const payload = {
3639
+ jwt_vp: await presentationEmail.sign(
3640
+ holderKid,
3641
+ holderKeys.privateKey,
3642
+ holderDid
3643
+ ),
3644
+ exchange_id: exchange._id,
3645
+ };
3646
+
3647
+ const response = await fastify.injectJson({
3648
+ method: 'POST',
3649
+ url: idUrl(tenant),
3650
+ headers,
3651
+ payload,
3652
+ });
3653
+
3654
+ expect(response.statusCode).toEqual(200);
3655
+ expect(response.json).toEqual({
3656
+ token: expect.any(String),
3657
+ exchange: {
3658
+ disclosureComplete: true,
3659
+ exchangeComplete: false,
3660
+ id: exchange._id,
3661
+ type: ExchangeTypes.ISSUING,
3662
+ },
3663
+ });
3664
+
3665
+ const response2 = await fastify.injectJson({
3666
+ method: 'POST',
3667
+ url: idUrl(tenant),
3668
+ headers,
3669
+ payload,
3670
+ });
3671
+
3672
+ expect(response2.statusCode).toEqual(409);
3673
+ expect(response2.json).toEqual(
3674
+ errorResponseMatcher({
3675
+ error: 'Conflict',
3676
+ errorCode: 'presentation_duplicate',
3677
+ message: 'Presentation has already been submitted',
3678
+ statusCode: 409,
3679
+ })
3680
+ );
3681
+ });
3682
+
3683
+ it('should be able to identify twice on the same exchange', async () => {
3684
+ const presentations = await Promise.all(
3685
+ map(() => generateKYCPresentation(exchange, 'email'), [1, 1])
3686
+ );
3687
+ let identifyWebhookPayload;
3688
+ nock(mockVendorUrl)
3689
+ .post(identifyUserOnVendorEndpoint)
3690
+ .twice()
3691
+ .reply(200, (uri, body) => {
3692
+ identifyWebhookPayload = body;
3693
+ return {
3694
+ vendorUserId,
3695
+ };
3696
+ });
3697
+
3698
+ const headers = {
3699
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3700
+ };
3701
+
3702
+ const response = await fastify.injectJson({
3703
+ method: 'POST',
3704
+ url: idUrl(tenant),
3705
+ headers,
3706
+ payload: {
3707
+ jwt_vp: await presentations[0].sign(
3708
+ holderKid,
3709
+ holderKeys.privateKey,
3710
+ holderDid
3711
+ ),
3712
+ exchange_id: exchange._id,
3713
+ },
3714
+ });
3715
+
3716
+ expect(response.statusCode).toEqual(200);
3717
+ expect(response.json).toEqual({
3718
+ token: expect.any(String),
3719
+ exchange: {
3720
+ disclosureComplete: true,
3721
+ exchangeComplete: false,
3722
+ id: exchange._id,
3723
+ type: ExchangeTypes.ISSUING,
3724
+ },
3725
+ });
3726
+
3727
+ const response2 = await fastify.injectJson({
3728
+ method: 'POST',
3729
+ url: idUrl(tenant),
3730
+ headers,
3731
+ payload: {
3732
+ jwt_vp: await presentations[1].sign(
3733
+ holderKid,
3734
+ holderKeys.privateKey,
3735
+ holderDid
3736
+ ),
3737
+ exchange_id: exchange._id,
3738
+ },
3739
+ });
3740
+
3741
+ expect(response2.statusCode).toEqual(200);
3742
+ expect(response2.json).toEqual({
3743
+ token: expect.any(String),
3744
+ exchange: {
3745
+ disclosureComplete: true,
3746
+ exchangeComplete: false,
3747
+ id: exchange._id,
3748
+ type: ExchangeTypes.ISSUING,
3749
+ },
3750
+ });
3751
+ const dbResult = await mongoDb()
3752
+ .collection('vendorUserIdMappings')
3753
+ .findOne({ vendorUserId });
3754
+ const { sub } = decodeJwt(response2.json.token);
3755
+ expect(dbResult).toEqual({
3756
+ _id: new ObjectId(sub),
3757
+ vendorUserId,
3758
+ tenantId: new ObjectId(tenant._id),
3759
+ createdAt: expect.any(Date),
3760
+ updatedAt: expect.any(Date),
3761
+ });
3762
+
3763
+ expect(
3764
+ await mongoDb()
3765
+ .collection('exchanges')
3766
+ .findOne({ _id: new ObjectId(exchange._id) })
3767
+ ).toEqual(
3768
+ expectedExchange(tenant, exchange, presentations[1], [
3769
+ ExchangeStates.NEW,
3770
+ ExchangeStates.DISCLOSURE_RECEIVED,
3771
+ ExchangeStates.DISCLOSURE_CHECKED,
3772
+ ExchangeStates.IDENTIFIED,
3773
+ ExchangeStates.DISCLOSURE_RECEIVED,
3774
+ ExchangeStates.DISCLOSURE_CHECKED,
3775
+ ExchangeStates.IDENTIFIED,
3776
+ ])
3777
+ );
3778
+
3779
+ expect(identifyWebhookPayload).toEqual({
3780
+ emailCredentials: [
3781
+ {
3782
+ id: emailPayload.vc.id,
3783
+ type: ['EmailV1.0', 'VerifiableCredential'],
3784
+ credentialSubject: emailPayload.vc.credentialSubject,
3785
+ credentialType: 'EmailV1.0',
3786
+ issuer: {
3787
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
3788
+ },
3789
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
3790
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
3791
+ },
3792
+ ],
3793
+ idDocumentCredentials: [],
3794
+ phoneCredentials: [],
3795
+ emails: [emailPayload.vc.credentialSubject.email],
3796
+ phones: [],
3797
+ tenantDID: tenant.did,
3798
+ tenantId: tenant._id,
3799
+ exchangeId: exchange._id,
3800
+ });
3801
+ });
3802
+
3803
+ it('should be able found on second attempt', async () => {
3804
+ const presentations = await Promise.all(
3805
+ map(() => generateKYCPresentation(exchange, 'email'), [1, 1])
3806
+ );
3807
+
3808
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(404);
3809
+ const headers = {
3810
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3811
+ };
3812
+
3813
+ const response = await fastify.injectJson({
3814
+ method: 'POST',
3815
+ url: idUrl(tenant),
3816
+ headers,
3817
+ payload: {
3818
+ jwt_vp: await presentations[0].sign(
3819
+ holderKid,
3820
+ holderKeys.privateKey,
3821
+ holderDid
3822
+ ),
3823
+ exchange_id: exchange._id,
3824
+ },
3825
+ });
3826
+
3827
+ expect(response.statusCode).toEqual(401);
3828
+
3829
+ let identifyWebhookPayload;
3830
+ nock(mockVendorUrl)
3831
+ .post(identifyUserOnVendorEndpoint)
3832
+ .reply(200, (uri, body) => {
3833
+ identifyWebhookPayload = body;
3834
+ return {
3835
+ vendorUserId,
3836
+ };
3837
+ });
3838
+
3839
+ const response2 = await fastify.injectJson({
3840
+ method: 'POST',
3841
+ url: idUrl(tenant),
3842
+ headers,
3843
+ payload: {
3844
+ jwt_vp: await presentations[1].sign(
3845
+ holderKid,
3846
+ holderKeys.privateKey,
3847
+ holderDid
3848
+ ),
3849
+ exchange_id: exchange._id,
3850
+ },
3851
+ });
3852
+
3853
+ expect(response2.statusCode).toEqual(200);
3854
+ expect(response2.json).toEqual({
3855
+ token: expect.any(String),
3856
+ exchange: {
3857
+ disclosureComplete: true,
3858
+ exchangeComplete: false,
3859
+ id: exchange._id,
3860
+ type: ExchangeTypes.ISSUING,
3861
+ },
3862
+ });
3863
+ const dbResult = await mongoDb()
3864
+ .collection('vendorUserIdMappings')
3865
+ .findOne({ vendorUserId });
3866
+ const { sub } = decodeJwt(response2.json.token);
3867
+ expect(dbResult).toEqual({
3868
+ _id: new ObjectId(sub),
3869
+ vendorUserId,
3870
+ tenantId: new ObjectId(tenant._id),
3871
+ createdAt: expect.any(Date),
3872
+ updatedAt: expect.any(Date),
3873
+ });
3874
+ expect(
3875
+ await mongoDb()
3876
+ .collection('exchanges')
3877
+ .findOne({ _id: new ObjectId(exchange._id) })
3878
+ ).toEqual(
3879
+ expectedExchange(
3880
+ tenant,
3881
+ exchange,
3882
+ presentations[1],
3883
+ [
3884
+ ExchangeStates.NEW,
3885
+ ExchangeStates.DISCLOSURE_RECEIVED,
3886
+ ExchangeStates.DISCLOSURE_CHECKED,
3887
+ ExchangeStates.NOT_IDENTIFIED,
3888
+ ExchangeStates.DISCLOSURE_RECEIVED,
3889
+ ExchangeStates.DISCLOSURE_CHECKED,
3890
+ ExchangeStates.IDENTIFIED,
3891
+ ],
3892
+ 'user not found'
3893
+ )
3894
+ );
3895
+ expect(identifyWebhookPayload).toEqual({
3896
+ emailCredentials: [
3897
+ {
3898
+ id: emailPayload.vc.id,
3899
+ type: ['EmailV1.0', 'VerifiableCredential'],
3900
+ credentialSubject: emailPayload.vc.credentialSubject,
3901
+ credentialType: 'EmailV1.0',
3902
+ issuer: {
3903
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
3904
+ },
3905
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
3906
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
3907
+ },
3908
+ ],
3909
+ idDocumentCredentials: [],
3910
+ phoneCredentials: [],
3911
+ emails: [emailPayload.vc.credentialSubject.email],
3912
+ phones: [],
3913
+ tenantDID: tenant.did,
3914
+ tenantId: tenant._id,
3915
+ exchangeId: exchange._id,
3916
+ });
3917
+ });
3918
+
3919
+ it('should be able to recover from a 500 and identify the second time', async () => {
3920
+ const presentations = await Promise.all(
3921
+ map(() => generateKYCPresentation(exchange, 'email'), [1, 1])
3922
+ );
3923
+
3924
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(500);
3925
+ const headers = {
3926
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
3927
+ };
3928
+
3929
+ const response = await fastify.injectJson({
3930
+ method: 'POST',
3931
+ url: idUrl(tenant),
3932
+ headers,
3933
+ payload: {
3934
+ jwt_vp: await presentations[0].sign(
3935
+ holderKid,
3936
+ holderKeys.privateKey,
3937
+ holderDid
3938
+ ),
3939
+ exchange_id: exchange._id,
3940
+ },
3941
+ });
3942
+
3943
+ expect(response.statusCode).toEqual(502);
3944
+
3945
+ let identifyWebhookPayload;
3946
+ nock(mockVendorUrl)
3947
+ .post(identifyUserOnVendorEndpoint)
3948
+ .reply(200, (uri, body) => {
3949
+ identifyWebhookPayload = body;
3950
+ return {
3951
+ vendorUserId,
3952
+ };
3953
+ });
3954
+
3955
+ const response2 = await fastify.injectJson({
3956
+ method: 'POST',
3957
+ url: idUrl(tenant),
3958
+ headers,
3959
+ payload: {
3960
+ jwt_vp: await presentations[1].sign(
3961
+ holderKid,
3962
+ holderKeys.privateKey,
3963
+ holderDid
3964
+ ),
3965
+ exchange_id: exchange._id,
3966
+ },
3967
+ });
3968
+
3969
+ expect(response2.statusCode).toEqual(200);
3970
+ expect(response2.json).toEqual({
3971
+ token: expect.any(String),
3972
+ exchange: {
3973
+ disclosureComplete: true,
3974
+ exchangeComplete: false,
3975
+ id: exchange._id,
3976
+ type: ExchangeTypes.ISSUING,
3977
+ },
3978
+ });
3979
+ const dbResult = await mongoDb()
3980
+ .collection('vendorUserIdMappings')
3981
+ .findOne({ vendorUserId });
3982
+ const { sub } = decodeJwt(response2.json.token);
3983
+ expect(dbResult).toEqual({
3984
+ _id: new ObjectId(sub),
3985
+ vendorUserId,
3986
+ tenantId: new ObjectId(tenant._id),
3987
+ createdAt: expect.any(Date),
3988
+ updatedAt: expect.any(Date),
3989
+ });
3990
+ expect(
3991
+ await mongoDb()
3992
+ .collection('exchanges')
3993
+ .findOne({ _id: new ObjectId(exchange._id) })
3994
+ ).toEqual(
3995
+ expectedExchange(
3996
+ tenant,
3997
+ exchange,
3998
+ presentations[1],
3999
+ [
4000
+ ExchangeStates.NEW,
4001
+ ExchangeStates.DISCLOSURE_RECEIVED,
4002
+ ExchangeStates.DISCLOSURE_CHECKED,
4003
+ ExchangeStates.UNEXPECTED_ERROR,
4004
+ ExchangeStates.DISCLOSURE_RECEIVED,
4005
+ ExchangeStates.DISCLOSURE_CHECKED,
4006
+ ExchangeStates.IDENTIFIED,
4007
+ ],
4008
+ 'Response code 500 (Internal Server Error)'
4009
+ )
4010
+ );
4011
+ expect(identifyWebhookPayload).toEqual({
4012
+ emailCredentials: [
4013
+ {
4014
+ id: emailPayload.vc.id,
4015
+ type: ['EmailV1.0', 'VerifiableCredential'],
4016
+ credentialSubject: emailPayload.vc.credentialSubject,
4017
+ credentialType: 'EmailV1.0',
4018
+ issuer: {
4019
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4020
+ },
4021
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4022
+ credentialChecks: DEFAULT_CREDENTIAL_CHECKS,
4023
+ },
4024
+ ],
4025
+ idDocumentCredentials: [],
4026
+ phoneCredentials: [],
4027
+ emails: [emailPayload.vc.credentialSubject.email],
4028
+ phones: [],
4029
+ tenantDID: tenant.did,
4030
+ tenantId: tenant._id,
4031
+ exchangeId: exchange._id,
4032
+ });
4033
+ });
4034
+
4035
+ describe('version1 of the webhook. Remove by 01/12/2023', () => {
4036
+ beforeEach(() => {
4037
+ fastify.overrides.reqConfig = (config) => ({
4038
+ ...config,
4039
+ identifyWebhookVersion: 1,
4040
+ });
4041
+ });
4042
+ afterAll(() => {
4043
+ fastify.overrides.reqConfig = (config) => ({
4044
+ ...config,
4045
+ identifyWebhookVersion: 2,
4046
+ });
4047
+ });
4048
+
4049
+ it('should 200 when identifying phone with version 1 of the webhook', async () => {
4050
+ const presentationPhone = await generateKYCPresentation(
4051
+ exchange,
4052
+ 'phone'
4053
+ );
4054
+
4055
+ let identifyWebhookPayload;
4056
+ nock(mockVendorUrl)
4057
+ .post(identifyUserOnVendorEndpoint)
4058
+ .reply(200, (uri, body) => {
4059
+ identifyWebhookPayload = body;
4060
+ return {
4061
+ vendorUserId,
4062
+ };
4063
+ });
4064
+
4065
+ const response = await fastify.injectJson({
4066
+ method: 'POST',
4067
+ url: idUrl(tenant),
4068
+ payload: {
4069
+ jwt_vp: await presentationPhone.selfSign(),
4070
+ exchange_id: exchange._id,
4071
+ },
4072
+ });
4073
+
4074
+ expect(response.statusCode).toEqual(200);
4075
+ expect(response.json).toEqual({
4076
+ exchange: {
4077
+ disclosureComplete: true,
4078
+ exchangeComplete: false,
4079
+ id: exchange._id,
4080
+ type: 'ISSUING',
4081
+ },
4082
+ token: expect.any(String),
4083
+ });
4084
+ const dbResult = await mongoDb()
4085
+ .collection('vendorUserIdMappings')
4086
+ .findOne({ vendorUserId });
4087
+ const { sub } = decodeJwt(response.json.token);
4088
+ expect(dbResult).toEqual({
4089
+ _id: new ObjectId(sub),
4090
+ vendorUserId,
4091
+ tenantId: new ObjectId(tenant._id),
4092
+ createdAt: expect.any(Date),
4093
+ updatedAt: expect.any(Date),
4094
+ });
4095
+ const exchangeDBResult = await mongoDb()
4096
+ .collection('exchanges')
4097
+ .findOne({ _id: new ObjectId(exchange._id) });
4098
+ expect(exchangeDBResult).toEqual(
4099
+ mongoify({
4100
+ _id: exchange._id,
4101
+ type: ExchangeTypes.ISSUING,
4102
+ tenantId: tenant._id,
4103
+ disclosureConsentedAt: expect.any(Date),
4104
+ disclosureId: disclosure._id,
4105
+ presentationId: presentationPhone.presentation.id,
4106
+ events: [
4107
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
4108
+ {
4109
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
4110
+ timestamp: expect.any(Date),
4111
+ },
4112
+ {
4113
+ state: ExchangeStates.DISCLOSURE_CHECKED,
4114
+ timestamp: expect.any(Date),
4115
+ },
4116
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
4117
+ ],
4118
+ offerHashes: [],
4119
+ ...credentialTypesObject,
4120
+ createdAt: expect.any(Date),
4121
+ updatedAt: expect.any(Date),
4122
+ })
4123
+ );
4124
+ expect(identifyWebhookPayload).toEqual({
4125
+ phoneCredentials: [
4126
+ {
4127
+ phone: phonePayload.vc.credentialSubject.phone,
4128
+ credentialType: 'PhoneV1.0',
4129
+ issuer: {
4130
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4131
+ },
4132
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4133
+ },
4134
+ ],
4135
+ idDocumentCredentials: [],
4136
+ emailCredentials: [],
4137
+ emails: [],
4138
+ phones: [phonePayload.vc.credentialSubject.phone],
4139
+ tenantDID: tenant.did,
4140
+ tenantId: tenant._id,
4141
+ exchangeId: exchange._id,
4142
+ });
4143
+ });
4144
+
4145
+ it('should 200 when identifying mixed with version 1 of the webhook', async () => {
4146
+ const presentationMixed = await generateKYCPresentation(exchange, [
4147
+ 'idDocument',
4148
+ 'phone',
4149
+ 'email',
4150
+ ]);
4151
+
4152
+ let identifyWebhookPayload;
4153
+ nock(mockVendorUrl)
4154
+ .post(identifyUserOnVendorEndpoint)
4155
+ .reply(200, (uri, body) => {
4156
+ identifyWebhookPayload = body;
4157
+ return {
4158
+ vendorUserId,
4159
+ };
4160
+ });
4161
+
4162
+ const response = await fastify.injectJson({
4163
+ method: 'POST',
4164
+ url: idUrl(tenant),
4165
+ payload: {
4166
+ jwt_vp: await presentationMixed.selfSign(),
4167
+ exchange_id: exchange._id,
4168
+ },
4169
+ });
4170
+
4171
+ expect(response.statusCode).toEqual(200);
4172
+ expect(response.json).toEqual({
4173
+ exchange: {
4174
+ disclosureComplete: true,
4175
+ exchangeComplete: false,
4176
+ id: exchange._id,
4177
+ type: 'ISSUING',
4178
+ },
4179
+ token: expect.any(String),
4180
+ });
4181
+ const dbResult = await mongoDb()
4182
+ .collection('vendorUserIdMappings')
4183
+ .findOne({ vendorUserId });
4184
+ const { sub } = decodeJwt(response.json.token);
4185
+ expect(dbResult).toEqual({
4186
+ _id: new ObjectId(sub),
4187
+ vendorUserId,
4188
+ tenantId: new ObjectId(tenant._id),
4189
+ createdAt: expect.any(Date),
4190
+ updatedAt: expect.any(Date),
4191
+ });
4192
+ const exchangeDBResult = await mongoDb()
4193
+ .collection('exchanges')
4194
+ .findOne({ _id: new ObjectId(exchange._id) });
4195
+ expect(exchangeDBResult).toEqual(
4196
+ mongoify({
4197
+ _id: exchange._id,
4198
+ type: ExchangeTypes.ISSUING,
4199
+ tenantId: tenant._id,
4200
+ disclosureConsentedAt: expect.any(Date),
4201
+ disclosureId: disclosure._id,
4202
+ presentationId: presentationMixed.presentation.id,
4203
+ events: [
4204
+ { state: ExchangeStates.NEW, timestamp: expect.any(Date) },
4205
+ {
4206
+ state: ExchangeStates.DISCLOSURE_RECEIVED,
4207
+ timestamp: expect.any(Date),
4208
+ },
4209
+ {
4210
+ state: ExchangeStates.DISCLOSURE_CHECKED,
4211
+ timestamp: expect.any(Date),
4212
+ },
4213
+ { state: ExchangeStates.IDENTIFIED, timestamp: expect.any(Date) },
4214
+ ],
4215
+ offerHashes: [],
4216
+ ...credentialTypesObject,
4217
+ createdAt: expect.any(Date),
4218
+ updatedAt: expect.any(Date),
4219
+ })
4220
+ );
4221
+ expect(identifyWebhookPayload).toEqual({
4222
+ phoneCredentials: [
4223
+ {
4224
+ phone: phonePayload.vc.credentialSubject.phone,
4225
+ credentialType: 'PhoneV1.0',
4226
+ issuer: {
4227
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4228
+ },
4229
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4230
+ },
4231
+ ],
4232
+ phones: [phonePayload.vc.credentialSubject.phone],
4233
+ emails: [emailPayload.vc.credentialSubject.email],
4234
+ idDocumentCredentials: [
4235
+ {
4236
+ ...idDocPayload.vc.credentialSubject,
4237
+ credentialType: 'IdDocumentV1.0',
4238
+ issuer: {
4239
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4240
+ },
4241
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4242
+ validFrom: '2017-09-01',
4243
+ validUntil: '2021-09-01',
4244
+ },
4245
+ ],
4246
+ emailCredentials: [
4247
+ {
4248
+ email: emailPayload.vc.credentialSubject.email,
4249
+ credentialType: 'EmailV1.0',
4250
+ issuer: {
4251
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4252
+ },
4253
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4254
+ },
4255
+ ],
4256
+ tenantDID: tenant.did,
4257
+ tenantId: tenant._id,
4258
+ exchangeId: exchange._id,
4259
+ });
4260
+ });
4261
+ });
4262
+
4263
+ describe('autoIdentityCheck Test Suite', () => {
4264
+ it('should 401 when autoIdentityCheck is on and credential has invalid check result', async () => {
4265
+ setMockVerifyCredentials({
4266
+ ...DEFAULT_CREDENTIAL_CHECKS,
4267
+ TRUSTED_ISSUER: CredentialCheckResultValue.FAIL,
4268
+ });
4269
+ const presentationEmail = await generateKYCPresentation(
4270
+ exchange,
4271
+ 'email'
4272
+ );
4273
+
4274
+ const response = await fastify.injectJson({
4275
+ method: 'POST',
4276
+ url: idUrl(tenant),
4277
+ headers: {
4278
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4279
+ },
4280
+ payload: {
4281
+ jwt_vp: await presentationEmail.sign(
4282
+ holderKid,
4283
+ holderKeys.privateKey,
4284
+ holderDid
4285
+ ),
4286
+ exchange_id: exchange._id,
4287
+ },
4288
+ });
4289
+
4290
+ expect(response.statusCode).toEqual(401);
4291
+ expect(response.json).toEqual(
4292
+ errorResponseMatcher({
4293
+ error: 'Unauthorized',
4294
+ message: 'presentation_credential_bad_issuer',
4295
+ statusCode: 401,
4296
+ errorCode: 'presentation_credential_bad_issuer',
4297
+ })
4298
+ );
4299
+ expect(
4300
+ await mongoDb()
4301
+ .collection('exchanges')
4302
+ .findOne({ _id: new ObjectId(exchange._id) })
4303
+ ).toEqual(
4304
+ expectedExchange(
4305
+ tenant,
4306
+ exchange,
4307
+ presentationEmail,
4308
+ [
4309
+ ExchangeStates.NEW,
4310
+ ExchangeStates.DISCLOSURE_RECEIVED,
4311
+ ExchangeStates.DISCLOSURE_CHECKED,
4312
+ ExchangeStates.NOT_IDENTIFIED,
4313
+ ],
4314
+ 'presentation_credential_bad_issuer'
4315
+ )
4316
+ );
4317
+ });
4318
+ it('should 200 when autoIdentityCheck is off, vendorEndpoint is issuing-identification and credential has invalid check result', async () => {
4319
+ setMockVerifyCredentials({
4320
+ ...DEFAULT_CREDENTIAL_CHECKS,
4321
+ TRUSTED_ISSUER: CredentialCheckResultValue.FAIL,
4322
+ });
4323
+ fastify.overrides.reqConfig = (config) => ({
4324
+ ...config,
4325
+ autoIdentityCheck: false,
4326
+ });
4327
+ const presentationEmail = await generateKYCPresentation(
4328
+ exchange,
4329
+ 'email'
4330
+ );
4331
+
4332
+ let identifyWebhookPayload;
4333
+ nock(mockVendorUrl)
4334
+ .post(identifyUserOnVendorEndpoint)
4335
+ .reply(200, (uri, body) => {
4336
+ identifyWebhookPayload = body;
4337
+ return {
4338
+ vendorUserId,
4339
+ };
4340
+ });
4341
+
4342
+ const response = await fastify.injectJson({
4343
+ method: 'POST',
4344
+ url: idUrl(tenant),
4345
+ headers: {
4346
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4347
+ },
4348
+ payload: {
4349
+ jwt_vp: await presentationEmail.sign(
4350
+ holderKid,
4351
+ holderKeys.privateKey,
4352
+ holderDid
4353
+ ),
4354
+ exchange_id: exchange._id,
4355
+ },
4356
+ });
4357
+
4358
+ expect(response.statusCode).toEqual(200);
4359
+ expect(response.json).toEqual({
4360
+ token: expect.any(String),
4361
+ exchange: {
4362
+ disclosureComplete: true,
4363
+ exchangeComplete: false,
4364
+ id: exchange._id,
4365
+ type: ExchangeTypes.ISSUING,
4366
+ },
4367
+ });
4368
+ const dbResult = await mongoDb()
4369
+ .collection('vendorUserIdMappings')
4370
+ .findOne({ vendorUserId });
4371
+ const { sub } = decodeJwt(response.json.token);
4372
+ expect(dbResult).toEqual({
4373
+ _id: new ObjectId(sub),
4374
+ vendorUserId,
4375
+ tenantId: new ObjectId(tenant._id),
4376
+ createdAt: expect.any(Date),
4377
+ updatedAt: expect.any(Date),
4378
+ });
4379
+ expect(
4380
+ await mongoDb()
4381
+ .collection('exchanges')
4382
+ .findOne({ _id: new ObjectId(exchange._id) })
4383
+ ).toEqual(
4384
+ expectedExchange(tenant, exchange, presentationEmail, [
4385
+ ExchangeStates.NEW,
4386
+ ExchangeStates.DISCLOSURE_RECEIVED,
4387
+ ExchangeStates.DISCLOSURE_CHECKED,
4388
+ ExchangeStates.IDENTIFIED,
4389
+ ])
4390
+ );
4391
+ expect(identifyWebhookPayload).toEqual({
4392
+ emailCredentials: [
4393
+ {
4394
+ id: emailPayload.vc.id,
4395
+ type: ['EmailV1.0', 'VerifiableCredential'],
4396
+ credentialSubject: emailPayload.vc.credentialSubject,
4397
+ credentialType: 'EmailV1.0',
4398
+ issuer: {
4399
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4400
+ },
4401
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4402
+ credentialChecks: {
4403
+ ...DEFAULT_CREDENTIAL_CHECKS,
4404
+ TRUSTED_ISSUER: CredentialCheckResultValue.FAIL,
4405
+ },
4406
+ },
4407
+ ],
4408
+ idDocumentCredentials: [],
4409
+ phoneCredentials: [],
4410
+ emails: [emailPayload.vc.credentialSubject.email],
4411
+ phones: [],
4412
+ tenantDID: tenant.did,
4413
+ tenantId: tenant._id,
4414
+ exchangeId: exchange._id,
4415
+ });
4416
+ });
4417
+ // eslint-disable-next-line max-len
4418
+ it('should 200 when autoIdentityCheck is off, vendorEndpoint is integrated-issuing-identification and credential has invalid check result', async () => {
4419
+ await mongoDb().collection('vendorUserIdMappings').deleteMany({});
4420
+ await mongoDb().collection('exchanges').deleteMany({});
4421
+ await mongoDb().collection('disclosures').deleteMany({});
4422
+ disclosure = await persistDisclosure({
4423
+ tenant,
4424
+ description: 'Credential Issuance disclosure',
4425
+ types: [{ type: 'EmailV1.0' }],
4426
+ vendorEndpoint: VendorEndpoint.INTEGRATED_ISSUING_IDENTIFICATION,
4427
+ purpose: 'Identification',
4428
+ duration: '6y',
4429
+ identityMatchers: {
4430
+ rules: [
4431
+ {
4432
+ valueIndex: 0, // used for identifying the value
4433
+ path: ['$.emails'], // jsonPath within the credential
4434
+ rule: 'pick', // Rule to execute can be pick, all (if the target is an array), equal (if the target is a singleValue)
4435
+ },
4436
+ ],
4437
+ vendorUserIdIndex: 0,
4438
+ },
4439
+ });
4440
+ const exchangeWithoutIdentityMatcherValues = await persistOfferExchange(
4441
+ {
4442
+ tenant,
4443
+ disclosure,
4444
+ }
4445
+ );
4446
+
4447
+ await persistOfferExchange({
4448
+ tenant,
4449
+ disclosure,
4450
+ identityMatcherValues: ['not-match.olivia@example.com'],
4451
+ });
4452
+ await persistOfferExchange({
4453
+ tenant,
4454
+ disclosure,
4455
+ identityMatcherValues: ['Adam.smith@example.com'],
4456
+ });
4457
+ vendorUserId = 'Adam.smith@example.com';
4458
+
4459
+ const presentationEmail = await generateKYCPresentation(
4460
+ exchangeWithoutIdentityMatcherValues,
4461
+ 'email'
4462
+ );
4463
+ setMockVerifyCredentials({
4464
+ ...DEFAULT_CREDENTIAL_CHECKS,
4465
+ TRUSTED_HOLDER: CredentialCheckResultValue.FAIL,
4466
+ });
4467
+ fastify.overrides.reqConfig = (config) => ({
4468
+ ...config,
4469
+ autoIdentityCheck: false,
4470
+ });
4471
+
4472
+ const response = await fastify.injectJson({
4473
+ method: 'POST',
4474
+ url: idUrl(tenant),
4475
+ headers: {
4476
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4477
+ },
4478
+ payload: {
4479
+ jwt_vp: await presentationEmail.sign(
4480
+ holderKid,
4481
+ holderKeys.privateKey,
4482
+ holderDid
4483
+ ),
4484
+ exchange_id: exchangeWithoutIdentityMatcherValues._id,
4485
+ },
4486
+ });
4487
+
4488
+ expect(response.statusCode).toEqual(200);
4489
+ expect(response.json).toEqual({
4490
+ token: expect.any(String),
4491
+ exchange: {
4492
+ disclosureComplete: true,
4493
+ exchangeComplete: false,
4494
+ id: exchangeWithoutIdentityMatcherValues._id,
4495
+ type: ExchangeTypes.ISSUING,
4496
+ },
4497
+ });
4498
+ const dbResult = await mongoDb()
4499
+ .collection('vendorUserIdMappings')
4500
+ .findOne({ vendorUserId });
4501
+ const { sub } = decodeJwt(response.json.token);
4502
+ expect(dbResult).toEqual({
4503
+ _id: new ObjectId(sub),
4504
+ vendorUserId,
4505
+ tenantId: new ObjectId(tenant._id),
4506
+ createdAt: expect.any(Date),
4507
+ updatedAt: expect.any(Date),
4508
+ });
4509
+ expect(
4510
+ await mongoDb()
4511
+ .collection('exchanges')
4512
+ .findOne({
4513
+ _id: new ObjectId(exchangeWithoutIdentityMatcherValues._id),
4514
+ })
4515
+ ).toEqual(
4516
+ expectedExchange(
4517
+ tenant,
4518
+ exchangeWithoutIdentityMatcherValues,
4519
+ presentationEmail,
4520
+ [
4521
+ ExchangeStates.NEW,
4522
+ ExchangeStates.DISCLOSURE_RECEIVED,
4523
+ ExchangeStates.DISCLOSURE_CHECKED,
4524
+ ExchangeStates.IDENTIFIED,
4525
+ ]
4526
+ )
4527
+ );
4528
+ });
4529
+ it('should 200 when autoIdentityCheck is on and credential has N/A expiration', async () => {
4530
+ setMockVerifyCredentials({
4531
+ ...DEFAULT_CREDENTIAL_CHECKS,
4532
+ UNEXPIRED: CredentialCheckResultValue.NOT_APPLICABLE,
4533
+ });
4534
+ const presentationEmail = await generateKYCPresentation(
4535
+ exchange,
4536
+ 'email'
4537
+ );
4538
+
4539
+ let identifyWebhookPayload;
4540
+ nock(mockVendorUrl)
4541
+ .post(identifyUserOnVendorEndpoint)
4542
+ .reply(200, (uri, body) => {
4543
+ identifyWebhookPayload = body;
4544
+ return {
4545
+ vendorUserId,
4546
+ };
4547
+ });
4548
+
4549
+ const response = await fastify.injectJson({
4550
+ method: 'POST',
4551
+ url: idUrl(tenant),
4552
+ headers: {
4553
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4554
+ },
4555
+ payload: {
4556
+ jwt_vp: await presentationEmail.sign(
4557
+ holderKid,
4558
+ holderKeys.privateKey,
4559
+ holderDid
4560
+ ),
4561
+ exchange_id: exchange._id,
4562
+ },
4563
+ });
4564
+
4565
+ expect(response.statusCode).toEqual(200);
4566
+ expect(response.json).toEqual({
4567
+ token: expect.any(String),
4568
+ exchange: {
4569
+ disclosureComplete: true,
4570
+ exchangeComplete: false,
4571
+ id: exchange._id,
4572
+ type: ExchangeTypes.ISSUING,
4573
+ },
4574
+ });
4575
+ const dbResult = await mongoDb()
4576
+ .collection('vendorUserIdMappings')
4577
+ .findOne({ vendorUserId });
4578
+ const { sub } = decodeJwt(response.json.token);
4579
+ expect(dbResult).toEqual({
4580
+ _id: new ObjectId(sub),
4581
+ vendorUserId,
4582
+ tenantId: new ObjectId(tenant._id),
4583
+ createdAt: expect.any(Date),
4584
+ updatedAt: expect.any(Date),
4585
+ });
4586
+ expect(
4587
+ await mongoDb()
4588
+ .collection('exchanges')
4589
+ .findOne({ _id: new ObjectId(exchange._id) })
4590
+ ).toEqual(
4591
+ expectedExchange(tenant, exchange, presentationEmail, [
4592
+ ExchangeStates.NEW,
4593
+ ExchangeStates.DISCLOSURE_RECEIVED,
4594
+ ExchangeStates.DISCLOSURE_CHECKED,
4595
+ ExchangeStates.IDENTIFIED,
4596
+ ])
4597
+ );
4598
+ expect(identifyWebhookPayload).toEqual({
4599
+ emailCredentials: [
4600
+ {
4601
+ id: emailPayload.vc.id,
4602
+ type: ['EmailV1.0', 'VerifiableCredential'],
4603
+ credentialSubject: emailPayload.vc.credentialSubject,
4604
+ credentialType: 'EmailV1.0',
4605
+ issuer: {
4606
+ id: 'did:velocity:0x0b154da48d0f213c26c4b1d040dc5ff1dbf99ffa',
4607
+ },
4608
+ issuanceDate: expect.stringMatching(ISO_DATETIME_FORMAT),
4609
+ credentialChecks: {
4610
+ ...DEFAULT_CREDENTIAL_CHECKS,
4611
+ UNEXPIRED: CredentialCheckResultValue.NOT_APPLICABLE,
4612
+ },
4613
+ },
4614
+ ],
4615
+ idDocumentCredentials: [],
4616
+ phoneCredentials: [],
4617
+ emails: [emailPayload.vc.credentialSubject.email],
4618
+ phones: [],
4619
+ tenantDID: tenant.did,
4620
+ tenantId: tenant._id,
4621
+ exchangeId: exchange._id,
4622
+ });
4623
+ });
4624
+ });
4625
+ });
4626
+
4627
+ describe('identity presentation @context validation enabled test suite', () => {
4628
+ let exchange;
4629
+ let disclosure;
4630
+ let vendorUserId;
4631
+
4632
+ beforeEach(async () => {
4633
+ fastify.overrides.reqConfig = (config) => ({
4634
+ ...config,
4635
+ enablePresentationContextValidation: true,
4636
+ });
4637
+ disclosure = await persistDisclosure({
4638
+ tenant,
4639
+ description: 'Credential Issuance disclosure',
4640
+ types: [{ type: 'EmailV1.0' }],
4641
+ vendorEndpoint: VendorEndpoint.ISSUING_IDENTIFICATION,
4642
+ purpose: 'Identification',
4643
+ duration: '6y',
4644
+ });
4645
+ ({ vendorUserId } = await newVendorUserIdMapping());
4646
+ exchange = await persistOfferExchange({ tenant, disclosure });
4647
+ });
4648
+
4649
+ it('should 400 when @context is a string and is incorrect value', async () => {
4650
+ const presentationIdDocument = (
4651
+ await generateKYCPresentation(exchange, 'idDocument')
4652
+ ).override({ '@context': 'foo' });
4653
+
4654
+ const response = await fastify.injectJson({
4655
+ method: 'POST',
4656
+ url: idUrl(tenant),
4657
+ headers: {
4658
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4659
+ },
4660
+ payload: {
4661
+ exchange_id: exchange._id,
4662
+ jwt_vp: await presentationIdDocument.sign(
4663
+ holderKid,
4664
+ holderKeys.privateKey,
4665
+ holderDid
4666
+ ),
4667
+ },
4668
+ });
4669
+
4670
+ expect(response.statusCode).toEqual(400);
4671
+ expect(response.json).toEqual(
4672
+ errorResponseMatcher({
4673
+ error: 'Bad Request',
4674
+ message: 'presentation @context is not set correctly',
4675
+ statusCode: 400,
4676
+ errorCode: 'presentation_invalid',
4677
+ })
4678
+ );
4679
+ });
4680
+ it('should 400 when @context is an array and is incorrect value', async () => {
4681
+ const presentationIdDocument = (
4682
+ await generateKYCPresentation(exchange, 'idDocument')
4683
+ ).override({ '@context': ['foo'] });
4684
+
4685
+ const response = await fastify.injectJson({
4686
+ method: 'POST',
4687
+ url: idUrl(tenant),
4688
+ headers: {
4689
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4690
+ },
4691
+ payload: {
4692
+ exchange_id: exchange._id,
4693
+ jwt_vp: await presentationIdDocument.sign(
4694
+ holderKid,
4695
+ holderKeys.privateKey,
4696
+ holderDid
4697
+ ),
4698
+ },
4699
+ });
4700
+
4701
+ expect(response.statusCode).toEqual(400);
4702
+ expect(response.json).toEqual(
4703
+ errorResponseMatcher({
4704
+ error: 'Bad Request',
4705
+ message: 'presentation @context is not set correctly',
4706
+ statusCode: 400,
4707
+ errorCode: 'presentation_invalid',
4708
+ })
4709
+ );
4710
+ });
4711
+
4712
+ it('should 400 when @context is an empty array', async () => {
4713
+ const presentationIdDocument = (
4714
+ await generateKYCPresentation(exchange, 'idDocument')
4715
+ ).override({ '@context': [] });
4716
+
4717
+ const response = await fastify.injectJson({
4718
+ method: 'POST',
4719
+ url: idUrl(tenant),
4720
+ headers: {
4721
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4722
+ },
4723
+ payload: {
4724
+ exchange_id: exchange._id,
4725
+ jwt_vp: await presentationIdDocument.sign(
4726
+ holderKid,
4727
+ holderKeys.privateKey,
4728
+ holderDid
4729
+ ),
4730
+ },
4731
+ });
4732
+
4733
+ expect(response.statusCode).toEqual(400);
4734
+ expect(response.json).toEqual(
4735
+ errorResponseMatcher({
4736
+ error: 'Bad Request',
4737
+ message: 'presentation @context is not set correctly',
4738
+ statusCode: 400,
4739
+ errorCode: 'presentation_invalid',
4740
+ })
4741
+ );
4742
+ });
4743
+
4744
+ it('should 200 when @context is a string and is correct value', async () => {
4745
+ const presentationIdDocument = (
4746
+ await generateKYCPresentation(exchange, 'idDocument')
4747
+ ).override({ '@context': 'https://www.w3.org/2018/credentials/v1' });
4748
+
4749
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(200, {
4750
+ vendorUserId,
4751
+ });
4752
+
4753
+ const response = await fastify.injectJson({
4754
+ method: 'POST',
4755
+ url: idUrl(tenant),
4756
+ headers: {
4757
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4758
+ },
4759
+ payload: {
4760
+ exchange_id: exchange._id,
4761
+ jwt_vp: await presentationIdDocument.sign(
4762
+ holderKid,
4763
+ holderKeys.privateKey,
4764
+ holderDid
4765
+ ),
4766
+ },
4767
+ });
4768
+
4769
+ expect(response.statusCode).toEqual(200);
4770
+ });
4771
+ it('should 200 when @context is an array and is correct value', async () => {
4772
+ const presentationIdDocument = (
4773
+ await generateKYCPresentation(exchange, 'idDocument')
4774
+ ).override({ '@context': ['https://www.w3.org/2018/credentials/v1'] });
4775
+
4776
+ nock(mockVendorUrl).post(identifyUserOnVendorEndpoint).reply(200, {
4777
+ vendorUserId,
4778
+ });
4779
+
4780
+ const response = await fastify.injectJson({
4781
+ method: 'POST',
4782
+ url: idUrl(tenant),
4783
+ headers: {
4784
+ 'x-vnf-protocol-version': `${VnfProtocolVersions.VNF_PROTOCOL_VERSION_2}`,
4785
+ },
4786
+ payload: {
4787
+ exchange_id: exchange._id,
4788
+ jwt_vp: await presentationIdDocument.sign(
4789
+ holderKid,
4790
+ holderKeys.privateKey,
4791
+ holderDid
4792
+ ),
4793
+ },
4794
+ });
4795
+
4796
+ expect(response.statusCode).toEqual(200);
4797
+ });
4798
+ });
4799
+ });
4800
+
4801
+ const expectedExchange = (tenant, exchange, presentationWrapper, states, err) =>
4802
+ mongoify(
4803
+ omitBy((v) => v == null, {
4804
+ ...exchange,
4805
+ tenantId: tenant._id,
4806
+ type: ExchangeTypes.ISSUING,
4807
+ presentationId: presentationWrapper?.presentation?.id,
4808
+ disclosureConsentedAt:
4809
+ presentationWrapper == null ? undefined : expect.any(Date),
4810
+ events: map((state) => ({ state, timestamp: expect.any(Date) }), states),
4811
+ ...credentialTypesObject,
4812
+ err,
4813
+ updatedAt: expect.any(Date),
4814
+ })
4815
+ );