diff --git a/.gitignore b/.gitignore
index 10d1bdb9..e3b659c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,6 @@
**/temp/*
**/tmp/*
*.tsbuildinfo
-
+*.tsimp
+*.log
+packages/siop-oid4vp/lib/schemas/validation/schemaValidation.js
diff --git a/.prettierignore b/.prettierignore
index a8fdfef0..7c4c109f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,4 @@
**/dist
**/coverage
**/*/node_modules
+packages/siop-oid4vp/lib/schemas
diff --git a/README.md b/README.md
index 2bf52657..de4c3015 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
- OpenID for Verifiable Credential Issuance - Client and Issuer
+ OpenID for Verifiable Credentials
@@ -11,9 +11,11 @@ _IMPORTANT the packages are still in an early development stage, which means tha
# Background
-This is a mono-repository with a client and issuer pacakge to request and receive Verifiable Credentials using
+This is a mono-repository with a client and issuer package to request and receive Verifiable Credentials using
the [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) (
-OpenID4VCI) specification for receiving Verifiable Credentials as a holder/subject.
+OpenID4VCI) specification for receiving Verifiable Credentials as a holder/subject. In addition the monorepo contains a package
+for requesting the presentation of Verifiable Credentials and Verifying these presentations [OpenID for Verifiable Presentations](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) (
+OpenID4VP)
OpenID4VCI defines an API designated as Credential Endpoint that is used to issue verifiable credentials and
corresponding OAuth 2.0 based authorization mechanisms (see [RFC6749]) that a Wallet uses to obtain authorization to
@@ -36,8 +38,11 @@ The OpenID4VCI client is typically used in wallet type of applications, where th
The OpenID4VCI issuer is used in issuer type applications, where an organization is issuing the credential(s). More info can be found in the issuer [README](./packages/issuer/README.md).
Please note that the Issuer is a library. It has some examples on how to run it with REST endpoints. If you are however looking for a full solution we suggest our [SSI SDK](https://github.com/Sphereon-Opensource/ssi-sdk) or the [demo](https://github.com/Sphereon-Opensource/OID4VC-demo)
+## OpenID for Verifiable Presentations
-# Flows
+The SIOP-OpenID4VP package is used in wallet type applications and verifier type of applications. Meaning it provides both Wallet (OpenId Provider) and Verifier (Relying Party) functionality. More info can be found in the siop-oid4vp package [README](./packages/siop-oid4vp/README.md)
+
+# OpenID for VCI Flows
The spec lists 2 flows:
@@ -53,3 +58,7 @@ authenticate first.
The below diagram shows the steps involved in the pre-authorized code flow. Note that inner wallet functionalities (like
saving VCs) are out of scope for this library. Also This library doesn't include any functionalities of a VC Issuer
![Flow diagram](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/Sphereon-Opensource/OID4VCI-client/develop/docs/preauthorized-code-flow.puml)
+
+# OpenID for VP Flows
+
+Visit the [README](./packages/siop-oid4vp/README.md) for more information.
diff --git a/jest.json b/jest.json
index 6abdc3d4..ed1dd8c5 100644
--- a/jest.json
+++ b/jest.json
@@ -1,11 +1,6 @@
{
"preset": "ts-jest",
- "moduleFileExtensions": [
- "ts",
- "tsx",
- "js",
- "jsx"
- ],
+ "moduleFileExtensions": ["ts", "tsx", "js", "jsx"],
"collectCoverage": true,
"collectCoverageFrom": [
"packages/**/src/**/*.ts",
@@ -20,23 +15,19 @@
"!**/node_modules/**",
"!**/packages/**/index.ts"
],
- "coverageReporters": [
- "text",
- "lcov",
- "json"
- ],
+ "coverageReporters": ["text", "lcov", "json"],
"coverageDirectory": "./coverage",
"transform": {
"\\.jsx?$": "babel-jest",
- "\\.tsx?$": ["ts-jest", {
- "tsconfig": "./packages/tsconfig-base.json"
- }
+ "\\.tsx?$": [
+ "ts-jest",
+ {
+ "tsconfig": "./packages/tsconfig-base.json"
+ }
]
},
- "testMatch": [
- "**/__tests__/**/*.spec.*",
- "**/tests/**/*.spec.*"
- ],
+ "testMatch": ["**/__tests__/**/*.spec.*", "**/tests/**/*.spec.*"],
+ "modulePathIgnorePatterns": ["/packages/siop-oid4vp"],
"testEnvironment": "node",
"automock": false,
"verbose": true
diff --git a/package.json b/package.json
index 55f888d3..86d6d178 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@sphereon/oid4vci-workspace",
"version": "0.11.0",
- "description": "OpenID for Verifiable Credential Issuance workspace",
+ "description": "OpenID for Verifiable Credentials",
"author": "Sphereon",
"license": "Apache-2.0",
"private": true,
@@ -12,8 +12,8 @@
"fix:prettier": "prettier --write \"{packages,__tests__,!dist}/**/*.{ts,tsx,js,json,md,yml}\"",
"build": "pnpm -r --stream build",
"build:clean": "lerna clean -y && pnpm install && lerna run build:clean --concurrency 1",
- "test:ci": "jest --config=jest.json",
- "test": "jest --verbose --config=jest.json --coverage=true --detectOpenHandles",
+ "test:ci": "jest --config=jest.json && jest --config=packages/siop-oid4vp/jest.json",
+ "test": "jest --verbose --config=jest.json --coverage=true --detectOpenHandles && jest --verbose --config=packages/siop-oid4vp/jest.json --coverage=true --detectOpenHandles",
"clean": "rimraf --glob **/dist **/coverage **/pnpm-lock.yaml packages/**/node_modules node_modules packages/**/tsconfig.tsbuildinfo",
"publish:latest": "lerna publish --conventional-commits --include-merged-tags --create-release github --yes --dist-tag latest --registry https://registry.npmjs.org",
"publish:next": "lerna publish --conventional-prerelease --force-publish --canary --no-git-tag-version --include-merged-tags --preid next --pre-dist-tag next --yes --registry https://registry.npmjs.org",
@@ -49,11 +49,17 @@
"Sphereon",
"Verifiable Credentials",
"OpenID",
+ "SIOP",
+ "Self Issued OpenID Provider",
+ "OPenId for Verifiable Presentations",
"OpenID for Verifiable Credential Issuance",
"OAuth2",
"SSI",
"OpenID4VCI",
+ "OpenID4VP",
"OIDC4VCI",
- "OID4VCI"
+ "OIDC4VP",
+ "OID4VCI",
+ "OID4VP"
]
}
diff --git a/packages/siop-oid4vp/CHANGELOG.md b/packages/siop-oid4vp/CHANGELOG.md
new file mode 100644
index 00000000..e6f76b85
--- /dev/null
+++ b/packages/siop-oid4vp/CHANGELOG.md
@@ -0,0 +1,244 @@
+# Release Notes
+
+
+The DID Auth SIOP typescript library is still in an beta state at this point. Please note that the interfaces might
+still change a bit as the software still is in active development.
+
+## 0.6.5
+- Added:
+ - Initial support for OID4VP draft 20
+ - Removed did-jwt and did-resolver dependencies
+ - Support for pluggable signing and verification methods
+ - Remove Signature Types
+ - Remove Verification Method Types
+ - This PR provides verification and signing 'adapters' for x5c, jwk, and did protected jwts (x5c, and jwk functionality was not present/possible previously)
+
+## 0.6.4 - 2024-04-24
+
+- Fixed:
+ - Success event was emitted even though presentation verification callback failed
+ - Always verify nonces, extract them from VP
+- Updated:
+ - Update to latest @sphereon/ssi-types
+
+## 0.6.3 - 2024-03-20
+
+- Updated:
+ - Update to latest @sphereon/ssi-types, including the latest @sd-jwt packages
+
+## 0.6.2 - 2024-03-04
+
+- Fixed:
+ - RP kept stale options to create the request object, resulting in recreation of the same request object over and over
+
+## 0.6.0 - 2024-02-29
+
+- Added:
+ - Initial support for SIOPv2 draft 11
+ - Initial support for OID4VP draft 18
+ - SD-JWT support
+ - Partial support for http(s) client_ids instead of DIDs. No validation for keys in this case yet though!
+ - Convert presentation submissions that inadvertently come in from external OPs as a string instead of an object
+ - Allow id-token only handling
+ - Allow vp-token only handling
+ - EBSI support
+- Fixed:
+ - issue with determining whether a Presentation Definition reference has been used
+ - vp_token handling and nonce management was incorrect in certain cases (for instance when no id token is used)
+ - Make sure a presentation verification callback result throws an error if it does not verify
+ - Do not put VP token in the id token as default for spec versions above v10 if no explicit location is provided
+ - Several small fixes
+
+## 0.4.2 - 2023-10-01
+
+Fixed an issue with did:key resolution used in Veramo
+
+- Fixed:
+ - Fixed an issue with did:key resolution from Veramo. The driver requires a mediaType which according to the spec is
+ optional. We now always set it as it doesn't hurt to begin with.
+
+## 0.4.1 - 2023-10-01
+
+Fixed not being able to configure the resolver for well-known DIDs
+
+- Fixed:
+ - Well-known DIDs did not use a configured DID resolver and thus always used the universal resolver, which has
+ issues quite often.
+
+## 0.4.0 - 2023-09-28
+
+- Fixed:
+
+ - Claims are not required in the auth request
+ - State is not required in payloads
+ - We didn't handle merging of verification options present on an object and passed in as argument nicely
+
+- Updated:
+
+ - Updated to another JSONPath implementation for improved security `@astronautlabs/jsonpath`
+ - Better error handling and logging in the session manager
+ - Allow for numbers in the scheme thus supporting openid4vp://
+
+- Added:
+ - Allow to pass additional claims as verified data in the authorization response. Which can be handy in case you
+ want to extract data from a VP and pass that to the app that uses this library
+
+## v0.3.1 - 2023-05-17
+
+Bugfix release, fixing RPBuilder export and a client_id bug when not explicitly provided to the RP.
+
+- Fixed:
+ - Changed RPBuilder default export to a named export
+ - Fix #54. The client_id took the whole registration object, instead of the client_id in case it was not provided
+ explicitly
+- Updated:
+ - SSI-types have been updated to the latest version.
+
+## v0.3.0 - 2023-04-30
+
+This release contains many breaking changes. Sorry for these, but this library still is in active development, as
+reflected by the major version still being 0.
+A lot of code has been refactored. Now certain classes have state, instead of passing around objects between static
+methods.
+
+- Added:
+ - Allow to restrict selecting VCs against Formats not communicated in a presentation definition. For instance useful
+ for filtering against a OID4VP RP, which signals support for certain Formats, but uses a definition which does not
+ include this information
+ - Allow to restrict selecting VCs against DID methods not communicated in a presentation definition. For instance
+ useful
+ for filtering against a OID4VP RP, which signals support for certain DID methods, but uses a definition which does
+ not
+ include this information
+ - Allow passing in submission data separately from a VP. Again useful in a OID4VP situation, where presentation
+ submission objects can be transferred next to the VP instead if in the VP
+ - A simple session/state manager for the RP side. This allows to find back definitions for responses coming back in.
+ As this is a library the only implementation is an in memory implementation. It is left up to implementers to
+ create their persistent implementations
+ - Added support for new version of the spec
+ - Support for JWT VC Presentation Profile
+ - Support for DID domain linkage
+- Removed:
+ - Several dependencies have been removed or moved to development dependencies. Mainly the cryptographic libraries
+ have
+ been removed
+- Changed:
+ - Requests and responses now contain state and can be instantiated from scratch/options or from an actual payload
+ - Schema's for AJV are now compiled at build time, instead of at runtime.
+- Fixed:
+ - JSON-LD contexts where not always fetched correctly (Github for instance)
+ - Signature callback function was not always working after creating copies of data
+ - React-native not playing nicely with AJV schema's
+ - JWT VCs/VPs were not always handled correctly
+ - Submission data contained several errors
+ - Holder was sometimes missing from the VP
+ - Too many other fixes to list
+
+## v0.2.14 - 2022-10-27
+
+- Updated:
+ - Updated some dependencies
+
+## v0.2.13 - 2022-08-15
+
+- Updated:
+ - Updated some dependencies
+
+## v0.2.12 - 2022-07-07
+
+- Fixed:
+ - We did not check the proper claims in an AuthResponse to determine the key type, resulting in an invalid JWT
+ header
+ - Removed some remnants of the DID-jwt fork
+
+## v0.2.11 - 2022-07-01
+
+- Updated:
+ - Update to PEX 1.1.2
+ - Update several other deps
+- Fixed:
+ - Only throw a PEX error in case PEX itself has flagged the submission to be in error
+ - Use nonce from request in response if available
+ - Remove DID-JWT fork as the current version supports SIOPv2 iss values
+
+## v0.2.10 - 2022-02-25
+
+- Added:
+ - Add default resolver support to builder
+
+## v0.2.9 - 2022-02-23
+
+- Fixed:
+ - Remove did-jwt dependency, since we use an internal fork for the time being anyway
+
+## v0.2.7 - 2022-02-11
+
+- Fixed:
+ - Revert back to commonjs
+
+## v0.2.6 - 2022-02-10
+
+- Added:
+ - Supplied withSignature support. Allowing to integrate withSignature callbacks, next to supplying private keys or
+ using external custodial signing with authn/authz
+
+## v0.2.5 - 2022-01-26
+
+- Updated:
+ - Update @sphereon/pex to the latest stable version v1.0.2
+ - Moved did-key dep to dev dependency and changed to @digitalcredentials/did-method-key
+
+## v0.2.4 - 2022-01-13
+
+- Updated:
+ - Update @sphereon/pex to latest stable version v1.0.1
+
+## v0.2.3 - 2021-12-10
+
+- Fixed:
+
+ - Check nonce and did support first before verifying JWT
+
+- Updated:
+ - Updated PEX dependency that fixed a JSON-path bug impacting us
+
+## v0.2.2 - 2021-11-29
+
+- Updated:
+ - Updated dependencies
+
+## v0.2.1 - 2021-11-28
+
+- Updated:
+ - Presentation Exchange updated to latest PEX version 0.5.x. The eventual Presentation is not a VP yet (proof will
+ be in next minor release)
+ - Update Uni Resolver client to latest version 0.3.3
+
+## v0.2.0 - 2021-10-06
+
+- Added:
+
+ - Presentation Exchange support [OpenID Connect for Verifiable
+ Presentations(https://openid.net/specs/openid-connect-4-verifiable-presentations-1_0.html)
+
+- Fixed:
+ - Many bug fixes (see git history)
+
+## v0.1.1 - 2021-09-29
+
+- Fixed:
+ - Packaging fix for the did-jwt fork we include for now
+
+## v0.1.0 - 2021-09-29
+
+This is the first Alpha release of the DID Auth SIOP typescript library. Please note that the interfaces might still
+change a bit as the software still is in active development.
+
+- Alpha release:
+
+ - Low level Auth Request and Response service classes
+ - High Level OP and RP role service classes
+ - Support for most of [SIOPv2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html)
+
+- Planned for Beta:
+ - [Support for OpenID Connect for Verifiable Presentations](https://openid.net/specs/openid-connect-4-verifiable-presentations-1_0.html)
diff --git a/packages/siop-oid4vp/LICENSE b/packages/siop-oid4vp/LICENSE
new file mode 100644
index 00000000..5f0d873b
--- /dev/null
+++ b/packages/siop-oid4vp/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2022] [Sphereon B.V.]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/siop-oid4vp/README.md b/packages/siop-oid4vp/README.md
new file mode 100644
index 00000000..50ef7de0
--- /dev/null
+++ b/packages/siop-oid4vp/README.md
@@ -0,0 +1,1078 @@
+
+
+
+
+ Self Issued OpenID Provider (SIOPv2)
+with OpenID4VP support
+
+
+
+
+[![CI](https://github.com/Sphereon-Opensource/SIOP-OpenID4VP/actions/workflows/main.yml/badge.svg)](https://github.com/Sphereon-Opensource/SIOP-OpenID4VP/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/Sphereon-Opensource/SIOP-OpenID4VP/branch/develop/graph/badge.svg?token=9P1JGUYA35)](https://codecov.io/gh/Sphereon-Opensource/SIOP-OpenID4VP) [![NPM Version](https://img.shields.io/npm/v/@sphereon/did-auth-siop.svg)](https://npm.im/@sphereon/did-auth-siop)
+
+An OpenID authentication library conforming to
+the [Self Issued OpenID Provider v2 (SIOPv2)](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html)
+and [OpenID for Verifiable Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html)
+as specified in the OpenID Connect working group.
+
+## Introduction
+
+[SIOP v2](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) is an OpenID specification to allow End-users to act as OpenID Providers (OPs) themselves. Using
+Self-Issued OPs, End-users can authenticate themselves and present claims directly to a Relying Party (RP),
+typically a webapp, without involving a third-party Identity Provider. This makes the interactions fully self sovereign, as
+it doesn't depend on any third parties and strictly happens peer 2 peer, yet still using well known constructs from the OpenID protocol.
+
+Next to the user acting as an OpenID Provider, this library also has support for Verifiable Presentations using
+the [Presentation Exchange](https://identity.foundation/presentation-exchange/) provided by
+our [PEX](https://github.com/Sphereon-Opensource/pex) library. This means that the Relying Party can express submission
+requirements in the form of Presentation Definitions, defining the Verifiable Credentials(s) types it would like to receive from the User/OP.
+The OP then checks whether it has the credentials to support the Presentation Definition. Only if that is the case it will send the relevant (parts of
+the) credentials as a Verifiable Presentation in the Authorization Response destined for the Webapp/Relying Party. The
+relying party in turn checks validity of the Verifiable Presentation(s) as well as the match with the submission
+requirements. Only if everything is verified successfully the RP serves the protected page(s). This means that the
+authentication can be extended with claims about the authenticating entity, but it can also be used to easily consume
+credentials from supporting applications, without having to setup DIDComm connections for instance. These credentials can either be self-asserted or from trusted 3rd party issuer.
+
+The term Self-Issued comes from the fact that the End-users (OP) issue self-signed ID Tokens to prove validity of the
+identifiers and claims. This is a trust model different from regular OpenID Connect where the OP is run by the
+third party who issues ID Tokens on behalf of the End-user to the Relying Party upon the End-user's consent. This means
+the End-User is in control about his/her data instead of the 3rd party OP.
+
+Demo: https://vimeo.com/630104529 and a more stripped down demo: https://youtu.be/cqoKuQWPj-s
+
+## Active Development
+
+_IMPORTANT:_
+
+- _This software still is in an early development stage. As such you should expect breaking changes in APIs, we
+ expect to keep that to a minimum though. Version 0.3.X has changed the external API, especially for Requests, Responses and slightly for the RP/OP classes._
+- _The name of the package also changed from [@sphereon/did-auth-siop](https://www.npmjs.com/package/@sphereon/did-auth-siop) to [@sphereon/siopv2-oid4vp](https://www.npmjs.com/package/@sphereon/SIOP-OpenID4VP), to better reflect specification name changes_
+
+## Functionality
+
+This library supports:
+
+- Generic methods to verify and create/sign Json Web Tokens (JWTs) as used in OpenID Connect, with adapter for Decentralized Identifiers (DIDs), JSON Web Keys (JWK), x509 certificates
+- OP class to create Authorization Requests and verify Authorization Responses
+- RP class to verify Authorization Requests and create Authorization Responses
+- Verifiable Presentation and Presentation Exchange support on the RP and OP sides, according to the OpenID for Verifiable Presentations (OID4VP) and Presentation Exchange specifications
+- SIOPv2 specification version discovery with support for the latest [development version (draft 11)](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html), [Implementers Draft 1](https://openid.net/specs/openid-connect-self-issued-v2-1_0-ID1.html) and the [JWT VC Presentation Interop Profile](https://identity.foundation/jwt-vc-presentation-profile/)
+
+## Steps involved
+
+Flow diagram:
+
+![Flow diagram](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/Sphereon-Opensource/did-auth-siop/develop/docs/auth-flow.puml)
+
+1. Client (OP) initiates an Auth request by POST-ing to an endpoint, like for instance `/did-siop/v1/authentications` or
+ clicking a Login button and scanning a QR code
+2. Web (RP) receives the request and access the RP object which creates the Auth Request as JWT, signs it and
+ returns the response as an OpenID Connect URI
+
+ 1. JWT example:
+
+ ```json
+ // JWT Header
+ {
+ "alg": "ES256K",
+ "kid": "did:ethr:0xcBe71d18b5F1259faA9fEE8f9a5FAbe2372BE8c9#controller",
+ "typ": "JWT"
+ }
+
+ // JWT Payload
+ {
+ "iat": 1632336634,
+ "exp": 1632337234,
+ "response_type": "id_token",
+ "scope": "openid",
+ "client_id": "did:ethr:0xcBe71d18b5F1259faA9fEE8f9a5FAbe2372BE8c9",
+ "redirect_uri": "https://acme.com/siop/v1/sessions",
+ "iss": "did:ethr:0xcBe71d18b5F1259faA9fEE8f9a5FAbe2372BE8c9",
+ "response_mode": "post",
+ "claims": ...,
+ "nonce": "qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg",
+ "state": "b32f0087fc9816eb813fd11f",
+ "registration": {
+ "did_methods_supported": [
+ "did:ethr:",
+ "did:web:"
+ ],
+ "subject_identifiers_supported": "did"
+ }
+ }
+ ```
+
+ 2. The Signed JWT, including the JWS follows the following scheme (JWS Compact
+ Serialization, https://datatracker.ietf.org/doc/html/rfc7515#section-7.1):
+
+ `BASE64URL(UTF8(JWT Protected Header)) || '.' || BASE64URL(JWT Payload) || '.' || BASE64URL(JWS Signature)`
+
+ 3. Create the URI containing the JWT:
+
+ ```
+ openid://?response_type=id_token
+ &scope=openid
+ &client_id=did%3Aethr%3A0xBC9484414c1DcA4Aa85BadBBd8a36E3973934444
+ &redirect_uri=https%3A%2F%2Frp.acme.com%2Fsiop%2Fjwts
+ &iss=did%3Aethr%3A0xBC9484414c1DcA4Aa85BadBBd8a36E3973934444
+ &response_mode=post
+ &claims=...
+ &state=af0ifjsldkj
+ &nonce=qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg&state=b32f0087fc9816eb813fd11f
+ ®istration=%5Bobject%20Object%5D
+ &request=
+ ```
+
+ 4. `claims` param can be either a `vp_token` or an `id_token`:
+
+ ```json
+ // vp_token example
+ {
+ "id_token": {
+ "email": null
+ },
+ "vp_token": {
+ "presentation_definition": {
+ "input_descriptors": [
+ {
+ "schema": [
+ {
+ "uri": "https://www.w3.org/2018/credentials/examples/v1/IDCardCredential"
+ }
+ ],
+ "constraints": {
+ "limit_disclosure": "required",
+ "fields": [
+ {
+ "path": [
+ "$.vc.credentialSubject.given_name"
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ // id_token example
+ {
+ "userinfo": {
+ "verifiable_presentations": [
+ "presentation_definition": {
+ "input_descriptors": [
+ {
+ "schema": [
+ {
+ "uri": "https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ },
+ "id_token": {
+ "auth_time": {
+ "essential": true
+ }
+ }
+ }
+ ```
+
+3. Web receives the Auth Request URI Object from RP
+4. Web sends the Auth Request URI in the response body to the client
+5. Client uses the OP instance to create an Auth response
+6. OP verifies the auth request, including checks on whether the RP DID method and key types are supported, next to
+ whether the OP can satisfy the RPs requested Verifiable Credentials
+7. Presentation Exchange process in case the RP had presentation definition(s) in the claims (see Presentation
+ Exchange chapter)
+8. OP creates the auth response object as follows:
+
+ 1. Create an ID token as shown below:
+
+ ```json
+ // JWT encoded ID Token
+ // JWT Header
+ {
+ "alg": "ES256K",
+ "kid": "did:ethr:0x998D43DA5d9d78500898346baf2d9B1E39Eb0Dda#keys-1",
+ "typ": "JWT"
+ }
+ // JWT Payload
+ {
+ "iat": 1632343857.084,
+ "exp": 1632344857.084,
+ "iss": "https://self-issued.me/v2",
+ "sub": "did:ethr:0x998D43DA5d9d78500898346baf2d9B1E39Eb0Dda",
+ "aud": "https://acme.com/siop/v1/sessions",
+ "did": "did:ethr:0x998D43DA5d9d78500898346baf2d9B1E39Eb0Dda",
+ "sub_type": "did",
+ "sub_jwk": {
+ "kid": "did:ethr:0x998D43DA5d9d78500898346baf2d9B1E39Eb0Dda#key-1",
+ "kty": "EC",
+ "crv": "secp256k1",
+ "x": "a4IvJILPHe3ddGPi9qvAyXY9qMTEHvQw5DpQYOJVA0c",
+ "y": "IKOy0JfBF8FOlsOJaC41xiKuGc2-_iqTI01jWHYIyJU"
+ },
+ "nonce": "qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg",
+ "state": "b32f0087fc9816eb813fd11f",
+ "registration": {
+ "issuer": "https://self-issued.me/v2",
+ "response_types_supported": "id_token",
+ "authorization_endpoint": "openid:",
+ "scopes_supported": "openid",
+ "id_token_signing_alg_values_supported": [
+ "ES256K",
+ "EdDSA"
+ ],
+ "request_object_signing_alg_values_supported": [
+ "ES256K",
+ "EdDSA"
+ ],
+ "subject_types_supported": "pairwise"
+ }
+ }
+ ```
+
+ 2. Sign the ID token using the DID key (kid) using JWS scheme (JWS Compact
+ Serialization, https://datatracker.ietf.org/doc/html/rfc7515#section-7.1) and send it to the RP:
+
+ `BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature)`
+
+9. OP returns the Auth response and jwt object to the client
+10. Client does a HTTP POST to redirect_uri from the request (and the aud in the
+ response): https://acme.com/siop/v1/sessions using "application/x-www-form-urlencoded"
+11. Web receives the ID token (auth response) and uses the RP's verify method
+12. RP performs the validation of the token, including withSignature validation, expiration and Verifiable Presentations if
+ any. It returns the Verified Auth Response to WEB
+13. WEB returns a 200 response to Client with a redirect to another page (logged in or confirmation of VP receipt etc).
+14. From that moment on Client can use the Auth Response as bearer token as long as it is valid
+
+## OP and RP setup and interactions
+
+This chapter is a walk-through for using the library using the high-level OP and RP classes. To keep
+it simple, the examples work without hosting partial request/response related objects using HTTP endpoints. They are passed by value, inlined in the respective payloads versus passed by reference.
+
+---
+
+**NOTE**
+
+The examples use Ethereum (ethr) DIDs, but these could be other DIDs as well. The creation of DIDs is out of scope. We
+provide an [ethereum DID example](ethr-dids-testnet.md), if you want to test it yourself without having DIDs currently.
+You could also use the actual example keys and DIDs, as they are valid Ethr Ropsten testnet keys.
+
+---
+
+### Relying Party and SIOP should have keys and DIDs
+
+This library does not provide methods for signing and verifying tokens and authorization requests. Verification and Signing functionality must be externally provided.
+
+### Setting up the Relying Party (RP)
+
+The Relying Party, typically a web app, but can also be something else, like a mobile app.
+The consumer of this library must provide means for creating and verifying JWT to the RP class instance.
+This library provides adapters for creating and verifying did, jwk, and x5c protected JWT`s.
+
+Both the actual JWT request and the
+registration metadata will be sent as part of the Auth Request since we pass them by value instead of by reference where
+we would have to host the data at the reference URL. The redirect URL means that the OP will need to deliver the
+auth response at the URL specified by the RP. We also populated the RP with a `PresentationDefinition` claim,
+meaning we expect the OP to send in a Verifiable Presentation that matches our definition.
+You can pass where you expect this presentation_definition to end up via the required `location` property.
+This is either a top-level vp_token or it becomes part of the id_token.
+
+```typescript
+// The relying party (web) private key and DID and DID key (public key)
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+
+function verifyJwtCallback(): VerifyJwtCallback {
+ return async (jwtVerifier, jwt) => {
+ if (jwtVerifier.method === 'did') {
+ // verify didJwt's
+ } else if (jwtVerifier.method === 'x5c') {
+ // verify x5c certificate protected jwt's
+ } else if (jwtVerifier.method === 'jwk') {
+ // verify jwk certificate protected jwt's
+ } else if (jwtVerifier.method === 'custom') {
+ // Only called if based on the jwt the verification method could not be determined
+ throw new Error(`Unsupported JWT verifier method ${jwtIssuer.method}`)
+ }
+ }
+}
+
+function createJwtCallback(): CreateJwtCallback {
+ return async (jwtIssuer, jwt) => {
+ if (jwtIssuer.method === 'did') {
+ // create didJwt
+ } else if (jwtIssuer.method === 'x5c') {
+ // create x5c certificate protected jwt
+ } else if (jwtIssuer.method === 'jwk') {
+ // create a jwk certificate protected jwt
+ } else if (jwtIssuer.method === 'custom') {
+ // Only called if no or a Custom jwtIssuer was passed to the respective methods
+ throw new Error(`Unsupported JWT issuer method ${jwtIssuer.method}`)
+ }
+ }
+}
+
+const rp = RP.builder()
+ .redirect(EXAMPLE_REDIRECT_URL)
+ .requestBy(PassBy.VALUE)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withCreateJwtCallback(createJwtCallback)
+ .withVerifyJwtCallback(verifyJwtCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withClientMetadata({
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subjectSyntaxTypesSupported: ['did', 'did:ethr'],
+ passBY: PassBy.VALUE,
+ })
+ .addPresentationDefinitionClaim({
+ definition: {
+ input_descriptors: [
+ {
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ },
+ location: PresentationLocation.VP_TOKEN, // Toplevel vp_token response expected. This also can be ID_TOKEN
+ })
+ .build()
+```
+
+### OpenID Provider (OP)
+
+The OP, typically a useragent together with a mobile phone in a cross device flow is accessing a protected resource at the RP, or needs to sent
+in Verifiable Presentations. The consumer of the library must provide means for creating and verifying JWT to the OP class instance.
+This library provides adapters for creating and verifying did, jwk, and x5c protected JWT`s.
+
+```typescript
+const op = OP.builder()
+ .withExpiresIn(6000)
+ .addDidMethod('ethr')
+ .withCreateJwtCallback(createJwtCallback)
+ .withVerifyJwtCallback(verifyJwtCallback)
+ .withClientMetadata({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subjectSyntaxTypesSupported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ })
+ .build()
+```
+
+### RP creates the Auth Request
+
+The Relying Party creates the Auth Request. This could have been triggered by the OP accessing a URL, or clicking a button
+for instance. The Created SIOP V2 Auth Request could also be displayed as a QR code for cross-device flows. In the below text we are
+leaving the transport out of scope.
+
+Given we already have configured the RP itself, all we need to provide is a nonce and state for this request. These will
+be communicated throughout the process. The RP definitely needs to keep track of these values for later usage. If no
+nonce and state are provided then the createAuthorizationRequest method will automatically provide values for these and
+return them in the object that is returned from the method.
+
+Next to the nonce we could also pass in claim options, for instance to specify a Presentation Definition. We have
+already configured the RP itself to have a Presentation Definition, so we can omit it in the request creation, as the RP
+class will take care of that on every Auth Request creation.
+When creating signed objects on the OP and RP side, a jwtIssuer can be specified.
+These adapters provide information about how the jwt will be signed later and metadata to set certain fields in the JWT,
+This means that the JWT only needs to be signed and not necessarily modified by the consumer of this library.
+If the jwtIssuer is omitted the createJwtCallback will be called with method 'custom' indicating that it's up to the consumer
+to populate required fields before the JWT is signed.
+
+```typescript
+const authRequest = await rp.createAuthorizationRequest({
+ correlationId: '1',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ jwtIssuer: { method: 'did', didUrl: 'did:key:v4zagSPkqFJxuNWu#zUC74VEqqhEHQc', alg: SigningAlgo.EDDSA },
+})
+
+console.log(`nonce: ${authRequest.requestOpts.nonce}, state: ${authRequest.requestOpts.state}`)
+// nonce: qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg, state: b32f0087fc9816eb813fd11f
+
+console.log(await authRequest.uri().then((uri) => uri.encodedUri))
+// openid://?response_type=id_token&scope=openid&client_id=did.......&jwt=ey..........
+```
+
+#### Optional: OP Auth Request Payload parsing access
+
+The OP class has a method that both parses the Auth Request URI as it was created by the RP, but it als
+resolves both the JWT and the Registration values from the Auth Request Payload. Both values can be either
+passed by value in the Auth Request, meaning they are present in the request, or passed by reference, meaning
+they are hosted by the OP. In the latter case the values have to be retrieved from an https endpoint. The parseAuthorizationRequestURI takes
+care of both values and returns the Auth Request Payload for easy access, the resolved signed JWT as well as
+the resolved registration metadata of the RP. Please note that the Auth Request Payload that is also returned
+is the original payload from the URI, so it will not contain the resolved JWT nor Registration if the OP passed one of
+them by reference instead of value. Only the direct access to jwt and registration in the Parsed Auth Request
+URI are guaranteed to be resolved.
+
+---
+
+**NOTE**
+
+Please note that the parsing also automatically happens when calling the verifyAuthorizationRequest method with a URI
+as input argument. This method allows for manual parsing if needed.
+
+---
+
+```typescript
+const parsedReqURI = op.parseAuthorizationRequestURI(reqURI.encodedUri)
+
+console.log(parsedReqURI.requestPayload.request)
+// ey....... , but could be empty if the OP would have passed the request by reference usiing request_uri!
+
+console.log(parsedReqURI.jwt)
+// ey....... , always resolved even if the OP would have passed the request by reference!
+```
+
+#### OP Auth Request verification
+
+The Auth Request from the RP in the form of a URI or JWT string needs to be verified by the OP. The
+verifyAuthorizationRequest method of the OP class takes care of this. As input it expects either the URI or the JWT
+string. IF a JWT is supplied it will use the JWT directly, if a URI is provided it
+will internally parse the URI and extract/resolve the JWT before passing it to the provided verifyJwtCallback.
+The jwtVerifier in the verifyJwtCallback is augmented with metadata to simplify jwt verification for each adapter.
+The options can contain an optional nonce, which means the
+request will be checked against the supplied nonce, otherwise the supplied nonce is only checked for presence. Normally
+the OP doesn't know the nonce beforehand, so this option can be left out.
+
+The verified Auth Request object returned again contains the Auth Request payload, and the issuer.
+
+---
+
+**NOTE**
+
+In the below example we directly access requestURI.encodedUri, in a real world scenario the RP and OP don't have access
+to shared objects. Normally you would have received the openid:// URI as a string, which you can also directly pass into
+the verifyAuthorizationRequest or parse methods of the OP class. The method accepts both a JWT or an openid:// URI as
+input
+
+---
+
+```typescript
+const verifiedReq = op.verifyAuthorizationRequest(reqURI.encodedUri) // When an HTTP endpoint is used this would be the uri found in the body
+// const verifiedReq = op.verifyAuthorizationRequest(parsedReqURI.jwt); // If we have parsed the URI using the above optional parsing
+
+console.log(`RP DID: ${verifiedReq.issuer}`)
+// RP DID: did:ethr:ropsten:0x028360fb95417724cb7dd2ff217b15d6f17fc45e0ffc1b3dce6c2b8dd1e704fa98
+```
+
+### OP Presentation Exchange
+
+The Verified Request object created in the previous step contains a `presentationDefinitions` array property in case the
+OP wants to receive a Verifiable Presentation according to
+the [OpenID Connect for Verifiable Presentations (OIDC4VP)](https://openid.net/specs/openid-connect-4-verifiable-presentations-1_0.html)
+specification. If this is the case we need to select credentials and create a Verifiable Presentation. If the OP doesn't
+need to receive a Verifiable Presentation, meaning the presentationDefinitions property is undefined or empty, you can
+continue to the next chapter and create the Auth Response immediately.
+
+See the below sub flow for Presentation Exchange to explain the process:
+
+![PE Flow diagram](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/Sphereon-Opensource/did-auth-siop/develop/docs/presentation-exchange.puml)
+
+#### Create PresentationExchange object
+
+If the `presentationDefinitions` array property is present it means the op.verifyAuthorizationRequest already has
+established that the Presentation Definition(s) itself were valid and present. It has populated the
+presentationDefinitions array for you. If the definition was not valid, the verify method would have thrown an error,
+which means you should never continue the authentication flow!
+
+Now we have to create a `PresentationExchange` object and pass in both the available Verifiable Credentials (typically
+from your wallet) and the holder DID.
+
+---
+
+**NOTE**
+
+The verifiable credentials you pass in to the PresentationExchange methods do not get sent to the RP. Only the
+submissionFrom method creates a VP, which you should manually add as an option to the createAuthorizationResponse
+method.
+
+---
+
+```typescript
+import { PresentationExchange } from './PresentationExchange'
+import { PresentationDefinition } from '@sphereon/pe-models'
+
+const verifiableCredentials: VerifiableCredential[] = [VC1, VC2, VC3] // This typically comes from your wallet
+const presentationDefs: PresentationDefinition[] = verifiedReq.presentationDefinitions
+
+if (presentationDefs) {
+ const pex = new PresentationExchange({
+ did: op.authResponseOpts.did,
+ allVerifiableCredentials: verifiableCredentials,
+ })
+}
+```
+
+#### Filter Credentials that match the Presentation Definition
+
+Now we need to filter the VCs from all the available VCs to an array that matches the Presentation Definition(s) from
+the RP. If the OP, or rather the PresentationExchange instance doesn't have all credentials to satisfy the Presentation
+Definition from the OP, the method will throw an error. Do not try to authenticate in that case!
+
+The selectVerifiableCredentialsForSubmission method returns the filtered VCs. These VCs can satisfy the submission
+requirements from the Presentation Definition. You have to do a manual selection yourself (see note below).
+
+---
+
+**NOTE**
+
+You can have multiple VCs that match a single definition. That can be because the OP uses a definition that wants to
+receive multiple different VCs as part of the Verifiable Presentation, but it can also be that you have multiple VCs
+that match a single constraint from a single definition. Lastly there can be multiple definitions. You always have to do
+a final manual selection of VCs from your application (outside of the scope of this library).
+
+---
+
+```typescript
+// We are only checking the first definition to not make the example too complex
+const checked = await pex.selectVerifiableCredentialsForSubmission(presentationDefs[0])
+// Has errors if the Presentation Definition has requirements we cannot satisfy.
+if (checked.errors) {
+ // error handling here
+}
+const matches: SubmissionRequirementMatch = checked.matches
+
+// Returns the filtered credentials that do match
+```
+
+#### Application specific selection and approval
+
+The previous step has filtered the VCs for you into the matches constant. But the user really has to acknowledge that
+he/she will be sending in a VP containing the VCs. As mentioned above the selected VCs might still need more filtering
+by the user. This part is out of the scope of this library as it is application specific. For more info also see
+the [PEX library](https://github.com/Sphereon-Opensource/pex).
+
+In the code examples we will use 'userSelectedCredentials' as variable for the outcome of this process.
+
+```typescript
+// Your application process here, resulting in:
+import { IVerifiableCredential } from '@sphereon/pex'
+
+const userSelectedCredentials: VerifiableCredential[] // Your selected credentials
+```
+
+#### Create the Verifiable Presentation from the user selected VCs
+
+Now that we have the final selection of VCs, the Presentation Exchange class will create the Verifiable Presentation for
+you. You can optionally sign the Verifiable Presentation, which is out of the scope of this library. As long as the VP
+contains VCs which as subject has the same DID as the OP, the RP can know that the VPs are valid, simply by the fact
+that withSignature of the resulting Auth Response is signed by the private key belonging to the OP and the VP.
+
+---
+
+**NOTE**
+
+We do not support signed selective disclosure yet. The VP will only contain attributes that are requested if the
+Presentation Definition wanted to limit disclosure. You need BBS+ signatures for instance to sign a VP with selective
+disclosure. Unsigned selective disclosure is possible, where the RP relies on the Auth Response being signed
+as long as the VP subject DIDs match the OP DID.
+
+---
+
+```typescript
+// We are only creating a presentation out of the first definition to keep the example simple
+const verifiablePresentation = await pex.submissionFrom(presentationDefs[0], userSelectedCredentials)
+
+// Optionally sign the verifiable presentation here (outside of SIOP library scope)
+```
+
+#### End of Presentation Exchange
+
+Once the VP is returned it means we have gone through the Presentation Exchange process as defined
+in [OpenID Connect for Verifiable Presentations (OIDC4VP)](https://openid.net/specs/openid-connect-4-verifiable-presentations-1_0.html)
+. We can now continue to the regular flow of creating the Auth Response below, all we have to do is pass the
+VP in as an option.
+
+### OP creates the Auth Response using the Verified Request
+
+Using the Verified Request object we got back from the op.verifyAuthorizationRequest method, we can now start to create
+the Auth Response. If we were in the Presentation Exchange flow because the request contained a Presentation
+Definition we now need to pass in the Verifiable Presentations using the vp option. If there was no Presentation
+Definition, do not supply a Verifiable Presentation! The method will check for these constraints.
+
+```typescript
+import { PresentationLocation, VerifiablePresentationTypeFormat } from './SIOP.types'
+
+// Example with Verifiabl Presentation in linked data proof format and as part of the vp_token
+const vpOpt = {
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ presentation: verifiablePresentation,
+ location: PresentationLocation.VP_TOKEN,
+}
+
+const authRespWithJWT = await op.createAuthorizationResponse(verifiedReq, { vp: [vpOpt] })
+
+// Without Verifiable Presentation
+// const authRespWithJWT = await op.createAuthorizationResponse(verifiedReq);
+```
+
+### OP submits the Auth Response to the RP
+
+We are now ready to submit the Auth Response to the RP. The OP class has the submitAuthorizationResponse
+method which accepts the response object. It will automatically submit to the correct location as specified by the RP in
+its request. It expects a response in the 200 range. You get access to the HTTP response from the fetch API as a return
+value.
+
+```typescript
+// Example with Verifiable Presentation
+const response = await op.submitAuthorizationResponse(authRespWithJWT)
+```
+
+### RP verifies the Auth Response
+
+```typescript
+const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponseJwt(authRespWithJWT.jwt, {
+ audience: EXAMPLE_REDIRECT_URL,
+})
+
+expect(verifiedAuthResponseWithJWT.jwt).toBeDefined()
+expect(verifiedAuthResponseWithJWT.payload.state).toMatch('b32f0087fc9816eb813fd11f')
+expect(verifiedAuthResponseWithJWT.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+```
+
+## AuthorizationRequest class
+
+In the previous chapter we have seen the highlevel OP and RP classes. These classes use the Auth Request and
+Response objects explained in this chapter and the next chapter. If you want you can do most interactions using these
+classes at a lower level. This however means you will not get automatic resolution of values passed by reference like
+for instance request and registration data.
+
+### createURI
+
+Create a signed URL encoded URI with a signed SIOP Auth Request
+
+#### Data Interface
+
+```typescript
+interface AuthorizationRequestURI extends SIOPURI {
+ jwt?: string; // The JWT when requestBy was set to mode Reference, undefined if the mode is Value
+ requestOpts: AuthorizationRequestOpts; // The supplied request opts as passed in to the method
+ requestPayload: AuthorizationRequestPayload; // The json payload that ends up signed in the JWT
+}
+
+export type SIOPURI = {
+ encodedUri: string; // The encode JWT as URI
+ encodingFormat: UrlEncodingFormat; // The encoding format used
+};
+
+// https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#section-8
+export interface AuthorizationRequestOpts {
+ authorizationEndpoint?: string;
+ redirectUri: string; // The redirect URI
+ requestBy: ObjectBy; // Whether the request is returned by value in the URI or retrieved by reference at the provided URL
+ signature: InternalSignature | ExternalSignature | NoSignature; // Whether no withSignature is being used, internal (access to private key), or external (hosted using authentication)
+ checkLinkedDomain?: CheckLinkedDomain; // determines how we'll handle the linked domains for this RP
+ responseMode?: ResponseMode; // How the URI should be returned. This is not being used by the library itself, allows an implementor to make a decision
+ responseContext?: ResponseContext; // Defines the context of these opts. Either RP side or OP side
+ responseTypesSupported?: ResponseType[];
+ claims?: ClaimOpts; // The claims, uncluding presentation definitions
+ registration: RequestRegistrationOpts; // Registration metadata options
+ nonce?: string; // An optional nonce, will be generated if not provided
+ state?: string; // An optional state, will be generated if not provided
+ scopesSupported?: Scope[];
+ subjectTypesSupported?: SubjectType[];
+ requestObjectSigningAlgValuesSupported?: SigningAlgo[];
+ revocationVerificationCallback?: RevocationVerificationCallback;
+ // slint-disable-next-line @typescript-eslint/no-explicit-any
+ // [x: string]: any;
+}
+
+static async createURI(opts: SIOP.AuthorizationRequestOpts): Promise
+```
+
+#### Usage
+
+```typescript
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1'
+
+const opts: AuthorizationRequestOpts = {
+ checkLinkedDomain: CheckLinkedDomain.NEVER,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ redirectUri: EXAMPLE_REDIRECT_URL,
+ requestBy: {
+ type: PassBy.VALUE,
+ },
+ signature: {
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectSyntaxTypesSupported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ registrationBy: {
+ type: PassBy.VALUE,
+ },
+ },
+}
+
+AuthorizationRequest.createURI(opts).then((uri) => console.log(uri.encodedUri))
+
+// Output:
+// openid://
+// ?response_type=id_token
+// &scope=openid
+// &client_id=did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0
+// &redirect_uri=https://acme.com/hello&iss=did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0
+// &response_mode=post
+// &response_context=rp
+// &nonce=HxhBU9jBRVP51Z6J0eQ5AxeKoWK9ChApWRrumIqnixc
+// &state=cbde3cdc5389f3be94063be3
+// ®istration={
+// "id_token_signing_alg_values_supported":["EdDSA","ES256"],
+// "request_object_signing_alg_values_supported":["EdDSA","ES256"],
+// "response_types_supported":["id_token"],
+// "scopes_supported":["openid did_authn","openid"],
+// "subject_types_supported":["pairwise"],
+// "subject_syntax_types_supported":["did:ethr:","did"],
+// "vp_formats":{
+// "ldp_vc":{
+// "proof_type":["EcdsaSecp256k1Signature2019","EcdsaSecp256k1Signature2019"]
+// }
+// }
+// }
+// &request=eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXRocjoweDAxMDZhMmU5ODViMUUxRGU5QjVkZGI0YUY2ZEM5ZTkyOEY0ZTk5RDAja2V5cy0xIiwidHlwIjoiSldUIn0.eyJpYXQiOjE2NjQ0Mzk3MzMsImV4cCI6MTY2NDQ0MDMzMywicmVzcG9uc2VfdHlwZSI6ImlkX3Rva2VuIiwic2NvcGUiOiJvcGVuaWQiLCJjbGllbnRfaWQiOiJkaWQ6ZXRocjoweDAxMDZhMmU5ODViMUUxRGU5QjVkZGI0YUY2ZEM5ZTkyOEY0ZTk5RDAiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjbWUuY29tL2hlbGxvIiwiaXNzIjoiZGlkOmV0aHI6MHgwMTA2YTJlOTg1YjFFMURlOUI1ZGRiNGFGNmRDOWU5MjhGNGU5OUQwIiwicmVzcG9uc2VfbW9kZSI6InBvc3QiLCJyZXNwb25zZV9jb250ZXh0IjoicnAiLCJub25jZSI6Ikh4aEJVOWpCUlZQNTFaNkowZVE1QXhlS29XSzlDaEFwV1JydW1JcW5peGMiLCJzdGF0ZSI6ImNiZGUzY2RjNTM4OWYzYmU5NDA2M2JlMyIsInJlZ2lzdHJhdGlvbiI6eyJpZF90b2tlbl9zaWduaW5nX2FsZ192YWx1ZXNfc3VwcG9ydGVkIjpbIkVkRFNBIiwiRVMyNTYiXSwicmVxdWVzdF9vYmplY3Rfc2lnbmluZ19hbGdfdmFsdWVzX3N1cHBvcnRlZCI6WyJFZERTQSIsIkVTMjU2Il0sInJlc3BvbnNlX3R5cGVzX3N1cHBvcnRlZCI6WyJpZF90b2tlbiJdLCJzY29wZXNfc3VwcG9ydGVkIjpbIm9wZW5pZCBkaWRfYXV0aG4iLCJvcGVuaWQiXSwic3ViamVjdF90eXBlc19zdXBwb3J0ZWQiOlsicGFpcndpc2UiXSwic3ViamVjdF9zeW50YXhfdHlwZXNfc3VwcG9ydGVkIjpbImRpZDpldGhyOiIsImRpZCJdLCJ2cF9mb3JtYXRzIjp7ImxkcF92YyI6eyJwcm9vZl90eXBlIjpbIkVjZHNhU2VjcDI1NmsxU2lnbmF0dXJlMjAxOSIsIkVjZHNhU2VjcDI1NmsxU2lnbmF0dXJlMjAxOSJdfX19fQ.owSdQP3ZfOyHryCIO86zB5qenzd5l2AUcEZhA3TvlUWNDJyhhzIgZmBgzV4OMilczr2AJss5HGqxHPmBRTaHcQ
+```
+
+### verifyJWT
+
+Verifies a SIOP Auth Request JWT. Throws an error if the verifation fails. Returns the verified JWT and
+metadata if the verification succeeds
+
+#### Data Interface
+
+```typescript
+export interface VerifiedAuthorizationRequestWithJWT extends VerifiedJWT {
+ payload: AuthorizationRequestPayload; // The unsigned Auth Request payload
+ presentationDefinitions?: PresentationDefinitionWithLocation[]; // The optional presentation definition objects that the RP requests
+ verifyOpts: VerifyAuthorizationRequestOpts; // The verification options for the Auth Request
+}
+
+export interface VerifiedJWT {
+ payload: Partial; // The JWT payload
+ didResolutionResult: DIDResolutionResult;// DID resolution result including DID document
+ issuer: string; // The issuer (did) of the JWT
+ signer: VerificationMethod; // The matching verification method from the DID that was used to sign
+ jwt: string; // The JWT
+}
+
+export interface VerifyAuthorizationRequestOpts {
+ verification: Verification
+ nonce?: string; // If provided the nonce in the request needs to match
+ verifyCallback?: VerifyCallback;
+}
+
+export interface DIDResolutionResult {
+ didResolutionMetadata: DIDResolutionMetadata // Did resolver metadata
+ didDocument: DIDDocument // The DID document
+ didDocumentMetadata: DIDDocumentMetadata // DID document metadata
+}
+
+export interface DIDDocument { // Standard DID Document, see DID spec for explanation
+ '@context'?: 'https://www.w3.org/ns/did/v1' | string | string[]
+ id: string
+ alsoKnownAs?: string[]
+ controller?: string | string[]
+ verificationMethod?: VerificationMethod[]
+ authentication?: (string | VerificationMethod)[]
+ assertionMethod?: (string | VerificationMethod)[]
+ keyAgreement?: (string | VerificationMethod)[]
+ capabilityInvocation?: (string | VerificationMethod)[]
+ capabilityDelegation?: (string | VerificationMethod)[]
+ service?: ServiceEndpoint[]
+}
+
+static async verifyJWT(jwt:string, opts: SIOP.VerifyAuthorizationRequestOpts): Promise
+```
+
+#### Usage
+
+```typescript
+const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verification: {
+ resolveOpts: {
+ subjectSyntaxTypesSupported: ['did:ethr'],
+ },
+ },
+}
+const jwt = 'ey..........' // JWT created by RP
+AuthorizationRequest.verifyJWT(jwt).then((req) => {
+ console.log(`issuer: ${req.issuer}`)
+ console.log(JSON.stringify(req.signer))
+})
+// issuer: "did:ethr:0x56C4b92D4a6083Fcee825893A29023cDdfff5c66"
+// "signer": {
+// "id": "did:ethr:0x56C4b92D4a6083Fcee825893A29023cDdfff5c66#controller",
+// "type": "EcdsaSecp256k1RecoveryMethod2020",
+// "controller": "did:ethr:0x56C4b92D4a6083Fcee825893A29023cDdfff5c66",
+// "blockchainAccountId": "0x56C4b92D4a6083Fcee825893A29023cDdfff5c66@eip155:1"
+// }
+```
+
+## AuthorizationResponse class
+
+### createJwtFromRequestJWT
+
+Creates an AuthorizationResponse object from the OP side, using the AuthorizationRequest of the RP and its
+verification as input together with settings from the OP. The Auth Response contains the ID token as well as
+optional Verifiable Presentations conforming to the Submission Requirements sent by the RP.
+
+#### Data interface
+
+```typescript
+export interface AuthorizationResponseOpts {
+ redirectUri?: string; // It's typically comes from the request opts as a measure to prevent hijacking.
+ registration: ResponseRegistrationOpts; // Registration options
+ checkLinkedDomain?: CheckLinkedDomain; // When the link domain should be checked
+ presentationVerificationCallback?: PresentationVerificationCallback; // Callback function to verify the presentations
+ signature: InternalSignature | ExternalSignature; // Using an internal/private key withSignature, or hosted withSignature
+ nonce?: string; // Allows to override the nonce, otherwise the nonce of the request will be used
+ state?: string; // Allows to override the state, otherwise the state of the request will be used
+ responseMode?: ResponseMode; // Response mode should be form in case a mobile device is being used together with a browser
+ did: string; // The DID of the OP
+ vp?: VerifiablePresentationResponseOpts[]; // Verifiable Presentations with location and format
+ expiresIn?: number; // Expiration
+}
+
+export interface VerifiablePresentationResponseOpts extends VerifiablePresentationPayload {
+ location: PresentationLocation;
+}
+
+export enum PresentationLocation {
+ VP_TOKEN = 'vp_token', // VP will be the toplevel vp_token
+ ID_TOKEN = 'id_token', // VP will be part of the id_token in the verifiable_presentations location
+}
+
+export interface VerifyAuthorizationRequestOpts {
+ verification: Verification
+ nonce?: string; // If provided the nonce in the request needs to match
+ verifyCallback?: VerifyCallback // Callback function to verify the domain linkage credential
+}
+
+export interface AuthorizationResponsePayload extends JWTPayload {
+ iss: ResponseIss.SELF_ISSUED_V2 | string; // The SIOP V2 spec mentions this is required
+ sub: string; // did (or thumbprint of sub_jwk key when type is jkt)
+ sub_jwk?: JWK; // JWK containing DID key if subtype is did, or thumbprint if it is JKT
+ aud: string; // redirect_uri from request
+ exp: number; // expiration time
+ iat: number; // issued at
+ state: string; // The state which should match the AuthRequest state
+ nonce: string; // The nonce which should match the AuthRequest nonce
+ did: string; // The DID of the OP
+ registration?: DiscoveryMetadataPayload; // The registration metadata from the OP
+ registration_uri?: string; // The URI of the registration metadata if it is returned by reference/URL
+ verifiable_presentations?: VerifiablePresentationPayload[]; // Verifiable Presentations
+ vp_token?: VerifiablePresentationPayload;
+}
+
+export interface AuthorizationResponseWithJWT {
+ jwt: string; // The signed Response JWT
+ nonce: string; // The nonce which should match the nonce from the request
+ state: string; // The state which should match the state from the request
+ payload: AuthorizationResponsePayload; // The unsigned payload object
+ verifyOpts?: VerifyAuthorizationRequestOpts;// The Auth Request verification parameters that were used
+ responseOpts: AuthorizationResponseOpts; // The Auth Response options used during generation of the Response
+}
+
+static async createJWTFromRequestJWT(requestJwt: string, responseOpts: SIOP.AuthorizationResponseOpts, verifyOpts: SIOP.VerifyAuthorizationRequestOpts): Promise
+```
+
+#### Usage
+
+```typescript
+const responseOpts: AuthorizationResponseOpts = {
+ checkLinkedDomain: CheckLinkedDomain.NEVER,
+ redirectUri: 'https://acme.com/hello',
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subjectSyntaxTypesSupported: ['did:ethr:'],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ registrationBy: {
+ type: PassBy.REFERENCE,
+ referenceUri: 'https://rp.acme.com/siop/jwts',
+ },
+ },
+ signature: {
+ did: 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0',
+ hexPrivateKey: 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f',
+ kid: 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#controller',
+ },
+ did: 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0',
+ responseMode: ResponseMode.POST,
+}
+```
+
+#### Usage
+
+```typescript
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const NONCE = '5c1d29c1-cf7d-4e14-9305-9db46d8c1916'
+const verifyOpts: VerifyAuthorizationResponseOpts = {
+ audience: 'https://rp.acme.com/siop/jwts',
+ nonce: NONCE,
+}
+
+verifyJWT('ey......', verifyOpts).then((jwt) => {
+ console.log(`nonce: ${jwt.payload.nonce}`)
+ // output: nonce: 5c1d29c1-cf7d-4e14-9305-9db46d8c1916
+})
+```
+
+### Verify Revocation
+
+Verifies whether a verifiable credential contained verifiable presentation is revoked
+
+#### Data Interface
+
+```typescript
+export type RevocationVerificationCallback = (
+ vc: W3CVerifiableCredential, // The Verifiable Credential to be checked
+ type: VerifiableCredentialTypeFormat, // Whether it is a LDP or JWT Verifiable Credential
+) => Promise
+```
+
+```typescript
+export interface IRevocationVerificationStatus {
+ status: RevocationStatus // Valid or invalid
+ error?: string
+}
+```
+
+```typescript
+export enum RevocationVerification {
+ NEVER = 'never', // We don't want to verify revocation
+ IF_PRESENT = 'if_present', // If credentialStatus is present, did-auth-siop will verify revocation. If present and not valid an exception is thrown
+ ALWAYS = 'always', // We'll always check the revocation, if not present or not valid, throws an exception
+}
+```
+
+#### Usage
+
+```typescript
+ const verifyRevocation = async (
+ vc: W3CVerifiableCredential,
+ type: VerifiableCredentialTypeFormat
+):Promise => {
+ // Logic to verify the credential status
+ ...
+ return { status, error }
+};
+```
+
+```typescript
+import { verifyRevocation } from './Revocation'
+
+const rp = RP.builder()
+ .withRevocationVerification(RevocationVerification.ALWAYS)
+ .withRevocationVerificationCallback((vc, type) => verifyRevocation(vc, type))
+```
+
+### Verify Presentation Callback
+
+The callback function to verify the verifiable presentation
+
+#### Data interface
+
+```typescript
+export type PresentationVerificationCallback = (args: IVerifiablePresentation) => Promise
+```
+
+```typescript
+export type IVerifiablePresentation = IPresentation & IHasProof
+```
+
+```typescript
+export type PresentationVerificationResult = { verified: boolean }
+```
+
+#### Usage
+
+JsonLD
+
+```typescript
+import { PresentationVerificationResult } from './SIOP.types'
+
+const verifyPresentation = async (vp: IVerifiablePresentation): Promise => {
+ const keyPair = await Ed25519VerificationKey2020.from(VC_KEY_PAIR)
+ const suite = new Ed25519Signature2020({ key: keyPair })
+ suite.verificationMethod = keyPair.id
+ // If the credentials are not verified individually by the library,
+ // it needs to be implemented. In this example, the library does it.
+ const { verified } = await vc.verify({ presentation: vp, suite, challenge: 'challenge', documentLoader: new DocumentLoader().getLoader() })
+ return Promise.resolve({ verified })
+}
+```
+
+or
+
+JWT
+
+```typescript
+import { IVerifiablePresentation } from '@sphereon/ssi-types'
+
+const verifyPresentation = async (vp: IVerifiablePresentation): Promise => {
+ // If the credentials are not verified individually by the library,
+ // it needs to be implemented. In this example, the library does it.
+ await verifyCredentialJWT(jwtVc, getResolver({ subjectSyntaxTypesSupported: ['did:key:'] }))
+ return Promise.resolve({ verified: true })
+}
+```
+
+```typescript
+const rp = RP.builder()
+ .withPresentationVerification((args) => verifyPresentation(args))
+ ...
+```
+
+## Class and Flow diagram of the interactions
+
+Services and objects:
+
+[![](./docs/services-class-diagram.svg)](https://mermaid-js.github.io/mermaid-live-editor/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG5cbmNsYXNzIFJQIHtcbiAgICA8PHNlcnZpY2U-PlxuICAgIGNyZWF0ZUF1dGhlbnRpY2F0aW9uUmVxdWVzdChvcHRzPykgUHJvbWlzZShBdXRoZW50aWNhdGlvblJlcXVlc3RVUkkpXG4gICAgdmVyaWZ5QXV0aGVudGljYXRpb25SZXNwb25zZUp3dChqd3Q6IHN0cmluZywgb3B0cz8pIFByb21pc2UoVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlc3BvbnNlV2l0aEpXVClcbn1cblJQIC0tPiBBdXRoZW50aWNhdGlvblJlcXVlc3RVUklcblJQIC0tPiBWZXJpZmllZEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUXG5SUCAtLT4gQXV0aGVudGljYXRpb25SZXF1ZXN0XG5SUCAtLT4gQXV0aGVudGljYXRpb25SZXNwb25zZVxuXG5jbGFzcyBPUCB7XG4gICAgPDxzZXJ2aWNlPj5cbiAgICBjcmVhdGVBdXRoZW50aWNhdGlvblJlc3BvbnNlKGp3dE9yVXJpOiBzdHJpbmcsIG9wdHM_KSBQcm9taXNlKEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUKVxuICAgIHZlcmlmeUF1dGhlbnRpY2F0aW9uUmVxdWVzdChqd3Q6IHN0cmluZywgb3B0cz8pIFByb21pc2UoVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUKVxufVxuT1AgLS0-IEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUXG5PUCAtLT4gVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUXG5PUCAtLT4gQXV0aGVudGljYXRpb25SZXF1ZXN0XG5PUCAtLT4gQXV0aGVudGljYXRpb25SZXNwb25zZVxuXG5cbmNsYXNzIEF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHMge1xuICA8PGludGVyZmFjZT4-XG4gIHJlZGlyZWN0VXJpOiBzdHJpbmc7XG4gIHJlcXVlc3RCeTogT2JqZWN0Qnk7XG4gIHNpZ25hdHVyZVR5cGU6IEludGVybmFsU2lnbmF0dXJlIHwgRXh0ZXJuYWxTaWduYXR1cmUgfCBOb1NpZ25hdHVyZTtcbiAgcmVzcG9uc2VNb2RlPzogUmVzcG9uc2VNb2RlO1xuICBjbGFpbXM_OiBPaWRjQ2xhaW07XG4gIHJlZ2lzdHJhdGlvbjogUmVxdWVzdFJlZ2lzdHJhdGlvbk9wdHM7XG4gIG5vbmNlPzogc3RyaW5nO1xuICBzdGF0ZT86IHN0cmluZztcbn1cbkF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHMgLS0-IFJlc3BvbnNlTW9kZVxuQXV0aGVudGljYXRpb25SZXF1ZXN0T3B0cyAtLT4gUlBSZWdpc3RyYXRpb25NZXRhZGF0YU9wdHNcblxuXG5cbmNsYXNzIFJQUmVnaXN0cmF0aW9uTWV0YWRhdGFPcHRzIHtcbiAgPDxpbnRlcmZhY2U-PlxuICBzdWJqZWN0SWRlbnRpZmllcnNTdXBwb3J0ZWQ6IFN1YmplY3RJZGVudGlmaWVyVHlwZVtdIHwgU3ViamVjdElkZW50aWZpZXJUeXBlO1xuICBkaWRNZXRob2RzU3VwcG9ydGVkPzogc3RyaW5nW10gfCBzdHJpbmc7XG4gIGNyZWRlbnRpYWxGb3JtYXRzU3VwcG9ydGVkOiBDcmVkZW50aWFsRm9ybWF0W10gfCBDcmVkZW50aWFsRm9ybWF0O1xufVxuXG5jbGFzcyBSZXF1ZXN0UmVnaXN0cmF0aW9uT3B0cyB7XG4gIDw8aW50ZXJmYWNlPj5cbiAgcmVnaXN0cmF0aW9uQnk6IFJlZ2lzdHJhdGlvblR5cGU7XG59XG5SZXF1ZXN0UmVnaXN0cmF0aW9uT3B0cyAtLXw-IFJQUmVnaXN0cmF0aW9uTWV0YWRhdGFPcHRzXG5cblxuY2xhc3MgVmVyaWZ5QXV0aGVudGljYXRpb25SZXF1ZXN0T3B0cyB7XG4gIDw8aW50ZXJmYWNlPj5cbiAgdmVyaWZpY2F0aW9uOiBJbnRlcm5hbFZlcmlmaWNhdGlvbiB8IEV4dGVybmFsVmVyaWZpY2F0aW9uO1xuICBub25jZT86IHN0cmluZztcbn1cblxuY2xhc3MgQXV0aGVudGljYXRpb25SZXF1ZXN0IHtcbiAgICA8PHNlcnZpY2U-PlxuICAgIGNyZWF0ZVVSSShvcHRzOiBBdXRoZW50aWNhdGlvblJlcXVlc3RPcHRzKSBQcm9taXNlKEF1dGhlbnRpY2F0aW9uUmVxdWVzdFVSSSlcbiAgICBjcmVhdGVKV1Qob3B0czogQXV0aGVudGljYXRpb25SZXF1ZXN0T3B0cykgUHJvbWlzZShBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUKTtcbiAgICB2ZXJpZnlKV1Qoand0OiBzdHJpbmcsIG9wdHM6IFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHMpIFByb21pc2UoVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUKVxufVxuQXV0aGVudGljYXRpb25SZXF1ZXN0IDwtLSBBdXRoZW50aWNhdGlvblJlcXVlc3RPcHRzXG5BdXRoZW50aWNhdGlvblJlcXVlc3QgPC0tIFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHNcbkF1dGhlbnRpY2F0aW9uUmVxdWVzdCAtLT4gQXV0aGVudGljYXRpb25SZXF1ZXN0VVJJXG5BdXRoZW50aWNhdGlvblJlcXVlc3QgLS0-IEF1dGhlbnRpY2F0aW9uUmVxdWVzdFdpdGhKV1RcbkF1dGhlbnRpY2F0aW9uUmVxdWVzdCAtLT4gVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUXG5cbmNsYXNzIEF1dGhlbnRpY2F0aW9uUmVzcG9uc2Uge1xuICA8PGludGVyZmFjZT4-XG4gIGNyZWF0ZUpXVEZyb21SZXF1ZXN0SldUKGp3dDogc3RyaW5nLCByZXNwb25zZU9wdHM6IEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VPcHRzLCB2ZXJpZnlPcHRzOiBWZXJpZnlBdXRoZW50aWNhdGlvblJlcXVlc3RPcHRzKSBQcm9taXNlKEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUKVxuICB2ZXJpZnlKV1Qoand0OiBzdHJpbmcsIHZlcmlmeU9wdHM6IFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVzcG9uc2VPcHRzKSBQcm9taXNlKFZlcmlmaWVkQXV0aGVudGljYXRpb25SZXNwb25zZVdpdGhKV1QpXG59XG5BdXRoZW50aWNhdGlvblJlc3BvbnNlIDwtLSBBdXRoZW50aWNhdGlvblJlc3BvbnNlT3B0c1xuQXV0aGVudGljYXRpb25SZXNwb25zZSA8LS0gVmVyaWZ5QXV0aGVudGljYXRpb25SZXF1ZXN0T3B0c1xuQXV0aGVudGljYXRpb25SZXNwb25zZSAtLT4gQXV0aGVudGljYXRpb25SZXNwb25zZVdpdGhKV1RcbkF1dGhlbnRpY2F0aW9uUmVzcG9uc2UgPC0tIFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVzcG9uc2VPcHRzXG5BdXRoZW50aWNhdGlvblJlc3BvbnNlIC0tPiBWZXJpZmllZEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUXG5cbmNsYXNzIEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VPcHRzIHtcbiAgPDxpbnRlcmZhY2U-PlxuICBzaWduYXR1cmVUeXBlOiBJbnRlcm5hbFNpZ25hdHVyZSB8IEV4dGVybmFsU2lnbmF0dXJlO1xuICBub25jZT86IHN0cmluZztcbiAgc3RhdGU_OiBzdHJpbmc7XG4gIHJlZ2lzdHJhdGlvbjogUmVzcG9uc2VSZWdpc3RyYXRpb25PcHRzO1xuICByZXNwb25zZU1vZGU_OiBSZXNwb25zZU1vZGU7XG4gIGRpZDogc3RyaW5nO1xuICB2cD86IFZlcmlmaWFibGVQcmVzZW50YXRpb247XG4gIGV4cGlyZXNJbj86IG51bWJlcjtcbn1cbkF1dGhlbnRpY2F0aW9uUmVzcG9uc2VPcHRzIC0tPiBSZXNwb25zZU1vZGVcblxuY2xhc3MgQXV0aGVudGljYXRpb25SZXNwb25zZVdpdGhKV1Qge1xuICA8PGludGVyZmFjZT4-XG4gIGp3dDogc3RyaW5nO1xuICBub25jZTogc3RyaW5nO1xuICBzdGF0ZTogc3RyaW5nO1xuICBwYXlsb2FkOiBBdXRoZW50aWNhdGlvblJlc3BvbnNlUGF5bG9hZDtcbiAgdmVyaWZ5T3B0cz86IFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHM7XG4gIHJlc3BvbnNlT3B0czogQXV0aGVudGljYXRpb25SZXNwb25zZU9wdHM7XG59XG5BdXRoZW50aWNhdGlvblJlc3BvbnNlV2l0aEpXVCAtLT4gQXV0aGVudGljYXRpb25SZXNwb25zZVBheWxvYWRcbkF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUIC0tPiBWZXJpZnlBdXRoZW50aWNhdGlvblJlcXVlc3RPcHRzXG5BdXRoZW50aWNhdGlvblJlc3BvbnNlV2l0aEpXVCAtLT4gQXV0aGVudGljYXRpb25SZXNwb25zZU9wdHNcblxuXG5jbGFzcyBWZXJpZnlBdXRoZW50aWNhdGlvblJlc3BvbnNlT3B0cyB7XG4gIDw8aW50ZXJmYWNlPj5cbiAgdmVyaWZpY2F0aW9uOiBJbnRlcm5hbFZlcmlmaWNhdGlvbiB8IEV4dGVybmFsVmVyaWZpY2F0aW9uO1xuICBub25jZT86IHN0cmluZztcbiAgc3RhdGU_OiBzdHJpbmc7XG4gIGF1ZGllbmNlOiBzdHJpbmc7XG59XG5cbmNsYXNzIFJlc3BvbnNlTW9kZSB7XG4gICAgPDxlbnVtPj5cbn1cblxuIGNsYXNzIFVyaVJlc3BvbnNlIHtcbiAgICA8PGludGVyZmFjZT4-XG4gICAgcmVzcG9uc2VNb2RlPzogUmVzcG9uc2VNb2RlO1xuICAgIGJvZHlFbmNvZGVkPzogc3RyaW5nO1xufVxuVXJpUmVzcG9uc2UgLS0-IFJlc3BvbnNlTW9kZVxuVXJpUmVzcG9uc2UgPHwtLSBTSU9QVVJJXG5cbmNsYXNzIFNJT1BVUkkge1xuICAgIDw8aW50ZXJmYWNlPj5cbiAgICBlbmNvZGVkVXJpOiBzdHJpbmc7XG4gICAgZW5jb2RpbmdGb3JtYXQ6IFVybEVuY29kaW5nRm9ybWF0O1xufVxuU0lPUFVSSSAtLT4gVXJsRW5jb2RpbmdGb3JtYXRcblNJT1BVUkkgPHwtLSBBdXRoZW50aWNhdGlvblJlcXVlc3RVUklcblxuY2xhc3MgQXV0aGVudGljYXRpb25SZXF1ZXN0VVJJIHtcbiAgPDxpbnRlcmZhY2U-PlxuICBqd3Q_OiBzdHJpbmc7IFxuICByZXF1ZXN0T3B0czogQXV0aGVudGljYXRpb25SZXF1ZXN0T3B0cztcbiAgcmVxdWVzdFBheWxvYWQ6IEF1dGhlbnRpY2F0aW9uUmVxdWVzdFBheWxvYWQ7XG59XG5BdXRoZW50aWNhdGlvblJlcXVlc3RVUkkgLS0-IEF1dGhlbnRpY2F0aW9uUmVxdWVzdFBheWxvYWRcblxuY2xhc3MgVXJsRW5jb2RpbmdGb3JtYXQge1xuICAgIDw8ZW51bT4-XG59XG5cbmNsYXNzIFJlc3BvbnNlTW9kZSB7XG4gIDw8ZW51bT4-XG59XG5cbmNsYXNzIEF1dGhlbnRpY2F0aW9uUmVxdWVzdFBheWxvYWQge1xuICAgIDw8aW50ZXJmYWNlPj5cbiAgICBzY29wZTogU2NvcGU7XG4gICAgcmVzcG9uc2VfdHlwZTogUmVzcG9uc2VUeXBlO1xuICAgIGNsaWVudF9pZDogc3RyaW5nO1xuICAgIHJlZGlyZWN0X3VyaTogc3RyaW5nO1xuICAgIHJlc3BvbnNlX21vZGU6IFJlc3BvbnNlTW9kZTtcbiAgICByZXF1ZXN0OiBzdHJpbmc7XG4gICAgcmVxdWVzdF91cmk6IHN0cmluZztcbiAgICBzdGF0ZT86IHN0cmluZztcbiAgICBub25jZTogc3RyaW5nO1xuICAgIGRpZF9kb2M_OiBESUREb2N1bWVudDtcbiAgICBjbGFpbXM_OiBSZXF1ZXN0Q2xhaW1zO1xufVxuQXV0aGVudGljYXRpb25SZXF1ZXN0UGF5bG9hZCAtLXw-IEpXVFBheWxvYWRcblxuY2xhc3MgIEpXVFBheWxvYWQge1xuICBpc3M_OiBzdHJpbmdcbiAgc3ViPzogc3RyaW5nXG4gIGF1ZD86IHN0cmluZyB8IHN0cmluZ1tdXG4gIGlhdD86IG51bWJlclxuICBuYmY_OiBudW1iZXJcbiAgZXhwPzogbnVtYmVyXG4gIHJleHA_OiBudW1iZXJcbiAgW3g6IHN0cmluZ106IGFueVxufVxuXG5cbmNsYXNzIFZlcmlmaWVkQXV0aGVudGljYXRpb25SZXF1ZXN0V2l0aEpXVCB7XG4gIDw8aW50ZXJmYWNlPj5cbiAgcGF5bG9hZDogQXV0aGVudGljYXRpb25SZXF1ZXN0UGF5bG9hZDsgXG4gIHZlcmlmeU9wdHM6IFZlcmlmeUF1dGhlbnRpY2F0aW9uUmVxdWVzdE9wdHM7IFxufVxuVmVyaWZpZWRKV1QgPHwtLSBWZXJpZmllZEF1dGhlbnRpY2F0aW9uUmVxdWVzdFdpdGhKV1RcblZlcmlmaWVkQXV0aGVudGljYXRpb25SZXF1ZXN0V2l0aEpXVCAtLT4gVmVyaWZ5QXV0aGVudGljYXRpb25SZXF1ZXN0T3B0c1xuVmVyaWZpZWRBdXRoZW50aWNhdGlvblJlcXVlc3RXaXRoSldUIC0tPiBBdXRoZW50aWNhdGlvblJlcXVlc3RQYXlsb2FkXG5cbmNsYXNzIFZlcmlmaWVkQXV0aGVudGljYXRpb25SZXNwb25zZVdpdGhKV1Qge1xuICA8PGludGVyZmFjZT4-XG4gIHBheWxvYWQ6IEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VQYXlsb2FkO1xuICB2ZXJpZnlPcHRzOiBWZXJpZnlBdXRoZW50aWNhdGlvblJlc3BvbnNlT3B0cztcbn1cblZlcmlmaWVkQXV0aGVudGljYXRpb25SZXNwb25zZVdpdGhKV1QgLS0-IEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VQYXlsb2FkXG5WZXJpZmllZEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUIC0tPiBWZXJpZnlBdXRoZW50aWNhdGlvblJlc3BvbnNlT3B0c1xuVmVyaWZpZWRKV1QgPHwtLSBWZXJpZmllZEF1dGhlbnRpY2F0aW9uUmVzcG9uc2VXaXRoSldUXG5cbmNsYXNzIFZlcmlmaWVkSldUIHtcbiAgPDxpbnRlcmZhY2U-PlxuICBwYXlsb2FkOiBQYXJ0aWFsPEpXVFBheWxvYWQ-O1xuICBkaWRSZXNvbHV0aW9uUmVzdWx0OiBESURSZXNvbHV0aW9uUmVzdWx0O1xuICBpc3N1ZXI6IHN0cmluZztcbiAgc2lnbmVyOiBWZXJpZmljYXRpb25NZXRob2Q7XG4gIGp3dDogc3RyaW5nO1xufVxuXG5cbiIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkYXJrXCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6ZmFsc2UsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ)
+
+## Acknowledgements
+
+This library has been partially sponsored by [Gimly](https://www.gimly.io/) as part of the [NGI Ontochain](https://ontochain.ngi.eu/) project. NGI Ontochain has received funding from the European Union’s Horizon 2020 research and innovation programme under grant agreement No 957338
+
+
diff --git a/packages/siop-oid4vp/docs/auth-flow.md b/packages/siop-oid4vp/docs/auth-flow.md
new file mode 100644
index 00000000..4fd2d14c
--- /dev/null
+++ b/packages/siop-oid4vp/docs/auth-flow.md
@@ -0,0 +1 @@
+![Diagram](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/Sphereon-Opensource/did-auth-siop/develop/docs/auth-flow-diagram.puml)
diff --git a/packages/siop-oid4vp/docs/auth-flow.puml b/packages/siop-oid4vp/docs/auth-flow.puml
new file mode 100644
index 00000000..41b56e4d
--- /dev/null
+++ b/packages/siop-oid4vp/docs/auth-flow.puml
@@ -0,0 +1,54 @@
+@startuml
+header SIOP flow diagram
+title
+DID OpenID SIOP Flow
+end title
+
+autonumber
+
+participant "Client\n(OP)" as CLIENT order 0
+participant "OP\nclass" as OP order 1 #White
+participant "RP\nclass" as RP order 2 #White
+participant "Web component\n(RP)" as WEB order 3
+
+activate WEB
+CLIENT -> WEB: HTTPS POST
+
+WEB -> RP: CreateAuthRequest\n(Request Opts)
+activate RP
+RP -> WEB: Return\n
+deactivate RP
+WEB -> CLIENT: 302: Redirect (can include VC request), optionally displays a QR
+deactivate WEB
+
+activate CLIENT
+CLIENT-> OP: create Auth Response Process\n(Auth Request,Response Opts, Verify Opts)
+activate OP
+OP -> OP: verifyAuthRequest\n(Auth Request, Verify Opts)
+OP -> OP: Presentation Exchange process (see below)
+OP -> OP: createAuthResponse\n(Auth Request, Response Opts)
+OP-> CLIENT: Return\n
+deactivate OP
+CLIENT-> WEB: HTTPS POST (can include Verifiable Credentials)
+deactivate CLIENT
+
+
+activate WEB
+WEB -> RP: Verify\n(Auth Response, Verify Opts)
+activate RP
+
+RP -> WEB: Return\n
+deactivate RP
+WEB -> CLIENT: 200
+deactivate WEB
+
+
+== Protected resources ==
+
+CLIENT-> WEB: HTTPS POST \n/protected-resources
+
+activate WEB
+WEB-> WEB: Verify\n
+WEB-> CLIENT: 200:
+deactivate WEB
+@enduml
diff --git a/packages/siop-oid4vp/docs/didjwt-class-diagram.md b/packages/siop-oid4vp/docs/didjwt-class-diagram.md
new file mode 100644
index 00000000..d233e7c0
--- /dev/null
+++ b/packages/siop-oid4vp/docs/didjwt-class-diagram.md
@@ -0,0 +1,107 @@
+```mermaid
+classDiagram
+class DidResolutionOptions {
+ <>
+ accept?: string
+}
+class Resolvable {
+ <>
+ resolve(didUrl: string, options: DidResolutionOptions) Promise(DidResolutionResult)
+}
+DidResolutionOptions --> Resolvable
+DIDResolutionResult <-- Resolvable
+
+class DIDResolutionResult {
+ <>
+ didResolutionMetadata: DIDResolutionMetadata
+ didDocument: DIDDocument | null
+ didDocumentMetadata: DIDDocumentMetadata
+}
+DIDDocumentMetadata <-- DIDResolutionResult
+DIDDocument <-- DIDResolutionResult
+
+class DIDDocumentMetadata {
+ <>
+ created?: string
+ updated?: string
+ deactivated?: boolean
+ versionId?: string
+ nextUpdate?: string
+ nextVersionId?: string
+ equivalentId?: string
+ canonicalId?: string
+}
+
+class DIDDocument {
+ <>
+ '@context'?: 'https://www.w3.org/ns/did/v1' | string | string[]
+ id: string
+ alsoKnownAs?: string[]
+ controller?: string | string[]
+ verificationMethod?: VerificationMethod[]
+ authentication?: (string | VerificationMethod)[]
+ assertionMethod?: (string | VerificationMethod)[]
+ keyAgreement?: (string | VerificationMethod)[]
+ capabilityInvocation?: (string | VerificationMethod)[]
+ capabilityDelegation?: (string | VerificationMethod)[]
+ service?: ServiceEndpoint[]
+}
+VerificationMethod <-- DIDDocument
+
+class VerificationMethod {
+ <>
+ id: string
+ type: string
+ controller: string
+ publicKeyBase58?: string
+ publicKeyJwk?: JsonWebKey
+ publicKeyHex?: string
+ blockchainAccountId?: string
+ ethereumAddress?: string
+}
+
+class JWTPayload {
+ <>
+ iss: string
+ sub?: string
+ aud?: string | string[]
+ iat?: number
+ nbf?: number
+ exp?: number
+ rexp?: number
+}
+class JWTHeader { // This is a standard JWT header
+ <>
+ typ: 'JWT'
+ alg: string // The JWT signing algorithm to use. Supports: [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K
+ [x: string]: any
+}
+
+JsonWebKey <|-- VerificationMethod
+class JsonWebKey {
+ <>
+ alg?: string
+ crv?: string
+ e?: string
+ ext?: boolean
+ key_ops?: string[]
+ kid?: string
+ kty: string
+ n?: string
+ use?: string
+ x?: string
+ y?: string
+}
+
+
+class DidJWT {
+ <>
+ createDidJWT(payload: JWTPayload, options: JWTOptions, header: JWTJHeader) Promise(string)
+ verifyDidJWT(JWT: string, resolver: Resolvable) Promise(boolean)
+}
+JWTPayload --> DidJWT
+JWTOptions --> DidJWT
+JWTHeader --> DidJWT
+Resolvable <-- DidJWT
+
+```
diff --git a/packages/siop-oid4vp/docs/eosio-dids-testnet.md b/packages/siop-oid4vp/docs/eosio-dids-testnet.md
new file mode 100644
index 00000000..fab9b3f5
--- /dev/null
+++ b/packages/siop-oid4vp/docs/eosio-dids-testnet.md
@@ -0,0 +1,110 @@
+# EOSIO DID creation Walk-through
+
+---
+
+#WARNING
+
+**DO NOT USE THIS WALK-THROUGH**
+
+_Currently eosio uses DIDs with a different Verfication Method then all other DIDs (verifiable conditions). This library cannot access/use the public keys in that method yet, so the EOSIO DIDs cannot be used for authentication currently.
+The document is still here, because Gimly hopes to make changes to the DID driver, so that we can support EOSIO DIDs as well_
+
+---
+
+Although this library simply expects DIDs to be present, we provide an example how to create DIDs on the EOSIO Junle testnet. There are 75+ DID methods and creation of DIDs typically happens from code and varies quite a bit. You can use your prefered DID method of choice.
+
+## Relying Party and SIOP should have keys and DIDs
+
+Since the library uses DIDs for both the Relying Party and the Self-Issued OpenID Provider, we expect these DIDs to be present on both sides, If you do not have DIDs this walk-through will result in EOSIO dids for the Relying Party and the OpenID Provider respectively. If you use another DID method the creation might vary, and hopefully is a bit more automated.
+
+### Generate EOS keypairs
+
+Go to: [Jungle3.0 - EOS Test Network Monitor - create keys](https://monitor3.jungletestnet.io/#createKey). You will see a popup/modal that give you a public EOS key and a private EOS key.
+
+- Save these values for the RP, for example:
+ - Public Key: EOS6kKhHvCuWkJDAoNb35qxHnyGCmFQpe1eBYBj9W18iKEQ82vsKZ
+ - Private key: 5JoQQVRYuXfEMBMjY9T96bvsHGfwaXMygnwFNA1enLA5coWQKSi
+- Repeat for the OP, for example:
+ - Public Key: EOS8ZcT5JhRUuLwdQt6j4f2b8opJH1guPrQefTpo9Fqd4fLbKCpyw
+ - Private key: 5Japr2nKKCzfZQHXupqm9hWmhMnifsuePRKgCHHwW4cQsLs4wvu
+
+### Create EOS accounts
+
+Go to : [Jungle3.0 - EOS Test Network Monitor - create account](https://monitor3.jungletestnet.io/#account). You will see a popup/modal in which you have to specify an account name and submit an owner and active public key. Lastly the reCaptcha needs to be checked after which the Create button can be used. Important: Only submit the public keys, never submit private keys! Use the public keys from the above respective steps.
+
+- Create an account for the RP:
+ - Account name example: sioprptest11 (needs to be unique and exactly 12 character and only allows a-z and 1-5!)
+ - Owner Public Key: EOS6kKhHvCuWkJDAoNb35qxHnyGCmFQpe1eBYBj9W18iKEQ82vsKZ (RP key from above step)
+ - Active Public Key: EOS6kKhHvCuWkJDAoNb35qxHnyGCmFQpe1eBYBj9W18iKEQ82vsKZ (RP key from above step)
+
+You will see debug output and red text, which might look like an error at first, but actually this means it succeeded
+
+- Create an account for the OP:
+ - Account name example: siopoptest11 (needs to be unique and exactly 12 character and only allows a-z and 1-5!)
+ - Owner Public Key: EOS8ZcT5JhRUuLwdQt6j4f2b8opJH1guPrQefTpo9Fqd4fLbKCpyw (RP key from above step)
+ - Active Public Key: EOS8ZcT5JhRUuLwdQt6j4f2b8opJH1guPrQefTpo9Fqd4fLbKCpyw (RP key from above step)
+
+You will see debug output and red text, which might look like an error at first, but actually this means it succeeded
+
+### Add balances using a Faucet
+
+Go to : [Jungle3.0 - EOS Test Network Monitor - faucet](https://monitor3.jungletestnet.io/#faucet)
+
+- For the RP:
+ - Fill in the “account name”, eg "sioprptest11", confirm you are not a robot and click “send coins“.
+ - This should result in a balance of 100 EOS and 100 JUNGLE
+- For the OP:
+ - Fill in the “account name”, eg "siopoptest11", confirm you are not a robot and click “send coins“.
+ - This should result in a balance of 100 EOS and 100 JUNGLE
+
+### Increase CPU usage limit
+
+To execute transactions we need the correct CPU limit.
+Go to [Jungle3.0 - EOS Test Network Monitor - powerup](https://monitor3.jungletestnet.io/#powerup)
+
+- For the RP:
+ - Fill in the “account name”, eg "sioprptest11", confirm you are not a robot and click “send coins“ (Don't be alarmed by the button reading 'Send Coins'. That is a mistake in the site as it should read Powerup)
+ - This should result in a transaction with a powerup
+- For the OP:
+ - Fill in the “account name”, eg "siopoptest11", confirm you are not a robot and click “send coins“ (Don't be alarmed by the button reading 'Send Coins'. That is a mistake in the site as it should read Powerup)
+ - This should result in a transaction with a powerup
+
+### Test resolution of the DIDs.
+
+- Go to : https://dev.uniresolver.io
+- In the did-url input box past the below dids and click on the Resolve button. You should get back results:
+ - did:eosio:eos:testnet:jungle:, eg:
+ - did:eosio:eos:testnet:jungle:sioprptest11
+ - did:eosio:eos:testnet:jungle:siopoptest11
+
+### Install/use eosio-did typescript library if you want to create additional DIDs
+
+We want to use the [Gimly-Blockchain/eosio-did](https://github.com/Gimly-Blockchain/eosio-did) typescript library to create eosio did’s. We expect the user to know its way around a development IDE and have npm installed on the computer.
+
+- git checkout https://github.com/Gimly-Blockchain/eosio-did.git
+- cd eosio-git
+- npm install
+- This project has a test to create those did’s, “create.test.ts“. This test requires that a jungleTestKeys.json is present in the root of the project. The content looks like:
+
+```json
+{
+ "name": "[ACCOUNT_NAME]",
+ "private": "[PRIV_KEY]",
+ "public": "[PUB_KEY]"
+}
+```
+
+For the RP:
+
+- Create a rp.json in the root of the project:
+
+```json
+{
+ "name": "sioprptest11",
+ "private": "5JoQQVRYuXfEMBMjY9T96bvsHGfwaXMygnwFNA1enLA5coWQKSi",
+ "public": "EOS6kKhHvCuWkJDAoNb35qxHnyGCmFQpe1eBYBj9W18iKEQ82vsKZ"
+}
+```
+
+- Adjust line 6 of create-test.ts to read `const jungleTestKeys = require('../rp.json');`
+- Execute the test
diff --git a/packages/siop-oid4vp/docs/gimly-logo.png b/packages/siop-oid4vp/docs/gimly-logo.png
new file mode 100644
index 00000000..6504e9c5
Binary files /dev/null and b/packages/siop-oid4vp/docs/gimly-logo.png differ
diff --git a/packages/siop-oid4vp/docs/presentation-exchange.puml b/packages/siop-oid4vp/docs/presentation-exchange.puml
new file mode 100644
index 00000000..03979d80
--- /dev/null
+++ b/packages/siop-oid4vp/docs/presentation-exchange.puml
@@ -0,0 +1,38 @@
+@startuml
+header Presentation Exchange flow diagram
+title
+Presentation Exchange Flow
+end title
+
+autonumber
+
+participant "Client\n(OP)" as CLIENT order 0
+participant "OP\n<>" as OP order 1 #White
+participant "Presentation Exchange\n<>" as PE order 2 #Gray
+
+activate OP
+OP -> OP: verifyAuthRequest\n(Auth Request, Verify Opts)
+== START: Presentation Definition from RP is present ==
+
+OP -> CLIENT: if presentationDefinition is present
+deactivate OP
+activate CLIENT
+CLIENT -> PE: Construct PE with DID and Verifiable Credentials
+deactivate CLIENT
+
+activate PE
+PE -> PE: selectVerifiableCredentialsForSubmission(Presentation Definition)
+PE -> CLIENT: Return matching VCs or an error
+activate CLIENT
+CLIENT -> CLIENT: Show UI to confirm and optionally subselect VCs from matches\n(NOTE: Not in scope of this library)
+CLIENT -> PE: selected VCs
+deactivate CLIENT
+PE -> PE: submissionFrom(Presentation Definition, selected VCs)
+PE -> OP: Return Verifiable Presentation (VP)
+deactivate PE
+== END: Presentation Definition from RP is present ==
+activate OP
+OP -> OP: createAuthResponse(Verified Auth request, opts and VP)
+
+
+@enduml
diff --git a/packages/siop-oid4vp/docs/services-class-diagram.md b/packages/siop-oid4vp/docs/services-class-diagram.md
new file mode 100644
index 00000000..f9369fe8
--- /dev/null
+++ b/packages/siop-oid4vp/docs/services-class-diagram.md
@@ -0,0 +1,209 @@
+```mermaid
+classDiagram
+
+class RP {
+ <>
+ createAuthenticationRequest(opts?) Promise(AuthenticationRequestURI)
+ verifyAuthenticationResponseJwt(jwt: string, opts?) Promise(VerifiedAuthenticationResponseWithJWT)
+}
+RP --> AuthenticationRequestURI
+RP --> VerifiedAuthenticationResponseWithJWT
+RP --> AuthorizationRequest
+RP --> AuthenticationResponse
+
+class OP {
+ <>
+ createAuthenticationResponse(jwtOrUri: string, opts?) Promise(AuthenticationResponseWithJWT)
+ verifyAuthenticationRequest(jwt: string, opts?) Promise(VerifiedAuthenticationRequestWithJWT)
+}
+OP --> AuthenticationResponseWithJWT
+OP --> VerifiedAuthenticationRequestWithJWT
+OP --> AuthorizationRequest
+OP --> AuthenticationResponse
+
+
+class AuthenticationRequestOpts {
+ <>
+ redirectUri: string;
+ requestBy: ObjectBy;
+ signature: InternalSignature | ExternalSignature | NoSignature;
+ responseMode?: ResponseMode;
+ claims?: ClaimPayload;
+ registration: RequestRegistrationOpts;
+ nonce?: string;
+ state?: string;
+}
+AuthenticationRequestOpts --> ResponseMode
+AuthenticationRequestOpts --> RPRegistrationMetadataOpts
+
+
+
+class RPRegistrationMetadataOpts {
+ <>
+ subjectIdentifiersSupported: SubjectIdentifierType[] | SubjectIdentifierType;
+ didMethodsSupported?: string[] | string;
+ credentialFormatsSupported: CredentialFormat[] | CredentialFormat;
+}
+
+class RequestRegistrationOpts {
+ <>
+ registrationBy: RegistrationType;
+}
+RequestRegistrationOpts --|> RPRegistrationMetadataOpts
+
+
+class VerifyAuthenticationRequestOpts {
+ <>
+ verification: Verification
+ nonce?: string;
+}
+
+class AuthorizationRequest {
+ <>
+ createURI(opts: AuthenticationRequestOpts) Promise(AuthenticationRequestURI)
+ createJWT(opts: AuthenticationRequestOpts) Promise(AuthenticationRequestWithJWT);
+ verifyJWT(jwt: string, opts: VerifyAuthenticationRequestOpts) Promise(VerifiedAuthenticationRequestWithJWT)
+}
+AuthorizationRequest <-- AuthenticationRequestOpts
+AuthorizationRequest <-- VerifyAuthenticationRequestOpts
+AuthorizationRequest --> AuthenticationRequestURI
+AuthorizationRequest --> AuthenticationRequestWithJWT
+AuthorizationRequest --> VerifiedAuthenticationRequestWithJWT
+
+class AuthenticationResponse {
+ <>
+ createJWTFromRequestJWT(jwt: string, responseOpts: AuthenticationResponseOpts, verifyOpts: VerifyAuthenticationRequestOpts) Promise(AuthenticationResponseWithJWT)
+ verifyJWT(jwt: string, verifyOpts: VerifyAuthenticationResponseOpts) Promise(VerifiedAuthenticationResponseWithJWT)
+}
+AuthenticationResponse <-- AuthenticationResponseOpts
+AuthenticationResponse <-- VerifyAuthenticationRequestOpts
+AuthenticationResponse --> AuthenticationResponseWithJWT
+AuthenticationResponse <-- VerifyAuthenticationResponseOpts
+AuthenticationResponse --> VerifiedAuthenticationResponseWithJWT
+
+class AuthenticationResponseOpts {
+ <>
+ signature: InternalSignature | ExternalSignature;
+ nonce?: string;
+ state?: string;
+ registration: ResponseRegistrationOpts;
+ responseMode?: ResponseMode;
+ did: string;
+ vp?: VerifiablePresentation;
+ expiresIn?: number;
+}
+AuthenticationResponseOpts --> ResponseMode
+
+class AuthenticationResponseWithJWT {
+ <>
+ jwt: string;
+ nonce: string;
+ state: string;
+ payload: AuthenticationResponsePayload;
+ verifyOpts?: VerifyAuthenticationRequestOpts;
+ responseOpts: AuthenticationResponseOpts;
+}
+AuthenticationResponseWithJWT --> AuthenticationResponsePayload
+AuthenticationResponseWithJWT --> VerifyAuthenticationRequestOpts
+AuthenticationResponseWithJWT --> AuthenticationResponseOpts
+
+
+class VerifyAuthenticationResponseOpts {
+ <>
+ verification: Verification
+ nonce?: string;
+ state?: string;
+ audience: string;
+}
+
+class ResponseMode {
+ <>
+}
+
+ class UriResponse {
+ <>
+ responseMode?: ResponseMode;
+ bodyEncoded?: string;
+}
+UriResponse --> ResponseMode
+UriResponse <|-- SIOPURI
+
+class SIOPURI {
+ <>
+ encodedUri: string;
+ encodingFormat: UrlEncodingFormat;
+}
+SIOPURI --> UrlEncodingFormat
+SIOPURI <|-- AuthenticationRequestURI
+
+class AuthenticationRequestURI {
+ <>
+ jwt?: string;
+ requestOpts: AuthenticationRequestOpts;
+ requestPayload: AuthenticationRequestPayload;
+}
+AuthenticationRequestURI --> AuthenticationRequestPayload
+
+class UrlEncodingFormat {
+ <>
+}
+
+class ResponseMode {
+ <>
+}
+
+class AuthenticationRequestPayload {
+ <>
+ scope: Scope;
+ response_type: ResponseType;
+ client_id: string;
+ redirect_uri: string;
+ response_mode: ResponseMode;
+ request: string;
+ request_uri: string;
+ state?: string;
+ nonce: string;
+ did_doc?: DIDDocument;
+ claims?: RequestClaims;
+}
+AuthenticationRequestPayload --|> JWTPayload
+
+class JWTPayload {
+ iss?: string
+ sub?: string
+ aud?: string | string[]
+ iat?: number
+ nbf?: number
+ exp?: number
+ rexp?: number
+ [x: string]: any
+}
+
+
+class VerifiedAuthenticationRequestWithJWT {
+ <>
+ payload: AuthenticationRequestPayload;
+ verifyOpts: VerifyAuthenticationRequestOpts;
+}
+VerifiedJWT <|-- VerifiedAuthenticationRequestWithJWT
+VerifiedAuthenticationRequestWithJWT --> VerifyAuthenticationRequestOpts
+VerifiedAuthenticationRequestWithJWT --> AuthenticationRequestPayload
+
+class VerifiedAuthenticationResponseWithJWT {
+ <>
+ payload: AuthenticationResponsePayload;
+ verifyOpts: VerifyAuthenticationResponseOpts;
+}
+VerifiedAuthenticationResponseWithJWT --> AuthenticationResponsePayload
+VerifiedAuthenticationResponseWithJWT --> VerifyAuthenticationResponseOpts
+VerifiedJWT <|-- VerifiedAuthenticationResponseWithJWT
+
+class VerifiedJWT {
+ <>
+ payload: Partial;
+ didResolutionResult: DIDResolutionResult;
+ issuer: string;
+ signer: VerificationMethod;
+ jwt: string;
+}
+```
diff --git a/packages/siop-oid4vp/docs/services-class-diagram.svg b/packages/siop-oid4vp/docs/services-class-diagram.svg
new file mode 100644
index 00000000..a4c8112b
--- /dev/null
+++ b/packages/siop-oid4vp/docs/services-class-diagram.svg
@@ -0,0 +1 @@
+
«service»
RP
createAuthenticationRequest(opts?) Promise(AuthenticationRequestURI)
verifyAuthenticationResponseJwt(jwt: string, opts?) Promise(VerifiedAuthenticationResponseWithJWT)
«interface»
AuthenticationRequestURI
jwt?: string;
requestOpts: AuthenticationRequestOpts;
requestPayload: AuthenticationRequestPayload;
«interface»
VerifiedAuthenticationResponseWithJWT
payload: AuthenticationResponsePayload;
verifyOpts: VerifyAuthenticationResponseOpts;
«service»
AuthorizationRequest
createURI(opts: AuthenticationRequestOpts) Promise(AuthenticationRequestURI)
createJWT(opts: AuthenticationRequestOpts)
verifyJWT(jwt: string, opts: VerifyAuthenticationRequestOpts) Promise(VerifiedAuthenticationRequestWithJWT)
«interface»
AuthenticationResponse
createJWTFromRequestJWT(jwt: string, responseOpts: AuthenticationResponseOpts, verifyOpts: VerifyAuthenticationRequestOpts) Promise(AuthenticationResponseWithJWT)
verifyJWT(jwt: string, verifyOpts: VerifyAuthenticationResponseOpts) Promise(VerifiedAuthenticationResponseWithJWT)
«service»
OP
createAuthenticationResponse(jwtOrUri: string, opts?) Promise(AuthenticationResponseWithJWT)
verifyAuthenticationRequest(jwt: string, opts?) Promise(VerifiedAuthenticationRequestWithJWT)
«interface»
AuthenticationResponseWithJWT
jwt: string;
nonce: string;
state: string;
payload: AuthenticationResponsePayload;
verifyOpts?: VerifyAuthenticationRequestOpts;
responseOpts: AuthenticationResponseOpts;
«interface»
VerifiedAuthenticationRequestWithJWT
payload: AuthenticationRequestPayload;
verifyOpts: VerifyAuthenticationRequestOpts;
«interface»
AuthenticationRequestOpts
redirectUri: string;
requestBy: ObjectBy;
signature: InternalSignature | ExternalSignature | NoSignature;
responseMode?: ResponseMode;
claims?: ClaimPayload;
registration: RequestRegistrationOpts;
nonce?: string;
state?: string;
«enum»
ResponseMode
«interface»
RPRegistrationMetadataOpts
subjectIdentifiersSupported: SubjectIdentifierType[] | SubjectIdentifierType;
didMethodsSupported?: string[] | string;
credentialFormatsSupported: CredentialFormat[] | CredentialFormat;
«interface»
RequestRegistrationOpts
registrationBy: RegistrationType;
«interface»
VerifyAuthenticationRequestOpts
verification: Verification;
nonce?: string;
AuthenticationRequestWithJWT
«interface»
AuthenticationResponseOpts
signature: InternalSignature | ExternalSignature;
nonce?: string;
state?: string;
registration: ResponseRegistrationOpts;
responseMode?: ResponseMode;
did: string;
vp?: VerifiablePresentation;
expiresIn?: number;
«interface»
VerifyAuthenticationResponseOpts
verification: Verification;
nonce?: string;
state?: string;
audience: string;
AuthenticationResponsePayload
«interface»
UriResponse
responseMode?: ResponseMode;
bodyEncoded?: string;
«interface»
SIOPURI
encodedUri: string;
encodingFormat: UrlEncodingFormat;
«enum»
UrlEncodingFormat
«interface»
AuthenticationRequestPayload
scope: Scope;
response_type: ResponseType;
client_id: string;
redirect_uri: string;
response_mode: ResponseMode;
request: string;
request_uri: string;
state?: string;
nonce: string;
did_doc?: DIDDocument;
claims?: RequestClaims;
JWTPayload
iss?: string
sub?: string
aud?: string | string[]
iat?: number
nbf?: number
exp?: number
rexp?: number
[x: string]: any
«interface»
VerifiedJWT
payload: Partial<JWTPayload>;
didResolutionResult: DIDResolutionResult;
issuer: string;
signer: VerificationMethod;
jwt: string;
diff --git a/packages/siop-oid4vp/docs/walk-through.md b/packages/siop-oid4vp/docs/walk-through.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/siop-oid4vp/generator/schemaGenerator.ts b/packages/siop-oid4vp/generator/schemaGenerator.ts
new file mode 100644
index 00000000..617da825
--- /dev/null
+++ b/packages/siop-oid4vp/generator/schemaGenerator.ts
@@ -0,0 +1,204 @@
+import fs from 'fs'
+import path from 'path'
+
+import Ajv from 'ajv'
+import standaloneCode from 'ajv/dist/standalone'
+import {
+ BaseType,
+ createFormatter,
+ createParser,
+ createProgram,
+ Definition,
+ FunctionType,
+ MutableTypeFormatter,
+ SchemaGenerator,
+ SubTypeFormatter,
+} from 'ts-json-schema-generator'
+import { Schema } from 'ts-json-schema-generator/dist/src/Schema/Schema'
+
+class CustomTypeFormatter implements SubTypeFormatter {
+ public supportsType(type: FunctionType): boolean {
+ return type instanceof FunctionType
+ }
+
+ public getDefinition(): Definition {
+ // Return a custom schema for the function property.
+ return {
+ properties: {
+ isFunction: {
+ type: 'boolean',
+ const: true,
+ },
+ },
+ }
+ }
+
+ public getChildren(): BaseType[] {
+ return []
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function writeSchema(config: any): Schema {
+ const formatter = createFormatter(config, (fmt: MutableTypeFormatter) => {
+ fmt.addTypeFormatter(new CustomTypeFormatter())
+ })
+
+ const program = createProgram(config)
+ const schema = new SchemaGenerator(program, createParser(program, config), formatter, config).createSchema(config.type)
+
+ let schemaString = JSON.stringify(schema, null, 2)
+ schemaString = correctSchema(schemaString)
+
+ fs.writeFile(config.outputPath, `export const ${config.schemaId}Obj = ${schemaString};`, (err) => {
+ if (err) {
+ throw err
+ }
+ })
+ return schema
+}
+
+function generateValidationCode(schemas: Schema[]) {
+ const ajv = new Ajv({ schemas, code: { source: true, lines: true, esm: false }, allowUnionTypes: true, strict: false })
+ const moduleCode = standaloneCode(ajv)
+ fs.writeFileSync(path.join(__dirname, '../lib/schemas/validation/schemaValidation.js'), moduleCode)
+}
+
+function correctSchema(schemaString: string) {
+ return schemaString.replace(
+ '"SuppliedSignature": {\n' +
+ ' "type": "object",\n' +
+ ' "properties": {\n' +
+ ' "withSignature": {\n' +
+ ' "properties": {\n' +
+ ' "isFunction": {\n' +
+ ' "type": "boolean",\n' +
+ ' "const": true\n' +
+ ' }\n' +
+ ' }\n' +
+ ' },\n' +
+ ' "did": {\n' +
+ ' "type": "string"\n' +
+ ' },\n' +
+ ' "kid": {\n' +
+ ' "type": "string"\n' +
+ ' }\n' +
+ ' },\n' +
+ ' "required": [\n' +
+ ' "withSignature",\n' +
+ ' "did",\n' +
+ ' "kid"\n' +
+ ' ],\n' +
+ ' "additionalProperties": false\n' +
+ ' },',
+ '"SuppliedSignature": {\n' +
+ ' "type": "object",\n' +
+ ' "properties": {\n' +
+ ' "did": {\n' +
+ ' "type": "string"\n' +
+ ' },\n' +
+ ' "kid": {\n' +
+ ' "type": "string"\n' +
+ ' }\n' +
+ ' },\n' +
+ ' "required": [\n' +
+ ' "did",\n' +
+ ' "kid"\n' +
+ ' ],\n' +
+ ' "additionalProperties": true\n' +
+ ' },',
+ )
+}
+/*
+const requestOptsConf = {
+ path: '../lib/authorization-request/types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'CreateAuthorizationRequestOpts', // Or if you want to generate schema for that one type only
+ schemaId: 'CreateAuthorizationRequestOptsSchema',
+ outputPath: 'lib/schemas/AuthorizationRequestOpts.schema.ts',
+ // outputConstName: 'AuthorizationRequestOptsSchema',
+ skipTypeCheck: true
+};*/
+
+const responseOptsConf = {
+ path: '../lib/authorization-response/types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'AuthorizationResponseOpts', // Or if you want to generate schema for that one type only
+ schemaId: 'AuthorizationResponseOptsSchema',
+ outputPath: 'lib/schemas/AuthorizationResponseOpts.schema.ts',
+ // outputConstName: 'AuthorizationResponseOptsSchema',
+ skipTypeCheck: true,
+}
+
+const rPRegistrationMetadataPayload = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'RPRegistrationMetadataPayload',
+ schemaId: 'RPRegistrationMetadataPayloadSchema',
+ outputPath: 'lib/schemas/RPRegistrationMetadataPayload.schema.ts',
+ // outputConstName: 'RPRegistrationMetadataPayloadSchema',
+ skipTypeCheck: true,
+}
+
+const discoveryMetadataPayload = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'DiscoveryMetadataPayload',
+ schemaId: 'DiscoveryMetadataPayloadSchema',
+ outputPath: 'lib/schemas/DiscoveryMetadataPayload.schema.ts',
+ // outputConstName: 'DiscoveryMetadataPayloadSchema',
+ skipTypeCheck: true,
+}
+
+const authorizationRequestPayloadVID1 = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'AuthorizationRequestPayloadVID1', // Or if you want to generate schema for that one type only
+ schemaId: 'AuthorizationRequestPayloadVID1Schema',
+ outputPath: 'lib/schemas/AuthorizationRequestPayloadVID1.schema.ts',
+ // outputConstName: 'AuthorizationRequestPayloadSchemaVID1',
+ skipTypeCheck: true,
+}
+
+const authorizationRequestPayloadVD11 = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'AuthorizationRequestPayloadVD11', // Or if you want to generate schema for that one type only
+ schemaId: 'AuthorizationRequestPayloadVD11Schema',
+ outputPath: 'lib/schemas/AuthorizationRequestPayloadVD11.schema.ts',
+ // outputConstName: 'AuthorizationRequestPayloadSchemaVD11',
+ skipTypeCheck: true,
+}
+
+const authorizationRequestPayloadVD12OID4VPD18 = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'AuthorizationRequestPayloadVD12OID4VPD18', // Or if you want to generate schema for that one type only
+ schemaId: 'AuthorizationRequestPayloadVD12OID4VPD18Schema',
+ outputPath: 'lib/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts',
+ // outputConstName: 'AuthorizationRequestPayloadSchemaVD11',
+ skipTypeCheck: true,
+}
+
+const authorizationRequestPayloadVD12OID4VPD20 = {
+ path: '../lib/types/SIOP.types.ts',
+ tsconfig: 'tsconfig.json',
+ type: 'AuthorizationRequestPayloadVD12OID4VPD20', // Or if you want to generate schema for that one type only
+ schemaId: 'AuthorizationRequestPayloadVD12OID4VPD20Schema',
+ outputPath: 'lib/schemas/AuthorizationRequestPayloadVD12OID4VPD20.schema.ts',
+ // outputConstName: 'AuthorizationRequestPayloadSchemaVD11',
+ skipTypeCheck: true,
+}
+
+const schemas: Schema[] = [
+ writeSchema(authorizationRequestPayloadVID1),
+ writeSchema(authorizationRequestPayloadVD11),
+ writeSchema(authorizationRequestPayloadVD12OID4VPD18),
+ writeSchema(authorizationRequestPayloadVD12OID4VPD20),
+ // writeSchema(requestOptsConf),
+ writeSchema(responseOptsConf),
+ writeSchema(rPRegistrationMetadataPayload),
+ writeSchema(discoveryMetadataPayload),
+]
+
+generateValidationCode(schemas)
diff --git a/packages/siop-oid4vp/jest.json b/packages/siop-oid4vp/jest.json
new file mode 100644
index 00000000..2a13352f
--- /dev/null
+++ b/packages/siop-oid4vp/jest.json
@@ -0,0 +1,28 @@
+{
+ "preset": "ts-jest",
+ "moduleFileExtensions": ["ts", "tsx", "js", "jsx"],
+ "collectCoverage": true,
+ "collectCoverageFrom": [
+ "packages/**/src/**/*.ts",
+ "packages/**/lib/**/*.ts",
+ "!**/examples/**",
+ "!packages/cli/**",
+ "!**/types/**",
+ "!**/dist/**",
+ "!**/coverage/**",
+ "!**/node_modules/**/__tests__/**",
+ "!**/node_modules/**/*.test.ts",
+ "!**/node_modules/**",
+ "!**/packages/**/index.ts"
+ ],
+ "coverageReporters": ["text", "lcov", "json"],
+ "coverageDirectory": "./coverage",
+ "transform": {
+ "\\.jsx?$": "babel-jest",
+ "\\.tsx?$": ["ts-jest", { "tsconfig": "./packages/siop-oid4vp/tsconfig.json" }]
+ },
+ "testMatch": ["**/__tests__/**/*.spec.*", "**/tests/**/*.spec.*"],
+ "testEnvironment": "node",
+ "automock": false,
+ "verbose": true
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.request.spec.ts b/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.request.spec.ts
new file mode 100644
index 00000000..47fbce65
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.request.spec.ts
@@ -0,0 +1,691 @@
+import { parse } from 'querystring'
+
+import { IPresentationDefinition } from '@sphereon/pex'
+import { IProofType } from '@sphereon/ssi-types'
+
+import {
+ CreateAuthorizationRequestOpts,
+ PassBy,
+ RequestObject,
+ ResponseType,
+ Scope,
+ SigningAlgo,
+ SubjectIdentifierType,
+ SubjectType,
+ SupportedVersion,
+ URI,
+} from '..'
+import SIOPErrors from '../types/Errors'
+
+import { getCreateJwtCallback } from './DidJwtTestUtils'
+import { WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1'
+
+describe('create Request Uri should', () => {
+ it('throw BAD_PARAMS when no responseOpts is passed', async () => {
+ expect.assertions(1)
+ await expect(URI.fromOpts(undefined as never)).rejects.toThrow(SIOPErrors.BAD_PARAMS)
+ })
+
+ it('throw BAD_PARAMS when no responseOpts.redirectUri is passed', async () => {
+ expect.assertions(1)
+ const opts = {}
+ await expect(URI.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.BAD_PARAMS)
+ })
+
+ it('throw BAD_PARAMS when no responseOpts.requestObject is passed', async () => {
+ expect.assertions(1)
+ const opts = { payload: { redirect_uri: EXAMPLE_REDIRECT_URL } }
+ await expect(URI.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.BAD_PARAMS)
+ })
+
+ it('throw BAD_PARAMS when no responseOpts.requestBy is passed', async () => {
+ expect.assertions(1)
+ const opts = {
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {},
+ }
+ await expect(URI.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.REQUEST_OBJECT_TYPE_NOT_SET)
+ })
+
+ it('throw REQUEST_OBJECT_TYPE_NOT_SET when responseOpts.requestBy type is different from REFERENCE or VALUE', async () => {
+ expect.assertions(1)
+ const opts = {
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ passBy: 'other type',
+ },
+ }
+ await expect(URI.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.REQUEST_OBJECT_TYPE_NOT_SET)
+ })
+
+ it('throw NO_REFERENCE_URI when responseOpts.requestBy type is REFERENCE and no referenceUri is passed', async () => {
+ expect.assertions(1)
+ const opts = {
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ },
+ }
+ await expect(URI.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.NO_REFERENCE_URI)
+ })
+
+ it('return a reference url', async () => {
+ expect.assertions(12)
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'openid',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: KID,
+ alg: SigningAlgo.ES256,
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ alg: SigningAlgo.ES256,
+ did: DID,
+ kid: KID,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'openid',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100300',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const uriRequest = await URI.fromOpts(opts)
+ expect(uriRequest).toBeDefined()
+ expect(uriRequest).toHaveProperty('encodedUri')
+ expect(uriRequest).toHaveProperty('encodingFormat')
+ expect(uriRequest).toHaveProperty('requestObjectJwt')
+ expect(uriRequest).toHaveProperty('authorizationRequestPayload')
+ expect(uriRequest.authorizationRequestPayload).toBeDefined()
+
+ const uriDecoded = decodeURIComponent(uriRequest.encodedUri)
+ expect(uriDecoded).toContain(`openid://`)
+ expect(uriDecoded).toContain(`response_type=${ResponseType.ID_TOKEN}`)
+ expect(uriDecoded).toContain(`&redirect_uri=${opts.payload?.redirect_uri}`)
+ expect(uriDecoded).toContain(`&scope=${Scope.OPENID}`)
+ expect(uriDecoded).toContain(`&request_uri=`)
+
+ const data = parse(uriDecoded)
+ expect(data.request_uri).toStrictEqual(opts.requestObject.reference_uri)
+ // expect(data.registration).toContain('client_purpose#nl-NL');
+ })
+
+ it('return a reference url when using did:key', async () => {
+ expect.assertions(4)
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256 },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey:
+ 'd474ffdb3ea75fbb3f07673e67e52002a3b7eb42767f709f4100acf493c7fc8743017577997b72e7a8b4bce8c32c8e78fd75c1441e95d6aaa888056d1200beb3',
+ did: 'did:key:z6MkixpejjET5qJK4ebN5m3UcdUPmYV4DPSCs1ALH8x2UCfc',
+ kid: 'did:key:z6MkixpejjET5qJK4ebN5m3UcdUPmYV4DPSCs1ALH8x2UCfc#z6MkixpejjET5qJK4ebN5m3UcdUPmYV4DPSCs1ALH8x2UCfc',
+ alg: SigningAlgo.EDDSA,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.ES256, SigningAlgo.EDDSA],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100301',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const uriRequest = await URI.fromOpts(opts)
+ const uriDecoded = decodeURIComponent(uriRequest.encodedUri)
+
+ const data = URI.parse(uriDecoded)
+ expect(uriRequest).toHaveProperty('requestObjectJwt')
+ expect(uriRequest.authorizationRequestPayload).toBeDefined()
+ expect(data.authorizationRequestPayload.request_uri).toEqual(opts.requestObject.reference_uri)
+ expect(uriRequest.authorizationRequestPayload.request_uri).toEqual(EXAMPLE_REFERENCE_URL)
+ })
+
+ it('return an url with an embedded token value', async () => {
+ expect.assertions(3)
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ requestObject: {
+ passBy: PassBy.VALUE,
+ jwtIssuer: {
+ method: 'did',
+ didUrl: KID,
+ alg: SigningAlgo.ES256K,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100302',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const uriRequest = await URI.fromOpts(opts)
+
+ const uriDecoded = decodeURIComponent(uriRequest.encodedUri)
+ expect(uriDecoded).toContain(`openid://?request=eyJhbGciOi`)
+
+ const data = URI.parse(uriDecoded)
+ expect(data.scheme).toEqual('openid://')
+ expect(data.authorizationRequestPayload.request).toContain(`eyJhbGciOi`)
+ })
+})
+
+describe('create Request JWT should', () => {
+ it('throw REQUEST_OBJECT_TYPE_NOT_SET when requestBy type is different from REFERENCE and VALUE', async () => {
+ expect.assertions(1)
+ const opts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ passBy: 'other type' as never,
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ },
+ }
+ await expect(RequestObject.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.REQUEST_OBJECT_TYPE_NOT_SET)
+ })
+
+ it('throw NO_REFERENCE_URI when no referenceUri is passed with REFERENCE requestBy type is set', async () => {
+ expect.assertions(1)
+ const opts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ },
+ }
+ await expect(RequestObject.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.NO_REFERENCE_URI)
+ })
+
+ it('throw REGISTRATION_OBJECT_TYPE_NOT_SET when registrationBy type is neither REFERENCE nor VALUE', async () => {
+ expect.assertions(1)
+ const opts = {
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+ signature: {
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ },
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ type: 'FAILURE',
+ },
+ }
+ await expect(RequestObject.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.REGISTRATION_OBJECT_TYPE_NOT_SET)
+ })
+
+ it('throw NO_REFERENCE_URI when registrationBy type is REFERENCE and no referenceUri is passed', async () => {
+ expect.assertions(1)
+ const opts = {
+ version: SupportedVersion.SIOPv2_ID1,
+
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ signature: {
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ },
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.REFERENCE,
+ },
+ }
+ await expect(RequestObject.fromOpts(opts as never)).rejects.toThrow(SIOPErrors.NO_REFERENCE_URI)
+ })
+
+ it('succeed when all params are set', async () => {
+ // expect.assertions(1);
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: 'test_client_id',
+ scope: 'test',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.ES256, SigningAlgo.EDDSA],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: 'test_client_id',
+ scope: 'test',
+ response_type: 'id_token',
+ request_object_signing_alg_values_supported: [SigningAlgo.ES256, SigningAlgo.EDDSA],
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: 'test_client_id',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+
+ passBy: PassBy.VALUE,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100303',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const expected = {
+ response_type: 'id_token',
+ scope: 'test',
+ client_id: 'test_client_id',
+ redirect_uri: 'https://acme.com/hello',
+ registration: {
+ id_token_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr:', 'did'],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: ['EcdsaSecp256k1Signature2019', 'EcdsaSecp256k1Signature2019'],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ client_name: VERIFIER_NAME_FOR_CLIENT,
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100303',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+
+ /*opts: {
+ redirectUri: 'https://acme.com/hello',
+ requestBy: {
+ type: 'REFERENCE',
+ reference_uri: 'https://rp.acme.com/siop/jwts',
+ },
+ withSignature: {
+ hexPrivateKey: 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f',
+ did: 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0',
+ kid: 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1',
+ },
+ registration: {
+ idTokenSigningAlgValuesSupported: ['EdDSA', 'ES256'],
+ subjectSyntaxTypesSupported: ['did:ethr:', 'did'],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: ['EcdsaSecp256k1Signature2019', 'EcdsaSecp256k1Signature2019'],
+ },
+ },
+ registrationBy: {
+ type: 'VALUE',
+ },
+ },
+ },*/
+ }
+
+ // await URI.fromOpts(opts).then((uri) => console.log(uri.encodedUri));
+ await expect((await RequestObject.fromOpts(opts)).getPayload()).resolves.toMatchObject(expected)
+ })
+
+ it('succeed when requesting with a valid PD', async () => {
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ /*payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ claims: {
+ vp_token: {
+ presentation_definition: {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },*/
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ claims: {
+ vp_token: {
+ presentation_definition: {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+
+ passBy: PassBy.VALUE,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100305',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const uriRequest = await URI.fromOpts(opts)
+
+ const uriDecoded = decodeURIComponent(uriRequest.encodedUri)
+ expect(uriDecoded).toEqual(`openid://?request_uri=https://rp.acme.com/siop/jwts`)
+ expect((await (await uriRequest.toAuthorizationRequest())?.requestObject?.getPayload())?.claims.vp_token).toBeDefined()
+ })
+
+ it('should throw error if presentation definition object is not valid', async () => {
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: 'test_client_id',
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ claims: {
+ vp_token: {
+ presentation_definition: {
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ } as IPresentationDefinition,
+ },
+ },
+ },
+
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: 'test_client_id',
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ claims: {
+ vp_token: {
+ presentation_definition: {
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ } as IPresentationDefinition,
+ },
+ },
+ },
+ },
+ clientMetadata: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+
+ passBy: PassBy.VALUE,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100306',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ await expect(URI.fromOpts(opts)).rejects.toThrow(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.verify.spec.ts b/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.verify.spec.ts
new file mode 100644
index 00000000..56f07fa2
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/AuthenticationRequest.verify.spec.ts
@@ -0,0 +1,440 @@
+import { IProofType } from '@sphereon/ssi-types'
+import Ajv from 'ajv'
+import * as dotenv from 'dotenv'
+
+import {
+ AuthorizationRequest,
+ CreateAuthorizationRequestOpts,
+ PassBy,
+ RequestObject,
+ ResponseType,
+ Scope,
+ SigningAlgo,
+ SubjectType,
+ SupportedVersion,
+ VerifyAuthorizationRequestOpts,
+} from '..'
+import { RPRegistrationMetadataPayloadSchemaObj } from '../schemas'
+import SIOPErrors from '../types/Errors'
+
+import { getCreateJwtCallback, getVerifyJwtCallback } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { metadata, mockedGetEnterpriseAuthToken, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ UNIT_TEST_TIMEOUT,
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+dotenv.config()
+
+describe('verifyJWT should', () => {
+ it('should compile schema', async () => {
+ const schema = {
+ $schema: 'http://json-schema.org/draft-07/schema#',
+ $ref: '#/definitions/RPRegistrationMetadataPayload',
+ definitions: {
+ RPRegistrationMetadataPayload: {
+ type: 'object',
+ properties: {
+ client_id: {
+ anyOf: [
+ {
+ type: 'string',
+ },
+ {},
+ ],
+ },
+ id_token_signing_alg_values_supported: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ $ref: '#/definitions/SigningAlgo',
+ },
+ },
+ {
+ $ref: '#/definitions/SigningAlgo',
+ },
+ ],
+ },
+ request_object_signing_alg_values_supported: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ $ref: '#/definitions/SigningAlgo',
+ },
+ },
+ {
+ $ref: '#/definitions/SigningAlgo',
+ },
+ ],
+ },
+ response_types_supported: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ $ref: '#/definitions/ResponseType',
+ },
+ },
+ {
+ $ref: '#/definitions/ResponseType',
+ },
+ ],
+ },
+ scopes_supported: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ $ref: '#/definitions/Scope',
+ },
+ },
+ {
+ $ref: '#/definitions/Scope',
+ },
+ ],
+ },
+ subject_types_supported: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ $ref: '#/definitions/SubjectType',
+ },
+ },
+ {
+ $ref: '#/definitions/SubjectType',
+ },
+ ],
+ },
+ subject_syntax_types_supported: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ vp_formats: {
+ anyOf: [
+ {
+ $ref: '#/definitions/Format',
+ },
+ {},
+ ],
+ },
+ client_name: {
+ anyOf: [
+ {
+ type: 'string',
+ },
+ {},
+ ],
+ },
+ logo_uri: {
+ anyOf: [
+ {},
+ {
+ type: 'string',
+ },
+ ],
+ },
+ client_purpose: {
+ anyOf: [
+ {},
+ {
+ type: 'string',
+ },
+ ],
+ },
+ },
+ },
+ SigningAlgo: {
+ type: 'string',
+ enum: ['EdDSA', 'RS256', 'ES256', 'ES256K'],
+ },
+ ResponseType: {
+ type: 'string',
+ enum: ['id_token', 'vp_token'],
+ },
+ Scope: {
+ type: 'string',
+ enum: ['openid', 'openid did_authn', 'profile', 'email', 'address', 'phone'],
+ },
+ SubjectType: {
+ type: 'string',
+ enum: ['public', 'pairwise'],
+ },
+ Format: {
+ type: 'object',
+ properties: {
+ jwt: {
+ $ref: '#/definitions/JwtObject',
+ },
+ jwt_vc: {
+ $ref: '#/definitions/JwtObject',
+ },
+ jwt_vp: {
+ $ref: '#/definitions/JwtObject',
+ },
+ ldp: {
+ $ref: '#/definitions/LdpObject',
+ },
+ ldp_vc: {
+ $ref: '#/definitions/LdpObject',
+ },
+ ldp_vp: {
+ $ref: '#/definitions/LdpObject',
+ },
+ },
+ additionalProperties: false,
+ },
+ JwtObject: {
+ type: 'object',
+ properties: {
+ alg: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ required: ['alg'],
+ additionalProperties: false,
+ },
+ LdpObject: {
+ type: 'object',
+ properties: {
+ proof_type: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ required: ['proof_type'],
+ additionalProperties: false,
+ },
+ },
+ }
+ const ajv = new Ajv({ allowUnionTypes: true, strict: false })
+ ajv.compile(RPRegistrationMetadataPayloadSchemaObj)
+ ajv.compile(schema)
+ })
+ it('throw VERIFY_BAD_PARAMETERS when no JWT is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationRequest.verify(undefined as never, undefined as never)).rejects.toThrow(SIOPErrors.VERIFY_BAD_PARAMS)
+ })
+
+ it('throw VERIFY_BAD_PARAMETERS when no responseOpts is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationRequest.verify('an invalid JWT bypassing the undefined check', undefined as never)).rejects.toThrow(
+ SIOPErrors.VERIFY_BAD_PARAMS,
+ )
+ })
+
+ it('throw VERIFY_BAD_PARAMETERS when no responseOpts.verification is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationRequest.verify('an invalid JWT bypassing the undefined check', {} as never)).rejects.toThrow(
+ SIOPErrors.VERIFY_BAD_PARAMS,
+ )
+ })
+
+ it('throw BAD_NONCE when a different nonce is supplied during verification', async () => {
+ expect.assertions(1)
+
+ const mockEntity = await mockedGetEnterpriseAuthToken('COMPANY AA INC')
+
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://my-request.com/here',
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockEntity.hexPrivateKey,
+ did: mockEntity.did,
+ kid: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ state: '12345',
+ nonce: '12345',
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ authorization_endpoint: '',
+ redirect_uri: 'https://acme.com/hello',
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID, Scope.OPENID_DIDAUTHN],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256K],
+ subject_syntax_types_supported: ['did:ethr:'],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100309',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+
+ const resolver = getResolver('ethr')
+ const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verifyJwtCallback: getVerifyJwtCallback(resolver, { checkLinkedDomain: 'if_present' }),
+ verification: {},
+ supportedVersions: [SupportedVersion.SIOPv2_ID1],
+ correlationId: '1234',
+ nonce: 'invalid_nonce',
+ }
+
+ const jwt = await requestObject.toJwt()
+ await expect(AuthorizationRequest.verify(jwt as string, verifyOpts)).rejects.toThrow(SIOPErrors.BAD_NONCE)
+ })
+
+ it(
+ 'succeed if a valid JWT is passed',
+ async () => {
+ const mockEntity = await mockedGetEnterpriseAuthToken('COMPANY AA INC')
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://my-request.com/here',
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockEntity.hexPrivateKey,
+ did: mockEntity.did,
+ kid: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ state: '12345',
+ nonce: '12345',
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ authorization_endpoint: '',
+ redirect_uri: 'https://acme.com/hello',
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID, Scope.OPENID_DIDAUTHN],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256K],
+ subject_syntax_types_supported: ['did:ethr:'],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100309',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+
+ const resolver = getResolver('ethr')
+ const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verifyJwtCallback: getVerifyJwtCallback(resolver, { checkLinkedDomain: 'if_present' }),
+ verification: {},
+ supportedVersions: [SupportedVersion.SIOPv2_ID1],
+ correlationId: '1234',
+ }
+
+ const verifyJWT = await AuthorizationRequest.verify((await requestObject.toJwt()) as string, verifyOpts)
+ expect(verifyJWT.jwt).toMatch(/^eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXRocjowe.*$/)
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+})
+
+describe('OP and RP communication should', () => {
+ it('work if both support the same did methods', () => {
+ const actualResult = metadata.verify()
+ const expectedResult = {
+ vp_formats: {
+ jwt_vc: { alg: [SigningAlgo.ES256, SigningAlgo.ES256K] },
+ ldp_vc: {
+ proof_type: ['EcdsaSecp256k1Signature2019', 'EcdsaSecp256k1Signature2019'],
+ },
+ },
+ subject_syntax_types_supported: ['did:web'],
+ }
+ expect(actualResult).toEqual(expectedResult)
+ })
+
+ it('work if RP supports any OP did methods', () => {
+ metadata.opMetadata.vp_formats = {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ }
+ metadata.rpMetadata.subject_syntax_types_supported = ['did:web']
+ expect(metadata.verify()).toEqual({
+ subject_syntax_types_supported: ['did:web'],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: ['EcdsaSecp256k1Signature2019', 'EcdsaSecp256k1Signature2019'],
+ },
+ },
+ })
+ })
+
+ it('work if RP supports any OP credential formats', () => {
+ metadata.opMetadata.vp_formats = {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ }
+ const result = metadata.verify() as Record
+ expect(result['subject_syntax_types_supported']).toContain('did:web')
+ expect(result['vp_formats']).toStrictEqual({
+ ldp_vc: {
+ proof_type: ['EcdsaSecp256k1Signature2019', 'EcdsaSecp256k1Signature2019'],
+ },
+ })
+ })
+
+ it('not work if RP does not support any OP did method', () => {
+ metadata.rpMetadata.subject_syntax_types_supported = ['did:notsupported']
+ expect(() => metadata.verify()).toThrowError(SIOPErrors.DID_METHODS_NOT_SUPORTED)
+ })
+
+ it('not work if RP does not support any OP credentials', () => {
+ metadata.rpMetadata.vp_formats = undefined
+ expect(() => metadata.verify()).toThrowError(SIOPErrors.CREDENTIALS_FORMATS_NOT_PROVIDED)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.response.spec.ts b/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.response.spec.ts
new file mode 100644
index 00000000..f178b0b2
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.response.spec.ts
@@ -0,0 +1,653 @@
+import { IPresentationDefinition } from '@sphereon/pex'
+import {
+ ICredential,
+ IPresentation,
+ IProofType,
+ IVerifiableCredential,
+ IVerifiablePresentation,
+ OriginalVerifiableCredential,
+} from '@sphereon/ssi-types'
+
+import {
+ AuthorizationResponse,
+ AuthorizationResponseOpts,
+ CreateAuthorizationRequestOpts,
+ PassBy,
+ PresentationExchange,
+ PresentationSignCallback,
+ RequestObject,
+ ResponseIss,
+ ResponseMode,
+ ResponseType,
+ Scope,
+ SigningAlgo,
+ SubjectIdentifierType,
+ SubjectType,
+ SupportedVersion,
+ VerifyAuthorizationRequestOpts,
+ VPTokenLocation,
+} from '..'
+import { createPresentationSubmission } from '../authorization-response/OpenID4VP'
+import SIOPErrors from '../types/Errors'
+
+import { getCreateJwtCallback, getVerifyJwtCallback } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { mockedGetEnterpriseAuthToken, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ UNIT_TEST_TIMEOUT,
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+jest.setTimeout(30000)
+
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1'
+
+const validButExpiredJWT =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NjEzNTEyOTAsImV4cCI6MTU2MTM1MTg5MCwicmVzcG9uc2VfdHlwZSI6ImlkX3Rva2VuIiwic2NvcGUiOiJvcGVuaWQiLCJjbGllbnRfaWQiOiJkaWQ6ZXRocjoweDQ4NzNFQzc0MUQ4RDFiMjU4YUYxQjUyNDczOEIzNjNhQTIxOTk5MjAiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjbWUuY29tL2hlbGxvIiwiaXNzIjoiZGlkOmV0aHI6MHg0ODczRUM3NDFEOEQxYjI1OGFGMUI1MjQ3MzhCMzYzYUEyMTk5OTIwIiwicmVzcG9uc2VfbW9kZSI6InBvc3QiLCJyZXNwb25zZV9jb250ZXh0IjoicnAiLCJub25jZSI6IlVTLU9wY1FHLXlXS3lWUTRlTU53UFB3Um10UVVGdmpkOHJXeTViRC10MXciLCJzdGF0ZSI6IjdmMjcxYzZjYjk2ZThmOThhMzkxYWU5ZCIsInJlZ2lzdHJhdGlvbiI6eyJpZF90b2tlbl9zaWduaW5nX2FsZ192YWx1ZXNfc3VwcG9ydGVkIjpbIkVkRFNBIiwiRVMyNTYiXSwicmVxdWVzdF9vYmplY3Rfc2lnbmluZ19hbGdfdmFsdWVzX3N1cHBvcnRlZCI6WyJFZERTQSIsIkVTMjU2Il0sInJlc3BvbnNlX3R5cGVzX3N1cHBvcnRlZCI6WyJpZF90b2tlbiJdLCJzY29wZXNfc3VwcG9ydGVkIjpbIm9wZW5pZCBkaWRfYXV0aG4iLCJvcGVuaWQiXSwic3ViamVjdF90eXBlc19zdXBwb3J0ZWQiOlsicGFpcndpc2UiXSwic3ViamVjdF9zeW50YXhfdHlwZXNfc3VwcG9ydGVkIjpbImRpZDpldGhyOiIsImRpZCJdLCJ2cF9mb3JtYXRzIjp7ImxkcF92YyI6eyJwcm9vZl90eXBlIjpbIkVjZHNhU2VjcDI1NmsxU2lnbmF0dXJlMjAxOSIsIkVjZHNhU2VjcDI1NmsxU2lnbmF0dXJlMjAxOSJdfX19fQ.Wd6I7BT7fWZSuYozUwHnyEsEoAe6OjdyzEEKXnWk8bY'
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+
+describe('create JWT from Request JWT should', () => {
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ responseMode: ResponseMode.POST,
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subject_syntax_types_supported: ['did:web'],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ issuer: ResponseIss.SELF_ISSUED_V2,
+
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100310',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ did: DID,
+ hexPrivateKey: HEX_KEY,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ }
+
+ const resolver = getResolver('ethr')
+ const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verifyJwtCallback: getVerifyJwtCallback(resolver),
+ verification: {},
+ supportedVersions: [SupportedVersion.SIOPv2_ID1],
+ correlationId: '1234',
+ }
+
+ it('throw NO_JWT when no jwt is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationResponse.fromRequestObject(undefined as never, responseOpts, verifyOpts)).rejects.toThrow(SIOPErrors.NO_JWT)
+ })
+ it('throw BAD_PARAMS when no responseOpts is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationResponse.fromRequestObject(validButExpiredJWT, undefined as never, verifyOpts)).rejects.toThrow(SIOPErrors.BAD_PARAMS)
+ })
+ it('throw VERIFY_BAD_PARAMS when no verifyOpts is passed', async () => {
+ expect.assertions(1)
+ await expect(AuthorizationResponse.fromRequestObject(validButExpiredJWT, responseOpts, undefined as never)).rejects.toThrow(
+ SIOPErrors.VERIFY_BAD_PARAMS,
+ )
+ })
+
+ it('throw JWT_ERROR when expired but valid JWT is passed in', async () => {
+ expect.assertions(1)
+ const mockReqEntity = await mockedGetEnterpriseAuthToken('REQ COMPANY')
+ const mockResEntity = await mockedGetEnterpriseAuthToken('RES COMPANY')
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ /*payload: {
+ nonce: '12345',
+ state: '12345',
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },*/
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ jwtIssuer: { method: 'did', didUrl: `${mockReqEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ reference_uri: 'https://my-request.com/here',
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockReqEntity.hexPrivateKey,
+ did: mockReqEntity.did,
+ kid: `${mockReqEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ nonce: '12345',
+ state: '12345',
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100311',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100312',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ did: mockResEntity.did,
+ hexPrivateKey: mockResEntity.hexPrivateKey,
+ kid: `${mockResEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: `${mockResEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ responseMode: ResponseMode.POST,
+ }
+
+ jest.useFakeTimers().setSystemTime(new Date('2020-01-01'))
+ jest.useFakeTimers().setSystemTime(new Date('2020-01-01'))
+
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+ const jwt = await requestObject.toJwt()
+ if (!jwt) throw new Error('JWT is undefined')
+ jest.useRealTimers()
+ await expect(AuthorizationResponse.fromRequestObject(jwt, responseOpts, verifyOpts)).rejects.toThrow(/invalid_jwt: JWT has expired: exp: /)
+ })
+
+ it(
+ 'succeed when valid JWT is passed in',
+ async () => {
+ expect.assertions(1)
+
+ const mockReqEntity = await mockedGetEnterpriseAuthToken('REQ COMPANY')
+ const mockResEntity = await mockedGetEnterpriseAuthToken('RES COMPANY')
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://my-request.com/here',
+ jwtIssuer: { method: 'did', didUrl: `${mockReqEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockReqEntity.hexPrivateKey,
+ did: mockReqEntity.did,
+ kid: `${mockReqEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100313',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100314',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ did: mockResEntity.did,
+ hexPrivateKey: mockResEntity.hexPrivateKey,
+ kid: `${mockResEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: `${mockResEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ responseMode: ResponseMode.POST,
+ }
+
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+ // console.log(JSON.stringify(await AuthorizationResponse.fromRequestObject(await requestObject.toJwt(), responseOpts, verifyOpts)));
+ const jwt = await requestObject.toJwt()
+ if (!jwt) throw new Error('JWT is undefined')
+ const response = await AuthorizationResponse.fromRequestObject(jwt, responseOpts, verifyOpts)
+ await expect(response).toBeDefined()
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+
+ it('succeed when valid JWT with PD is passed in', async () => {
+ expect.assertions(1)
+
+ const mockReqEntity = await mockedGetEnterpriseAuthToken('REQ COMPANY')
+ const mockResEntity = await mockedGetEnterpriseAuthToken('RES COMPANY')
+ const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+ })
+ const definition: IPresentationDefinition = {
+ id: 'Credentials',
+ input_descriptors: [
+ {
+ id: 'ID Card Credential',
+ schema: [
+ {
+ uri: 'https://www.w3.org/2018/credentials/examples/v1/IDCardCredential',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'required',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ requestObject: {
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://my-request.com/here',
+ jwtIssuer: { method: 'did', didUrl: mockReqEntity.did, alg: SigningAlgo.ES256K },
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockReqEntity.hexPrivateKey,
+ did: mockReqEntity.did,
+ kid: `${mockReqEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token vp_token',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ claims: {
+ vp_token: {
+ presentation_definition: definition,
+ },
+ },
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100315',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const vc: ICredential = {
+ id: 'https://example.com/credentials/1872',
+ type: ['VerifiableCredential', 'IDCardCredential'],
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1/IDCardCredential'],
+ issuer: {
+ id: 'did:example:issuer',
+ },
+ issuanceDate: '2010-01-01T19:23:24Z',
+ credentialSubject: {
+ given_name: 'Fredrik',
+ family_name: 'Stremberg',
+ birthdate: '1949-01-22',
+ },
+ }
+ const presentation: IVerifiablePresentation = {
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ presentation_submission: undefined,
+ type: ['verifiablePresentation'],
+ holder: 'did:example:holder',
+ verifiableCredential: [vc as IVerifiableCredential],
+ proof: undefined as any,
+ }
+
+ const pex = new PresentationExchange({
+ allDIDs: ['did:example:holder'],
+ allVerifiableCredentials: presentation.verifiableCredential as OriginalVerifiableCredential[],
+ })
+ await pex.selectVerifiableCredentialsForSubmission(definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(
+ definition,
+ presentation.verifiableCredential as OriginalVerifiableCredential[],
+ presentationSignCallback,
+ {},
+ )
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100316',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ did: mockResEntity.did,
+ hexPrivateKey: mockResEntity.hexPrivateKey,
+ kid: `${mockResEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: `${mockResEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ vpTokenLocation: VPTokenLocation.ID_TOKEN,
+ presentationSubmission: await createPresentationSubmission([verifiablePresentationResult.verifiablePresentation], {
+ presentationDefinitions: [definition],
+ }),
+ },
+ responseMode: ResponseMode.POST,
+ }
+
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+ /* console.log(
+ JSON.stringify(await AuthenticationResponse.createJWTFromRequestJWT(requestWithJWT.jwt, responseOpts, verifyOpts))
+ );*/
+ const jwt = await requestObject.toJwt()
+ if (!jwt) throw new Error('JWT is undefined')
+ const authorizationRequest = await AuthorizationResponse.fromRequestObject(jwt, responseOpts, verifyOpts)
+ await expect(authorizationRequest).toBeDefined()
+ })
+
+ it('succeed when valid JWT with PD is passed in for id_token', async () => {
+ const mockReqEntity = await mockedGetEnterpriseAuthToken('REQ COMPANY')
+ const mockResEntity = await mockedGetEnterpriseAuthToken('RES COMPANY')
+ const definition: IPresentationDefinition = {
+ id: 'Credentials',
+ input_descriptors: [
+ {
+ id: 'ID Card Credential',
+ schema: [
+ {
+ uri: 'https://www.w3.org/2018/credentials/examples/v1/IDCardCredential',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'required',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ /*scope: 'test',
+ response_type: 'token_id',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ claims: {
+ vp_token: {
+ presentation_definition: definition,
+ },
+ },*/
+ },
+ requestObject: {
+ jwtIssuer: { method: 'did', didUrl: `${mockReqEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://my-request.com/here',
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockReqEntity.hexPrivateKey,
+ did: mockReqEntity.did,
+ kid: `${mockReqEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: ResponseType.ID_TOKEN,
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ claims: {
+ vp_token: {
+ presentation_definition: definition,
+ },
+ },
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL,
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+ const vc: ICredential = {
+ id: 'https://example.com/credentials/1872',
+ type: ['VerifiableCredential', 'IDCardCredential'],
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1/IDCardCredential'],
+ issuer: {
+ id: 'did:example:issuer',
+ },
+ issuanceDate: '2010-01-01T19:23:24Z',
+ credentialSubject: {
+ given_name: 'Fredrik',
+ family_name: 'Stremberg',
+ birthdate: '1949-01-22',
+ },
+ }
+ const presentation: IVerifiablePresentation = {
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ presentation_submission: undefined,
+ type: ['verifiablePresentation'],
+ holder: 'did:example:holder',
+ verifiableCredential: [vc as IVerifiableCredential],
+ proof: undefined as any,
+ }
+
+ const pex = new PresentationExchange({
+ allDIDs: ['did:example:holder'],
+ allVerifiableCredentials: presentation.verifiableCredential as OriginalVerifiableCredential[],
+ })
+ await pex.selectVerifiableCredentialsForSubmission(definition)
+ const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+ })
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(
+ definition,
+ presentation.verifiableCredential as OriginalVerifiableCredential[],
+ presentationSignCallback,
+ {},
+ )
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL,
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ did: mockResEntity.did,
+ hexPrivateKey: mockResEntity.hexPrivateKey,
+ kid: `${mockResEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: `${mockResEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: await createPresentationSubmission([verifiablePresentationResult.verifiablePresentation], {
+ presentationDefinitions: [definition],
+ }),
+ vpTokenLocation: VPTokenLocation.ID_TOKEN,
+ },
+
+ responseMode: ResponseMode.POST,
+ }
+
+ const requestObject = await RequestObject.fromOpts(requestOpts)
+ const jwt = await requestObject.toJwt()
+ if (!jwt) throw new Error('JWT is undefined')
+ const authResponse = AuthorizationResponse.fromRequestObject(jwt, responseOpts, verifyOpts)
+ await expect(authResponse).toBeDefined()
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.verify.spec.ts b/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.verify.spec.ts
new file mode 100644
index 00000000..02573bca
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/AuthenticationResponse.verify.spec.ts
@@ -0,0 +1,45 @@
+import { IDToken, VerifyAuthorizationResponseOpts } from '..'
+import SIOPErrors from '../types/Errors'
+
+import { getVerifyJwtCallback } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+
+// const EXAMPLE_REDIRECT_URL = "https://acme.com/hello";
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+
+const validButExpiredResJWT =
+ 'eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXRocjoweDk3NTgzNmREM0Y1RTk4QzE5RjBmM2I4N0Y5OWFGMzA1MDAyNkREQzIjY29udHJvbGxlciIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzIyNzE4MDMuMjEyLCJleHAiOjE2MzIyNzI0MDMuMjEyLCJpc3MiOiJodHRwczovL3NlbGYtaXNzdWVkLm1lL3YyIiwic3ViIjoiZGlkOmV0aHI6MHg5NzU4MzZkRDNGNUU5OEMxOUYwZjNiODdGOTlhRjMwNTAwMjZEREMyIiwiYXVkIjoiaHR0cHM6Ly9hY21lLmNvbS9oZWxsbyIsImRpZCI6ImRpZDpldGhyOjB4OTc1ODM2ZEQzRjVFOThDMTlGMGYzYjg3Rjk5YUYzMDUwMDI2RERDMiIsInN1Yl90eXBlIjoiZGlkIiwic3ViX2p3ayI6eyJraWQiOiJkaWQ6ZXRocjoweDk3NTgzNmREM0Y1RTk4QzE5RjBmM2I4N0Y5OWFGMzA1MDAyNkREQzIjY29udHJvbGxlciIsImt0eSI6IkVDIiwiY3J2Ijoic2VjcDI1NmsxIiwieCI6IkloUXVEek5BY1dvczVXeDd4U1NHMks2Zkp6MnBobU1nbUZ4UE1xaEU4XzgiLCJ5IjoiOTlreGpCMVgzaUtkRXZkbVFDbllqVm5PWEJyc2VwRGdlMFJrek1aUDN1TSJ9LCJzdGF0ZSI6ImQ2NzkzYjQ2YWIyMzdkMzczYWRkNzQwMCIsIm5vbmNlIjoiU1JXSzltSVpFd1F6S3dsZlZoMkE5SV9weUtBT0tnNDAtWDJqbk5aZEN0byIsInJlZ2lzdHJhdGlvbiI6eyJpc3N1ZXIiOiJodHRwczovL3NlbGYtaXNzdWVkLm1lL3YyIiwicmVzcG9uc2VfdHlwZXNfc3VwcG9ydGVkIjoiaWRfdG9rZW4iLCJhdXRob3JpemF0aW9uX2VuZHBvaW50Ijoib3BlbmlkOiIsInNjb3Blc19zdXBwb3J0ZWQiOiJvcGVuaWQiLCJpZF90b2tlbl9zaWduaW5nX2FsZ192YWx1ZXNfc3VwcG9ydGVkIjpbIkVTMjU2SyIsIkVkRFNBIl0sInJlcXVlc3Rfb2JqZWN0X3NpZ25pbmdfYWxnX3ZhbHVlc19zdXBwb3J0ZWQiOlsiRVMyNTZLIiwiRWREU0EiXSwic3ViamVjdF90eXBlc19zdXBwb3J0ZWQiOiJwYWlyd2lzZSJ9fQ.coLQr2hQuMwEfYUd3HdFt-ixhsaicc37cC9cwmQ2U5hfxRhAb871s9G1GAo3qhsa9v3t0G1bTX2J9WhLaC5J_Q'
+
+describe('verify JWT from Request JWT should', () => {
+ const verifyOpts: VerifyAuthorizationResponseOpts = {
+ correlationId: '1234',
+ audience: DID,
+ verifyJwtCallback: getVerifyJwtCallback(getResolver('ethr'), {
+ checkLinkedDomain: 'if_present',
+ }),
+ verification: {},
+ }
+
+ it('throw NO_JWT when no jwt is passed', async () => {
+ expect.assertions(1)
+ await expect(IDToken.verify(undefined as never, verifyOpts)).rejects.toThrow(SIOPErrors.NO_JWT)
+ })
+ it('throw VERIFY_BAD_PARAMS when no verifyOpts is passed', async () => {
+ expect.assertions(1)
+ await expect(IDToken.verify(validButExpiredResJWT, undefined as never)).rejects.toThrow(SIOPErrors.VERIFY_BAD_PARAMS)
+ })
+
+ it('throw JWT_ERROR when expired but valid JWT is passed in', async () => {
+ expect.assertions(1)
+ await expect(IDToken.verify(validButExpiredResJWT, { ...verifyOpts, audience: 'https://acme.com/hello' })).rejects.toThrow(
+ /invalid_jwt: JWT has expired: exp: 1632272403/,
+ )
+ })
+
+ it('throw JWT_ERROR when expired but valid JWT is passed in', async () => {
+ expect.assertions(1)
+ await expect(IDToken.verify(validButExpiredResJWT, { ...verifyOpts, audience: 'https://acme.com/hello' })).rejects.toThrow(
+ /invalid_jwt: JWT has expired: exp: 1632272403/,
+ )
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/DidJwtTestUtils.ts b/packages/siop-oid4vp/lib/__tests__/DidJwtTestUtils.ts
new file mode 100644
index 00000000..c034c439
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/DidJwtTestUtils.ts
@@ -0,0 +1,168 @@
+import { VerifyCallback } from '@sphereon/wellknown-dids-client'
+import { createJWT, EdDSASigner, ES256KSigner, ES256Signer, hexToBytes, JWTOptions, JWTVerifyOptions, Signer, verifyJWT } from 'did-jwt'
+import { Resolvable } from 'did-resolver'
+
+import { parseJWT } from '../helpers/jwtUtils'
+import { DEFAULT_EXPIRATION_TIME, JwtPayload, ResponseIss, SigningAlgo, SIOPErrors, VerifiedJWT, VerifyJwtCallback } from '../types'
+import { CreateJwtCallback } from '../types/JwtIssuer'
+
+import { getResolver } from './ResolverTestUtils'
+
+export async function verifyDidJWT(jwt: string, resolver: Resolvable, options: JWTVerifyOptions): Promise {
+ return verifyJWT(jwt, { ...options, resolver })
+}
+
+/**
+ * Creates a signed JWT given an address which becomes the issuer, a signer function, and a payload for which the withSignature is over.
+ *
+ * @example
+ * const signer = ES256KSigner(process.env.PRIVATE_KEY)
+ * createJWT({address: '5A8bRWU3F7j3REx3vkJ...', signer}, {key1: 'value', key2: ..., ... }).then(JWT => {
+ * ...
+ * })
+ *
+ * @param {Object} payload payload object
+ * @param {Object} [options] an unsigned credential object
+ * @param {String} options.issuer The DID of the issuer (signer) of JWT
+ * @param {Signer} options.signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
+ * @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
+ * @param {Object} header optional object to specify or customize the JWT header
+ * @return {Promise} a promise which resolves with a signed JSON Web Token or rejects with an error
+ */
+export async function createDidJWT(
+ payload: Partial,
+ { issuer, signer, expiresIn, canonicalize }: JWTOptions,
+ header: Partial,
+): Promise {
+ return createJWT(payload, { issuer, signer, expiresIn, canonicalize }, header)
+}
+export interface InternalSignature {
+ hexPrivateKey: string // hex private key Only secp256k1 format
+ did: string
+
+ alg: SigningAlgo
+ kid?: string // Optional: key identifier
+
+ customJwtSigner?: Signer
+}
+
+export function getAudience(jwt: string) {
+ const { payload } = parseJWT(jwt)
+ if (!payload) {
+ throw new Error(SIOPErrors.NO_AUDIENCE)
+ } else if (!payload.aud) {
+ return undefined
+ } else if (Array.isArray(payload.aud)) {
+ throw new Error(SIOPErrors.INVALID_AUDIENCE)
+ }
+
+ return payload.aud
+}
+
+export const internalSignature = (hexPrivateKey: string, did: string, didUrl: string, alg: SigningAlgo) => {
+ return getCreateJwtCallback({
+ hexPrivateKey,
+ kid: didUrl,
+ alg,
+ did,
+ })
+}
+
+export function getCreateJwtCallback(signature: InternalSignature): CreateJwtCallback {
+ return (jwtIssuer, jwt) => {
+ if (jwtIssuer.method === 'did') {
+ const issuer = jwtIssuer.didUrl.split('#')[0]
+ if (!signature.kid) throw new Error('Missing kid')
+ return signDidJwtInternal(jwt.payload, issuer, signature.hexPrivateKey, signature.alg, signature.kid, signature.customJwtSigner)
+ } else if (jwtIssuer.method === 'custom') {
+ if (jwtIssuer.type === 'request-object') {
+ const did = signature.did
+ jwt.payload.iss = jwt.payload.iss ?? did
+ jwt.payload.sub = jwt.payload.sub ?? did
+ jwt.payload.client_id = jwt.payload.client_id ?? did
+ }
+
+ if (!signature.kid) throw new Error('Missing kid')
+ if (jwtIssuer.type === 'id-token') {
+ if (!jwt.payload.sub) jwt.payload.sub = signature.did
+
+ const issuer = jwtIssuer.authorizationResponseOpts.registration?.issuer || this._payload.iss
+ if (!issuer || !(issuer.includes(ResponseIss.SELF_ISSUED_V2) || issuer === this._payload.sub)) {
+ throw new Error(SIOPErrors.NO_SELF_ISSUED_ISS)
+ }
+ if (!jwt.payload.iss) {
+ jwt.payload.iss = issuer
+ }
+ return signDidJwtInternal(jwt.payload, issuer, signature.hexPrivateKey, signature.alg, signature.kid, signature.customJwtSigner)
+ }
+
+ return signDidJwtInternal(jwt.payload, signature.did, signature.hexPrivateKey, signature.alg, signature.kid, signature.customJwtSigner)
+ }
+ throw new Error('Not implemented yet')
+ }
+}
+
+export function getVerifyJwtCallback(
+ resolver?: Resolvable,
+ verifyOpts?: JWTVerifyOptions & {
+ checkLinkedDomain: 'never' | 'if_present' | 'always'
+ wellknownDIDVerifyCallback?: VerifyCallback
+ },
+): VerifyJwtCallback {
+ return async (jwtVerifier, jwt) => {
+ resolver = resolver ?? getResolver(['ethr', 'ion'])
+ const audience =
+ jwtVerifier.type === 'request-object'
+ ? verifyOpts?.audience ?? getAudience(jwt.raw)
+ : jwtVerifier.type === 'id-token'
+ ? verifyOpts?.audience ?? getAudience(jwt.raw)
+ : undefined
+
+ await verifyDidJWT(jwt.raw, resolver, { audience, ...verifyOpts })
+ // we can always because the verifyDidJWT will throw an error if the JWT is invalid
+ return true
+ }
+}
+
+async function signDidJwtInternal(
+ payload: JwtPayload,
+ issuer: string,
+ hexPrivateKey: string,
+ alg: SigningAlgo,
+ kid: string,
+ customJwtSigner?: Signer,
+): Promise {
+ const signer = determineSigner(alg, hexPrivateKey, customJwtSigner)
+ const header = {
+ alg,
+ kid,
+ }
+ const options = {
+ issuer,
+ signer,
+ expiresIn: DEFAULT_EXPIRATION_TIME,
+ }
+
+ return await createDidJWT({ ...payload }, options, header)
+}
+
+const determineSigner = (alg: SigningAlgo, hexPrivateKey?: string, customSigner?: Signer): Signer => {
+ if (customSigner) {
+ return customSigner
+ } else if (!hexPrivateKey) {
+ throw new Error('no private key provided')
+ }
+ const privateKey = hexToBytes(hexPrivateKey.replace('0x', ''))
+ switch (alg) {
+ case SigningAlgo.EDDSA:
+ return EdDSASigner(privateKey)
+ case SigningAlgo.ES256:
+ return ES256Signer(privateKey)
+ case SigningAlgo.ES256K:
+ return ES256KSigner(privateKey)
+ case SigningAlgo.PS256:
+ throw Error('PS256 is not supported yet. Please provide a custom signer')
+ case SigningAlgo.RS256:
+ throw Error('RS256 is not supported yet. Please provide a custom signer')
+ }
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/DocumentLoader.ts b/packages/siop-oid4vp/lib/__tests__/DocumentLoader.ts
new file mode 100644
index 00000000..225daf24
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/DocumentLoader.ts
@@ -0,0 +1,48 @@
+import { extendContextLoader } from '@digitalcredentials/jsonld-signatures'
+import vc from '@digitalcredentials/vc'
+import fetch from 'cross-fetch'
+
+export class DocumentLoader {
+ getLoader() {
+ return extendContextLoader(async (url: string) => {
+ if (url === 'https://identity.foundation/.well-known/did-configuration/v1') {
+ // Not sure what is happening, but this URL is failing in Github. Probably, cloudflare getting in the way, which might have impact in production settings to
+ return {
+ document: {
+ documentUrl: url,
+ '@context': [
+ {
+ '@version': 1.1,
+ '@protected': true,
+ LinkedDomains: 'https://identity.foundation/.well-known/resources/did-configuration/#LinkedDomains',
+ DomainLinkageCredential: 'https://identity.foundation/.well-known/resources/did-configuration/#DomainLinkageCredential',
+ origin: 'https://identity.foundation/.well-known/resources/did-configuration/#origin',
+ linked_dids: 'https://identity.foundation/.well-known/resources/did-configuration/#linked_dids',
+ },
+ ],
+ },
+ }
+ }
+ try {
+ const response = await fetch(url)
+ if (response.status >= 200 && response.status < 300) {
+ const document = await response.json()
+ return {
+ contextUrl: null,
+ documentUrl: url,
+ document,
+ }
+ } else {
+ console.log(`ERROR: ${url}`)
+ console.log(`url: ${url}, status: ${response.status}: ${response.statusText}`)
+ console.log(`response: ${await response.text()}`)
+ }
+ } catch (error) {
+ console.log(`ERROR:::::::: ${url}: ${JSON.stringify(error.message)}`)
+ }
+
+ const { nodeDocumentLoader } = vc
+ return nodeDocumentLoader(url)
+ })
+ }
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/HttpUtils.fetch.spec.ts b/packages/siop-oid4vp/lib/__tests__/HttpUtils.fetch.spec.ts
new file mode 100644
index 00000000..681b9821
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/HttpUtils.fetch.spec.ts
@@ -0,0 +1,40 @@
+import nock from 'nock'
+
+import { post } from '..'
+
+const URL = 'https://example.com'
+nock(URL)
+ .post('/404', { iss: 'mock' }, { reqheaders: { Authorization: 'Bearer bearerToken' } })
+ .reply(404, 'Not found')
+nock(URL)
+ .post('/200', { iss: 'mock' }, { reqheaders: { Authorization: 'Bearer bearerToken' } })
+ .reply(200, '{"status": "ok"}')
+nock(URL)
+ .post('/201', { iss: 'mock' }, { reqheaders: { Authorization: 'Bearer bearerToken' } })
+ .reply(201, '{"status": "ok"}')
+
+describe('HttpUtils should', () => {
+ it('have an error body when response is not 200 or 201', async () => {
+ expect.assertions(1)
+ await expect(
+ post(`${URL}/404`, JSON.stringify({ iss: 'mock' }), { bearerToken: 'bearerToken' }).then((value) => value.errorBody),
+ ).resolves.toMatch('Not found')
+ })
+
+ it('return response when response HTTP status is 200', async () => {
+ expect.assertions(1)
+ await expect(
+ post(`${URL}/200`, JSON.stringify({ iss: 'mock' }), { bearerToken: 'bearerToken' }).then((value) => value.successBody),
+ ).resolves.toMatchObject({
+ status: 'ok',
+ })
+ })
+ it('return response when response HTTP status is 201', async () => {
+ expect.assertions(1)
+ await expect(
+ post(`${URL}/201`, JSON.stringify({ iss: 'mock' }), { bearerToken: 'bearerToken' }).then((value) => value.successBody),
+ ).resolves.toMatchObject({
+ status: 'ok',
+ })
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/IT.spec.ts b/packages/siop-oid4vp/lib/__tests__/IT.spec.ts
new file mode 100644
index 00000000..3c28184d
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/IT.spec.ts
@@ -0,0 +1,1833 @@
+import { EventEmitter } from 'events'
+
+import { IPresentationDefinition } from '@sphereon/pex'
+import { CredentialMapper, IPresentation, IProofType, IVerifiableCredential, W3CVerifiablePresentation } from '@sphereon/ssi-types'
+import nock from 'nock'
+
+import { InMemoryRPSessionManager } from '..'
+import {
+ OP,
+ PassBy,
+ PresentationDefinitionWithLocation,
+ PresentationExchange,
+ PresentationSignCallback,
+ PresentationVerificationCallback,
+ PropertyTarget,
+ ResponseIss,
+ ResponseType,
+ RevocationStatus,
+ RevocationVerification,
+ RP,
+ Scope,
+ SigningAlgo,
+ SubjectType,
+ SupportedVersion,
+ verifyRevocation,
+ VPTokenLocation,
+} from '../'
+import { checkSIOPSpecVersionSupported } from '../helpers/SIOPSpecVersion'
+
+import { getVerifyJwtCallback, internalSignature } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { mockedGetEnterpriseAuthToken, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ UNIT_TEST_TIMEOUT,
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+jest.setTimeout(30000)
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+
+const HOLDER_DID = 'did:example:ebfeb1f712ebc6f1c276e12ec21'
+
+const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+})
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const presentationVerificationCallback: PresentationVerificationCallback = async (_args: W3CVerifiablePresentation) => ({
+ verified: true,
+})
+
+function getPresentationDefinition(): IPresentationDefinition {
+ return {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ {
+ uri: 'https://www.w3.org/2018/credentials/v1',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'preferred',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }
+}
+
+function getVCs(): IVerifiableCredential[] {
+ const vcs: IVerifiableCredential[] = [
+ {
+ identifier: '83627465',
+ name: 'Permanent Resident Card',
+ type: ['PermanentResidentCard', 'VerifiableCredential'],
+ id: 'https://issuer.oidp.uscis.gov/credentials/83627465dsdsdsd',
+ credentialSubject: {
+ birthCountry: 'Bahamas',
+ id: 'did:example:b34ca6cd37bbf23',
+ type: ['PermanentResident', 'Person'],
+ gender: 'Female',
+ familyName: 'SMITH',
+ givenName: 'JANE',
+ residentSince: '2015-01-01',
+ lprNumber: '999-999-999',
+ birthDate: '1958-07-17',
+ commuterClassification: 'C1',
+ lprCategory: 'C09',
+ image: 'data:image/png;base64,iVBORw0KGgokJggg==',
+ },
+ expirationDate: '2029-12-03T12:19:52Z',
+ description: 'Government of Example Permanent Resident Card.',
+ issuanceDate: '2019-12-03T12:19:52Z',
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/citizenship/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'],
+ issuer: 'did:key:z6MkhfRoL9n7ko9d6LnB5jLB4aejd3ir2q6E2xkuzKUYESig',
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ nonce: '6i3dTz5yFfWJ8zgsamuyZa4yAHPm75tUOOXddR6krCvCYk77sbCOuEVcdBCDd/l6tIY=',
+ },
+ },
+ ]
+ vcs[0]['@context'] = ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1']
+ vcs[0]['issuer'] = {
+ id: 'did:example:issuer',
+ }
+ return vcs
+}
+
+describe('RP and OP interaction should', () => {
+ it(
+ 'succeed when calling each other in the full flow',
+ async () => {
+ // expect.assertions(1);
+ const rpMockEntity = await mockedGetEnterpriseAuthToken('ACME RP')
+ const opMockEntity = await mockedGetEnterpriseAuthToken('ACME OP')
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver(['ethr'])
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.REFERENCE, EXAMPLE_REFERENCE_URL)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, `${rpMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100317',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, `${opMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ //FIXME: Move payload options to seperate property
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100318',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f' },
+ })
+
+ nock('https://rp.acme.com').get('/siop/jwts').times(3).reply(200, requestURI.requestObjectJwt)
+
+ if (!op.verifyRequestOptions.supportedVersions) throw new Error('Supported versions not set')
+ await checkSIOPSpecVersionSupported(requestURI.authorizationRequestPayload, op.verifyRequestOptions.supportedVersions)
+ // The create method also calls the verifyRequest method, so no need to do it manually
+ const verifiedRequest = await op.verifyAuthorizationRequest(requestURI.encodedUri)
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedRequest, {})
+
+ nock(EXAMPLE_REDIRECT_URL).post(/.*/).times(3).reply(200, { result: 'ok' })
+ const response = await op.submitAuthorizationResponse(authenticationResponseWithJWT)
+ await expect(response.json()).resolves.toMatchObject({ result: 'ok' })
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+
+ it('succeed when calling optional steps in the full flow', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100319',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100320',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f' },
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+
+ if (!op.verifyRequestOptions.supportedVersions) throw new Error('Supported versions not set')
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt, { correlationId: '1234' })
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {})
+ expect(authenticationResponseWithJWT).toBeDefined()
+ expect(authenticationResponseWithJWT.correlationId).toEqual('1234')
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ /*audience: EXAMPLE_REDIRECT_URL,*/
+ })
+
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('fail when calling with presentation definitions and without verifiable presentation', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(WELL_KNOWN_OPENID_FEDERATION)
+ .withScope('test')
+ .withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withClientMetadata({
+ client_id: rpMockEntity.did,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100321',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100321',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f' },
+ })
+
+ //The schema validation needs to be done here otherwise it fails because of JWT properties
+ if (!op.verifyRequestOptions.supportedVersions) throw new Error('Supported versions not set')
+ await checkSIOPSpecVersionSupported(requestURI.authorizationRequestPayload, op.verifyRequestOptions.supportedVersions)
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ await expect(op.createAuthorizationResponse(verifiedAuthReqWithJWT, {})).rejects.toThrow(
+ Error('authentication request expects a verifiable presentation in the response'),
+ )
+
+ expect(verifiedAuthReqWithJWT.payload?.['registration'].client_name).toEqual(VERIFIER_NAME_FOR_CLIENT)
+ expect(verifiedAuthReqWithJWT.payload?.['registration']['client_name#nl-NL']).toEqual(VERIFIER_NAME_FOR_CLIENT_NL + '2022100321')
+ })
+
+ it('succeed when calling with presentation definitions and right verifiable presentation', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100322',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100323',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Supported versions not set')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ },
+ ],*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ /*audience: EXAMPLE_REDIRECT_URL,*/
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ })
+
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('succeed when calling with RevocationVerification.ALWAYS with ldp_vp', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType([ResponseType.VP_TOKEN, ResponseType.ID_TOKEN])
+ .withRevocationVerification(RevocationVerification.ALWAYS)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerificationCallback(async () => {
+ return { status: RevocationStatus.VALID }
+ })
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100330',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withPresentationSignCallback(presentationSignCallback)
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100331',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ if (!op.verifyRequestOptions.supportedVersions) throw new Error('Supported versions not set')
+ await checkSIOPSpecVersionSupported(requestURI.authorizationRequestPayload, op.verifyRequestOptions.supportedVersions)
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt) //, rp.authRequestOpts
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ },
+ ],*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const DID_CONFIGURATION = {
+ '@context': 'https://identity.foundation/.well-known/did-configuration/v1',
+ linked_dids: [
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRvbWFpbkxpbmthZ2VDcmVkZW50aWFsIl19fQ.YZnpPMAW3GdaPXC2YKoJ7Igt1OaVZKq09XZBkptyhxTAyHTkX2Ewtew-JKHKQjyDyabY3HAy1LUPoIQX0jrU0J82pIYT3k2o7nNTdLbxlgb49FcDn4czntt5SbY0m1XwrMaKEvV0bHQsYPxNTqjYsyySccgPfmvN9IT8gRS-M9a6MZQxuB3oEMrVOQ5Vco0bvTODXAdCTHibAk1FlvKz0r1vO5QMhtW4OlRrVTI7ibquf9Nim_ch0KeMMThFjsBDKetuDF71nUcL5sf7PCFErvl8ZVw3UK4NkZ6iM-XIRsLL6rXP2SnDUVovcldhxd_pyKEYviMHBOgBdoNP6fOgRQ',
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6b3RoZXIiLCJuYmYiOjE2MDcxMTI3MzksInN1YiI6ImRpZDprZXk6b3RoZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6b3RoZXIiLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6b3RoZXIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.rRuc-ojuEgyq8p_tBYK7BayuiNTBeXNyAnC14Rnjs-jsnhae4_E1Q12W99K2NGCGBi5KjNsBcZmdNJPxejiKPrjjcB99poFCgTY8tuRzDjVo0lIeBwfx9qqjKHTRTUR8FGM_imlOpVfBF4AHYxjkHvZn6c9lYvatYcDpB2UfH4BNXkdSVrUXy_kYjpMpAdRtyCAnD_isN1YpEHBqBmnfuVUbYcQK5kk6eiokRFDtWruL1OEeJMYPqjuBSd2m-H54tSM84Oic_pg2zXDjjBlXNelat6MPNT2QxmkwJg7oyewQWX2Ot2yyhSp9WyAQWMlQIe2x84R0lADUmZ1TPQchNw',
+ ],
+ }
+ nock('https://ldtest.sphereon.com').get('/.well-known/did-configuration.json').times(3).reply(200, DID_CONFIGURATION)
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it(
+ 'should succeed when calling with CheckLinkedDomain.IF_PRESENT',
+ async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'if_present' }))
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100328',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [
+ PropertyTarget.REQUEST_OBJECT,
+ PropertyTarget.AUTHORIZATION_REQUEST,
+ ])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+
+ .withExpiresIn(1000)
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'never' }))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100329',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ },
+ ],*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+
+ it('succeed when calling with RevocationVerification.ALWAYS with ldp_vp', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType([ResponseType.VP_TOKEN, ResponseType.ID_TOKEN])
+ .withRevocationVerification(RevocationVerification.ALWAYS)
+ .withPresentationVerification(presentationVerificationCallback)
+
+ .withRevocationVerificationCallback(async () => {
+ return { status: RevocationStatus.VALID }
+ })
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100330',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withPresentationSignCallback(presentationSignCallback)
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100331',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ if (!op.verifyRequestOptions.supportedVersions) throw new Error('Supported versions not set')
+ await checkSIOPSpecVersionSupported(requestURI.authorizationRequestPayload, op.verifyRequestOptions.supportedVersions)
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt) //, rp.authRequestOpts
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ },
+ ],*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const DID_CONFIGURATION = {
+ '@context': 'https://identity.foundation/.well-known/did-configuration/v1',
+ linked_dids: [
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRvbWFpbkxpbmthZ2VDcmVkZW50aWFsIl19fQ.YZnpPMAW3GdaPXC2YKoJ7Igt1OaVZKq09XZBkptyhxTAyHTkX2Ewtew-JKHKQjyDyabY3HAy1LUPoIQX0jrU0J82pIYT3k2o7nNTdLbxlgb49FcDn4czntt5SbY0m1XwrMaKEvV0bHQsYPxNTqjYsyySccgPfmvN9IT8gRS-M9a6MZQxuB3oEMrVOQ5Vco0bvTODXAdCTHibAk1FlvKz0r1vO5QMhtW4OlRrVTI7ibquf9Nim_ch0KeMMThFjsBDKetuDF71nUcL5sf7PCFErvl8ZVw3UK4NkZ6iM-XIRsLL6rXP2SnDUVovcldhxd_pyKEYviMHBOgBdoNP6fOgRQ',
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6b3RoZXIiLCJuYmYiOjE2MDcxMTI3MzksInN1YiI6ImRpZDprZXk6b3RoZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6b3RoZXIiLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6b3RoZXIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.rRuc-ojuEgyq8p_tBYK7BayuiNTBeXNyAnC14Rnjs-jsnhae4_E1Q12W99K2NGCGBi5KjNsBcZmdNJPxejiKPrjjcB99poFCgTY8tuRzDjVo0lIeBwfx9qqjKHTRTUR8FGM_imlOpVfBF4AHYxjkHvZn6c9lYvatYcDpB2UfH4BNXkdSVrUXy_kYjpMpAdRtyCAnD_isN1YpEHBqBmnfuVUbYcQK5kk6eiokRFDtWruL1OEeJMYPqjuBSd2m-H54tSM84Oic_pg2zXDjjBlXNelat6MPNT2QxmkwJg7oyewQWX2Ot2yyhSp9WyAQWMlQIe2x84R0lADUmZ1TPQchNw',
+ ],
+ }
+ nock('https://ldtest.sphereon.com').get('/.well-known/did-configuration.json').times(3).reply(200, DID_CONFIGURATION)
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('succeed when calling with CheckLinkedDomain.ALWAYS', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'always' }))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100326',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'always' }))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100327',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ },
+ ],*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+
+ const DID_CONFIGURATION = {
+ '@context': 'https://identity.foundation/.well-known/did-configuration/v1',
+ linked_dids: [
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRvbWFpbkxpbmthZ2VDcmVkZW50aWFsIl19fQ.YZnpPMAW3GdaPXC2YKoJ7Igt1OaVZKq09XZBkptyhxTAyHTkX2Ewtew-JKHKQjyDyabY3HAy1LUPoIQX0jrU0J82pIYT3k2o7nNTdLbxlgb49FcDn4czntt5SbY0m1XwrMaKEvV0bHQsYPxNTqjYsyySccgPfmvN9IT8gRS-M9a6MZQxuB3oEMrVOQ5Vco0bvTODXAdCTHibAk1FlvKz0r1vO5QMhtW4OlRrVTI7ibquf9Nim_ch0KeMMThFjsBDKetuDF71nUcL5sf7PCFErvl8ZVw3UK4NkZ6iM-XIRsLL6rXP2SnDUVovcldhxd_pyKEYviMHBOgBdoNP6fOgRQ',
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6b3RoZXIiLCJuYmYiOjE2MDcxMTI3MzksInN1YiI6ImRpZDprZXk6b3RoZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6b3RoZXIiLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6b3RoZXIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.rRuc-ojuEgyq8p_tBYK7BayuiNTBeXNyAnC14Rnjs-jsnhae4_E1Q12W99K2NGCGBi5KjNsBcZmdNJPxejiKPrjjcB99poFCgTY8tuRzDjVo0lIeBwfx9qqjKHTRTUR8FGM_imlOpVfBF4AHYxjkHvZn6c9lYvatYcDpB2UfH4BNXkdSVrUXy_kYjpMpAdRtyCAnD_isN1YpEHBqBmnfuVUbYcQK5kk6eiokRFDtWruL1OEeJMYPqjuBSd2m-H54tSM84Oic_pg2zXDjjBlXNelat6MPNT2QxmkwJg7oyewQWX2Ot2yyhSp9WyAQWMlQIe2x84R0lADUmZ1TPQchNw',
+ ],
+ }
+ nock('https://ldtest.sphereon.com').get('/.well-known/did-configuration.json').times(3).reply(200, DID_CONFIGURATION)
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('should verify revocation ldp_vp with RevocationVerification.ALWAYS', async () => {
+ const presentation = {
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://identity.foundation/presentation-exchange/submission/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ presentation_submission: {
+ id: 'K7Zu3C6yJv3TGXYCB3B3n',
+ definition_id: 'Insurance Plans',
+ descriptor_map: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ format: 'ldp_vc',
+ path: '$.verifiableCredential[0]',
+ },
+ ],
+ },
+ verifiableCredential: [
+ {
+ identifier: '83627465',
+ name: 'Permanent Resident Card',
+ type: ['PermanentResidentCard', 'VerifiableCredential'],
+ id: 'https://issuer.oidp.uscis.gov/credentials/83627465dsdsdsd',
+ credentialSubject: {
+ birthCountry: 'Bahamas',
+ id: 'did:example:b34ca6cd37bbf23',
+ type: ['PermanentResident', 'Person'],
+ gender: 'Female',
+ familyName: 'SMITH',
+ givenName: 'JANE',
+ residentSince: '2015-01-01',
+ lprNumber: '999-999-999',
+ birthDate: '1958-07-17',
+ commuterClassification: 'C1',
+ lprCategory: 'C09',
+ image: 'data:image/png;base64,iVBORw0KGgokJggg==',
+ },
+ expirationDate: '2029-12-03T12:19:52Z',
+ description: 'Government of Example Permanent Resident Card.',
+ issuanceDate: '2019-12-03T12:19:52Z',
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
+ issuer: {
+ id: 'did:example:issuer',
+ },
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ nonce: '6i3dTz5yFfWJ8zgsamuyZa4yAHPm75tUOOXddR6krCvCYk77sbCOuEVcdBCDd/l6tIY=',
+ },
+ },
+ ],
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ nonce: '6i3dTz5yFfWJ8zgsamuyZa4yAHPm75tUOOXddR6krCvCYk77sbCOuEVcdBCDd/l6tIY=',
+ },
+ }
+
+ await expect(
+ verifyRevocation(
+ CredentialMapper.toWrappedVerifiablePresentation(presentation),
+ async () => {
+ return { status: RevocationStatus.VALID }
+ },
+ RevocationVerification.ALWAYS,
+ ),
+ ).resolves.not.toThrow()
+ })
+
+ it('should verify revocation ldp_vp with RevocationVerification.IF_PRESENT', async () => {
+ const presentation = {
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://identity.foundation/presentation-exchange/submission/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ presentation_submission: {
+ id: 'K7Zu3C6yJv3TGXYCB3B3n',
+ definition_id: 'Insurance Plans',
+ descriptor_map: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ format: 'ldp_vc',
+ path: '$.verifiableCredential[0]',
+ },
+ ],
+ },
+ verifiableCredential: [
+ {
+ identifier: '83627465',
+ name: 'Permanent Resident Card',
+ type: ['PermanentResidentCard', 'VerifiableCredential'],
+ id: 'https://issuer.oidp.uscis.gov/credentials/83627465dsdsdsd',
+ credentialSubject: {
+ birthCountry: 'Bahamas',
+ id: 'did:example:b34ca6cd37bbf23',
+ type: ['PermanentResident', 'Person'],
+ gender: 'Female',
+ familyName: 'SMITH',
+ givenName: 'JANE',
+ residentSince: '2015-01-01',
+ lprNumber: '999-999-999',
+ birthDate: '1958-07-17',
+ commuterClassification: 'C1',
+ lprCategory: 'C09',
+ image: 'data:image/png;base64,iVBORw0KGgokJggg==',
+ },
+ credentialStatus: {
+ id: 'https://example.com/credentials/status/3#94567',
+ type: 'StatusList2021Entry',
+ statusPurpose: 'revocation',
+ statusListIndex: '94567',
+ statusListCredential: 'https://example.com/credentials/status/3',
+ },
+ expirationDate: '2029-12-03T12:19:52Z',
+ description: 'Government of Example Permanent Resident Card.',
+ issuanceDate: '2019-12-03T12:19:52Z',
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
+ issuer: {
+ id: 'did:example:issuer',
+ },
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ nonce: '6i3dTz5yFfWJ8zgsamuyZa4yAHPm75tUOOXddR6krCvCYk77sbCOuEVcdBCDd/l6tIY=',
+ },
+ },
+ ],
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ nonce: '6i3dTz5yFfWJ8zgsamuyZa4yAHPm75tUOOXddR6krCvCYk77sbCOuEVcdBCDd/l6tIY=',
+ },
+ }
+
+ await expect(
+ verifyRevocation(
+ CredentialMapper.toWrappedVerifiablePresentation(presentation),
+ async () => {
+ return { status: RevocationStatus.VALID }
+ },
+ RevocationVerification.ALWAYS,
+ ),
+ ).resolves.not.toThrow()
+ })
+
+ it('should verify revocation ldp_vp with location id_token', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => ({ verified: true })
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+ // expect(parsedAuthReqURI.registration).toBeDefined();
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('Request object JWT not found')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.ID_TOKEN,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.ID_TOKEN
+ }
+ ]*/
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ audience: 'test_client_id',
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('succeed with nonce verification with ldp_vp', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100330',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withPresentationSignCallback(presentationSignCallback)
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100331',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('No requestObjectJwt')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: getVCs() })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {})
+
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ vpTokenLocation: VPTokenLocation.ID_TOKEN,
+ /*credentialsAndDefinitions: [
+ {
+ presentation: vp,
+ format: VerifiablePresentationTypeFormat.LDP_VP,
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE
+ }
+ ]*/
+ },
+ })
+
+ const DID_CONFIGURATION = {
+ '@context': 'https://identity.foundation/.well-known/did-configuration/v1',
+ linked_dids: [
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRvbWFpbkxpbmthZ2VDcmVkZW50aWFsIl19fQ.YZnpPMAW3GdaPXC2YKoJ7Igt1OaVZKq09XZBkptyhxTAyHTkX2Ewtew-JKHKQjyDyabY3HAy1LUPoIQX0jrU0J82pIYT3k2o7nNTdLbxlgb49FcDn4czntt5SbY0m1XwrMaKEvV0bHQsYPxNTqjYsyySccgPfmvN9IT8gRS-M9a6MZQxuB3oEMrVOQ5Vco0bvTODXAdCTHibAk1FlvKz0r1vO5QMhtW4OlRrVTI7ibquf9Nim_ch0KeMMThFjsBDKetuDF71nUcL5sf7PCFErvl8ZVw3UK4NkZ6iM-XIRsLL6rXP2SnDUVovcldhxd_pyKEYviMHBOgBdoNP6fOgRQ',
+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6b3RoZXIiLCJuYmYiOjE2MDcxMTI3MzksInN1YiI6ImRpZDprZXk6b3RoZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6b3RoZXIiLCJvcmlnaW4iOiJodHRwczovL2lkZW50aXR5LmZvdW5kYXRpb24ifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDI1LTEyLTA0VDE0OjEyOjE5LTA2OjAwIiwiaXNzdWFuY2VEYXRlIjoiMjAyMC0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VlciI6ImRpZDprZXk6b3RoZXIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.rRuc-ojuEgyq8p_tBYK7BayuiNTBeXNyAnC14Rnjs-jsnhae4_E1Q12W99K2NGCGBi5KjNsBcZmdNJPxejiKPrjjcB99poFCgTY8tuRzDjVo0lIeBwfx9qqjKHTRTUR8FGM_imlOpVfBF4AHYxjkHvZn6c9lYvatYcDpB2UfH4BNXkdSVrUXy_kYjpMpAdRtyCAnD_isN1YpEHBqBmnfuVUbYcQK5kk6eiokRFDtWruL1OEeJMYPqjuBSd2m-H54tSM84Oic_pg2zXDjjBlXNelat6MPNT2QxmkwJg7oyewQWX2Ot2yyhSp9WyAQWMlQIe2x84R0lADUmZ1TPQchNw',
+ ],
+ }
+ nock('https://ldtest.sphereon.com').get('/.well-known/did-configuration.json').times(3).reply(200, DID_CONFIGURATION)
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ audience: 'test_client_id',
+ })
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('should register authorization request on create', async () => {
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ const eventEmitter = new EventEmitter()
+ const replayRegistry = new InMemoryRPSessionManager(eventEmitter)
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256K],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp_vp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ ldp: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ion'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100330',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withPresentationDefinition({ definition: getPresentationDefinition() })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .withSessionManager(replayRegistry)
+ .withEventEmitter(eventEmitter)
+ .build()
+
+ await rp.createAuthorizationRequest({
+ correlationId: '1234',
+ nonce: { propertyValue: 'bcceb347-1374-49b8-ace0-b868162c122d', targets: PropertyTarget.REQUEST_OBJECT },
+ state: { propertyValue: '8006b5fb-6e3b-42d1-a2be-55ed2a08073d', targets: PropertyTarget.REQUEST_OBJECT },
+ claims: {
+ propertyValue: {
+ vp_token: {
+ presentation_definition: {
+ input_descriptors: [
+ {
+ schema: [
+ {
+ uri: 'https://VerifiedEmployee',
+ },
+ ],
+ purpose: 'We need to verify that you have a valid VerifiedEmployee Verifiable Credential.',
+ name: 'VerifiedEmployeeVC',
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ id: '8006b5fb-6e3b-42d1-a2be-55ed2a08073d',
+ },
+ },
+ },
+ targets: PropertyTarget.REQUEST_OBJECT,
+ },
+ })
+
+ const state = await replayRegistry.getRequestStateByCorrelationId('1234', true)
+ expect(state?.status).toBe('created')
+ })
+
+ it('should register authorization request on create with uri', async () => {
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+ const eventEmitter = new EventEmitter()
+ const replayRegistry = new InMemoryRPSessionManager(eventEmitter)
+
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(WELL_KNOWN_OPENID_FEDERATION)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.REFERENCE, EXAMPLE_REFERENCE_URL)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100317',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .withSessionManager(replayRegistry)
+ .withEventEmitter(eventEmitter)
+ .build()
+
+ await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f' },
+ })
+
+ const state = await replayRegistry.getRequestStateByCorrelationId('1234')
+ expect(state?.status).toBe('created')
+ })
+
+ it('should register authorization response on successful verification', async () => {
+ await nock.cleanAll()
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ const eventEmitter = new EventEmitter()
+ const replayRegistry = new InMemoryRPSessionManager(eventEmitter)
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.REFERENCE, EXAMPLE_REFERENCE_URL)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100317',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .withEventEmitter(eventEmitter)
+ .withSessionManager(replayRegistry)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, opMockEntity.didKey, SigningAlgo.ES256K))
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ //FIXME: Move payload options to seperate property
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100318',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '12345',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f1' },
+ })
+ const reqStateCreated = await replayRegistry.getRequestStateByState('b32f0087fc9816eb813fd11f1', true)
+ expect(reqStateCreated?.status).toBe('created')
+ nock('https://rp.acme.com').get('/siop/jwts').times(3).reply(200, requestURI.requestObjectJwt)
+ const verifiedRequest = await op.verifyAuthorizationRequest(requestURI.encodedUri)
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedRequest, {})
+ nock(EXAMPLE_REDIRECT_URL).post(/.*/).times(3).reply(200, { result: 'ok' })
+ await op.submitAuthorizationResponse(authenticationResponseWithJWT)
+ await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ // audience: EXAMPLE_REDIRECT_URL,
+ })
+ const reqStateAfterResponse = await replayRegistry.getRequestStateByState('incorrect', false)
+ expect(reqStateAfterResponse).toBeUndefined()
+
+ const resStateAfterResponse = await replayRegistry.getResponseStateByState('b32f0087fc9816eb813fd11f1', true)
+ expect(resStateAfterResponse?.status).toBe('verified')
+ })
+
+ it('should set error status on failed authorization response verification', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+ const eventEmitter = new EventEmitter()
+ const replayRegistry = new InMemoryRPSessionManager(eventEmitter)
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.REFERENCE, EXAMPLE_REFERENCE_URL)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, rpMockEntity.didKey, SigningAlgo.ES256K))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100317',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .withSessionManager(replayRegistry)
+ .withEventEmitter(eventEmitter)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, `${opMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ //FIXME: Move payload options to seperate property
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100318',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: { propertyValue: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' },
+ state: { propertyValue: 'b32f0087fc9816eb813fd11f' },
+ })
+ const state = await replayRegistry.getRequestStateByCorrelationId('1234', true)
+ expect(state?.status).toBe('created')
+
+ nock('https://rp.acme.com').get('/siop/jwts').times(3).reply(200, requestURI.requestObjectJwt)
+ const verifiedRequest = await op.verifyAuthorizationRequest(requestURI.encodedUri)
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedRequest, {})
+ nock(EXAMPLE_REDIRECT_URL).post(/.*/).reply(200, { result: 'ok' })
+ await op.submitAuthorizationResponse(authenticationResponseWithJWT)
+ authenticationResponseWithJWT.response.payload.state = 'wrong_value'
+ await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, { correlationId: '1234' }).catch(() => {
+ //swallow this exception;
+ })
+ const reqState = await replayRegistry.getRequestStateByCorrelationId('1234', true)
+ expect(reqState?.status).toBe('created')
+
+ const resState = await replayRegistry.getResponseStateByCorrelationId('1234', true)
+ expect(resState?.status).toBe('error')
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/OP.request.spec.ts b/packages/siop-oid4vp/lib/__tests__/OP.request.spec.ts
new file mode 100644
index 00000000..ff6d5c48
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/OP.request.spec.ts
@@ -0,0 +1,274 @@
+import { IProofType } from '@sphereon/ssi-types'
+import nock from 'nock'
+
+import {
+ AuthorizationResponseOpts,
+ CreateAuthorizationRequestOpts,
+ OP,
+ PassBy,
+ ResponseIss,
+ ResponseMode,
+ ResponseType,
+ RP,
+ Scope,
+ SigningAlgo,
+ SubjectIdentifierType,
+ SubjectType,
+ SupportedVersion,
+ VerifyAuthorizationRequestOpts,
+} from '..'
+
+import { getCreateJwtCallback, getVerifyJwtCallback, internalSignature } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { mockedGetEnterpriseAuthToken, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ UNIT_TEST_TIMEOUT,
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#controller'
+
+describe('OP OPBuilder should', () => {
+ /*it('throw Error when no arguments are passed', async () => {
+ expect.assertions(1);
+ await expect(() => new OPBuilder().build()).toThrowError(Error);
+ });*/
+ it('build an OP when all arguments are set', async () => {
+ expect.assertions(1)
+
+ expect(
+ OP.builder()
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withResponseMode(ResponseMode.POST)
+ .withRegistration({
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://registration.here',
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100332',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withCreateJwtCallback(internalSignature('myprivatekey', 'did:example:123', 'did:example:123#key', SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(getResolver('ethr'), { checkLinkedDomain: 'never' }))
+ .withExpiresIn(1000)
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .build(),
+ ).toBeInstanceOf(OP)
+ })
+})
+
+describe('OP should', () => {
+ const responseOpts: AuthorizationResponseOpts = {
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subject_syntax_types_supported: ['did:web'],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100333',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ //TODO: fill it up with actual value
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ passBy: PassBy.VALUE,
+ },
+ responseMode: ResponseMode.POST,
+ expiresIn: 2000,
+ }
+
+ const resolver = getResolver('ethr')
+ const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verifyJwtCallback: getVerifyJwtCallback(resolver),
+ verification: {},
+ correlationId: '1234',
+ supportedVersions: [SupportedVersion.SIOPv2_ID1],
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ }
+
+ /*it('throw Error when build from request opts without enough params', async () => {
+ expect.assertions(1);
+ await expect(() => OP.fromOpts({} as never, {} as never)).toThrowError(Error);
+ });*/
+
+ it('return an OP when all request arguments are set', async () => {
+ expect.assertions(1)
+
+ expect(OP.fromOpts(responseOpts, verifyOpts)).toBeInstanceOf(OP)
+ })
+
+ it(
+ 'succeed from request opts when all params are set',
+ async () => {
+ const mockEntity = await mockedGetEnterpriseAuthToken('ACME Corp')
+ const requestOpts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ options: {
+ kid: '1234',
+ },
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: mockEntity.hexPrivateKey,
+ did: mockEntity.did,
+ kid: `${mockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ payload: {
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'test',
+ response_type: 'id_token',
+ },
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100334',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ const requestURI = await RP.fromRequestOpts(requestOpts).createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ jwtIssuer: { method: 'did', didUrl: `${mockEntity.did}#controller`, alg: SigningAlgo.ES256K, options: { kid: '1234' } },
+ })
+
+ nock('https://rp.acme.com').get('/siop/jwts').reply(200, requestURI.requestObjectJwt)
+
+ const verifiedRequest = await OP.fromOpts(responseOpts, verifyOpts).verifyAuthorizationRequest(requestURI.encodedUri)
+ // console.log(JSON.stringify(verifiedRequest));
+ expect(verifiedRequest.issuer).toMatch(mockEntity.did)
+ expect(verifiedRequest.jwt).toBeDefined()
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+
+ it('succeed from builder when all params are set', async () => {
+ const rpMockEntity = await mockedGetEnterpriseAuthToken('ACME RP')
+ const opMockEntity = await mockedGetEnterpriseAuthToken('ACME OP')
+
+ const requestURI = await RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(WELL_KNOWN_OPENID_FEDERATION)
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withRedirectUri(EXAMPLE_REFERENCE_URL)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(
+ getCreateJwtCallback({
+ hexPrivateKey: rpMockEntity.hexPrivateKey,
+ did: rpMockEntity.did,
+ kid: `${rpMockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ )
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100335',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .build()
+
+ .createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ jwtIssuer: { method: 'did', didUrl: `${rpMockEntity.did}#controller`, alg: SigningAlgo.ES256K },
+ })
+
+ const verifiedRequest = await OP.builder()
+ .withSupportedVersions([SupportedVersion.SIOPv2_ID1])
+ .withExpiresIn(1000)
+ .withIssuer(ResponseIss.SELF_ISSUED_V2)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'never' }))
+ .withCreateJwtCallback(
+ getCreateJwtCallback({
+ hexPrivateKey: opMockEntity.hexPrivateKey,
+ did: opMockEntity.did,
+ kid: `${opMockEntity.did}#controller`,
+ alg: SigningAlgo.ES256K,
+ }),
+ )
+ .withRegistration({
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormats: { ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100336',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .build()
+
+ .verifyAuthorizationRequest(requestURI.encodedUri)
+ // console.log(JSON.stringify(verifiedRequest));
+ expect(verifiedRequest.issuer).toMatch(rpMockEntity.did)
+ expect(verifiedRequest.jwt).toBeDefined()
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts b/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts
new file mode 100644
index 00000000..af90caa9
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts
@@ -0,0 +1,457 @@
+import { PresentationDefinitionV1 } from '@sphereon/pex-models'
+import { CredentialMapper, IPresentation, IProofType, IVerifiableCredential } from '@sphereon/ssi-types'
+import { W3CVerifiablePresentation } from '@sphereon/ssi-types/src/types/w3c-vc'
+import nock from 'nock'
+
+import {
+ AuthorizationRequestPayload,
+ AuthorizationRequestPayloadVID1,
+ getNonce,
+ getState,
+ IdTokenType,
+ PresentationDefinitionWithLocation,
+ PresentationExchange,
+ PresentationSignCallback,
+ ResponseType,
+ Scope,
+ SigningAlgo,
+ SubjectIdentifierType,
+ SubjectType,
+} from '..'
+import { SIOPErrors } from '../types'
+
+import { mockedGetEnterpriseAuthToken } from './TestUtils'
+import {
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+const HOLDER_DID = 'did:example:ebfeb1f712ebc6f1c276e12ec21'
+const EXAMPLE_PD_URL = 'http://my_own_pd.com/pd/'
+
+async function getPayloadVID1Val(): Promise {
+ const mockEntity = await mockedGetEnterpriseAuthToken('ACME Corp')
+ const state = getState()
+ return {
+ redirect_uri: '',
+ scope: Scope.OPENID,
+ response_type: ResponseType.ID_TOKEN,
+ client_id: mockEntity.did,
+ state,
+ nonce: getNonce(state),
+ registration: {
+ client_id: mockEntity.did,
+ id_token_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ id_token_types_supported: [IdTokenType.SUBJECT_SIGNED],
+ request_object_signing_alg_values_supported: [SigningAlgo.ES256K, SigningAlgo.ES256, SigningAlgo.EDDSA],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID, Scope.OPENID_DIDAUTHN],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ client_name: VERIFIER_NAME_FOR_CLIENT,
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100337',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ claims: {
+ id_token: {
+ acr: null,
+ },
+ vp_token: {
+ presentation_definition: {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ {
+ uri: 'https://www.w3.org/2018/credentials/v1',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'preferred',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ }
+}
+
+async function getPayloadPdRef(): Promise {
+ const mockEntity = await mockedGetEnterpriseAuthToken('ACME Corp')
+ const state = getState()
+ return {
+ redirect_uri: '',
+ scope: Scope.OPENID,
+ response_type: ResponseType.ID_TOKEN,
+ client_id: mockEntity.did,
+ state,
+ nonce: getNonce(state),
+ registration: {
+ id_token_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ id_token_types_supported: [IdTokenType.SUBJECT_SIGNED],
+ request_object_signing_alg_values_supported: [SigningAlgo.ES256K, SigningAlgo.ES256, SigningAlgo.EDDSA],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID, Scope.OPENID_DIDAUTHN],
+ subject_syntax_types_supported: ['did:ethr:', SubjectIdentifierType.DID],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ client_name: VERIFIER_NAME_FOR_CLIENT,
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100338',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ claims: {
+ id_token: {
+ acr: null,
+ },
+ vp_token: {
+ presentation_definition: {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ {
+ uri: 'https://www.w3.org/2018/credentials/v1',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'preferred',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ }
+}
+
+function getVCs(): IVerifiableCredential[] {
+ return [
+ {
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
+ issuanceDate: '2021-11-01T03:05:06T000z',
+ id: 'https://example.com/credentials/1872',
+ type: ['VerifiableCredential', 'IDCardCredential'],
+ issuer: {
+ id: 'did:example:issuer',
+ },
+ credentialSubject: {
+ given_name: 'Fredrik',
+ family_name: 'Stremberg',
+ birthdate: '1949-01-22',
+ },
+ proof: {
+ type: 'BbsBlsSignatureProof2020',
+ created: '2020-04-25',
+ verificationMethod: 'did:example:489398593#test',
+ proofPurpose: 'assertionMethod',
+ proofValue:
+ 'kTTbA3pmDa6Qia/JkOnIXDLmoBz3vsi7L5t3DWySI/VLmBqleJ/Tbus5RoyiDERDBEh5rnACXlnOqJ/U8yFQFtcp/mBCc2FtKNPHae9jKIv1dm9K9QK1F3GI1AwyGoUfjLWrkGDObO1ouNAhpEd0+et+qiOf2j8p3MTTtRRx4Hgjcl0jXCq7C7R5/nLpgimHAAAAdAx4ouhMk7v9dXijCIMaG0deicn6fLoq3GcNHuH5X1j22LU/hDu7vvPnk/6JLkZ1xQAAAAIPd1tu598L/K3NSy0zOy6obaojEnaqc1R5Ih/6ZZgfEln2a6tuUp4wePExI1DGHqwj3j2lKg31a/6bSs7SMecHBQdgIYHnBmCYGNQnu/LZ9TFV56tBXY6YOWZgFzgLDrApnrFpixEACM9rwrJ5ORtxAAAAAgE4gUIIC9aHyJNa5TBklMOh6lvQkMVLXa/vEl+3NCLXblxjgpM7UEMqBkE9/QcoD3Tgmy+z0hN+4eky1RnJsEg=',
+ },
+ },
+ ]
+}
+
+const presentation_submission = {
+ id: 'L4tK2DK3rLC9sJhkPgeg_',
+ definition_id: 'Insurance Plans',
+ descriptor_map: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ format: 'ldp_vc',
+ path: '$.verifiableCredential[0]',
+ },
+ ],
+}
+
+describe('presentation exchange manager tests', () => {
+ it("validatePresentationAgainstDefinition: should throw error if provided VP doesn't match the PD val", async function () {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const vcs = getVCs()
+ vcs[0].issuer = { id: 'did:example:totallyDifferentIssuer' }
+ await expect(
+ PresentationExchange.validatePresentationAgainstDefinition(
+ pd[0].definition,
+ CredentialMapper.toWrappedVerifiablePresentation({
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ verifiableCredential: vcs,
+ presentation_submission,
+ } as W3CVerifiablePresentation),
+ ),
+ ).rejects.toThrow(SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD)
+ })
+
+ it("validatePresentationAgainstDefinition: should throw error if provided VP doesn't match the PD ref", async function () {
+ const payload: AuthorizationRequestPayload = await getPayloadPdRef()
+ const response = {
+ id: 'Insurance Plans',
+ input_descriptors: [
+ {
+ id: 'Ontario Health Insurance Plan',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ {
+ uri: 'https://www.w3.org/2018/credentials/v1',
+ },
+ ],
+ constraints: {
+ limit_disclosure: 'preferred',
+ fields: [
+ {
+ path: ['$.issuer.id'],
+ purpose: 'We can only verify bank accounts if they are attested by a source.',
+ filter: {
+ type: 'string',
+ pattern: 'did:example:issuer',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }
+ nock('http://my_own_pd.com')
+ .persist()
+ .get(/pd/)
+ .reply(200, { ...response })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const vcs = getVCs()
+ vcs[0].issuer = { id: 'did:example:totallyDifferentIssuer' }
+ await expect(
+ PresentationExchange.validatePresentationAgainstDefinition(
+ pd[0].definition,
+ CredentialMapper.toWrappedVerifiablePresentation({
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ presentation_submission,
+ verifiableCredential: vcs,
+ } as W3CVerifiablePresentation),
+ ),
+ ).rejects.toThrow(SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD)
+ })
+
+ it('validatePresentationAgainstDefinition: should throw error if both pd and pd_ref is present', async function () {
+ const payload = await getPayloadVID1Val()
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ;(payload.claims?.vp_token as any).presentation_definition_uri = EXAMPLE_PD_URL
+ await expect(PresentationExchange.findValidPresentationDefinitions(payload)).rejects.toThrow(
+ SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_BY_REF_AND_VALUE_NON_EXCLUSIVE,
+ )
+ })
+
+ it('validatePresentationAgainstDefinition: should pass if provided VP match the PD', async function () {
+ const payload = await getPayloadVID1Val()
+ const vcs = getVCs()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const result = await PresentationExchange.validatePresentationAgainstDefinition(
+ pd[0].definition,
+ CredentialMapper.toWrappedVerifiablePresentation({
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ presentation_submission,
+ verifiableCredential: vcs,
+ } as W3CVerifiablePresentation),
+ )
+ expect(result.errors?.length).toBe(0)
+ expect(result.value?.definition_id).toBe('Insurance Plans')
+ })
+
+ it('submissionFrom: should pass if a valid presentationSubmission object created', async function () {
+ const vcs = getVCs()
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: vcs })
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ await PresentationExchange.validatePresentationAgainstDefinition(
+ pd[0].definition,
+ CredentialMapper.toWrappedVerifiablePresentation({
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
+ type: ['VerifiablePresentation', 'PresentationSubmission'],
+ presentation_submission,
+ verifiableCredential: vcs,
+ } as W3CVerifiablePresentation),
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+ })
+ const result = await pex.createVerifiablePresentation(pd[0].definition, vcs, presentationSignCallback, {})
+ expect(result.presentationSubmission.definition_id).toBe('Insurance Plans')
+ expect(result.presentationSubmission.descriptor_map.length).toBe(1)
+ expect(result.presentationSubmission.descriptor_map[0]).toStrictEqual({
+ format: 'ldp_vp',
+ id: 'Ontario Health Insurance Plan',
+ path: '$',
+ path_nested: {
+ format: 'ldp_vc',
+ id: 'Ontario Health Insurance Plan',
+ path: '$.verifiableCredential[0]',
+ },
+ })
+ })
+
+ it('selectVerifiableCredentialsForSubmission: should fail if selectResults object contains error', async function () {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const vcs = getVCs()
+ vcs[0].issuer = undefined as any
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: vcs })
+ try {
+ await expect(pex.selectVerifiableCredentialsForSubmission(pd[0].definition)).rejects.toThrow()
+ } catch (e) {
+ expect(e.message).toContain(SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD)
+ }
+ })
+
+ it('selectVerifiableCredentialsForSubmission: should pass if a valid selectResults object created', async function () {
+ const vcs = getVCs()
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: vcs })
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const result = await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ expect(result.errors?.length).toBe(0)
+ expect(result.matches?.length).toBe(1)
+ expect(result.matches[0].vc_path.length).toBe(1)
+ expect(result.matches[0].vc_path[0]).toBe('$.verifiableCredential[0]')
+ })
+
+ it('pass if no PresentationDefinition is found', async () => {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ payload.claims = undefined
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ expect(pd.length).toBe(0)
+ })
+
+ it('pass if findValidPresentationDefinitions finds a valid presentation_definition', async () => {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const definition = pd[0].definition as PresentationDefinitionV1
+ expect(definition['id']).toBe('Insurance Plans')
+ expect(definition['input_descriptors'][0].schema.length).toBe(2)
+ })
+
+ it('should validate a list of VerifiablePresentations against a list of PresentationDefinitions', async () => {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const vcs = getVCs()
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: vcs })
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+ })
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, vcs, presentationSignCallback, {})
+ try {
+ await PresentationExchange.validatePresentationsAgainstDefinitions(
+ pd,
+ [CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentationResult.verifiablePresentation)],
+ undefined,
+ {},
+ )
+ } catch (e) {
+ console.log(e)
+ }
+ })
+
+ it("'validatePresentationsAgainstDefinitions' should fail if provided VP verification callback fails", async () => {
+ const payload: AuthorizationRequestPayload = await getPayloadVID1Val()
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(payload)
+ const vcs = getVCs()
+ const pex = new PresentationExchange({ allDIDs: [HOLDER_DID], allVerifiableCredentials: vcs })
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const presentationSignCallback: PresentationSignCallback = async (_args) => ({
+ ...(_args.presentation as IPresentation),
+ proof: {
+ type: 'RsaSignature2018',
+ created: '2018-09-14T21:19:10Z',
+ proofPurpose: 'authentication',
+ verificationMethod: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
+ challenge: '1f44d55f-f161-4938-a659-f8026467f126',
+ domain: '4jt78h47fh47',
+ jws: 'eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqsLfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh4vGHSrQyHUGlcTwLtjPAnKb78',
+ },
+ })
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, vcs, presentationSignCallback, {})
+
+ await expect(
+ PresentationExchange.validatePresentationsAgainstDefinitions(
+ pd,
+ [CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentationResult.verifiablePresentation)],
+ () => {
+ throw new Error('Verification failed')
+ },
+ {},
+ ),
+ ).rejects.toThrow(SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/RP.request.spec.ts b/packages/siop-oid4vp/lib/__tests__/RP.request.spec.ts
new file mode 100644
index 00000000..aae56def
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/RP.request.spec.ts
@@ -0,0 +1,310 @@
+import { IProofType } from '@sphereon/ssi-types'
+
+import {
+ CreateAuthorizationRequestOpts,
+ PassBy,
+ PropertyTarget,
+ ResponseMode,
+ ResponseType,
+ RP,
+ Scope,
+ SigningAlgo,
+ SubjectIdentifierType,
+ SubjectType,
+ SupportedVersion,
+} from '..'
+
+import { getCreateJwtCallback, getVerifyJwtCallback, internalSignature } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts'
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1'
+
+const alltargets = [PropertyTarget.AUTHORIZATION_REQUEST, PropertyTarget.REQUEST_OBJECT]
+
+describe('RP OPBuilder should', () => {
+ /*it('throw Error when no arguments are passed', async () => {
+ expect.assertions(1);
+ await expect(() => new OPBuilder().build()).toThrowError(Error);
+ });*/
+
+ it('build an RP when all arguments are set', async () => {
+ expect.assertions(1)
+
+ const resolver = getResolver('ethr')
+ expect(
+ RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId('test_client_id')
+ .withScope('test')
+ .withResponseType(ResponseType.ID_TOKEN)
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withRedirectUri('https://redirect.me')
+ .withRequestBy(PassBy.VALUE)
+ .withResponseMode(ResponseMode.POST)
+ .withClientMetadata({
+ passBy: PassBy.REFERENCE,
+ reference_uri: 'https://registration.here',
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100339',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+
+ .withCreateJwtCallback(internalSignature('myprivatekye', 'did:example:123', 'did:example:123#key', SigningAlgo.ES256K))
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build(),
+ ).toBeInstanceOf(RP)
+ })
+})
+
+describe('RP should', () => {
+ it('throw Error when build from request opts without enough params', async () => {
+ expect.assertions(1)
+ await expect(() => RP.fromRequestOpts({} as never)).toThrowError(Error)
+ })
+ it('return an RP when all request arguments are set', async () => {
+ expect.assertions(1)
+
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: 'test',
+ scope: 'test',
+ response_type: 'test',
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: KID,
+ alg: SigningAlgo.ES256K,
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ },
+ clientMetadata: {
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ },
+
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '202210040',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ },
+ }
+
+ expect(RP.fromRequestOpts(opts)).toBeInstanceOf(RP)
+ })
+
+ it('succeed from request opts when all params are set', async () => {
+ // expect.assertions(1);
+ const opts: CreateAuthorizationRequestOpts = {
+ version: SupportedVersion.SIOPv2_ID1,
+ payload: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ scope: 'openid',
+ response_type: 'id_token',
+ response_mode: ResponseMode.POST,
+ redirect_uri: EXAMPLE_REDIRECT_URL,
+ },
+ requestObject: {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: KID,
+ alg: SigningAlgo.ES256K,
+ },
+ passBy: PassBy.REFERENCE,
+ reference_uri: EXAMPLE_REFERENCE_URL,
+
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ }),
+ },
+ clientMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ subject_syntax_types_supported: ['did:ethr', SubjectIdentifierType.DID],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ vpFormatsSupported: {
+ jwt_vc: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt_vp: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ jwt: { alg: [SigningAlgo.EDDSA, SigningAlgo.ES256K, SigningAlgo.ES256] },
+ ldp_vc: { proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019] },
+ },
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 00',
+ clientName: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 00',
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 00',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 00',
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 00',
+ },
+ }
+
+ const expectedPayloadWithoutRequest = {
+ response_type: 'id_token',
+ scope: 'openid',
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ redirect_uri: 'https://acme.com/hello',
+ // nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ /* registration: {
+ id_token_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr', 'did'],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ jwt: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ jwt_vc: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 00',
+ client_name: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 00',
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 00',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 00',
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 00',
+ },*/
+ }
+
+ const expectedUri =
+ 'openid://?client_id=https%3A%2F%2Fwww.example.com%2F.well-known%2Fopenid-federation&scope=openid&response_type=id_token&response_mode=post&redirect_uri=https%3A%2F%2Facme.com%2Fhello&request_uri=https%3A%2F%2Frp.acme.com%2Fsiop%2Fjwts'
+ const expectedJwtRegex =
+ /^eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXRocjoweDAxMDZhMmU5ODViMUUxRGU5QjVkZGI0YUY2ZEM5ZTkyOEY0ZTk5RDAja2V5cy0xIiwidHlwIjoiSldUIn0\.ey.*$/
+
+ const request = await RP.fromRequestOpts(opts).createAuthorizationRequestURI({
+ correlationId: '1234',
+ state: 'b32f0087fc9816eb813fd11f',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ })
+ expect(request.authorizationRequestPayload).toMatchObject(expectedPayloadWithoutRequest)
+ expect(request.encodedUri).toMatch(expectedUri)
+ expect(request.requestObjectJwt).toMatch(expectedJwtRegex)
+ })
+
+ it('succeed from builder when all params are set', async () => {
+ const expectedPayloadWithoutRequest = {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ // nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ redirect_uri: 'https://acme.com/hello',
+ registration: {
+ id_token_signing_alg_values_supported: [SigningAlgo.EDDSA],
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subject_syntax_types_supported: ['did:ethr'],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ jwt: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ jwt_vc: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ jwt_vp: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 01',
+ client_name: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 01',
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 01',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 01',
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 01',
+ },
+ }
+
+ const expectedUri =
+ 'openid://?client_id=https%3A%2F%2Fwww.example.com%2F.well-known%2Fopenid-federation&scope=test&response_type=id_token&redirect_uri=https%3A%2F%2Facme.com%2Fhello®istration=%7B%22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%2C%22request_object_signing_alg_values_supported%22%3A%5B%22EdDSA%22%2C%22ES256%22%5D%2C%22response_types_supported%22%3A%5B%22id_token%22%5D%2C%22scopes_supported%22%3A%5B%22openid%20did_authn%22%2C%22openid%22%5D%2C%22subject_types_supported%22%3A%5B%22pairwise%22%5D%2C%22subject_syntax_types_supported%22%3A%5B%22did%3Aethr%22%5D%2C%22vp_formats%22%3A%7B%22jwt%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%2C%22ES256K%22%2C%22ES256%22%5D%7D%2C%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%2C%22ES256K%22%2C%22ES256%22%5D%7D%2C%22jwt_vp%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%2C%22ES256K%22%2C%22ES256%22%5D%7D%7D%2C%22client_name%22%3A%22Client%20Verifier%20Relying%20Party%20Sphereon%20INC%202022-09-29%2001%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fsphereon.com%2Fcontent%2Fthemes%2Fsphereon%2Fassets%2Ffavicons%2Fsafari-pinned-tab.svg%202022-09-29%2001%22%2C%22client_purpose%22%3A%22To%20request%2C%20receive%20and%20verify%20your%20credential%20about%20the%20the%20valid%20subject.%202022-09-29%2001%22%2C%22client_id%22%3A%22https%3A%2F%2Fwww.example.com%2F.well-known%2Fopenid-federation%22%2C%22client_name%23nl-NL%22%3A%22%20***%20dutch%20***%20Client%20Verifier%20Relying%20Party%20Sphereon%20B.V.%202022-09-29%2001%22%2C%22client_purpose%23nl-NL%22%3A%22%20***%20Dutch%20***%20To%20request%2C%20receive%20and%20verify%20your%20credential%20about%20the%20the%20valid%20subject.%202022-09-29%2001%22%7D&request_uri=https%3A%2F%2Frp.acme.com%2Fsiop%2Fjwts'
+
+ const expectedJwtRegex =
+ /^eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXRocjoweDAxMDZhMmU5ODViMUUxRGU5QjVkZGI0YUY2ZEM5ZTkyOEY0ZTk5RDAja2V5cy0xIiwidHlwIjoiSldUIn0\.eyJpYXQiO.*$/
+
+ const request = await RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(WELL_KNOWN_OPENID_FEDERATION, alltargets)
+ .withScope('test', alltargets)
+ .withResponseType(ResponseType.ID_TOKEN, alltargets)
+ .withVerifyJwtCallback(getVerifyJwtCallback(getResolver('ethr')))
+ .withRedirectUri(EXAMPLE_REDIRECT_URL, alltargets)
+ .withRequestBy(PassBy.REFERENCE, EXAMPLE_REFERENCE_URL)
+ .withCreateJwtCallback(internalSignature(HEX_KEY, DID, KID, SigningAlgo.ES256K))
+ .withClientMetadata(
+ {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: {
+ jwt: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ jwt_vc: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ jwt_vp: {
+ alg: ['EdDSA', 'ES256K', 'ES256'],
+ },
+ },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 01',
+ clientName: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 01',
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 01',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 01',
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 01',
+ },
+ alltargets,
+ )
+ .withSupportedVersions([SupportedVersion.SIOPv2_D11])
+ .build()
+
+ .createAuthorizationRequestURI({
+ correlationId: '1234',
+ state: 'b32f0087fc9816eb813fd11f',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ jwtIssuer: { method: 'did', didUrl: KID, alg: SigningAlgo.ES256K },
+ })
+ expect(request.authorizationRequestPayload).toMatchObject(expectedPayloadWithoutRequest)
+ expect(request.encodedUri).toMatch(expectedUri)
+ expect(request.requestObjectJwt).toMatch(expectedJwtRegex)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/RequestObjectJwtVerifier.test.ts b/packages/siop-oid4vp/lib/__tests__/RequestObjectJwtVerifier.test.ts
new file mode 100644
index 00000000..d259498a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/RequestObjectJwtVerifier.test.ts
@@ -0,0 +1,123 @@
+import * as dotenv from 'dotenv'
+
+import { getJwtVerifierWithContext, getRequestObjectJwtVerifier, JwtVerifier, SIOPErrors } from '..'
+import { parseJWT } from '../helpers/jwtUtils'
+
+dotenv.config()
+
+const baseJwtPayload = {
+ nonce: '1234',
+ scope: 'openid',
+ state: '1234',
+ response_type: 'id_token',
+ client_id: '1234',
+}
+
+describe('requestObjectJwtVerifier', () => {
+ it('should throw when an invalid schema is passed', async () => {
+ expect(
+ getRequestObjectJwtVerifier(
+ {
+ header: {},
+ payload: { ...baseJwtPayload, client_id_scheme: 'wrong' as never },
+ },
+ { type: 'request-object', raw: '' },
+ ),
+ ).rejects.toThrow(SIOPErrors.INVALID_CLIENT_ID_SCHEME)
+ })
+
+ it('should succeed with a client_id_scheme did', async () => {
+ const jwtVerifier = await getRequestObjectJwtVerifier(
+ {
+ header: { kid: 'did:example.com#1234' },
+ payload: { ...baseJwtPayload, client_id_scheme: 'did' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ const expectedJwtVerifier: JwtVerifier = { type: 'request-object', method: 'did', didUrl: 'did:example.com#1234' }
+ expect(jwtVerifier).toEqual(expectedJwtVerifier)
+ })
+
+ it('should error with a client_id_scheme did and invalid header', async () => {
+ const jwtVerifier = getRequestObjectJwtVerifier(
+ {
+ header: {},
+ payload: { ...baseJwtPayload, client_id_scheme: 'did' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ await expect(jwtVerifier).rejects.toThrow(SIOPErrors.INVALID_REQUEST_OBJECT_DID_SCHEME_JWT)
+ })
+
+ it('should succeed with a client_id_scheme pre-registered', async () => {
+ const jwtVerifier = await getRequestObjectJwtVerifier(
+ {
+ header: {},
+ payload: { ...baseJwtPayload, client_id_scheme: 'pre-registered' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ const expectedJwtVerifier: JwtVerifier = { type: 'request-object', method: 'custom' }
+ expect(jwtVerifier).toEqual(expectedJwtVerifier)
+ })
+
+ it('should succeed with a client_id_scheme x509_san_dns', async () => {
+ const jwtVerifier = await getRequestObjectJwtVerifier(
+ {
+ header: { x5c: [''] },
+ payload: { ...baseJwtPayload, iss: 'issuer', client_id_scheme: 'x509_san_dns' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ const expectedJwtVerifier: JwtVerifier = { type: 'request-object', method: 'x5c', x5c: [''], issuer: 'issuer' }
+ expect(jwtVerifier).toEqual(expectedJwtVerifier)
+ })
+
+ it('should error with a client_id_scheme x509_san_dns and invalid header', async () => {
+ const jwtVerifier = getRequestObjectJwtVerifier(
+ {
+ header: {},
+ payload: { ...baseJwtPayload, client_id_scheme: 'x509_san_dns' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ await expect(jwtVerifier).rejects.toThrow(SIOPErrors.INVALID_REQUEST_OBJECT_X509_SCHEME_JWT)
+ })
+
+ it('should error with a client_id_scheme verifier_attestation and invalid header', async () => {
+ const jwtVerifier = getRequestObjectJwtVerifier(
+ {
+ header: {},
+ payload: { ...baseJwtPayload, client_id_scheme: 'verifier_attestation' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ await expect(jwtVerifier).rejects.toThrow(SIOPErrors.MISSING_ATTESTATION_JWT)
+ })
+
+ it('should succeed with a client_id_scheme verifier_attestation', async () => {
+ const attestationJwt =
+ 'eyJ0eXAiOiJ2ZXJpZmllci1hdHRlc3RhdGlvbitqd3QiLCAia2lkIjogImRpZDpleGFtcGxlLmNvbSMxMjM0In0.eyJzdWIiOiAiY2xpZW50X2lkIiwiaXNzIjogImlzc3VlciIsImV4cCI6IDEyMzQsImNuZiI6IHsgImp3ayI6IHt9fX0='
+
+ const jwtVerifier = await getRequestObjectJwtVerifier(
+ {
+ header: { jwt: attestationJwt, typ: 'verifier-attestation+jwt' },
+ payload: { ...baseJwtPayload, client_id: 'client_id', client_id_scheme: 'verifier_attestation' },
+ },
+ { type: 'request-object', raw: '' },
+ )
+
+ const expectedJwtVerifier: JwtVerifier = { type: 'request-object', method: 'jwk', jwk: {} }
+ expect(jwtVerifier).toEqual(expectedJwtVerifier)
+
+ const expectedAttestationVerifier: JwtVerifier = { type: 'verifier-attestation', method: 'did', didUrl: 'did:example.com#1234' }
+ const attestationJwtVerifier = await getJwtVerifierWithContext(parseJWT(attestationJwt), { type: 'verifier-attestation' })
+ expect(attestationJwtVerifier).toEqual(expectedAttestationVerifier)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/ResolverTestUtils.ts b/packages/siop-oid4vp/lib/__tests__/ResolverTestUtils.ts
new file mode 100644
index 00000000..e9b78bcd
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/ResolverTestUtils.ts
@@ -0,0 +1,43 @@
+import { getUniResolver } from '@sphereon/did-uni-client'
+import { Resolvable, Resolver, ResolverRegistry } from 'did-resolver'
+import { DIDDocument as DIFDIDDocument } from 'did-resolver'
+
+import { SIOPErrors } from '..'
+
+export interface DIDDocument extends DIFDIDDocument {
+ owner?: string
+ created?: string
+ updated?: string
+ proof?: LinkedDataProof
+}
+
+export interface LinkedDataProof {
+ type: string
+ created: string
+ creator: string
+ nonce: string
+ signatureValue: string
+}
+
+export function getResolver(methods: string | string[]): Resolvable {
+ function getMethodFromDid(did: string): string {
+ if (!did) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+ const split = did.split(':')
+ if (split.length == 1 && did.length > 0) {
+ return did
+ } else if (!did.startsWith('did:') || split.length < 2) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+
+ return split[1]
+ }
+
+ const uniResolvers: ResolverRegistry[] = []
+ for (const didMethod of typeof methods === 'string' ? [methods] : methods) {
+ const uniResolver = getUniResolver(getMethodFromDid(didMethod))
+ uniResolvers.push(uniResolver)
+ }
+ return new Resolver(...uniResolvers)
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/SdJwt.spec.ts b/packages/siop-oid4vp/lib/__tests__/SdJwt.spec.ts
new file mode 100644
index 00000000..e9e882f1
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/SdJwt.spec.ts
@@ -0,0 +1,362 @@
+import { createHash } from 'node:crypto'
+
+import { IPresentationDefinition, SdJwtDecodedVerifiableCredentialWithKbJwtInput } from '@sphereon/pex'
+import { OriginalVerifiableCredential } from '@sphereon/ssi-types'
+
+import {
+ OP,
+ PassBy,
+ PresentationDefinitionWithLocation,
+ PresentationExchange,
+ PresentationSignCallback,
+ PresentationVerificationCallback,
+ PropertyTarget,
+ ResponseIss,
+ ResponseType,
+ RevocationVerification,
+ RP,
+ Scope,
+ SigningAlgo,
+ SubjectType,
+ SupportedVersion,
+ VPTokenLocation,
+} from '../'
+
+import { getVerifyJwtCallback, internalSignature } from './DidJwtTestUtils'
+import { getResolver } from './ResolverTestUtils'
+import { mockedGetEnterpriseAuthToken, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'
+import {
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+const hasher = (data: string) => createHash('sha256').update(data).digest()
+jest.setTimeout(30000)
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+
+const HOLDER_DID = 'did:example:ebfeb1f712ebc6f1c276e12ec21'
+const SD_JWT_VC =
+ 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCJ9.eyJpYXQiOjE3MDA0NjQ3MzYwNzYsImlzcyI6ImRpZDprZXk6c29tZS1yYW5kb20tZGlkLWtleSIsIm5iZiI6MTcwMDQ2NDczNjE3NiwidmN0IjoiaHR0cHM6Ly9oaWdoLWFzc3VyYW5jZS5jb20vU3RhdGVCdXNpbmVzc0xpY2Vuc2UiLCJ1c2VyIjp7Il9zZCI6WyI5QmhOVDVsSG5QVmpqQUp3TnR0NDIzM216MFVVMUd3RmFmLWVNWkFQV0JNIiwiSVl5d1FQZl8tNE9hY2Z2S2l1cjRlSnFMa1ZleWRxcnQ1Y2UwMGJReWNNZyIsIlNoZWM2TUNLakIxeHlCVl91QUtvLURlS3ZvQllYbUdBd2VGTWFsd05xbUEiLCJXTXpiR3BZYmhZMkdoNU9pWTRHc2hRU1dQREtSeGVPZndaNEhaQW5YS1RZIiwiajZ6ZFg1OUJYZHlTNFFaTGJITWJ0MzJpenRzWXdkZzRjNkpzWUxNc3ZaMCIsInhKR3Radm41cFM4VEhqVFlJZ3MwS1N5VC1uR3BSR3hDVnp6c1ZEbmMyWkUiXX0sImxpY2Vuc2UiOnsibnVtYmVyIjoxMH0sImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX0sIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIl90YnpMeHBaeDBQVHVzV2hPOHRUZlVYU2ZzQjVlLUtrbzl3dmZaaFJrYVkiLCJ1WmNQaHdUTmN4LXpNQU1zemlYMkFfOXlJTGpQSEhobDhEd2pvVXJLVVdZIl19.HAcudVInhNpXkTPQGNosjKTFRJWgKj90NpfloRaDQchGd4zxc1ChWTCCPXzUXTBypASKrzgjZCiXlTr0bzmLAg~WyJHeDZHRUZvR2t6WUpWLVNRMWlDREdBIiwiZGF0ZU9mQmlydGgiLCIyMDAwMDEwMSJd~WyJ1LUt3cmJvMkZfTExQekdSZE1XLUtBIiwibmFtZSIsIkpvaG4iXQ~WyJNV1ZieGJqVFZxUXdLS3h2UGVZdWlnIiwibGFzdE5hbWUiLCJEb2UiXQ~'
+
+const presentationSignCallback: PresentationSignCallback = async (_args) => {
+ const kbJwt = (_args.presentation as SdJwtDecodedVerifiableCredentialWithKbJwtInput).kbJwt
+
+ // In real life scenario, the KB-JWT must be signed
+ // As the KB-JWT is a normal JWT, the user does not need an sd-jwt implementation in the presentation sign callback
+ // NOTE: should the presentation just be the KB-JWT header + payload instead of the whole decoded SD JWT?
+ expect(kbJwt).toEqual({
+ header: {
+ typ: 'kb+jwt',
+ },
+ payload: {
+ _sd_hash: expect.any(String),
+ iat: expect.any(Number),
+ nonce: expect.any(String),
+ },
+ })
+
+ const header = {
+ ...kbJwt.header,
+ alg: 'ES256K',
+ }
+ const payload = {
+ ...kbJwt.payload,
+ aud: '123',
+ }
+
+ const kbJwtCompact = `${Buffer.from(JSON.stringify(header)).toString('base64url')}.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.signature`
+ return SD_JWT_VC + kbJwtCompact
+}
+
+function getPresentationDefinition(): IPresentationDefinition {
+ return {
+ id: '32f54163-7166-48f1-93d8-ff217bdb0653',
+ name: 'Conference Entry Requirements',
+ purpose: 'We can only allow people associated with Washington State business representatives into conference areas',
+ format: {
+ 'vc+sd-jwt': {},
+ },
+ input_descriptors: [
+ {
+ id: 'wa_driver_license',
+ name: 'Washington State Business License',
+ purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference',
+ constraints: {
+ limit_disclosure: 'required',
+ fields: [
+ {
+ path: ['$.vct'],
+ filter: {
+ type: 'string',
+ const: 'https://high-assurance.com/StateBusinessLicense',
+ },
+ },
+ {
+ path: ['$.license.number'],
+ filter: {
+ type: 'number',
+ },
+ },
+ {
+ path: ['$.user.name'],
+ filter: {
+ type: 'string',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }
+}
+
+function getVCs(): OriginalVerifiableCredential[] {
+ return [SD_JWT_VC]
+}
+
+describe('RP and OP interaction should', () => {
+ it('succeed when calling with presentation definitions and right verifiable presentation', async () => {
+ const opMock = await mockedGetEnterpriseAuthToken('OP')
+ const opMockEntity = {
+ ...opMock,
+ didKey: `${opMock.did}#controller`,
+ }
+ const rpMock = await mockedGetEnterpriseAuthToken('RP')
+ const rpMockEntity = {
+ ...rpMock,
+ didKey: `${rpMock.did}#controller`,
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => {
+ return { verified: true }
+ }
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
+ .withClientId(rpMockEntity.did)
+ .withScope('test')
+ .withHasher(hasher)
+ .withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, `${rpMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:key'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100322',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withHasher(hasher)
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, `${opMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100323',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('requestObjectJwt is undefined')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({
+ allDIDs: [HOLDER_DID],
+ allVerifiableCredentials: getVCs(),
+ hasher,
+ })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {
+ proofOptions: {
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ },
+ })
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeDefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ })
+
+ expect(verifiedAuthResponseWithJWT.idToken?.jwt).toBeDefined()
+ expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ })
+
+ it('succeed when calling with presentation definitions and right verifiable presentation without id token', async () => {
+ const opMockEntity = await mockedGetEnterpriseAuthToken('OP')
+ const rpMockEntity = await mockedGetEnterpriseAuthToken('RP')
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const presentationVerificationCallback: PresentationVerificationCallback = async (_args) => {
+ return { verified: true }
+ }
+
+ const resolver = getResolver('ethr')
+ const rp = RP.builder({
+ requestVersion: SupportedVersion.SIOPv2_D12_OID4VP_D18,
+ })
+ .withClientId(rpMockEntity.did)
+ .withHasher(hasher)
+ .withResponseType([ResponseType.VP_TOKEN])
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withPresentationDefinition({ definition: getPresentationDefinition() }, [PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST])
+ .withPresentationVerification(presentationVerificationCallback)
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withRequestBy(PassBy.VALUE)
+ .withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, `${rpMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withAuthorizationEndpoint('www.myauthorizationendpoint.com')
+ .withClientMetadata({
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.VP_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did', 'did:key'],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100322',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+ const op = OP.builder()
+ .withPresentationSignCallback(presentationSignCallback)
+ .withExpiresIn(1000)
+ .withHasher(hasher)
+ .withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, `${opMockEntity.did}#controller`, SigningAlgo.ES256K))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver))
+ .withRegistration({
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
+ vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: [],
+ passBy: PassBy.VALUE,
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100323',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ })
+ .withSupportedVersions(SupportedVersion.SIOPv2_ID1)
+ .build()
+
+ const requestURI = await rp.createAuthorizationRequestURI({
+ correlationId: '1234',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ jwtIssuer: { method: 'did', alg: SigningAlgo.ES256K, didUrl: `${rpMockEntity.did}#controller` },
+ })
+
+ // Let's test the parsing
+ const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
+ expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
+ expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()
+
+ if (!parsedAuthReqURI.requestObjectJwt) throw new Error('requestObjectJwt is undefined')
+ const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
+ expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
+ const pex = new PresentationExchange({
+ allDIDs: [HOLDER_DID],
+ allVerifiableCredentials: getVCs(),
+ hasher,
+ })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ parsedAuthReqURI.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, getVCs(), presentationSignCallback, {
+ proofOptions: {
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ },
+ })
+ const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
+ jwtIssuer: {
+ method: 'did',
+ alg: SigningAlgo.ES256K,
+ didUrl: `${rpMockEntity.did}#controller`,
+ },
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ },
+ })
+ expect(authenticationResponseWithJWT.response.payload).toBeDefined()
+ expect(authenticationResponseWithJWT.response.idToken).toBeUndefined()
+
+ const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
+ presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
+ })
+
+ expect(verifiedAuthResponseWithJWT.oid4vpSubmission?.nonce).toEqual('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg')
+ expect(verifiedAuthResponseWithJWT.idToken).toBeUndefined()
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/TestUtils.ts b/packages/siop-oid4vp/lib/__tests__/TestUtils.ts
new file mode 100644
index 00000000..2d2384b6
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/TestUtils.ts
@@ -0,0 +1,280 @@
+import crypto from 'crypto'
+
+import { IProofType } from '@sphereon/ssi-types'
+import base58 from 'bs58'
+import { ethers } from 'ethers'
+import { exportJWK, importJWK, JWK, SignJWT } from 'jose'
+import moment from 'moment'
+import { v4 as uuidv4 } from 'uuid'
+
+import {
+ assertValidMetadata,
+ base64ToHexString,
+ DiscoveryMetadataPayload,
+ JwtPayload,
+ KeyCurve,
+ KeyType,
+ ResponseIss,
+ ResponseType,
+ RPRegistrationMetadataPayload,
+ Scope,
+ SigningAlgo,
+ SubjectSyntaxTypesSupportedValues,
+ SubjectType,
+} from '../'
+import { parseJWT } from '../helpers/jwtUtils'
+import SIOPErrors from '../types/Errors'
+
+import { DIDDocument } from './ResolverTestUtils'
+import {
+ DID_DOCUMENT_PUBKEY_B58,
+ DID_DOCUMENT_PUBKEY_JWK,
+ VERIFIER_LOGO_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT,
+ VERIFIER_NAME_FOR_CLIENT_NL,
+ VERIFIERZ_PURPOSE_TO_VERIFY,
+ VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+} from './data/mockedData'
+
+export interface TESTKEY {
+ key: JWK
+ did: string
+ didDoc?: DIDDocument
+}
+
+export async function generateTestKey(kty: string): Promise {
+ if (kty !== KeyType.EC) throw new Error(SIOPErrors.NO_ALG_SUPPORTED)
+ const key = crypto.generateKeyPairSync('ec', {
+ namedCurve: KeyCurve.SECP256k1,
+ })
+ const privateJwk = await exportJWK(key.privateKey)
+
+ const did = getDIDFromKey(privateJwk)
+
+ return {
+ key: privateJwk,
+ did,
+ }
+}
+
+function getDIDFromKey(key: JWK): string {
+ return `did:ethr:${getEthAddress(key)}`
+}
+
+function getEthAddress(key: JWK): string {
+ return getEthWallet(key).address
+}
+
+function getEthWallet(key: JWK): ethers.Wallet {
+ return new ethers.Wallet(prefixWith0x(base64ToHexString(key.d as string)))
+}
+
+export const prefixWith0x = (key: string): string => (key.startsWith('0x') ? key : `0x${key}`)
+
+export interface IEnterpriseAuthZToken extends JwtPayload {
+ sub?: string
+ did: string
+ aud: string
+ nonce: string
+}
+
+export interface LegalEntityTestAuthN {
+ iss: string // legal entity name identifier
+ aud: string // RP Application Name.
+ iat: number
+ exp: number
+ nonce: string
+ callbackUrl?: string // Entity url to send notifications
+ image?: string // base64 encoded image data
+ icon?: string // base64 encoded image icon data
+}
+
+export const mockedKeyAndDid = async (): Promise<{
+ hexPrivateKey: string
+ did: string
+ jwk: JWK
+ hexPublicKey: string
+}> => {
+ // generate a new keypair
+ const key = crypto.generateKeyPairSync('ec', {
+ namedCurve: KeyCurve.SECP256k1,
+ })
+ const privateJwk = await exportJWK(key.privateKey)
+ const hexPrivateKey = base64ToHexString(privateJwk.d as string)
+ const wallet: ethers.Wallet = new ethers.Wallet(prefixWith0x(hexPrivateKey))
+ const did = `did:ethr:${wallet.address}`
+ const hexPublicKey = wallet.signingKey.publicKey
+
+ return {
+ hexPrivateKey,
+ did,
+ jwk: privateJwk,
+ hexPublicKey,
+ }
+}
+
+const mockedEntityAuthNToken = async (
+ enterpiseName?: string,
+): Promise<{
+ jwt: string
+ jwk: JWK
+ did: string
+ hexPrivateKey: string
+ hexPublicKey: string
+}> => {
+ // generate a new keypair
+ const { did, jwk, hexPrivateKey, hexPublicKey } = await mockedKeyAndDid()
+
+ const payload: LegalEntityTestAuthN = {
+ iss: enterpiseName || 'Test Entity',
+ aud: 'test',
+ iat: moment().unix(),
+ exp: moment().add(15, 'minutes').unix(),
+ nonce: uuidv4(),
+ }
+
+ const privateKey = await importJWK(jwk, SigningAlgo.ES256K)
+ const jwt = await new SignJWT(payload as unknown as JwtPayload)
+ .setProtectedHeader({
+ alg: 'ES256K',
+ typ: 'JWT',
+ })
+ .sign(privateKey)
+ return { jwt, jwk, did, hexPrivateKey, hexPublicKey }
+}
+
+export async function mockedGetEnterpriseAuthToken(enterpriseName?: string): Promise<{
+ jwt: string
+ did: string
+ jwk: JWK
+ hexPrivateKey: string
+ hexPublicKey: string
+}> {
+ const testAuth = await mockedEntityAuthNToken(enterpriseName)
+ const { payload: _payload } = parseJWT(testAuth.jwt)
+
+ const payload = _payload as JwtPayload
+ const inputPayload: IEnterpriseAuthZToken = {
+ did: testAuth.did,
+
+ aud: payload?.iss ? payload.iss : 'Test Entity',
+ nonce: (payload as IEnterpriseAuthZToken).nonce,
+ }
+
+ const testApiPayload = {
+ ...inputPayload,
+ ...{
+ sub: (payload as JwtPayload).iss, // Should be the id of the app that is requesting the token
+ iat: moment().unix(),
+ exp: moment().add(15, 'minutes').unix(),
+ aud: 'test',
+ },
+ }
+
+ const privateKey = await importJWK(testAuth.jwk, SigningAlgo.ES256K)
+ const jwt = await new SignJWT(testApiPayload)
+ .setProtectedHeader({
+ alg: 'ES256K',
+ typ: 'JWT',
+ })
+ .sign(privateKey)
+
+ return {
+ jwt,
+ did: testAuth.did,
+ jwk: testAuth.jwk,
+ hexPrivateKey: testAuth.hexPrivateKey,
+ hexPublicKey: testAuth.hexPublicKey,
+ }
+}
+
+export interface DidKey {
+ did: string
+ publicKeyHex?: string
+ jwk?: JWK
+}
+
+interface FixJwk extends JWK {
+ kty: string
+}
+
+export const getParsedDidDocument = (didKey: DidKey): DIDDocument => {
+ if (didKey.publicKeyHex) {
+ const didDocB58 = DID_DOCUMENT_PUBKEY_B58
+ if (!didDocB58 || !didDocB58.verificationMethod?.[0]) throw new Error('Invalid DID Document')
+ didDocB58.id = didKey.did
+ didDocB58.controller = didKey.did
+ didDocB58.verificationMethod[0].id = `${didKey.did}#keys-1`
+ didDocB58.verificationMethod[0].controller = didKey.did
+ didDocB58.verificationMethod[0].publicKeyBase58 = base58.encode(Buffer.from(didKey.publicKeyHex.replace('0x', ''), 'hex'))
+ return didDocB58
+ }
+ // then didKey jws public key
+ const didDocJwk = DID_DOCUMENT_PUBKEY_JWK
+ if (!didDocJwk || !didDocJwk.verificationMethod?.[0]) throw new Error('Invalid DID Document')
+ const { jwk } = didKey
+ if (!jwk) throw new Error('Invalid didKey')
+ jwk.kty = didKey?.jwk?.kty || 'EC'
+ didDocJwk.id = didKey.did
+ didDocJwk.controller = didKey.did
+ didDocJwk.verificationMethod[0].id = `${didKey.did}#keys-1`
+ didDocJwk.verificationMethod[0].controller = didKey.did
+ didDocJwk.verificationMethod[0].publicKeyJwk = jwk as FixJwk
+ return didDocJwk
+}
+
+export const WELL_KNOWN_OPENID_FEDERATION = 'https://www.example.com/.well-known/openid-federation'
+export const metadata: {
+ opMetadata: DiscoveryMetadataPayload
+ rpMetadata: RPRegistrationMetadataPayload
+ verify(): unknown
+} = {
+ opMetadata: {
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ authorization_endpoint: 'http://test.com',
+ subject_syntax_types_supported: ['did:web'],
+ id_token_signing_alg_values_supported: undefined,
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA],
+ response_types_supported: ResponseType.ID_TOKEN,
+ scopes_supported: [Scope.OPENID_DIDAUTHN],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 02',
+ client_name: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 02',
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 02',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 02',
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 02',
+ },
+ rpMetadata: {
+ client_id: WELL_KNOWN_OPENID_FEDERATION,
+ id_token_signing_alg_values_supported: [],
+ request_object_signing_alg_values_supported: [SigningAlgo.EDDSA],
+ response_types_supported: [ResponseType.ID_TOKEN],
+ scopes_supported: [Scope.OPENID, Scope.OPENID_DIDAUTHN],
+ subject_syntax_types_supported: [SubjectSyntaxTypesSupportedValues.DID.valueOf(), 'did:web', 'did:key'],
+ subject_types_supported: [SubjectType.PAIRWISE],
+ vp_formats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT + ' 2022-09-29 03',
+ client_name: VERIFIER_NAME_FOR_CLIENT + ' 2022-09-29 03',
+ 'client_name#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + ' 2022-09-29 03',
+ client_purpose: VERIFIERZ_PURPOSE_TO_VERIFY + ' 2022-09-29 03',
+ 'client_purpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL + ' 2022-09-29 03',
+ },
+ verify() {
+ return assertValidMetadata(this.opMetadata, this.rpMetadata)
+ },
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/data/mockedData.ts b/packages/siop-oid4vp/lib/__tests__/data/mockedData.ts
new file mode 100644
index 00000000..fdc442ac
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/data/mockedData.ts
@@ -0,0 +1,141 @@
+import { ServiceTypesEnum } from '@sphereon/wellknown-dids-client'
+
+import { DIDDocument } from '../ResolverTestUtils'
+
+export const UNIT_TEST_TIMEOUT = 90000
+
+export const VERIFIER_LOGO_FOR_CLIENT = 'https://sphereon.com/content/themes/sphereon/assets/favicons/safari-pinned-tab.svg'
+
+export const VERIFIER_NAME_FOR_CLIENT = 'Client Verifier Relying Party Sphereon INC'
+export const VERIFIER_NAME_FOR_CLIENT_NL = ' *** dutch *** Client Verifier Relying Party Sphereon B.V.'
+
+export const VERIFIERZ_PURPOSE_TO_VERIFY = 'To request, receive and verify your credential about the the valid subject.'
+export const VERIFIERZ_PURPOSE_TO_VERIFY_NL = ' *** Dutch *** To request, receive and verify your credential about the the valid subject.'
+
+export const DID_DOCUMENT_PUBKEY_B58: DIDDocument = {
+ assertionMethod: [],
+ capabilityDelegation: [],
+ capabilityInvocation: [],
+ keyAgreement: [],
+ '@context': 'https://w3id.org/did/v1',
+ id: 'did:ethr:0xE3f80bcbb360F04865AfA795B7507d384154216C',
+ controller: 'did:ethr:0xE3f80bcbb360F04865AfA795B7507d384154216C',
+ authentication: ['did:ethr:0xE3f80bcbb360F04865AfA795B7507d384154216C#key-1'],
+ verificationMethod: [
+ {
+ id: 'did:ethr:0xE3f80bcbb360F04865AfA795B7507d384154216C#key-1',
+ type: 'EcdsaSecp256k1VerificationKey2019',
+ controller: 'did:ethr:0xE3f80bcbb360F04865AfA795B7507d384154216C',
+ publicKeyBase58: 'PSPfR29Snu5yxJcLHf2t6SyJ9mttet19ECkDHr4HY3FD5YC8ZenjvspPSAGSpaQ8B8kXADV97WSd7JqaNAUTn8YG',
+ },
+ ],
+}
+
+export const DID_DOCUMENT_PUBKEY_JWK: DIDDocument = {
+ assertionMethod: [],
+ capabilityDelegation: [],
+ capabilityInvocation: [],
+ keyAgreement: [],
+ '@context': 'https://w3id.org/did/v1',
+ id: 'did:ethr:0x96e9A346905a8F8D5ee0e6BA5D13456965e74513',
+ controller: 'did:ethr:0x96e9A346905a8F8D5ee0e6BA5D13456965e74513',
+ authentication: ['did:ethr:0x96e9A346905a8F8D5ee0e6BA5D13456965e74513#JTa8+HgHPyId90xmMFw6KRD4YUYLosBuWJw33nAuRS0='],
+ verificationMethod: [
+ {
+ id: 'did:ethr:0x96e9A346905a8F8D5ee0e6BA5D13456965e74513#JTa8+HgHPyId90xmMFw6KRD4YUYLosBuWJw33nAuRS0=',
+ type: 'EcdsaSecp256k1VerificationKey2019',
+ controller: 'did:ethr:0x96e9A346905a8F8D5ee0e6BA5D13456965e74513',
+ publicKeyJwk: {
+ kty: 'EC',
+ crv: 'secp256k1',
+ x: '62451c7a3e0c6e2276960834b79ae491ba0a366cd6a1dd814571212ffaeaaf5a',
+ y: '1ede3d754090437db67eca78c1659498c9cf275d2becc19cdc8f1ef76b9d8159',
+ kid: 'JTa8+HgHPyId90xmMFw6KRD4YUYLosBuWJw33nAuRS0=',
+ },
+ },
+ ],
+}
+
+export const DID_KEY = 'did:key:z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS'
+
+export const DID_KEY_ORIGIN = 'https://example.com'
+
+export const DID_KEY_DOCUMENT = {
+ '@context': [
+ 'https://www.w3.org/ns/did/v1',
+ {
+ Ed25519VerificationKey2018: 'https://w3id.org/security#Ed25519VerificationKey2018',
+ publicKeyJwk: {
+ '@id': 'https://w3id.org/security#publicKeyJwk',
+ '@type': '@json',
+ },
+ },
+ ],
+ id: DID_KEY,
+ verificationMethod: [
+ {
+ id: `${DID_KEY}#z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS`,
+ type: 'Ed25519VerificationKey2018',
+ controller: DID_KEY,
+ publicKeyJwk: {
+ kty: 'OKP',
+ crv: 'Ed25519',
+ x: '1ztBkC3x-8Eu8uPNTkTgH1Q0tkuO8v8RJDqfqWFl1N8',
+ },
+ },
+ ],
+ authentication: [`${DID_KEY}#z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS`],
+ assertionMethod: [`${DID_KEY}#z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS`],
+ service: [
+ {
+ id: `${DID_KEY}#z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS`,
+ type: ServiceTypesEnum.LINKED_DOMAINS,
+ serviceEndpoint: DID_KEY_ORIGIN,
+ },
+ ],
+} as DIDDocument
+
+export const VC_KEY_PAIR = {
+ type: 'Ed25519VerificationKey2020',
+ id: `${DID_KEY}#z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS`,
+ controller: `${DID_KEY_ORIGIN}/1234`,
+ publicKeyMultibase: 'z6MktwS79rvBjzRX8a8PPiURqG7HMJAfACTiozFkPJeJHRxS',
+ privateKeyMultibase: 'zrv4UTisGEUxoZr1enXeC7NMVapzq48KkS1rLSpBvpTyg1v3cLo7g5SnprD1eD4bdKdYHHMu5feATzatSAkbhgXgtZU',
+}
+
+export const DID_ION =
+ 'did:ion:EiCMvVdXv6iL3W8i4n-LmqUhE614kX4TYxVR5kTY2QGOjg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXkxIiwicHVibGljS2V5SndrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ii1MbHNpQVk5b3JmMXpKQlJOV0NuN0RpNUpoYl8tY2xhNlY5R3pHa3FmSFUiLCJ5IjoiRXBIU25GZHQ2ZU5lRkJEZzNVNVFIVDE0TVRsNHZIc0h5NWRpWU9DWEs1TSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dLCJzZXJ2aWNlcyI6W3siaWQiOiJsZCIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vbGR0ZXN0LnNwaGVyZW9uLmNvbSIsInR5cGUiOiJMaW5rZWREb21haW5zIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlBem8wTVVZUW5HNWM0VFJKZVFsNFR5WVRrSmRyeTJoeXlQUlpENzdFQm1CdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQUwtaEtrLUVsODNsRVJiZkFDUk1kSWNQVjRXWGJqZ3dsZ1ZDWTNwbDhhMGciLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUItT2NSbTlTNXdhU3QxbU4zSG4zM2RnMzJKN25MOEdBVHpGQ2ZXaWdIXzh3In19'
+
+export const DID_ION_ORIGIN = 'https://ldtest.sphereon.com'
+
+export const DID_ION_DOCUMENT = {
+ id: DID_ION,
+ '@context': [
+ 'https://www.w3.org/ns/did/v1',
+ {
+ '@base': DID_ION,
+ },
+ ],
+ service: [
+ {
+ id: '#ld',
+ type: 'LinkedDomains',
+ serviceEndpoint: DID_ION_ORIGIN,
+ },
+ ],
+ verificationMethod: [
+ {
+ id: '#key1',
+ controller: DID_ION,
+ type: 'EcdsaSecp256k1VerificationKey2019',
+ publicKeyJwk: {
+ kty: 'EC',
+ crv: 'secp256k1',
+ x: '-LlsiAY9orf1zJBRNWCn7Di5Jhb_-cla6V9GzGkqfHU',
+ y: 'EpHSnFdt6eNeFBDg3U5QHT14MTl4vHsHy5diYOCXK5M',
+ },
+ },
+ ],
+ authentication: ['#key1'],
+ assertionMethod: ['#key1'],
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/e2e/EBSI.spec.ts b/packages/siop-oid4vp/lib/__tests__/e2e/EBSI.spec.ts
new file mode 100644
index 00000000..1a78d511
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/e2e/EBSI.spec.ts
@@ -0,0 +1,123 @@
+import { getResolver as getKeyResolver } from '@cef-ebsi/key-did-resolver'
+// import { EbsiWallet } from '@cef-ebsi/wallet-lib';
+import EbsiWallet from '@cef-ebsi/wallet-lib'
+import { PresentationSignCallBackParams } from '@sphereon/pex'
+import { parseDid, W3CVerifiablePresentation } from '@sphereon/ssi-types'
+import { Resolver } from 'did-resolver'
+import { importJWK, JWK, SignJWT } from 'jose'
+import { v4 as uuidv4 } from 'uuid'
+
+import { OP, SigningAlgo } from '../../'
+import { getCreateJwtCallback, getVerifyJwtCallback } from '../DidJwtTestUtils'
+
+const ID_TOKEN_REQUEST_URL = 'https://api-conformance.ebsi.eu/conformance/v3/auth-mock/id_token_request'
+
+export const UNIT_TEST_TIMEOUT = 30000
+export const jwk: JWK = {
+ alg: 'ES256',
+ kty: 'EC',
+ use: 'sig',
+ crv: 'P-256',
+ x: '9ggs4Cm4VXcKOePpjkL9iSyMCa22yOjbo-oUXpy-aw0',
+ y: 'lEXW7b_J7lceiVEtrfptvuPeENsOJl-fhzmu654GPR8',
+}
+const hexPrivateKey = '47dc6ae067aa011f8574d2da7cf8c326538af08b85e6779d192a9893291c9a0a'
+
+const nonce = uuidv4()
+export const generateDid = () => {
+ const did = EbsiWallet.createDid('NATURAL_PERSON', jwk)
+ return did
+}
+
+const keyResolver = getKeyResolver()
+
+const didStr = generateDid()
+const kid = `${didStr}#${parseDid(didStr).id}`
+
+describe('EBSI SIOPv2 should', () => {
+ async function testWithOp() {
+ const did = generateDid(/*{ seed: u8a.fromString(hexPrivateKey, 'base16') }*/)
+ expect(did).toBeDefined()
+
+ const authRequestURL = await getAuthRequestURL({ nonce })
+ expect(authRequestURL).toBeDefined()
+ expect(authRequestURL).toContain('openid://?state=')
+ expect(authRequestURL).toContain(nonce)
+
+ const correlationId = 'test'
+
+ const resolver = new Resolver(keyResolver)
+ const op: OP = OP.builder()
+ .withPresentationSignCallback(presentationSignCalback)
+ .withCreateJwtCallback(getCreateJwtCallback({ alg: SigningAlgo.ES256, kid, did: didStr, hexPrivateKey }))
+ .withVerifyJwtCallback(getVerifyJwtCallback(resolver, { checkLinkedDomain: 'never' }))
+ .build()
+
+ const verifiedAuthRequest = await op.verifyAuthorizationRequest(authRequestURL, { correlationId })
+ expect(verifiedAuthRequest).toBeDefined()
+
+ const authResponse = await op.createAuthorizationResponse(verifiedAuthRequest, {
+ issuer: didStr,
+ correlationId,
+ jwtIssuer: {
+ method: 'did',
+ didUrl: kid,
+ alg: SigningAlgo.ES256,
+ },
+ })
+
+ expect(authResponse).toBeDefined()
+ expect(authResponse.response.payload).toBeDefined()
+ console.log(JSON.stringify(authResponse))
+
+ const result = await op.submitAuthorizationResponse(authResponse)
+ console.log(result.statusText)
+ console.log(await result.text())
+ expect(result.status).toEqual(204)
+ }
+
+ it.skip(
+ 'succeed with an id token only',
+ async () => {
+ await testWithOp()
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+
+ async function getAuthRequestURL({ nonce }: { nonce: string }): Promise {
+ const credentialOffer = await fetch(ID_TOKEN_REQUEST_URL, {
+ method: 'post',
+ headers: {
+ Accept: 'text/plain',
+ 'Content-Type': 'application/json',
+ },
+
+ //make sure to serialize your JSON body
+ body: JSON.stringify({
+ nonce,
+ }),
+ })
+
+ return await credentialOffer.text()
+ }
+
+ async function presentationSignCalback(args: PresentationSignCallBackParams): Promise {
+ const importedJwk = await importJWK(jwk, 'ES256')
+ const jwt = await new SignJWT({
+ vp: { ...args.presentation },
+ nonce: args.options.proofOptions?.nonce,
+ iss: args.options.holderDID,
+ })
+ .setProtectedHeader({
+ typ: 'JWT',
+ alg: 'ES256',
+ kid,
+ })
+ .setIssuedAt()
+ .setExpirationTime('2h')
+ .sign(importedJwk)
+
+ console.log(`VP: ${jwt}`)
+ return jwt
+ }
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts b/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts
new file mode 100644
index 00000000..08bb969a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts
@@ -0,0 +1,265 @@
+import { PresentationSignCallBackParams, PresentationSubmissionLocation } from '@sphereon/pex'
+import { W3CVerifiablePresentation } from '@sphereon/ssi-types'
+import * as ed25519 from '@transmute/did-key-ed25519'
+import { fetch } from 'cross-fetch'
+import { DIDDocument, DIDResolutionResult } from 'did-resolver'
+import { importJWK, JWK, SignJWT } from 'jose'
+import * as u8a from 'uint8arrays'
+
+import {
+ AuthorizationRequest,
+ AuthorizationResponse,
+ OP,
+ PresentationDefinitionWithLocation,
+ PresentationExchange,
+ SigningAlgo,
+ SupportedVersion,
+} from '../..'
+import { getCreateJwtCallback, getVerifyJwtCallback } from '../DidJwtTestUtils'
+
+export interface InitiateOfferRequest {
+ types: string[]
+}
+
+export interface InitiateOfferResponse {
+ authorizeRequestUri: string
+ state: string
+ nonce: string
+}
+
+export const UNIT_TEST_TIMEOUT = 30000
+
+export const VP_CREATE_URL = 'https://launchpad.mattrlabs.com/api/vp/create'
+
+export const OPENBADGE_JWT_VC =
+ 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1raXRHVmduTGRORlpqbUE5WEpwQThrM29lakVudU1GN205NkJEN3BaTGprWTIiLCJuYmYiOjE2OTYzNjA1MTEsImV4cCI6MTcyNzk4MjkxMSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raXRHVmduTGRORlpqbUE5WEpwQThrM29lakVudU1GN205NkJEN3BaTGprWTIiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.JDQ5kp_nvqJbL9Q8o2xIdt_r_WG0cB1o-Boy1RiDZhXRlVTgwAxvCa41OiL97VnbovN98tL7VtXbM6slAt6TBg'
+
+export const jwk: JWK = {
+ crv: 'Ed25519',
+ d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
+ x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
+ kty: 'OKP',
+}
+
+// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
+const hexPrivateKey = '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5'
+
+const didStr = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`
+const kid = `${didStr}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const generateCustomDid = async (opts?: { seed?: Uint8Array }): Promise<{ keys: any; didDocument: DIDDocument }> => {
+ const { didDocument, keys } = await ed25519.generate(
+ {
+ secureRandom: () => {
+ return opts?.seed ?? '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5'
+ },
+ },
+ { accept: 'application/did+json' },
+ )
+
+ return { keys, didDocument }
+}
+
+const resolve = async (didUrl: string): Promise => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return await didKeyResolve(didUrl, options)
+}
+
+const getResolver = () => {
+ return { resolve }
+}
+
+describe('OID4VCI-Client using Mattr issuer should', () => {
+ async function testWithOp(format: string | string[]) {
+ const did = await generateCustomDid({ seed: u8a.fromString(hexPrivateKey, 'base16') })
+ expect(did).toBeDefined()
+ expect(did.didDocument).toBeDefined()
+
+ const offer = await getOffer(format)
+ const { authorizeRequestUri, state, nonce } = offer
+ expect(authorizeRequestUri).toBeDefined()
+ expect(state).toBeDefined()
+ expect(nonce).toBeDefined()
+
+ const correlationId = 'test'
+
+ const op: OP = OP.builder()
+ .withPresentationSignCallback(presentationSignCalback)
+ .withCreateJwtCallback(getCreateJwtCallback({ alg: SigningAlgo.EDDSA, kid, did: didStr, hexPrivateKey }))
+ .withVerifyJwtCallback(getVerifyJwtCallback(getResolver(), { checkLinkedDomain: 'never' }))
+ .build()
+
+ const verifiedAuthRequest = await op.verifyAuthorizationRequest(authorizeRequestUri, { correlationId })
+ expect(verifiedAuthRequest).toBeDefined()
+ expect(verifiedAuthRequest.presentationDefinitions).toHaveLength(1)
+
+ const pex = new PresentationExchange({ allDIDs: [didStr], allVerifiableCredentials: [OPENBADGE_JWT_VC] })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ verifiedAuthRequest.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, [OPENBADGE_JWT_VC], presentationSignCalback, {
+ presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
+ proofOptions: { nonce },
+ holderDID: didStr,
+ })
+
+ const authResponse = await op.createAuthorizationResponse(verifiedAuthRequest, {
+ issuer: didStr,
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ },
+ correlationId,
+ jwtIssuer: {
+ method: 'did',
+ didUrl: kid,
+ alg: SigningAlgo.EDDSA,
+ },
+ })
+
+ expect(authResponse).toBeDefined()
+ expect(authResponse.response.payload).toBeDefined()
+ expect(authResponse.response.payload.presentation_submission).toBeDefined()
+ expect(authResponse.response.payload.vp_token).toBeDefined()
+
+ const result = await op.submitAuthorizationResponse(authResponse)
+ expect(result.status).toEqual(200)
+ }
+
+ async function testWithPayloads(format: string | string[]) {
+ const did = await generateCustomDid({ seed: u8a.fromString(hexPrivateKey, 'base16') })
+ expect(did).toBeDefined()
+ expect(did.didDocument).toBeDefined()
+
+ const offer = await getOffer(format)
+ const { authorizeRequestUri, state, nonce } = offer
+ expect(authorizeRequestUri).toBeDefined()
+ expect(state).toBeDefined()
+ expect(nonce).toBeDefined()
+
+ const correlationId = 'test'
+
+ const verifiedAuthRequest = await AuthorizationRequest.verify(authorizeRequestUri, {
+ correlationId,
+ verifyJwtCallback: getVerifyJwtCallback(getResolver()),
+ verification: {},
+ })
+ expect(verifiedAuthRequest).toBeDefined()
+ expect(verifiedAuthRequest.presentationDefinitions).toHaveLength(1)
+
+ const pex = new PresentationExchange({ allDIDs: [didStr], allVerifiableCredentials: [OPENBADGE_JWT_VC] })
+ const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
+ verifiedAuthRequest.authorizationRequestPayload,
+ )
+ await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
+ const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, [OPENBADGE_JWT_VC], presentationSignCalback, {
+ presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
+ proofOptions: { nonce },
+ holderDID: didStr,
+ })
+
+ const authResponse = await AuthorizationResponse.fromVerifiedAuthorizationRequest(
+ verifiedAuthRequest,
+ {
+ jwtIssuer: {
+ method: 'did',
+ didUrl: kid,
+ alg: SigningAlgo.EDDSA,
+ },
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: verifiablePresentationResult.presentationSubmission,
+ },
+ createJwtCallback: getCreateJwtCallback({
+ hexPrivateKey: '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5',
+ did: didStr,
+ kid,
+ alg: SigningAlgo.EDDSA,
+ }),
+ },
+ {
+ correlationId,
+ verifyJwtCallback: getVerifyJwtCallback(getResolver()),
+ verification: {},
+ nonce,
+ state,
+ },
+ )
+
+ expect(authResponse).toBeDefined()
+ expect(authResponse.payload).toBeDefined()
+ expect(authResponse.payload.presentation_submission).toBeDefined()
+ expect(authResponse.payload.vp_token).toBeDefined()
+ }
+
+ it(
+ 'succeed using OpenID4VCI version 11 and ldp_vc request/responses',
+ async () => {
+ await testWithPayloads('OpenBadgeCredential')
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+ it(
+ 'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
+ async () => {
+ await testWithOp('OpenBadgeCredential')
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+})
+
+async function getOffer(types: string | string[]): Promise {
+ const credentialOffer = await fetch(VP_CREATE_URL, {
+ method: 'post',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+
+ //make sure to serialize your JSON body
+ body: JSON.stringify({
+ types: Array.isArray(types) ? types : [types],
+ }),
+ })
+
+ return (await credentialOffer.json()) as InitiateOfferResponse
+}
+
+describe('Mattr OID4VP v18 credential offer', () => {
+ test('should verify using request directly', async () => {
+ const offer = await getOffer('OpenBadgeCredential')
+ const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(offer.authorizeRequestUri)
+
+ const verification = await authorizationRequest.verify({
+ verifyJwtCallback: getVerifyJwtCallback(getResolver()),
+ correlationId: 'test',
+ verification: {},
+ })
+
+ expect(verification).toBeDefined()
+ expect(verification.versions).toEqual([SupportedVersion.SIOPv2_D12_OID4VP_D20, SupportedVersion.SIOPv2_D12_OID4VP_D18])
+
+ /**
+ * pd value: {"id":"dae5d9b6-8145-4297-99b2-b8fcc5abb5ad","input_descriptors":[{"id":"OpenBadgeCredential","format":{"jwt_vc_json":{"alg":["EdDSA"]},"jwt_vc":{"alg":["EdDSA"]}},"constraints":{"fields":[{"path":["$.vc.type"],"filter":{"type":"array","items":{"type":"string"},"contains":{"const":"OpenBadgeCredential"}}}]}}]}
+ */
+ })
+})
+
+async function presentationSignCalback(args: PresentationSignCallBackParams): Promise {
+ const importedJwk = await importJWK(jwk, 'EdDSA')
+ const jwt = await new SignJWT({ vp: { ...args.presentation }, nonce: args.options.proofOptions?.nonce, iss: args.options.holderDID })
+ .setProtectedHeader({
+ typ: 'JWT',
+ alg: 'EdDSA',
+ kid,
+ })
+ .setIssuedAt()
+ .setExpirationTime('2h')
+ .sign(importedJwk)
+
+ console.log(`VP: ${jwt}`)
+ return jwt
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/functions/DidSiopMetadata.spec.ts b/packages/siop-oid4vp/lib/__tests__/functions/DidSiopMetadata.spec.ts
new file mode 100644
index 00000000..a6f8d6aa
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/functions/DidSiopMetadata.spec.ts
@@ -0,0 +1,64 @@
+import { Format } from '@sphereon/pex-models'
+import { IProofType } from '@sphereon/ssi-types'
+
+import { SigningAlgo, SIOPErrors, supportedCredentialsFormats } from '../..'
+
+describe('DidSiopMetadata should ', () => {
+ it('find supportedCredentialsFormats correctly', async function () {
+ const rpFormat: Format = {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ }
+ const opFormat: Format = {
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ }
+ expect(supportedCredentialsFormats(rpFormat, opFormat)).toStrictEqual({ jwt_vc: { alg: ['ES256', 'ES256K'] } })
+ })
+
+ it('throw CREDENTIAL_FORMATS_NOT_SUPPORTED for algs not matching', async function () {
+ const rpFormat: Format = {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ jwt_vc: {
+ alg: [SigningAlgo.ES256K],
+ },
+ }
+ const opFormat: Format = {
+ jwt_vc: {
+ alg: [SigningAlgo.ES256],
+ },
+ }
+ expect(() => supportedCredentialsFormats(rpFormat, opFormat)).toThrow(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED)
+ })
+
+ it('throw CREDENTIAL_FORMATS_NOT_SUPPORTED for types not matching', async function () {
+ const rpFormat: Format = {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ }
+ const opFormat: Format = {
+ jwt_vc: {
+ alg: [SigningAlgo.ES256],
+ },
+ }
+ expect(() => supportedCredentialsFormats(rpFormat, opFormat)).toThrow(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED)
+ })
+
+ it('throw CREDENTIALS_FORMATS_NOT_PROVIDED', async function () {
+ const rpFormat: Format = {}
+ const opFormat: Format = {
+ jwt_vc: {
+ alg: [SigningAlgo.ES256, SigningAlgo.ES256K],
+ },
+ }
+ expect(() => supportedCredentialsFormats(rpFormat, opFormat)).toThrow(SIOPErrors.CREDENTIALS_FORMATS_NOT_PROVIDED)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/functions/Encodings.spec.ts b/packages/siop-oid4vp/lib/__tests__/functions/Encodings.spec.ts
new file mode 100644
index 00000000..7990aa62
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/functions/Encodings.spec.ts
@@ -0,0 +1,51 @@
+import { encodeJsonAsURI } from '../..'
+
+describe('Encodings', () => {
+ /*test('encodeAsUriValue', () => {
+ expect(encodeAsUriValue(undefined, { a: { b: { c: 'd', e: 'f' } } })).toBe('a%5Bb%5D%5Bc%5D=d&a%5Bb%5D%5Be%5D=f');
+
+ expect(encodeAsUriValue(undefined, { a: ['b', 'c', 'd'] })).toBe('a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d');
+
+ expect(
+ encodeAsUriValue(undefined, {
+ a: {
+ b: {
+ 'a$s939very-2eweird-==key': {
+ c: 'd',
+ },
+ },
+ },
+ })
+ ).toBe('a%5Bb%5D%5Ba%24s939very-2eweird-%3D%3Dkey%5D%5Bc%5D=d');
+ });*/
+
+ test('encodeJsonAsURI', () => {
+ const encoded = encodeJsonAsURI(
+ {
+ presentation_submission: {
+ id: 'bbYJTQe7YPvVx-3rLl4Aq',
+ definition_id: '000fc41b-2859-4fc3-b797-510492a9479a',
+ descriptor_map: [
+ {
+ id: 'OpenBadgeCredential',
+ format: 'jwt_vp',
+ path: '$',
+ path_nested: {
+ id: 'OpenBadgeCredential',
+ format: 'jwt_vc_json',
+ path: '$.vp.verifiableCredential[0]',
+ },
+ },
+ ],
+ },
+ vp_token: ['ey...1', 'ey...2'],
+ vp_token_single: 'ey...3',
+ },
+ /*{ arraysWithIndex: ['presentation_submission', 'vp_token', 'vp_token_single'] }*/
+ )
+
+ expect(encoded).toBe(
+ `presentation_submission=%7B%22id%22%3A%22bbYJTQe7YPvVx-3rLl4Aq%22%2C%22definition_id%22%3A%22000fc41b-2859-4fc3-b797-510492a9479a%22%2C%22descriptor_map%22%3A%5B%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%22jwt_vp%22%2C%22path%22%3A%22%24%22%2C%22path_nested%22%3A%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22path%22%3A%22%24.vp.verifiableCredential%5B0%5D%22%7D%7D%5D%7D&vp_token=%5B%22ey...1%22%2C%22ey...2%22%5D&vp_token_single=ey...3`,
+ )
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts b/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts
new file mode 100644
index 00000000..18eb5551
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts
@@ -0,0 +1,258 @@
+import { LanguageTagUtils } from '../..'
+
+describe('Language tag util should', () => {
+ it('return no lingually tagged fields if there are no lingually tagged fields in the source object', async () => {
+ expect.assertions(1)
+ const source = { nonLanguageTaggedFieldName: 'value' }
+ expect(LanguageTagUtils.getAllLanguageTaggedProperties(source)).toEqual(new Map())
+ })
+
+ it('return all lingually tagged fields if there are lingually tagged fields in the source object', async () => {
+ expect.assertions(1)
+ const source = {
+ FieldNameWithoutLanguageTag: 'value',
+ 'FieldNameWithLanguageTag#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag#en-US': 'englishValue',
+ }
+
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields regardless of capitalization if there are lingually tagged fields in the source object', async () => {
+ expect.assertions(1)
+ const source = {
+ FieldNameWithoutLanguageTag: 'value',
+ 'FieldNameWithLanguageTag#nl-nl': 'dutchValue',
+ 'FieldNameWithLanguageTag#en-US': 'englishValue',
+ }
+
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag#nl-nl', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields if there are only lingually tagged fields in the source object', async () => {
+ expect.assertions(1)
+ const source = {
+ 'FieldNameWithLanguageTag#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields if there are multiple lingually tagged fields in the source object but no non-lingually tagged fields', async () => {
+ expect.assertions(1)
+ const source = {
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but no non-lingually tagged fields', async () => {
+ expect.assertions(1)
+ const source = {
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#en-US', 'englishValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but there is a non-lingually tagged field', async () => {
+ expect.assertions(1)
+ const source = {
+ nonLanguageTaggedFieldName: 'value',
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#en-US', 'englishValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return all lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but there are non-lingually tagged fields', async () => {
+ expect.assertions(1)
+ const source = {
+ nonLanguageTaggedFieldName: 'value',
+ nonLanguageTaggedFieldName2: 'value',
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#en-US', 'englishValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return no lingually tagged fields if there are incorrect lingually tagged fields in the source object', async () => {
+ expect.assertions(1)
+ const source = {
+ 'FieldNameWithLanguageTag2#en-EN': 'englishValue',
+ }
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getAllLanguageTaggedProperties(source)
+ expect(allLanguageTaggedProperties).toEqual(new Map())
+ })
+
+ it('return non-mapped lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but there are non-lingually tagged fields as well', async () => {
+ expect.assertions(1)
+ const source = {
+ nonLanguageTaggedFieldName: 'value',
+ nonLanguageTaggedFieldName2: 'value',
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#en-US', 'englishValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag2#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getLanguageTaggedProperties(source, [
+ 'FieldNameWithLanguageTag1',
+ 'FieldNameWithLanguageTag2',
+ ])
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return only desired non-mapped lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but there are non-lingually tagged fields as well', async () => {
+ expect.assertions(1)
+ const source = {
+ nonLanguageTaggedFieldName: 'value',
+ nonLanguageTaggedFieldName2: 'value',
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('FieldNameWithLanguageTag1#en-US', 'englishValue')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getLanguageTaggedProperties(source, ['FieldNameWithLanguageTag1'])
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('return only desired mapped lingually tagged fields if there are multiple lingually tagged fields in multiple languages in the source object but there are non-lingually tagged fields as well', async () => {
+ expect.assertions(1)
+ const source = {
+ nonLanguageTaggedFieldName: 'value',
+ nonLanguageTaggedFieldName2: 'value',
+ 'FieldNameWithLanguageTag1#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag1#en-US': 'englishValue',
+ 'FieldNameWithLanguageTag2#nl-NL': 'dutchValue',
+ 'FieldNameWithLanguageTag2#en-US': 'englishValue',
+ }
+ const expectedTaggedFields = new Map()
+ expectedTaggedFields.set('field_name_with_Language_tag_1#nl-NL', 'dutchValue')
+ expectedTaggedFields.set('field_name_with_Language_tag_1#en-US', 'englishValue')
+
+ const languageTagEnabledFieldsNamesMapping = new Map()
+ languageTagEnabledFieldsNamesMapping.set('FieldNameWithLanguageTag1', 'field_name_with_Language_tag_1')
+
+ const allLanguageTaggedProperties = LanguageTagUtils.getLanguageTaggedPropertiesMapped(source, languageTagEnabledFieldsNamesMapping)
+ expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields)
+ })
+
+ it('throw error if source is null', async () => {
+ expect.assertions(1)
+ await expect(() => LanguageTagUtils.getAllLanguageTaggedProperties(null)).toThrowError()
+ })
+
+ it('throw error if list is null', async () => {
+ expect.assertions(1)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ expect(() => LanguageTagUtils.getLanguageTaggedProperties({}, null as any)).toThrowError()
+ })
+
+ it('throw error if list is given but not effective', async () => {
+ expect.assertions(1)
+ await expect(() => LanguageTagUtils.getLanguageTaggedProperties({}, [])).toThrowError()
+ })
+
+ it('throw error if list is given but no proper field names', async () => {
+ expect.assertions(1)
+ await expect(() => LanguageTagUtils.getLanguageTaggedProperties({}, [''])).toThrowError()
+ })
+
+ it('do not throw error if mapping is null', async () => {
+ expect.assertions(1)
+ expect(LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, null as any)).toEqual(new Map())
+ })
+
+ it('throw error if mapping is given but not effective', async () => {
+ expect.assertions(1)
+ await expect(() => LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, new Map())).toThrowError()
+ })
+
+ it('throw error if mapping is given but no proper names', async () => {
+ expect.assertions(1)
+ const languageTagEnabledFieldsNamesMapping: Map = new Map()
+ languageTagEnabledFieldsNamesMapping.set(null as any, 'valid')
+ await expect(() => LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, languageTagEnabledFieldsNamesMapping)).toThrowError()
+ })
+
+ it('throw error if mapping is given but no proper field names', async () => {
+ expect.assertions(1)
+ const languageTagEnabledFieldsNamesMapping: Map = new Map()
+ languageTagEnabledFieldsNamesMapping.set('', 'valid')
+ await expect(() => LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, languageTagEnabledFieldsNamesMapping)).toThrowError()
+ })
+
+ it('throw error if mapping is given but no mapped names', async () => {
+ expect.assertions(1)
+ const languageTagEnabledFieldsNamesMapping: Map = new Map()
+ languageTagEnabledFieldsNamesMapping.set('valid', null as any)
+ await expect(() => LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, languageTagEnabledFieldsNamesMapping)).toThrowError()
+ })
+
+ it('throw error if mapping is given but no proper mapped names', async () => {
+ expect.assertions(1)
+ const languageTagEnabledFieldsNamesMapping: Map = new Map()
+ languageTagEnabledFieldsNamesMapping.set('valid', '')
+ await expect(() => LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, languageTagEnabledFieldsNamesMapping)).toThrowError()
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/interop/EBSI/EBSI.spec.ts b/packages/siop-oid4vp/lib/__tests__/interop/EBSI/EBSI.spec.ts
new file mode 100644
index 00000000..9caf78ae
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/interop/EBSI/EBSI.spec.ts
@@ -0,0 +1,66 @@
+import nock from 'nock'
+
+import { AuthorizationResponseOpts, OP, SupportedVersion, VerifyAuthorizationRequestOpts } from '../../../'
+import { getVerifyJwtCallback } from '../../DidJwtTestUtils'
+import { getResolver } from '../../ResolverTestUtils'
+import { UNIT_TEST_TIMEOUT } from '../../data/mockedData'
+
+const SIOP_URI =
+ 'openid://?state=3f3a673a-7835-42f1-a03e-b186fd042dcc&client_id=https%3A%2F%2Fconformance-test.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock&redirect_uri=https%3A%2F%2Fconformance-test.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock%2Fdirect_post&response_type=id_token&response_mode=direct_post&scope=openid&nonce=3a50effa-4505-42ce-8708-0c4ab32378dd&request_uri=https%3A%2F%2Fconformance-test.ebsi.eu%2Fconformance%2Fv3%2Fauth-mock%2Frequest_uri%2F4cb2dc1f-61a4-46b7-9660-06d62dd99700'
+const JWT =
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkZMeEkzTE04bUZDRkNEMUg0VmpacVd0MVBmaWQyaThBQ1lpRHZFelo5VU0ifQ.eyJzdGF0ZSI6IjNmM2E2NzNhLTc4MzUtNDJmMS1hMDNlLWIxODZmZDA0MmRjYyIsImNsaWVudF9pZCI6Imh0dHBzOi8vY29uZm9ybWFuY2UtdGVzdC5lYnNpLmV1L2NvbmZvcm1hbmNlL3YzL2F1dGgtbW9jayIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vY29uZm9ybWFuY2UtdGVzdC5lYnNpLmV1L2NvbmZvcm1hbmNlL3YzL2F1dGgtbW9jay9kaXJlY3RfcG9zdCIsInJlc3BvbnNlX3R5cGUiOiJpZF90b2tlbiIsInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsInNjb3BlIjoib3BlbmlkIiwibm9uY2UiOiIzYTUwZWZmYS00NTA1LTQyY2UtODcwOC0wYzRhYjMyMzc4ZGQiLCJpc3MiOiJodHRwczovL2NvbmZvcm1hbmNlLXRlc3QuZWJzaS5ldS9jb25mb3JtYW5jZS92My9hdXRoLW1vY2siLCJhdWQiOiJkaWQ6a2V5OnoyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnFTWlpGakc0dFZnS2hFd0twcm9qcUxCM0MyWXBqNEg3M1N0Z2pNa1NYZzJtUXh1V0xmenVSMTJRc052Z1FXenJ6S1NmN1lSQk5yUlhLNzF2ZnExMkJieXhUTEZFWkJXZm5IcWV6QlZHUWlOTGZxZXV5d1pIZ3N0TUNjUzQ0VFhmYjIifQ.h0nQfHq2sck4PizIleqlTTPPjYPgEH8OPKK0ug7r_O7N4qEghfILnL07cs5y1gARIH7hJLNNvI7qXEerl-SdDw'
+describe('EBSI', () => {
+ const responseOpts: AuthorizationResponseOpts = {
+ createJwtCallback: () => {
+ throw new Error('Not implemented')
+ },
+ /*checkLinkedDomain: CheckLinkedDomain.NEVER,
+ responseURI: EXAMPLE_REDIRECT_URL,
+ responseURIType: 'redirect_uri',
+ signature: {
+ hexPrivateKey: HEX_KEY,
+ did: DID,
+ kid: KID,
+ alg: SigningAlgo.ES256K,
+ },
+ registration: {
+ authorizationEndpoint: 'www.myauthorizationendpoint.com',
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ subject_syntax_types_supported: ['did:web'],
+ vpFormats: {
+ ldp_vc: {
+ proof_type: [IProofType.EcdsaSecp256k1Signature2019, IProofType.EcdsaSecp256k1Signature2019],
+ },
+ },
+ logo_uri: VERIFIER_LOGO_FOR_CLIENT,
+ clientName: VERIFIER_NAME_FOR_CLIENT,
+ 'clientName#nl-NL': VERIFIER_NAME_FOR_CLIENT_NL + '2022100333',
+ clientPurpose: VERIFIERZ_PURPOSE_TO_VERIFY,
+ 'clientPurpose#nl-NL': VERIFIERZ_PURPOSE_TO_VERIFY_NL,
+ //TODO: fill it up with actual value
+ issuer: ResponseIss.SELF_ISSUED_V2,
+ passBy: PassBy.VALUE,
+ },
+ responseMode: ResponseMode.POST,
+ expiresIn: 2000,*/
+ }
+
+ const verifyOpts: VerifyAuthorizationRequestOpts = {
+ verifyJwtCallback: getVerifyJwtCallback(getResolver('ebsi')),
+ verification: {},
+ correlationId: '1234',
+ supportedVersions: [SupportedVersion.SIOPv2_D12_OID4VP_D18],
+ }
+ it.skip(
+ 'succeed from request opts when all params are set',
+ async () => {
+ nock('https://conformance-test.ebsi.eu/conformance/v3/auth-mock/request_uri/4cb2dc1f-61a4-46b7-9660-06d62dd99700').get('').reply(200, JWT)
+
+ const op = OP.fromOpts(responseOpts, verifyOpts)
+ const verifiedRequest = await op.verifyAuthorizationRequest(SIOP_URI)
+ expect(verifiedRequest.issuer).toMatch('https://conformance-test.ebsi.eu/conformance/v3/auth-mock')
+ expect(verifiedRequest.jwt).toBeDefined()
+ },
+ UNIT_TEST_TIMEOUT,
+ )
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/interop/auth0/auth0.spec.ts b/packages/siop-oid4vp/lib/__tests__/interop/auth0/auth0.spec.ts
new file mode 100644
index 00000000..6e8fd31a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/interop/auth0/auth0.spec.ts
@@ -0,0 +1,12 @@
+import { PEX } from '@sphereon/pex'
+
+import { anyDef, VCs } from './fixtures'
+
+describe('auth0 presentation tool', () => {
+ it('any match definition should return all credentials', async () => {
+ const pex = new PEX()
+ expect(VCs).toHaveLength(5)
+ const selectResult = await pex.selectFrom(anyDef, VCs)
+ expect(selectResult.matches).toHaveLength(5)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/interop/auth0/fixtures.ts b/packages/siop-oid4vp/lib/__tests__/interop/auth0/fixtures.ts
new file mode 100644
index 00000000..cd5810c6
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/interop/auth0/fixtures.ts
@@ -0,0 +1,110 @@
+import { PresentationDefinitionV1 } from '@sphereon/pex-models'
+
+export const anyDef: PresentationDefinitionV1 = {
+ id: '1',
+ input_descriptors: [
+ {
+ id: '1',
+ name: 'A specific type of VC',
+ purpose: 'We want a VC of this type',
+ schema: [{ uri: 'VerifiableCredential' }],
+ },
+ ],
+}
+
+export const multiple = {
+ id: '00000000-0000-0000-0000-000000000000',
+ input_descriptors: [
+ {
+ id: '1',
+ name: 'A specific type of VC',
+ purpose: 'We want a VC of this type',
+ schema: [
+ {
+ uri: '',
+ },
+ ],
+ },
+ ],
+}
+export const VCs = [
+ {
+ '@context': [
+ 'https://www.w3.org/2018/credentials/v1',
+ 'https://sphereon-opensource.github.io/ssi-mobile-wallet/context/sphereon-wallet-identity-v1.jsonld',
+ ],
+ id: 'urn:uuid:69f05612-a6f4-415f-90588f91819aa2c1',
+ type: ['VerifiableCredential', 'SphereonWalletIdentityCredential'],
+ issuer: 'did:key:z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM',
+ issuanceDate: '2023-04-20T15:10:19.356Z',
+ credentialSubject: {
+ id: 'did:key:z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM',
+ firstName: 'Niels',
+ lastName: 'Klomp',
+ emailAddress: 'nklomp@sphereon.com',
+ },
+ proof: {
+ type: 'Ed25519Signature2018',
+ created: '2023-04-20T15:10:19Z',
+ verificationMethod: 'did:key:z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM#z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM',
+ proofPurpose: 'assertionMethod',
+ jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..-IwvA3_naB-p6lz20T8wp5PVPmrUm47HgICMlJ9Z6yfoRkupGvGtEHfzY6fZOr9r0QmAj_6nRlf-MKhwVxv4BQ',
+ },
+ },
+ 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0oxYzJVaU9pSnphV2NpTENKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0luZ2lPaUkyTm1kR1VqZDNhVjl1VFU1VlZIQmxVM3BEY0hKNmFWRllORXBDUzFOT1ptcG1lbmN6VVRWaFZITTRJaXdpZVNJNklqSktkRVpwVTJOdmIzRlhXV1EyVVZoT1pYVlJUSGhQU1MxMFpXSnVNSEZTWmxoNlRYWXlTM1UwY0VVaWZRIiwibmJmIjoxNjgyMDA0NjA1LCJpYXQiOjE2ODIwMDQ2MDUsInZjIjp7InR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQuanNvbiJdLCJpZCI6InVybjp1dWlkOjUwMGI1NWU0LWQxNmItNDkxOS05NWQ0LTJhNmFjNDhhM2M2ZiIsImlzc3VlciI6eyJpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSjFjMlVpT2lKemFXY2lMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaU4yUTJZMkptTWpRNE9XSXpOREkzTm1JeE56SXhPVEExTkRsa01qTTVNVGdpTENKNElqb2lSbTVGVlZWaGRXUnRPVGxPTXpCaU9EQnFjemhXZERSQmJrOTRkbEozV0hSblVtTkxjVE5uUWtsMU9DSXNJbUZzWnlJNklrVmtSRk5CSW4wIiwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn0sIm5hbWUiOiJKb2JzIGZvciB0aGUgRnV0dXJlIChKRkYpIiwidHlwZSI6IlByb2ZpbGUiLCJ1cmwiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDQtMjBUMTU6MzA6MDVaIiwiaXNzdWVkIjoiMjAyMy0wNC0yMFQxNTozMDowNVoiLCJ2YWxpZEZyb20iOiIyMDIzLTA0LTIwVDE1OjMwOjA1WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSTJObWRHVWpkM2FWOXVUVTVWVkhCbFUzcERjSEo2YVZGWU5FcENTMU5PWm1wbWVuY3pVVFZoVkhNNElpd2llU0k2SWpKS2RFWnBVMk52YjNGWFdXUTJVVmhPWlhWUlRIaFBTUzEwWldKdU1IRlNabGg2VFhZeVMzVTBjRVVpZlEiLCJhY2hpZXZlbWVudCI6eyJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUaGUgY29ob3J0IG9mIHRoZSBKRkYgUGx1Z2Zlc3QgMiBpbiBBdWd1c3QtTm92ZW1iZXIgb2YgMjAyMiBjb2xsYWJvcmF0ZWQgdG8gcHVzaCBpbnRlcm9wZXJhYmlsaXR5IG9mIFZDcyBpbiBlZHVjYXRpb24gZm9yd2FyZC4iLCJ0eXBlIjoiQ3JpdGVyaWEifSwiZGVzY3JpcHRpb24iOiJUaGlzIHdhbGxldCBjYW4gZGlzcGxheSB0aGlzIE9wZW4gQmFkZ2UgMy4wIiwiaWQiOiIwIiwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn0sIm5hbWUiOiJPdXIgV2FsbGV0IFBhc3NlZCBKRkYgUGx1Z2Zlc3QgIzIgMjAyMiIsInR5cGUiOiJBY2hpZXZlbWVudCJ9LCJ0eXBlIjoiQWNoaWV2ZW1lbnRTdWJqZWN0In0sIm5hbWUiOiJBY2hpZXZlbWVudCBDcmVkZW50aWFsIn0sImp0aSI6InVybjp1dWlkOjUwMGI1NWU0LWQxNmItNDkxOS05NWQ0LTJhNmFjNDhhM2M2ZiJ9.Is1GlvsyjScNWOqldkV_dt1jRaF9FHO3EK1IBgaE4ey7d-zs535yZY0YvH9gWnzDPisGbN2L4xBFRcZWxaHwDg',
+ 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0oxYzJVaU9pSnphV2NpTENKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0luZ2lPaUkyTm1kR1VqZDNhVjl1VFU1VlZIQmxVM3BEY0hKNmFWRllORXBDUzFOT1ptcG1lbmN6VVRWaFZITTRJaXdpZVNJNklqSktkRVpwVTJOdmIzRlhXV1EyVVZoT1pYVlJUSGhQU1MxMFpXSnVNSEZTWmxoNlRYWXlTM1UwY0VVaWZRIiwibmJmIjoxNjgyMDA0NjI3LCJpYXQiOjE2ODIwMDQ2MjcsInZjIjp7InR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJWZXJpZmlhYmxlQXR0ZXN0YXRpb24iLCJWZXJpZmlhYmxlSWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaWQiOiJ1cm46dXVpZDpiYjA1MmRiYi0xYTY5LTQ1N2MtYWM4Zi0zMzc3NWE0MzY0MGYiLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDQtMjBUMTU6MzA6MjdaIiwiaXNzdWVkIjoiMjAyMy0wNC0yMFQxNTozMDoyN1oiLCJ2YWxpZEZyb20iOiIyMDIzLTA0LTIwVDE1OjMwOjI3WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vd2FsdC1pZC93YWx0aWQtc3Npa2l0LXZjbGliL21hc3Rlci9zcmMvdGVzdC9yZXNvdXJjZXMvc2NoZW1hcy9WZXJpZmlhYmxlSWQuanNvbiIsInR5cGUiOiJGdWxsSnNvblNjaGVtYVZhbGlkYXRvcjIwMjEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lJMk5tZEdVamQzYVY5dVRVNVZWSEJsVTNwRGNISjZhVkZZTkVwQ1MxTk9abXBtZW5jelVUVmhWSE00SWl3aWVTSTZJakpLZEVacFUyTnZiM0ZYV1dRMlVWaE9aWFZSVEhoUFNTMTBaV0p1TUhGU1psaDZUWFl5UzNVMGNFVWlmUSIsImN1cnJlbnRBZGRyZXNzIjpbIjEgQm91bGV2YXJkIGRlIGxhIExpYmVydMOpLCA1OTgwMCBMaWxsZSJdLCJkYXRlT2ZCaXJ0aCI6IjE5OTMtMDQtMDgiLCJmYW1pbHlOYW1lIjoiRE9FIiwiZmlyc3ROYW1lIjoiSmFuZSIsImdlbmRlciI6IkZFTUFMRSIsIm5hbWVBbmRGYW1pbHlOYW1lQXRCaXJ0aCI6IkphbmUgRE9FIiwicGVyc29uYWxJZGVudGlmaWVyIjoiMDkwNDAwODA4NEgiLCJwbGFjZU9mQmlydGgiOiJMSUxMRSwgRlJBTkNFIn0sImV2aWRlbmNlIjpbeyJkb2N1bWVudFByZXNlbmNlIjpbIlBoeXNpY2FsIl0sImV2aWRlbmNlRG9jdW1lbnQiOlsiUGFzc3BvcnQiXSwic3ViamVjdFByZXNlbmNlIjoiUGh5c2ljYWwiLCJ0eXBlIjpbIkRvY3VtZW50VmVyaWZpY2F0aW9uIl0sInZlcmlmaWVyIjoiZGlkOmVic2k6MkE5Qlo5U1VlNkJhdGFjU3B2czFWNUNkakh2THBRN2JFc2kySmI2TGRIS25ReGFOIn1dfSwianRpIjoidXJuOnV1aWQ6YmIwNTJkYmItMWE2OS00NTdjLWFjOGYtMzM3NzVhNDM2NDBmIn0.7p-Bi5zX-5LJIJV-xYhJHdpiivbKcG7TldirI5cDL-RhKohO58zzAa964oYM_03s4AA8SboIxPT47LT6MO9FBQ',
+ {
+ type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'PermanentResidentCard'],
+ issuer: {
+ id: 'did:web:launchpad.vii.electron.mattrlabs.io',
+ name: 'Government of Kakapo',
+ logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg',
+ iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg',
+ image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg',
+ },
+ name: 'Permanent Resident Card',
+ description: 'Government of Kakapo PRC.',
+ credentialBranding: {
+ backgroundColor: '#3a2d2d',
+ watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png',
+ },
+ issuanceDate: '2023-04-24T18:13:50.848Z',
+ credentialSubject: {
+ id: 'did:key:z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM',
+ image:
+ 'data:image/png;base64,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',
+ gender: 'Male',
+ birthDate: '1958-08-17',
+ givenName: 'Louis',
+ lprNumber: '1958-08-17',
+ familyName: 'Pasteur',
+ lprCategory: 'C09',
+ birthCountry: 'France',
+ residentSince: '2015-01-01',
+ commuterClassification: 'C1',
+ },
+ '@context': [
+ 'https://www.w3.org/2018/credentials/v1',
+ {
+ '@vocab': 'https://w3id.org/security/undefinedTerm#',
+ },
+ 'https://mattr.global/contexts/vc-extensions/v1',
+ 'https://schema.org',
+ 'https://w3id.org/vc-revocation-list-2020/v1',
+ ],
+ credentialStatus: {
+ id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/25ce0f22-975a-43f8-8936-b93983b3e8f0#79',
+ type: 'RevocationList2020Status',
+ revocationListIndex: '79',
+ revocationListCredential: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/25ce0f22-975a-43f8-8936-b93983b3e8f0',
+ },
+ proof: {
+ type: 'Ed25519Signature2018',
+ created: '2023-04-24T18:13:51Z',
+ jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..6QxpmD1apUezx-_0zNNBEuRDCohh0EDuSQyWPPG_VgFx-eDtDfgpfm7-JcErW2xn0FihqMzxCxgMvRL2lzJJDQ',
+ proofPurpose: 'assertionMethod',
+ verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg',
+ },
+ },
+ 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0oxYzJVaU9pSnphV2NpTENKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0luZ2lPaUkyTm1kR1VqZDNhVjl1VFU1VlZIQmxVM3BEY0hKNmFWRllORXBDUzFOT1ptcG1lbmN6VVRWaFZITTRJaXdpZVNJNklqSktkRVpwVTJOdmIzRlhXV1EyVVZoT1pYVlJUSGhQU1MxMFpXSnVNSEZTWmxoNlRYWXlTM1UwY0VVaWZRIiwibmJmIjoxNjgyNjIzNzY5LCJpYXQiOjE2ODI2MjM3NjksInZjIjp7InR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQuanNvbiJdLCJpZCI6InVybjp1dWlkOjU3OWE1YjljLTI2Y2MtNDgwNi1hMDYwLTIyOWMwYjE5ZmI2OCIsImlzc3VlciI6eyJpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSjFjMlVpT2lKemFXY2lMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaU4yUTJZMkptTWpRNE9XSXpOREkzTm1JeE56SXhPVEExTkRsa01qTTVNVGdpTENKNElqb2lSbTVGVlZWaGRXUnRPVGxPTXpCaU9EQnFjemhXZERSQmJrOTRkbEozV0hSblVtTkxjVE5uUWtsMU9DSXNJbUZzWnlJNklrVmtSRk5CSW4wIiwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn0sIm5hbWUiOiJKb2JzIGZvciB0aGUgRnV0dXJlIChKRkYpIiwidHlwZSI6IlByb2ZpbGUiLCJ1cmwiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDQtMjdUMTk6Mjk6MjlaIiwiaXNzdWVkIjoiMjAyMy0wNC0yN1QxOToyOToyOVoiLCJ2YWxpZEZyb20iOiIyMDIzLTA0LTI3VDE5OjI5OjI5WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSTJObWRHVWpkM2FWOXVUVTVWVkhCbFUzcERjSEo2YVZGWU5FcENTMU5PWm1wbWVuY3pVVFZoVkhNNElpd2llU0k2SWpKS2RFWnBVMk52YjNGWFdXUTJVVmhPWlhWUlRIaFBTUzEwWldKdU1IRlNabGg2VFhZeVMzVTBjRVVpZlEiLCJhY2hpZXZlbWVudCI6eyJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUaGUgY29ob3J0IG9mIHRoZSBKRkYgUGx1Z2Zlc3QgMiBpbiBBdWd1c3QtTm92ZW1iZXIgb2YgMjAyMiBjb2xsYWJvcmF0ZWQgdG8gcHVzaCBpbnRlcm9wZXJhYmlsaXR5IG9mIFZDcyBpbiBlZHVjYXRpb24gZm9yd2FyZC4iLCJ0eXBlIjoiQ3JpdGVyaWEifSwiZGVzY3JpcHRpb24iOiJUaGlzIHdhbGxldCBjYW4gZGlzcGxheSB0aGlzIE9wZW4gQmFkZ2UgMy4wIiwiaWQiOiIwIiwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTItMjAyMi9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDItYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn0sIm5hbWUiOiJPdXIgV2FsbGV0IFBhc3NlZCBKRkYgUGx1Z2Zlc3QgIzIgMjAyMiIsInR5cGUiOiJBY2hpZXZlbWVudCJ9LCJ0eXBlIjoiQWNoaWV2ZW1lbnRTdWJqZWN0In0sIm5hbWUiOiJBY2hpZXZlbWVudCBDcmVkZW50aWFsIn0sImp0aSI6InVybjp1dWlkOjU3OWE1YjljLTI2Y2MtNDgwNi1hMDYwLTIyOWMwYjE5ZmI2OCJ9.UazGcYVGIW0xKeqdP6cW-TFgkk9in2pXz5v4LPRUmHshj5O1m_i91O5XsMAZP0WC5n1bClKlXcMGrBqlrrH2Aw',
+]
diff --git a/packages/siop-oid4vp/lib/__tests__/interop/mattr/fixtures.ts b/packages/siop-oid4vp/lib/__tests__/interop/mattr/fixtures.ts
new file mode 100644
index 00000000..4aa7e5e0
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/interop/mattr/fixtures.ts
@@ -0,0 +1,53 @@
+export const MattrPRCVC = {
+ type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'PermanentResidentCard'],
+ issuer: {
+ id: 'did:web:launchpad.vii.electron.mattrlabs.io',
+ name: 'Government of Kakapo',
+ logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg',
+ iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg',
+ image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg',
+ },
+ name: 'Permanent Resident Card',
+ description: 'Government of Kakapo PRC.',
+ credentialBranding: {
+ backgroundColor: '#3a2d2d',
+ watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png',
+ },
+ issuanceDate: '2023-04-24T18:13:50.848Z',
+ credentialSubject: {
+ id: 'did:key:z6MkkVP8oAK9wSpE5pX5A8u5pXZzdkYxpzEUrgGyQD11DsAM',
+ image:
+ 'data:image/png;base64,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',
+ gender: 'Male',
+ birthDate: '1958-08-17',
+ givenName: 'Louis',
+ lprNumber: '1958-08-17',
+ familyName: 'Pasteur',
+ lprCategory: 'C09',
+ birthCountry: 'France',
+ residentSince: '2015-01-01',
+ commuterClassification: 'C1',
+ },
+ '@context': [
+ 'https://www.w3.org/2018/credentials/v1',
+ {
+ '@vocab': 'https://w3id.org/security/undefinedTerm#',
+ },
+ 'https://mattr.global/contexts/vc-extensions/v1',
+ 'https://schema.org',
+ 'https://w3id.org/vc-revocation-list-2020/v1',
+ ],
+ credentialStatus: {
+ id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/25ce0f22-975a-43f8-8936-b93983b3e8f0#79',
+ type: 'RevocationList2020Status',
+ revocationListIndex: '79',
+ revocationListCredential: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/25ce0f22-975a-43f8-8936-b93983b3e8f0',
+ },
+ proof: {
+ type: 'Ed25519Signature2018',
+ created: '2023-04-24T18:13:51Z',
+ jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..6QxpmD1apUezx-_0zNNBEuRDCohh0EDuSQyWPPG_VgFx-eDtDfgpfm7-JcErW2xn0FihqMzxCxgMvRL2lzJJDQ',
+ proofPurpose: 'assertionMethod',
+ verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg',
+ },
+}
diff --git a/packages/siop-oid4vp/lib/__tests__/modules.d.ts b/packages/siop-oid4vp/lib/__tests__/modules.d.ts
new file mode 100644
index 00000000..12117752
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/modules.d.ts
@@ -0,0 +1,3 @@
+declare module '@digitalcredentials/vc'
+declare module '@digitalcredentials/jsonld-signatures'
+declare module '@digitalcredentials/ed25519-signature-2020'
diff --git a/packages/siop-oid4vp/lib/__tests__/regressions/ClientIdIsObject.spec.ts b/packages/siop-oid4vp/lib/__tests__/regressions/ClientIdIsObject.spec.ts
new file mode 100644
index 00000000..ab842788
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/regressions/ClientIdIsObject.spec.ts
@@ -0,0 +1,59 @@
+import { PassBy, ResponseType, RevocationVerification, RP, Scope, SigningAlgo, SubjectType, SupportedVersion } from '../..'
+import { parseJWT } from '../../helpers/jwtUtils'
+import { internalSignature } from '../DidJwtTestUtils'
+
+const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'
+// const EXAMPLE_REFERENCE_URL = 'https://rp.acme.com/siop/jwts';
+const HEX_KEY = 'f857544a9d1097e242ff0b287a7e6e90f19cf973efe2317f2a4678739664420f'
+const DID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0'
+const KID = 'did:ethr:0x0106a2e985b1E1De9B5ddb4aF6dC9e928F4e99D0#keys-1'
+
+const rp = RP.builder()
+ // .withClientId('test')
+ .withRedirectUri(EXAMPLE_REDIRECT_URL)
+ .withRequestByValue()
+ .withRevocationVerification(RevocationVerification.NEVER)
+ .withCreateJwtCallback(internalSignature(HEX_KEY, DID, KID, SigningAlgo.ES256K))
+ .withSupportedVersions([SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1])
+ .withClientMetadata({
+ idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
+ passBy: PassBy.VALUE,
+ requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
+ responseTypesSupported: [ResponseType.ID_TOKEN],
+ vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
+ scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
+ subjectTypesSupported: [SubjectType.PAIRWISE],
+ subject_syntax_types_supported: ['did:ethr:', 'did:key:', 'did'],
+ })
+ .withPresentationDefinition({
+ definition: {
+ id: '1234-1234-1234-1234',
+ input_descriptors: [
+ {
+ id: 'ExampleInputDescriptor',
+ schema: [
+ {
+ uri: 'https://did.itsourweb.org:3000/smartcredential/Ontario-Health-Insurance-Plan',
+ },
+ ],
+ },
+ ],
+ },
+ })
+ .build()
+
+describe('Creating an AuthRequest with an RP from builder', () => {
+ it('should have a client_id that is a string when not explicitly provided', async () => {
+ // see: https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/54
+ // When not supplying a clientId to the builder, the request object creates an object of the clientId
+ const authRequest = await rp.createAuthorizationRequest({
+ correlationId: '1',
+ nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
+ state: 'b32f0087fc9816eb813fd11f',
+ })
+
+ const requestObjectJwt = await authRequest.requestObject?.toJwt()
+ const { payload } = parseJWT(requestObjectJwt!)
+ await expect(payload.client_id).toEqual(DID)
+ })
+})
diff --git a/packages/siop-oid4vp/lib/__tests__/spec-compliance/jwtVCPresentationProfile.spec.ts b/packages/siop-oid4vp/lib/__tests__/spec-compliance/jwtVCPresentationProfile.spec.ts
new file mode 100644
index 00000000..1f75d92c
--- /dev/null
+++ b/packages/siop-oid4vp/lib/__tests__/spec-compliance/jwtVCPresentationProfile.spec.ts
@@ -0,0 +1,578 @@
+import { PresentationSignCallBackParams } from '@sphereon/pex'
+import { IProofType } from '@sphereon/ssi-types'
+import * as jose from 'jose'
+import { KeyLike } from 'jose'
+import nock from 'nock'
+import * as u8a from 'uint8arrays'
+
+import {
+ AuthorizationRequest,
+ AuthorizationResponse,
+ IDToken,
+ OP,
+ PassBy,
+ PresentationDefinitionLocation,
+ PresentationExchange,
+ PresentationSignCallback,
+ PresentationVerificationCallback,
+ PropertyTarget,
+ ResponseMode,
+ ResponseType,
+ RevocationVerification,
+ RP,
+ SigningAlgo,
+ SupportedVersion,
+ VPTokenLocation,
+} from '../..'
+import { getVerifyJwtCallback, internalSignature } from '../DidJwtTestUtils'
+import { getResolver } from '../ResolverTestUtils'
+
+let rp: RP
+let op: OP
+
+afterEach(() => {
+ nock.cleanAll()
+})
+
+const presentationVerificationCallback: PresentationVerificationCallback = async () => ({ verified: true })
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const presentationSignCallback: PresentationSignCallback = async (_args: PresentationSignCallBackParams) =>
+ TestVectors.authorizationResponsePayload.vp_token
+
+const verifyJwtCallback = getVerifyJwtCallback(getResolver('ion'), {
+ policies: { exp: false, iat: false, aud: false, nbf: false },
+ checkLinkedDomain: 'if_present',
+})
+
+beforeEach(async () => {
+ await TestVectors.init()
+
+ TestVectors.mockDID(TestVectors.issuerDID, TestVectors.issuerKID, TestVectors.issuerJwk)
+ TestVectors.mockDID(TestVectors.holderDID, TestVectors.holderKID, TestVectors.holderJwk)
+ TestVectors.mockDID(TestVectors.verifierDID, TestVectors.verifierKID, TestVectors.verifierJwk)
+
+ rp = RP.builder({ requestVersion: SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1 })
+ .withResponseType(ResponseType.ID_TOKEN, PropertyTarget.REQUEST_OBJECT)
+ .withClientId(TestVectors.issuerDID, PropertyTarget.REQUEST_OBJECT)
+ .withScope('openid', PropertyTarget.REQUEST_OBJECT)
+ .withResponseMode(ResponseMode.POST, PropertyTarget.REQUEST_OBJECT)
+ .withClientMetadata(
+ {
+ passBy: PassBy.VALUE,
+ // targets: [PropertyTarget.REQUEST_OBJECT],
+ logo_uri: 'https://example.com/verifier-icon.png',
+ tos_uri: 'https://example.com/verifier-info',
+ clientName: 'Example Verifier',
+ vpFormatsSupported: {
+ jwt_vc: {
+ alg: ['EdDSA', 'ES256K'],
+ },
+ jwt_vp: {
+ alg: ['EdDSA', 'ES256K'],
+ },
+ },
+ subject_syntax_types_supported: ['did:ion'],
+ },
+ PropertyTarget.REQUEST_OBJECT,
+ )
+ .withRedirectUri('https://example.com/siop-response', PropertyTarget.REQUEST_OBJECT)
+ .withRequestBy(PassBy.REFERENCE, TestVectors.request_uri)
+ .withCreateJwtCallback(internalSignature(TestVectors.verifierHexPrivateKey, TestVectors.verifierDID, TestVectors.verifierKID, SigningAlgo.EDDSA))
+ .withVerifyJwtCallback(verifyJwtCallback)
+ .build()
+
+ op = OP.builder()
+ .withCreateJwtCallback(internalSignature(TestVectors.holderHexPrivateKey, TestVectors.holderDID, TestVectors.holderKID, SigningAlgo.EDDSA))
+ .withVerifyJwtCallback(verifyJwtCallback)
+ .addSupportedVersion(SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1)
+ .build()
+})
+
+describe('RP using test vectors', () => {
+ it('should create matching auth request and URI', async () => {
+ const authRequest = await createAuthRequest()
+ expect(await authRequest.requestObject?.getPayload()).toMatchObject({
+ response_type: 'id_token',
+ nonce: '40252afc-6a82-4a2e-905f-e41f122ef575',
+ client_id:
+ 'did:ion:EiBAA99TAezxKRc2wuuBnr4zzGsS2YcsOA4IPQV0KY64Xg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkdnWkdUZzhlQ2E3bFYyOE1MOUpUbUJVdms3RFlCYmZSS1dMaHc2NUpvMXMiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURKV0Z2WUJ5Qzd2azA2MXAzdHYwd29WSTk5MTFQTGgwUVp4cWpZM2Y4MVFRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlBX1RvVlNBZDBTRWxOU2VrQ1k1UDVHZ01KQy1MTVpFY2ZSV2ZqZGNaYXJFQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpRDN0ZTV4eFliemJod0pYdEUwZ2tZV3Z3MlZ2VFB4MU9la0RTcXduZzRTWmcifX0',
+ response_mode: 'post',
+ // "nbf" : 1674772063,
+ scope: 'openid',
+ claims: {
+ vp_token: {
+ presentation_definition: {
+ input_descriptors: [
+ {
+ schema: [
+ {
+ uri: 'VerifiedEmployee',
+ },
+ ],
+ purpose: 'We need to verify that you have a valid VerifiedEmployee Verifiable Credential.',
+ name: 'VerifiedEmployeeVC',
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ },
+ },
+ },
+ registration: {
+ logo_uri: 'https://example.com/verifier-icon.png',
+ tos_uri: 'https://example.com/verifier-info',
+ client_name: 'Example Verifier',
+ vp_formats: {
+ jwt_vc: {
+ alg: ['EdDSA', 'ES256K'],
+ },
+ jwt_vp: {
+ alg: ['EdDSA', 'ES256K'],
+ },
+ },
+ subject_syntax_types_supported: ['did:ion'],
+ },
+ state: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ redirect_uri: 'https://example.com/siop-response' /*,
+ "exp" : 1674775663,
+ "iat" : 1674772063,
+ "jti" : "f0e6dcf5-3fe6-4507-adc9-b496daf34512"*/,
+ })
+ })
+ it('should re-create uri', async () => {
+ const authRequest = await createAuthRequest()
+ const uri = await authRequest.uri()
+ expect(uri.encodedUri).toEqual(
+ 'openid-vc://?request_uri=https%3A%2F%2Fexample%2Fservice%2Fapi%2Fv1%2Fpresentation-request%2F649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ )
+ })
+ it('should get presentation definition', async () => {
+ const authRequest = await createAuthRequest()
+ const defs = await authRequest.getPresentationDefinitions()
+ expect(defs).toBeDefined()
+ expect(defs).toHaveLength(1)
+ })
+ it('should decode id token jwt', async () => {
+ const idToken = await IDToken.fromIDToken(TestVectors.idTokenJwt)
+ expect(idToken).toBeDefined()
+ const payload = await idToken.payload()
+ expect(payload).toEqual(TestVectors.idTokenPayload)
+ expect(
+ await idToken.verify({
+ correlationId: '1234',
+ audience:
+ 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ verifyJwtCallback: verifyJwtCallback,
+ verification: {
+ presentationVerificationCallback,
+ },
+ }),
+ ).toBeTruthy()
+ })
+
+ it('should decode auth response', async () => {
+ const authorizationResponse = await AuthorizationResponse.fromPayload(TestVectors.authorizationResponsePayload)
+ expect(authorizationResponse).toBeDefined()
+ expect(authorizationResponse.payload).toEqual(TestVectors.authorizationResponsePayload)
+ expect(await authorizationResponse.idToken?.payload()).toEqual(TestVectors.idTokenPayload)
+ expect(
+ await authorizationResponse.idToken?.verify({
+ verifyJwtCallback: verifyJwtCallback,
+ correlationId: '1234',
+ audience:
+ 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ verification: {
+ presentationVerificationCallback,
+ revocationOpts: {
+ revocationVerification: RevocationVerification.NEVER,
+ },
+ },
+ }),
+ ).toBeTruthy()
+
+ const authRequest = await createAuthRequest()
+ const presentationDefinitions = await authRequest.getPresentationDefinitions()
+
+ const verified = await authorizationResponse.verify({
+ correlationId: '1234',
+ verifyJwtCallback: verifyJwtCallback,
+ audience:
+ 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ verification: {
+ presentationVerificationCallback,
+ revocationOpts: {
+ revocationVerification: RevocationVerification.NEVER,
+ },
+ },
+ presentationDefinitions,
+ })
+ expect(verified).toBeDefined()
+ })
+})
+
+describe('OP using test vectors', () => {
+ it('should import auth request and be able to provide the auth request unaltered', async () => {
+ nock('https://example').get('/service/api/v1/presentation-request/649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9').reply(200, TestVectors.requestObjectJwt)
+ // expect.assertions(1);
+ const result = await op.verifyAuthorizationRequest(TestVectors.auth_request, {
+ verification: {},
+ })
+ expect(result).toBeDefined()
+ })
+
+ it('should use test vector auth response', async () => {
+ const authorizationResponse = await AuthorizationResponse.fromPayload(TestVectors.authorizationResponsePayload)
+
+ expect(authorizationResponse.payload.vp_token).toBeDefined()
+ expect(authorizationResponse.payload.id_token).toBeDefined()
+ expect(await authorizationResponse.idToken?.payload()).toEqual({
+ _vp_token: {
+ presentation_submission: {
+ definition_id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ descriptor_map: [
+ {
+ format: 'jwt_vp',
+ id: 'VerifiedEmployeeVC',
+ path: '$',
+ path_nested: {
+ format: 'jwt_vc',
+ id: 'VerifiedEmployeeVC',
+ path: '$.verifiableCredential[0]',
+ },
+ },
+ ],
+ id: '9af24e8a-c8f3-4b9a-9161-b715e77a6010',
+ },
+ },
+ aud: 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ exp: 1674786463,
+ iat: 1674772063,
+ iss: 'https://self-issued.me/v2/openid-vc',
+ jti: '0f5dafed-0d82-43b1-af79-40440e3f1366',
+ nonce: '40252afc-6a82-4a2e-905f-e41f122ef575',
+ sub: 'did:ion:EiAeM6No9kdpos6_ehBUDh4RINY4USDMh-QdWksmsI3WkA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IncwNk9WN2U2blR1cnQ2RzlWcFZYeEl3WW55amZ1cHhlR3lLQlMtYmxxdmciLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFSNGRVQmxqNWNGa3dMdkpTWUYzVExjLV81MWhDX2xZaGxXZkxWZ29seTRRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEcVJyWU5fV3JTakFQdnlFYlJQRVk4WVhPRmNvT0RTZExUTWItM2FKVElGQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQUwyMFdYakpQQW54WWdQY1U5RV9POE1OdHNpQk00QktpaVNwT3ZFTWpVOUEifX0',
+ })
+ await rp.verifyAuthorizationResponse(TestVectors.authorizationResponsePayload, {
+ audience:
+ 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ verification: {
+ revocationOpts: {
+ revocationVerification: RevocationVerification.NEVER,
+ },
+ presentationVerificationCallback,
+ },
+ presentationDefinitions: [
+ {
+ definition: TestVectors.presentationDef,
+ location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN,
+ },
+ ],
+ })
+
+ nock('https://example', {}).post('/resp').reply(200, {})
+ await op.submitAuthorizationResponse({
+ response: authorizationResponse,
+ correlationId: '12345',
+ responseURI: 'https://example/resp',
+ })
+ })
+ it('should create auth response', async () => {
+ nock('https://example')
+ .get('/service/api/v1/presentation-request/649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9')
+ .times(1)
+ .reply(200, TestVectors.requestObjectJwt)
+ const result = await op.verifyAuthorizationRequest(TestVectors.auth_request, {
+ verification: {},
+ })
+ const presentationExchange = new PresentationExchange({
+ allDIDs: [TestVectors.holderDID],
+ allVerifiableCredentials: [TestVectors.jwtVCFromVPToken],
+ })
+ const verifiablePresentationResult = await presentationExchange.createVerifiablePresentation(
+ TestVectors.presentationDef,
+ [TestVectors.jwtVCFromVPToken],
+ presentationSignCallback,
+ {
+ holderDID: TestVectors.holderDID,
+ signatureOptions: {},
+ proofOptions: {
+ type: IProofType.JwtProof2020,
+ },
+ },
+ )
+ await op.createAuthorizationResponse(result, {
+ presentationExchange: {
+ verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
+ presentationSubmission: TestVectors.presentation_submission,
+ vpTokenLocation: VPTokenLocation.ID_TOKEN,
+ },
+ })
+ })
+})
+
+async function createAuthRequest(): Promise {
+ return await rp.createAuthorizationRequest({
+ correlationId: '1234',
+ nonce: { propertyValue: '40252afc-6a82-4a2e-905f-e41f122ef575', targets: PropertyTarget.REQUEST_OBJECT },
+ state: { propertyValue: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9', targets: PropertyTarget.REQUEST_OBJECT },
+ jwtIssuer: { method: 'did', alg: SigningAlgo.EDDSA, didUrl: TestVectors.verifierKID },
+ claims: {
+ propertyValue: {
+ vp_token: {
+ presentation_definition: {
+ input_descriptors: [
+ {
+ schema: [
+ {
+ uri: 'VerifiedEmployee',
+ },
+ ],
+ purpose: 'We need to verify that you have a valid VerifiedEmployee Verifiable Credential.',
+ name: 'VerifiedEmployeeVC',
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ },
+ },
+ },
+ targets: PropertyTarget.REQUEST_OBJECT,
+ },
+ })
+}
+
+class TestVectors {
+ public static issuerDID =
+ 'did:ion:EiBAA99TAezxKRc2wuuBnr4zzGsS2YcsOA4IPQV0KY64Xg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkdnWkdUZzhlQ2E3bFYyOE1MOUpUbUJVdms3RFlCYmZSS1dMaHc2NUpvMXMiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURKV0Z2WUJ5Qzd2azA2MXAzdHYwd29WSTk5MTFQTGgwUVp4cWpZM2Y4MVFRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlBX1RvVlNBZDBTRWxOU2VrQ1k1UDVHZ01KQy1MTVpFY2ZSV2ZqZGNaYXJFQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpRDN0ZTV4eFliemJod0pYdEUwZ2tZV3Z3MlZ2VFB4MU9la0RTcXduZzRTWmcifX0'
+ public static issuerKID = `${TestVectors.issuerDID}#key-1`
+ public static issuerJwk = {
+ kty: 'OKP',
+ d: 'jGLXxgOFN5DuQQFrRBN58Xll5SRizDXyVL5uiDY60_4',
+ crv: 'Ed25519',
+ kid: 'key-1',
+ x: 'GgZGTg8eCa7lV28ML9JTmBUvk7DYBbfRKWLhw65Jo1s',
+ }
+ public static issuerKey
+ public static issuerPrivateKey
+ public static issuerPublicKey
+ public static issuerHexPrivateKey
+
+ public static holderJwk = {
+ kty: 'OKP',
+ d: 'ZeDOVmemqzPAK0R2F1BHVfRYC7g65p_UpyXhEaX03N4',
+ crv: 'Ed25519',
+ kid: 'key-1',
+ x: 'w06OV7e6nTurt6G9VpVXxIwYnyjfupxeGyKBS-blqvg',
+ }
+ public static holderDID =
+ 'did:ion:EiAeM6No9kdpos6_ehBUDh4RINY4USDMh-QdWksmsI3WkA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IncwNk9WN2U2blR1cnQ2RzlWcFZYeEl3WW55amZ1cHhlR3lLQlMtYmxxdmciLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFSNGRVQmxqNWNGa3dMdkpTWUYzVExjLV81MWhDX2xZaGxXZkxWZ29seTRRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEcVJyWU5fV3JTakFQdnlFYlJQRVk4WVhPRmNvT0RTZExUTWItM2FKVElGQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQUwyMFdYakpQQW54WWdQY1U5RV9POE1OdHNpQk00QktpaVNwT3ZFTWpVOUEifX0'
+ public static holderKID = `${TestVectors.holderDID}#key-1`
+ public static holderKey
+ public static holderPrivateKey
+ public static holderPublicKey
+ public static holderHexPrivateKey
+
+ public static verifierJwk = {
+ kty: 'OKP',
+ d: 'SP18SnbU9f-Rph0GwulyvmLFyCXDHqZVKWDo2E41llQ',
+ crv: 'Ed25519',
+ kid: 'key-1',
+ x: 'C_OUJxH6iIcC6XdNh7JmC-THXAVaXnvu9OEEZ8tq9NI',
+ }
+ public static verifierDID =
+ 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0'
+
+ public static verifierKID = `${TestVectors.verifierDID}#key-1`
+ public static verifierKey: jose.KeyLike | Uint8Array
+ public static verifierPrivateKey: string
+ public static verifierPublicKey: string
+ public static verifierHexPrivateKey: string
+
+ public static async init() {
+ TestVectors.issuerKey = (await jose.importJWK(TestVectors.issuerJwk, 'EdDSA', true)) as KeyLike
+ TestVectors.issuerPrivateKey = u8a.toString(u8a.fromString(TestVectors.issuerJwk.d, 'base64url'), 'hex')
+ TestVectors.issuerPublicKey = u8a.toString(u8a.fromString(TestVectors.issuerJwk.x, 'base64url'), 'hex')
+ TestVectors.issuerHexPrivateKey = `${TestVectors.issuerPrivateKey}${TestVectors.issuerPublicKey}`
+
+ TestVectors.holderKey = (await jose.importJWK(TestVectors.holderJwk, 'EdDSA', true)) as KeyLike
+ TestVectors.holderPrivateKey = u8a.toString(u8a.fromString(TestVectors.holderJwk.d, 'base64url'), 'hex')
+ TestVectors.holderPublicKey = u8a.toString(u8a.fromString(TestVectors.holderJwk.x, 'base64url'), 'hex')
+ TestVectors.holderHexPrivateKey = `${TestVectors.holderPrivateKey}${TestVectors.holderPublicKey}`
+
+ TestVectors.verifierKey = (await jose.importJWK(TestVectors.verifierJwk, 'EdDSA', true)) as KeyLike
+ TestVectors.verifierPrivateKey = u8a.toString(u8a.fromString(TestVectors.verifierJwk.d, 'base64url'), 'hex')
+ TestVectors.verifierPublicKey = u8a.toString(u8a.fromString(TestVectors.verifierJwk.x, 'base64url'), 'hex')
+ TestVectors.verifierHexPrivateKey = `${TestVectors.verifierPrivateKey}${TestVectors.verifierPublicKey}`
+ }
+
+ public static auth_request = 'openid-vc://?request_uri=https://example/service/api/v1/presentation-request/649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9'
+ public static request_uri = 'https://example/service/api/v1/presentation-request/649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9'
+
+ public static idTokenJwt =
+ 'eyJraWQiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCNrZXktMSIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCIsImF1ZCI6ImRpZDppb246RWlCV2U5UnRIVDdWWi1KdWZmOE9ubkpBeUZKdENva2NZSHgxQ1FrRnRwbDdwdzpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUpyWlhrdE1TSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWtOZlQxVktlRWcyYVVsalF6WllaRTVvTjBwdFF5MVVTRmhCVm1GWWJuWjFPVTlGUlZvNGRIRTVUa2tpTENKcmFXUWlPaUpyWlhrdE1TSjlMQ0p3ZFhKd2IzTmxjeUk2V3lKaGRYUm9aVzUwYVdOaGRHbHZiaUpkTENKMGVYQmxJam9pU25OdmJsZGxZa3RsZVRJd01qQWlmVjE5ZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVU5ZVGtKcVNXWk1WR1pPVjBOSE1GUTJNMlZhWW1KRVpGWm9TbUpVVGpndFNtWmxhVXg0ZFcxb1pXNTNJbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsQ1pWWjVSWEJEYjBOUGVYSjZWRGhEU0hsdlFXMWFjVTFDVDFvMFZUWnFjbTFzZFV0MVNqbHhTMHBrWnlJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUW5oa2NIbHlhbWxWU0ZaMWFrTlJXVEJLTWtoQlVGRllabk53V0ZCS1lXbHVWMjFtVjNSTmNGaG5lRkVpZlgwIiwiaXNzIjoiaHR0cHM6XC9cL3NlbGYtaXNzdWVkLm1lXC92Mlwvb3BlbmlkLXZjIiwiZXhwIjoxNjc0Nzg2NDYzLCJpYXQiOjE2NzQ3NzIwNjMsIm5vbmNlIjoiNDAyNTJhZmMtNmE4Mi00YTJlLTkwNWYtZTQxZjEyMmVmNTc1IiwianRpIjoiMGY1ZGFmZWQtMGQ4Mi00M2IxLWFmNzktNDA0NDBlM2YxMzY2IiwiX3ZwX3Rva2VuIjp7InByZXNlbnRhdGlvbl9zdWJtaXNzaW9uIjp7ImlkIjoiOWFmMjRlOGEtYzhmMy00YjlhLTkxNjEtYjcxNWU3N2E2MDEwIiwiZGVmaW5pdGlvbl9pZCI6IjY0OWQ4YzNjLWY1YWMtNDFiZC05YzE5LTU4MDRlYTFiOGZlOSIsImRlc2NyaXB0b3JfbWFwIjpbeyJpZCI6IlZlcmlmaWVkRW1wbG95ZWVWQyIsImZvcm1hdCI6Imp3dF92cCIsInBhdGgiOiIkIiwicGF0aF9uZXN0ZWQiOnsiaWQiOiJWZXJpZmllZEVtcGxveWVlVkMiLCJmb3JtYXQiOiJqd3RfdmMiLCJwYXRoIjoiJC52ZXJpZmlhYmxlQ3JlZGVudGlhbFswXSJ9fV19fX0.jh-SnpQcYPGEb_N5mqKUKCi9pA2OqxXw7BbAYuQwQat69KqpHA0sEZ1tOTOwsVP9UCfjmVg_8z0I_TvKkEkCBA'
+
+ public static presentation_submission = {
+ descriptor_map: [
+ {
+ path: '$',
+ format: 'jwt_vp',
+ path_nested: {
+ path: '$.verifiableCredential[0]',
+ format: 'jwt_vc',
+ id: 'VerifiedEmployeeVC',
+ },
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ definition_id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ id: '9af24e8a-c8f3-4b9a-9161-b715e77a6010',
+ }
+
+ public static idTokenPayload = {
+ sub: 'did:ion:EiAeM6No9kdpos6_ehBUDh4RINY4USDMh-QdWksmsI3WkA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IncwNk9WN2U2blR1cnQ2RzlWcFZYeEl3WW55amZ1cHhlR3lLQlMtYmxxdmciLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFSNGRVQmxqNWNGa3dMdkpTWUYzVExjLV81MWhDX2xZaGxXZkxWZ29seTRRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEcVJyWU5fV3JTakFQdnlFYlJQRVk4WVhPRmNvT0RTZExUTWItM2FKVElGQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQUwyMFdYakpQQW54WWdQY1U5RV9POE1OdHNpQk00QktpaVNwT3ZFTWpVOUEifX0',
+ aud: 'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
+ iss: 'https://self-issued.me/v2/openid-vc',
+ exp: 1674786463,
+ iat: 1674772063,
+ nonce: '40252afc-6a82-4a2e-905f-e41f122ef575',
+ jti: '0f5dafed-0d82-43b1-af79-40440e3f1366',
+ _vp_token: {
+ presentation_submission: {
+ descriptor_map: [
+ {
+ path: '$',
+ format: 'jwt_vp',
+ path_nested: {
+ path: '$.verifiableCredential[0]',
+ format: 'jwt_vc',
+ id: 'VerifiedEmployeeVC',
+ },
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ definition_id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ id: '9af24e8a-c8f3-4b9a-9161-b715e77a6010',
+ },
+ },
+ }
+ public static presentationDef = {
+ input_descriptors: [
+ {
+ schema: [
+ {
+ uri: 'VerifiedEmployee',
+ },
+ ],
+ purpose: 'We need to verify that you have a valid VerifiedEmployee Verifiable Credential.',
+ name: 'VerifiedEmployeeVC',
+ id: 'VerifiedEmployeeVC',
+ },
+ ],
+ id: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ }
+
+ public static requestObjectJwt =
+ 'eyJraWQiOiJkaWQ6aW9uOkVpQldlOVJ0SFQ3VlotSnVmZjhPbm5KQXlGSnRDb2tjWUh4MUNRa0Z0cGw3cHc6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklrTmZUMVZLZUVnMmFVbGpRelpZWkU1b04wcHRReTFVU0ZoQlZtRllibloxT1U5RlJWbzRkSEU1VGtraUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWVRrSnFTV1pNVkdaT1YwTkhNRlEyTTJWYVltSkVaRlpvU21KVVRqZ3RTbVpsYVV4NGRXMW9aVzUzSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbENaVlo1UlhCRGIwTlBlWEo2VkRoRFNIbHZRVzFhY1UxQ1QxbzBWVFpxY20xc2RVdDFTamx4UzBwa1p5SXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFuaGtjSGx5YW1sVlNGWjFha05SV1RCS01raEJVRkZZWm5Od1dGQktZV2x1VjIxbVYzUk5jRmhuZUZFaWZYMCNrZXktMSIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJyZXNwb25zZV90eXBlIjoiaWRfdG9rZW4iLCJub25jZSI6IjQwMjUyYWZjLTZhODItNGEyZS05MDVmLWU0MWYxMjJlZjU3NSIsImNsaWVudF9pZCI6ImRpZDppb246RWlCV2U5UnRIVDdWWi1KdWZmOE9ubkpBeUZKdENva2NZSHgxQ1FrRnRwbDdwdzpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUpyWlhrdE1TSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWtOZlQxVktlRWcyYVVsalF6WllaRTVvTjBwdFF5MVVTRmhCVm1GWWJuWjFPVTlGUlZvNGRIRTVUa2tpTENKcmFXUWlPaUpyWlhrdE1TSjlMQ0p3ZFhKd2IzTmxjeUk2V3lKaGRYUm9aVzUwYVdOaGRHbHZiaUpkTENKMGVYQmxJam9pU25OdmJsZGxZa3RsZVRJd01qQWlmVjE5ZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVU5ZVGtKcVNXWk1WR1pPVjBOSE1GUTJNMlZhWW1KRVpGWm9TbUpVVGpndFNtWmxhVXg0ZFcxb1pXNTNJbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsQ1pWWjVSWEJEYjBOUGVYSjZWRGhEU0hsdlFXMWFjVTFDVDFvMFZUWnFjbTFzZFV0MVNqbHhTMHBrWnlJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUW5oa2NIbHlhbWxWU0ZaMWFrTlJXVEJLTWtoQlVGRllabk53V0ZCS1lXbHVWMjFtVjNSTmNGaG5lRkVpZlgwIiwicmVzcG9uc2VfbW9kZSI6InBvc3QiLCJuYmYiOjE2NzQ3NzIwNjMsInNjb3BlIjoib3BlbmlkIiwiY2xhaW1zIjp7InZwX3Rva2VuIjp7InByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjp7ImlkIjoiNjQ5ZDhjM2MtZjVhYy00MWJkLTljMTktNTgwNGVhMWI4ZmU5IiwiaW5wdXRfZGVzY3JpcHRvcnMiOlt7ImlkIjoiVmVyaWZpZWRFbXBsb3llZVZDIiwibmFtZSI6IlZlcmlmaWVkRW1wbG95ZWVWQyIsInB1cnBvc2UiOiJXZSBuZWVkIHRvIHZlcmlmeSB0aGF0IHlvdSBoYXZlIGEgVmVyaWZpZWRFbXBsb3llZSBWZXJpZmlhYmxlIENyZWRlbnRpYWwuIiwic2NoZW1hIjpbeyJ1cmkiOiJWZXJpZmllZEVtcGxveWVlIn1dfV19fX0sInJlZ2lzdHJhdGlvbiI6eyJjbGllbnRfbmFtZSI6IkV4YW1wbGUgVmVyaWZpZXIiLCJ0b3NfdXJpIjoiaHR0cHM6XC9cL2V4YW1wbGUuY29tXC92ZXJpZmllci1pbmZvIiwibG9nb191cmkiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cL3ZlcmlmaWVyLWljb24ucG5nIiwic3ViamVjdF9zeW50YXhfdHlwZXNfc3VwcG9ydGVkIjpbImRpZDppb24iXSwidnBfZm9ybWF0cyI6eyJqd3RfdnAiOnsiYWxnIjpbIkVkRFNBIiwiRVMyNTZLIl19LCJqd3RfdmMiOnsiYWxnIjpbIkVkRFNBIiwiRVMyNTZLIl19fX0sInN0YXRlIjoiNjQ5ZDhjM2MtZjVhYy00MWJkLTljMTktNTgwNGVhMWI4ZmU5IiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6XC9cL2V4YW1wbGUuY29tXC9zaW9wLXJlc3BvbnNlIiwiZXhwIjoxNjc0Nzc1NjYzLCJpYXQiOjE2NzQ3NzIwNjMsImp0aSI6ImYwZTZkY2Y1LTNmZTYtNDUwNy1hZGM5LWI0OTZkYWYzNDUxMiJ9.znX9h8l8JYoy8BHlnZzRDBEpaAv3hkb_XfUEzG-9eZID3tJjJdrO7PAr4kTay-nxvMhkzNsQg1rCZsjOMbKbBg'
+ public static requestObjectPayload =
+ '\n' +
+ ' {\n' +
+ ' "kid" : "did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0#key-1",\n' +
+ ' "typ" : "JWT",\n' +
+ ' "alg" : "EdDSA"\n' +
+ ' }.\n' +
+ ' {\n' +
+ ' "response_type" : "id_token",\n' +
+ ' "nonce" : "40252afc-6a82-4a2e-905f-e41f122ef575",\n' +
+ ' "client_id" : "did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0",\n' +
+ ' "response_mode" : "post",\n' +
+ ' "nbf" : 1674772063,\n' +
+ ' "scope" : "openid",\n' +
+ ' "claims" : {\n' +
+ ' "vp_token" : {\n' +
+ ' "presentation_definition" : {\n' +
+ ' "input_descriptors" : [ {\n' +
+ ' "schema" : [ {\n' +
+ ' "uri" : "VerifiedEmployee"\n' +
+ ' } ],\n' +
+ ' "purpose" : "We need to verify that you have a valid VerifiedEmployee Verifiable Credential.",\n' +
+ ' "name" : "VerifiedEmployeeVC",\n' +
+ ' "id" : "VerifiedEmployeeVC"\n' +
+ ' } ],\n' +
+ ' "id" : "649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9"\n' +
+ ' }\n' +
+ ' }\n' +
+ ' },\n' +
+ ' "registration" : {\n' +
+ ' "logo_uri" : "https://example.com/verifier-icon.png",\n' +
+ ' "tos_uri" : "https://example.com/verifier-info",\n' +
+ ' "client_name" : "Example Verifier",\n' +
+ ' "vp_formats" : {\n' +
+ ' "jwt_vc" : {\n' +
+ ' "alg" : [ "EdDSA", "ES256K" ]\n' +
+ ' },\n' +
+ ' "jwt_vp" : {\n' +
+ ' "alg" : [ "EdDSA", "ES256K" ]\n' +
+ ' }\n' +
+ ' },\n' +
+ ' "subject_syntax_types_supported" : [ "did:ion" ]\n' +
+ ' },\n' +
+ ' "state" : "649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9",\n' +
+ ' "redirect_uri" : "https://example.com/siop-response",\n' +
+ ' "exp" : 1674775663,\n' +
+ ' "iat" : 1674772063,\n' +
+ ' "jti" : "f0e6dcf5-3fe6-4507-adc9-b496daf34512"\n' +
+ ' }.\n' +
+ ' [withSignature]\n'
+
+ public static authorizationResponsePayload = {
+ state: '649d8c3c-f5ac-41bd-9c19-5804ea1b8fe9',
+ id_token:
+ 'eyJraWQiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCNrZXktMSIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCIsImF1ZCI6ImRpZDppb246RWlCV2U5UnRIVDdWWi1KdWZmOE9ubkpBeUZKdENva2NZSHgxQ1FrRnRwbDdwdzpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUpyWlhrdE1TSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWtOZlQxVktlRWcyYVVsalF6WllaRTVvTjBwdFF5MVVTRmhCVm1GWWJuWjFPVTlGUlZvNGRIRTVUa2tpTENKcmFXUWlPaUpyWlhrdE1TSjlMQ0p3ZFhKd2IzTmxjeUk2V3lKaGRYUm9aVzUwYVdOaGRHbHZiaUpkTENKMGVYQmxJam9pU25OdmJsZGxZa3RsZVRJd01qQWlmVjE5ZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVU5ZVGtKcVNXWk1WR1pPVjBOSE1GUTJNMlZhWW1KRVpGWm9TbUpVVGpndFNtWmxhVXg0ZFcxb1pXNTNJbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsQ1pWWjVSWEJEYjBOUGVYSjZWRGhEU0hsdlFXMWFjVTFDVDFvMFZUWnFjbTFzZFV0MVNqbHhTMHBrWnlJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUW5oa2NIbHlhbWxWU0ZaMWFrTlJXVEJLTWtoQlVGRllabk53V0ZCS1lXbHVWMjFtVjNSTmNGaG5lRkVpZlgwIiwiaXNzIjoiaHR0cHM6XC9cL3NlbGYtaXNzdWVkLm1lXC92Mlwvb3BlbmlkLXZjIiwiZXhwIjoxNjc0Nzg2NDYzLCJpYXQiOjE2NzQ3NzIwNjMsIm5vbmNlIjoiNDAyNTJhZmMtNmE4Mi00YTJlLTkwNWYtZTQxZjEyMmVmNTc1IiwianRpIjoiMGY1ZGFmZWQtMGQ4Mi00M2IxLWFmNzktNDA0NDBlM2YxMzY2IiwiX3ZwX3Rva2VuIjp7InByZXNlbnRhdGlvbl9zdWJtaXNzaW9uIjp7ImlkIjoiOWFmMjRlOGEtYzhmMy00YjlhLTkxNjEtYjcxNWU3N2E2MDEwIiwiZGVmaW5pdGlvbl9pZCI6IjY0OWQ4YzNjLWY1YWMtNDFiZC05YzE5LTU4MDRlYTFiOGZlOSIsImRlc2NyaXB0b3JfbWFwIjpbeyJpZCI6IlZlcmlmaWVkRW1wbG95ZWVWQyIsImZvcm1hdCI6Imp3dF92cCIsInBhdGgiOiIkIiwicGF0aF9uZXN0ZWQiOnsiaWQiOiJWZXJpZmllZEVtcGxveWVlVkMiLCJmb3JtYXQiOiJqd3RfdmMiLCJwYXRoIjoiJC52ZXJpZmlhYmxlQ3JlZGVudGlhbFswXSJ9fV19fX0.jh-SnpQcYPGEb_N5mqKUKCi9pA2OqxXw7BbAYuQwQat69KqpHA0sEZ1tOTOwsVP9UCfjmVg_8z0I_TvKkEkCBA',
+ vp_token:
+ 'eyJraWQiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCNrZXktMSIsImFsZyI6IkVkRFNBIn0.eyJhdWQiOiJkaWQ6aW9uOkVpQldlOVJ0SFQ3VlotSnVmZjhPbm5KQXlGSnRDb2tjWUh4MUNRa0Z0cGw3cHc6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklrTmZUMVZLZUVnMmFVbGpRelpZWkU1b04wcHRReTFVU0ZoQlZtRllibloxT1U5RlJWbzRkSEU1VGtraUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWVRrSnFTV1pNVkdaT1YwTkhNRlEyTTJWYVltSkVaRlpvU21KVVRqZ3RTbVpsYVV4NGRXMW9aVzUzSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbENaVlo1UlhCRGIwTlBlWEo2VkRoRFNIbHZRVzFhY1UxQ1QxbzBWVFpxY20xc2RVdDFTamx4UzBwa1p5SXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFuaGtjSGx5YW1sVlNGWjFha05SV1RCS01raEJVRkZZWm5Od1dGQktZV2x1VjIxbVYzUk5jRmhuZUZFaWZYMCIsImlzcyI6ImRpZDppb246RWlBZU02Tm85a2Rwb3M2X2VoQlVEaDRSSU5ZNFVTRE1oLVFkV2tzbXNJM1drQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUpyWlhrdE1TSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5jd05rOVdOMlUyYmxSMWNuUTJSemxXY0ZaWWVFbDNXVzU1YW1aMWNIaGxSM2xMUWxNdFlteHhkbWNpTENKcmFXUWlPaUpyWlhrdE1TSjlMQ0p3ZFhKd2IzTmxjeUk2V3lKaGRYUm9aVzUwYVdOaGRHbHZiaUpkTENKMGVYQmxJam9pU25OdmJsZGxZa3RsZVRJd01qQWlmVjE5ZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVUZTTkdSVlFteHFOV05HYTNkTWRrcFRXVVl6VkV4akxWODFNV2hEWDJ4WmFHeFhaa3hXWjI5c2VUUlJJbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsRWNWSnlXVTVmVjNKVGFrRlFkbmxGWWxKUVJWazRXVmhQUm1OdlQwUlRaRXhVVFdJdE0yRktWRWxHUVNJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUVV3eU1GZFlha3BRUVc1NFdXZFFZMVU1UlY5UE9FMU9kSE5wUWswMFFrdHBhVk53VDNaRlRXcFZPVUVpZlgwIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6XC9cL3d3dy53My5vcmdcLzIwMThcL2NyZWRlbnRpYWxzXC92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKcmFXUWlPaUprYVdRNmFXOXVPa1ZwUWtGQk9UbFVRV1Y2ZUV0U1l6SjNkWFZDYm5JMGVucEhjMU15V1dOelQwRTBTVkJSVmpCTFdUWTBXR2M2WlhsS2ExcFhlREJaVTBrMlpYbEtkMWxZVW1waFIxWjZTV3B3WW1WNVNtaFpNMUp3WWpJMGFVOXBTbmxhV0VKeldWZE9iRWxwZDJsYVJ6bHFaRmN4YkdKdVVXbFBibk5wWTBoV2FXSkhiR3BUTWxZMVkzbEpObGN6YzJsaFYxRnBUMmxLY2xwWWEzUk5VMGx6U1c1Q01WbHRlSEJaTUhSc1pWVndNMkY1U1RabGVVcHFZMjVaYVU5cFNrWmFSRWt4VGxSRk5VbHBkMmxoTTFJMVNXcHZhVlF3ZEZGSmFYZHBaVU5KTmtsclpHNVhhMlJWV25wb2JGRXlSVE5pUmxsNVQwVXhUVTlWY0ZWaVZVcFdaRzF6TTFKR2JFTlpiVnBUVXpGa1RXRklZekpPVlhCMlRWaE5hVXhEU25KaFYxRnBUMmxLY2xwWWEzUk5VMG81VEVOS2QyUllTbmRpTTA1c1kzbEpObGQ1U21oa1dGSnZXbGMxTUdGWFRtaGtSMngyWW1sS1pFeERTakJsV0VKc1NXcHZhVk51VG5aaWJHUnNXV3QwYkdWVVNYZE5ha0ZwWmxZeE9XWldNSE5KYmxaM1drZEdNRnBWVG5aaVZ6RndaRWN4YkdKdVVXbFBhVXBHWVZWU1MxWXdXakpYVlVvMVVYcGtNbUY2UVRKTldFRjZaRWhaZDJReU9WZFRWR3MxVFZSR1VWUkhaM2RWVm5BMFkxZHdXazB5V1RSTlZrWlNTVzR3YzBsdVRqRmFiVnB3WlVWU2FHUkhSV2xQYm5OcFdrZFdjMlJIUmtsWldFNXZTV3B2YVZKWGJFSllNVkoyVm14T1FscEVRbFJTVjNoUFZUSldjbEV4YXpGVlJGWklXakF4UzFGNU1VMVVWbkJHV1RKYVUxWXlXbkZhUjA1aFdWaEtSbEZUU1hOSmJrcHNXVEk1TWxwWVNqVlJNamwwWWxkc01HSlhWblZrUTBrMlNXdFdjRkpFVGpCYVZGWTBaVVpzYVdWdFNtOWtNSEJaWkVWVmQxb3lkRnBXTTFvelRXeGFNbFpHUWpSTlZUbHNZVEJTVkdOWVpIVmFlbEpVVjIxamFXWllNQ05yWlhrdE1TSXNJblI1Y0NJNklrcFhWQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMC5leUp6ZFdJaU9pSmthV1E2YVc5dU9rVnBRV1ZOTms1dk9XdGtjRzl6Tmw5bGFFSlZSR2cwVWtsT1dUUlZVMFJOYUMxUlpGZHJjMjF6U1ROWGEwRTZaWGxLYTFwWGVEQlpVMGsyWlhsS2QxbFlVbXBoUjFaNlNXcHdZbVY1U21oWk0xSndZakkwYVU5cFNubGFXRUp6V1ZkT2JFbHBkMmxhUnpscVpGY3hiR0p1VVdsUGJuTnBZMGhXYVdKSGJHcFRNbFkxWTNsSk5sY3pjMmxoVjFGcFQybEtjbHBZYTNSTlUwbHpTVzVDTVZsdGVIQlpNSFJzWlZWd00yRjVTVFpsZVVwcVkyNVphVTlwU2taYVJFa3hUbFJGTlVscGQybGhNMUkxU1dwdmFWUXdkRkZKYVhkcFpVTkpOa2x1WTNkT2F6bFhUakpWTW1Kc1VqRmpibEV5VW5wc1YyTkdXbGxsUld3elYxYzFOV0Z0V2pGalNHaHNVak5zVEZGc1RYUlpiWGg0WkcxamFVeERTbkpoVjFGcFQybEtjbHBZYTNSTlUwbzVURU5LZDJSWVNuZGlNMDVzWTNsSk5sZDVTbWhrV0ZKdldsYzFNR0ZYVG1oa1IyeDJZbWxLWkV4RFNqQmxXRUpzU1dwdmFWTnVUblppYkdSc1dXdDBiR1ZVU1hkTmFrRnBabFl4T1daV01ITkpibFozV2tkR01GcFZUblppVnpGd1pFY3hiR0p1VVdsUGFVcEdZVlZHVTA1SFVsWlJiWGh4VGxkT1IyRXpaRTFrYTNCVVYxVlplbFpGZUdwTVZqZ3hUVmRvUkZneWVGcGhSM2hZV210NFYxb3lPWE5sVkZKU1NXNHdjMGx1VGpGYWJWcHdaVVZTYUdSSFJXbFBibk5wV2tkV2MyUkhSa2xaV0U1dlNXcHZhVkpYYkVWalZrcDVWMVUxWmxZelNsUmhhMFpSWkc1c1JsbHNTbEZTVm1zMFYxWm9VRkp0VG5aVU1GSlVXa1Y0VlZSWFNYUk5Na1pMVmtWc1IxRlRTWE5KYmtwc1dUSTVNbHBZU2pWUk1qbDBZbGRzTUdKWFZuVmtRMGsyU1d0V2NGRlZkM2xOUm1SWllXdHdVVkZYTlRSWFYyUlJXVEZWTlZKV09WQlBSVEZQWkVoT2NGRnJNREJSYTNSd1lWWk9kMVF6V2taVVYzQldUMVZGYVdaWU1DSXNJbTVpWmlJNk1UWTNORGMzTWpBMk15d2lhWE56SWpvaVpHbGtPbWx2YmpwRmFVSkJRVGs1VkVGbGVuaExVbU15ZDNWMVFtNXlOSHA2UjNOVE1sbGpjMDlCTkVsUVVWWXdTMWsyTkZobk9tVjVTbXRhVjNnd1dWTkpObVY1U25kWldGSnFZVWRXZWtscWNHSmxlVXBvV1ROU2NHSXlOR2xQYVVwNVdsaENjMWxYVG14SmFYZHBXa2M1YW1SWE1XeGlibEZwVDI1emFXTklWbWxpUjJ4cVV6SldOV041U1RaWE0zTnBZVmRSYVU5cFNuSmFXR3QwVFZOSmMwbHVRakZaYlhod1dUQjBiR1ZWY0ROaGVVazJaWGxLYW1OdVdXbFBhVXBHV2tSSk1VNVVSVFZKYVhkcFlUTlNOVWxxYjJsVU1IUlJTV2wzYVdWRFNUWkphMlJ1VjJ0a1ZWcDZhR3hSTWtVellrWlplVTlGTVUxUFZYQlZZbFZLVm1SdGN6TlNSbXhEV1cxYVUxTXhaRTFoU0dNeVRsVndkazFZVFdsTVEwcHlZVmRSYVU5cFNuSmFXR3QwVFZOS09VeERTbmRrV0VwM1lqTk9iR041U1RaWGVVcG9aRmhTYjFwWE5UQmhWMDVvWkVkc2RtSnBTbVJNUTBvd1pWaENiRWxxYjJsVGJrNTJZbXhrYkZscmRHeGxWRWwzVFdwQmFXWldNVGxtVmpCelNXNVdkMXBIUmpCYVZVNTJZbGN4Y0dSSE1XeGlibEZwVDJsS1JtRlZVa3RXTUZveVYxVktOVkY2WkRKaGVrRXlUVmhCZW1SSVdYZGtNamxYVTFSck5VMVVSbEZVUjJkM1ZWWndOR05YY0ZwTk1sazBUVlpHVWtsdU1ITkpiazR4V20xYWNHVkZVbWhrUjBWcFQyNXphVnBIVm5Oa1IwWkpXVmhPYjBscWIybFNWMnhDV0RGU2RsWnNUa0phUkVKVVVsZDRUMVV5Vm5KUk1Xc3hWVVJXU0Zvd01VdFJlVEZOVkZad1Jsa3lXbE5XTWxweFdrZE9ZVmxZU2taUlUwbHpTVzVLYkZreU9USmFXRW8xVVRJNWRHSlhiREJpVjFaMVpFTkpOa2xyVm5CU1JFNHdXbFJXTkdWR2JHbGxiVXB2WkRCd1dXUkZWWGRhTW5SYVZqTmFNMDFzV2pKV1JrSTBUVlU1YkdFd1VsUmpXR1IxV25wU1ZGZHRZMmxtV0RBaUxDSnBZWFFpT2pFMk56UTNOekl3TmpNc0luWmpJanA3SWtCamIyNTBaWGgwSWpwYkltaDBkSEJ6T2x3dlhDOTNkM2N1ZHpNdWIzSm5YQzh5TURFNFhDOWpjbVZrWlc1MGFXRnNjMXd2ZGpFaVhTd2lkSGx3WlNJNld5SldaWEpwWm1saFlteGxRM0psWkdWdWRHbGhiQ0lzSWxabGNtbG1hV1ZrUlcxd2JHOTVaV1VpWFN3aVkzSmxaR1Z1ZEdsaGJGTjFZbXBsWTNRaU9uc2laR2x6Y0d4aGVVNWhiV1VpT2lKUVlYUWdVMjFwZEdnaUxDSm5hWFpsYms1aGJXVWlPaUpRWVhRaUxDSnFiMkpVYVhSc1pTSTZJbGR2Y210bGNpSXNJbk4xY201aGJXVWlPaUpUYldsMGFDSXNJbkJ5WldabGNuSmxaRXhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0p0WVdsc0lqb2ljR0YwTG5OdGFYUm9RR1Y0WVcxd2JHVXVZMjl0SW4wc0ltTnlaV1JsYm5ScFlXeFRkR0YwZFhNaU9uc2lhV1FpT2lKb2RIUndjenBjTDF3dlpYaGhiWEJzWlM1amIyMWNMMkZ3YVZ3dllYTjBZWFIxYzJ4cGMzUmNMMlJwWkRwcGIyNDZSV2xDUVVFNU9WUkJaWHA0UzFKak1uZDFkVUp1Y2pSNmVrZHpVekpaWTNOUFFUUkpVRkZXTUV0Wk5qUllaMXd2TVNNd0lpd2lkSGx3WlNJNklsSmxkbTlqWVhScGIyNU1hWE4wTWpBeU1WTjBZWFIxY3lJc0luTjBZWFIxYzB4cGMzUkpibVJsZUNJNklqQWlMQ0p6ZEdGMGRYTk1hWE4wUTNKbFpHVnVkR2xoYkNJNkltaDBkSEJ6T2x3dlhDOWxlR0Z0Y0d4bExtTnZiVnd2WVhCcFhDOWhjM1JoZEhWemJHbHpkRnd2Wkdsa09tbHZianBGYVVKQlFUazVWRUZsZW5oTFVtTXlkM1YxUW01eU5IcDZSM05UTWxsamMwOUJORWxRVVZZd1MxazJORmhuWEM4eEluMTlMQ0pxZEdraU9pSmlPREExTW1ZNVl5MDBaamhqTFRRek16QXRZbUpqTVMwME1ETXpZamhsWlRWa05tSWlmUS5WRWlLQ3IzUlZTY1VNRjgxRnhnckdDbGRZeEtJSmM0dWNMWDN6MHhha21sX0dPeG5udndrbzNDNlFxajdKTVVJOUs3dlFVVU1Wakk4MUt4a3RZdDBBUSJdfSwiZXhwIjoxNjc0Nzg2NDYzLCJpYXQiOjE2NzQ3NzIwNjMsIm5vbmNlIjoiNDAyNTJhZmMtNmE4Mi00YTJlLTkwNWYtZTQxZjEyMmVmNTc1IiwianRpIjoiOWNlZGFjODYtYWU1MS00MWQwLWFlNmYtOTI5NjZhNjFlMWY1In0.X9q1amromvW0WuA7bkanc-8BC9axhXh8RhN9i87FluTBzK3SRtKBS0O0alHU3Ii5HixENljCnncTKxi5_rbvDg',
+ }
+
+ public static jwtVCFromVPToken =
+ 'eyJraWQiOiJkaWQ6aW9uOkVpQkFBOTlUQWV6eEtSYzJ3dXVCbnI0enpHc1MyWWNzT0E0SVBRVjBLWTY0WGc6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklrZG5Xa2RVWnpobFEyRTNiRll5T0UxTU9VcFViVUpWZG1zM1JGbENZbVpTUzFkTWFIYzJOVXB2TVhNaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVSS1YwWjJXVUo1UXpkMmF6QTJNWEF6ZEhZd2QyOVdTVGs1TVRGUVRHZ3dVVnA0Y1dwWk0yWTRNVkZSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEJYMVJ2VmxOQlpEQlRSV3hPVTJWclExazFVRFZIWjAxS1F5MU1UVnBGWTJaU1YyWnFaR05hWVhKRlFTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFJETjBaVFY0ZUZsaWVtSm9kMHBZZEVVd1oydFpWM1ozTWxaMlZGQjRNVTlsYTBSVGNYZHVaelJUV21jaWZYMCNrZXktMSIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCIsIm5iZiI6MTY3NDc3MjA2MywiaXNzIjoiZGlkOmlvbjpFaUJBQTk5VEFlenhLUmMyd3V1Qm5yNHp6R3NTMlljc09BNElQUVYwS1k2NFhnOmV5SmtaV3gwWVNJNmV5SndZWFJqYUdWeklqcGJleUpoWTNScGIyNGlPaUp5WlhCc1lXTmxJaXdpWkc5amRXMWxiblFpT25zaWNIVmliR2xqUzJWNWN5STZXM3NpYVdRaU9pSnJaWGt0TVNJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aWVDSTZJa2RuV2tkVVp6aGxRMkUzYkZZeU9FMU1PVXBVYlVKVmRtczNSRmxDWW1aU1MxZE1hSGMyTlVwdk1YTWlMQ0pyYVdRaU9pSnJaWGt0TVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZWMTlmVjBzSW5Wd1pHRjBaVU52YlcxcGRHMWxiblFpT2lKRmFVUktWMFoyV1VKNVF6ZDJhekEyTVhBemRIWXdkMjlXU1RrNU1URlFUR2d3VVZwNGNXcFpNMlk0TVZGUkluMHNJbk4xWm1acGVFUmhkR0VpT25zaVpHVnNkR0ZJWVhOb0lqb2lSV2xCWDFSdlZsTkJaREJUUld4T1UyVnJRMWsxVURWSFowMUtReTFNVFZwRlkyWlNWMlpxWkdOYVlYSkZRU0lzSW5KbFkyOTJaWEo1UTI5dGJXbDBiV1Z1ZENJNklrVnBSRE4wWlRWNGVGbGllbUpvZDBwWWRFVXdaMnRaVjNaM01sWjJWRkI0TVU5bGEwUlRjWGR1WnpSVFdtY2lmWDAiLCJpYXQiOjE2NzQ3NzIwNjMsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWVkRW1wbG95ZWUiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZGlzcGxheU5hbWUiOiJQYXQgU21pdGgiLCJnaXZlbk5hbWUiOiJQYXQiLCJqb2JUaXRsZSI6IldvcmtlciIsInN1cm5hbWUiOiJTbWl0aCIsInByZWZlcnJlZExhbmd1YWdlIjoiZW4tVVMiLCJtYWlsIjoicGF0LnNtaXRoQGV4YW1wbGUuY29tIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cL2FwaVwvYXN0YXR1c2xpc3RcL2RpZDppb246RWlCQUE5OVRBZXp4S1JjMnd1dUJucjR6ekdzUzJZY3NPQTRJUFFWMEtZNjRYZ1wvMSMwIiwidHlwZSI6IlJldm9jYXRpb25MaXN0MjAyMVN0YXR1cyIsInN0YXR1c0xpc3RJbmRleCI6IjAiLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOlwvXC9leGFtcGxlLmNvbVwvYXBpXC9hc3RhdHVzbGlzdFwvZGlkOmlvbjpFaUJBQTk5VEFlenhLUmMyd3V1Qm5yNHp6R3NTMlljc09BNElQUVYwS1k2NFhnXC8xIn19LCJqdGkiOiJiODA1MmY5Yy00ZjhjLTQzMzAtYmJjMS00MDMzYjhlZTVkNmIifQ.VEiKCr3RVScUMF81FxgrGCldYxKIJc4ucLX3z0xakml_GOxnnvwko3C6Qqj7JMUI9K7vQUUMVjI81KxktYt0AQ'
+
+ public static didDocument(did: string, vm: string, publicKeyJwk: JsonWebKey) {
+ return {
+ id: did,
+ '@context': [
+ 'https://www.w3.org/ns/did/v1',
+ {
+ '@base': did,
+ },
+ ],
+ service: [
+ {
+ id: '#linkedin',
+ type: 'linkedin',
+ serviceEndpoint: 'linkedin.com/in/henry-tsai-6b884014',
+ },
+ {
+ id: '#github',
+ type: 'github',
+ serviceEndpoint: 'github.com/thehenrytsai',
+ },
+ ],
+ verificationMethod: [
+ {
+ id: vm,
+ controller: did,
+ type: 'JsonWebKey2020',
+ publicKeyJwk,
+ },
+ ],
+ authentication: [vm],
+ assertionMethod: [vm],
+ }
+ }
+
+ public static mockDID(did: string, vm: string, publickKeyJwk: JsonWebKey) {
+ nock('https://dev.uniresolver.io')
+ .get(`/1.0/identifiers/${did}`)
+ .times(100)
+ .reply(200, TestVectors.didDocument(did, vm, publickKeyJwk))
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts b/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts
new file mode 100644
index 00000000..552a3667
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts
@@ -0,0 +1,264 @@
+import { PresentationDefinitionWithLocation } from '../authorization-response'
+import { PresentationExchange } from '../authorization-response/PresentationExchange'
+import { fetchByReferenceOrUseByValue, removeNullUndefined } from '../helpers'
+import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion'
+import { parseJWT } from '../helpers/jwtUtils'
+import { RequestObject } from '../request-object'
+import {
+ AuthorizationRequestPayload,
+ getJwtVerifierWithContext,
+ getRequestObjectJwtVerifier,
+ PassBy,
+ RequestObjectJwt,
+ RequestObjectPayload,
+ RequestStateInfo,
+ ResponseType,
+ ResponseURIType,
+ RPRegistrationMetadataPayload,
+ Schema,
+ SIOPErrors,
+ SupportedVersion,
+ VerifiedAuthorizationRequest,
+} from '../types'
+
+import { assertValidAuthorizationRequestOpts, assertValidVerifyAuthorizationRequestOpts } from './Opts'
+import { assertValidRPRegistrationMedataPayload, createAuthorizationRequestPayload } from './Payload'
+import { URI } from './URI'
+import { CreateAuthorizationRequestOpts, VerifyAuthorizationRequestOpts } from './types'
+
+export class AuthorizationRequest {
+ private readonly _requestObject?: RequestObject
+ private readonly _payload: AuthorizationRequestPayload
+ private readonly _options: CreateAuthorizationRequestOpts
+ private _uri: URI
+
+ private constructor(payload: AuthorizationRequestPayload, requestObject?: RequestObject, opts?: CreateAuthorizationRequestOpts, uri?: URI) {
+ this._options = opts
+ this._payload = removeNullUndefined(payload)
+ this._requestObject = requestObject
+ this._uri = uri
+ }
+
+ public static async fromUriOrJwt(jwtOrUri: string | URI): Promise {
+ if (!jwtOrUri) {
+ throw Error(SIOPErrors.NO_REQUEST)
+ }
+ return typeof jwtOrUri === 'string' && jwtOrUri.startsWith('ey')
+ ? await AuthorizationRequest.fromJwt(jwtOrUri)
+ : await AuthorizationRequest.fromURI(jwtOrUri)
+ }
+
+ public static async fromPayload(payload: AuthorizationRequestPayload): Promise {
+ if (!payload) {
+ throw Error(SIOPErrors.NO_REQUEST)
+ }
+ const requestObject = await RequestObject.fromAuthorizationRequestPayload(payload)
+ return new AuthorizationRequest(payload, requestObject)
+ }
+
+ public static async fromOpts(opts: CreateAuthorizationRequestOpts, requestObject?: RequestObject): Promise {
+ // todo: response_uri/redirect_uri is not hooked up from opts!
+ if (!opts || !opts.requestObject) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ assertValidAuthorizationRequestOpts(opts)
+
+ const requestObjectArg =
+ opts.requestObject.passBy !== PassBy.NONE ? (requestObject ? requestObject : await RequestObject.fromOpts(opts)) : undefined
+ const requestPayload = opts?.payload ? await createAuthorizationRequestPayload(opts, requestObjectArg) : undefined
+ return new AuthorizationRequest(requestPayload, requestObjectArg, opts)
+ }
+
+ get payload(): AuthorizationRequestPayload {
+ return this._payload
+ }
+
+ get requestObject(): RequestObject | undefined {
+ return this._requestObject
+ }
+
+ get options(): CreateAuthorizationRequestOpts | undefined {
+ return this._options
+ }
+
+ public hasRequestObject(): boolean {
+ return this.requestObject !== undefined
+ }
+
+ public async getSupportedVersion() {
+ if (this.options?.version) {
+ return this.options.version
+ } else if (this._uri?.encodedUri?.startsWith(Schema.OPENID_VC) || this._uri?.scheme?.startsWith(Schema.OPENID_VC)) {
+ return SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1
+ }
+
+ return (await this.getSupportedVersionsFromPayload())[0]
+ }
+
+ public async getSupportedVersionsFromPayload(): Promise {
+ const mergedPayload = { ...this.payload, ...(await this.requestObject?.getPayload()) }
+ return authorizationRequestVersionDiscovery(mergedPayload)
+ }
+
+ async uri(): Promise {
+ if (!this._uri) {
+ this._uri = await URI.fromAuthorizationRequest(this)
+ }
+ return this._uri
+ }
+
+ /**
+ * Verifies a SIOP Request JWT on OP side
+ *
+ * @param opts
+ */
+ async verify(opts: VerifyAuthorizationRequestOpts): Promise {
+ assertValidVerifyAuthorizationRequestOpts(opts)
+
+ let requestObjectPayload: RequestObjectPayload
+
+ const jwt = await this.requestObjectJwt()
+ const parsedJwt = jwt ? parseJWT(jwt) : undefined
+
+ if (parsedJwt) {
+ requestObjectPayload = parsedJwt.payload as RequestObjectPayload
+
+ const jwtVerifier = await getRequestObjectJwtVerifier({ ...parsedJwt, payload: requestObjectPayload }, { type: 'request-object', raw: jwt })
+ const result = await opts.verifyJwtCallback(jwtVerifier, { ...parsedJwt, raw: jwt })
+ if (!result) {
+ throw Error(SIOPErrors.ERROR_VERIFYING_SIGNATURE)
+ }
+
+ // verify the verifier attestation
+ if (requestObjectPayload.client_id_scheme === 'verifier_attestation') {
+ const jwtVerifier = await getJwtVerifierWithContext(parsedJwt, { type: 'verifier-attestation' })
+ const result = await opts.verifyJwtCallback(jwtVerifier, { ...parsedJwt, raw: jwt })
+ if (!result) {
+ throw Error(SIOPErrors.ERROR_VERIFYING_SIGNATURE)
+ }
+ }
+
+ if (this.hasRequestObject() && !this.payload.request_uri) {
+ // Put back the request object as that won't be present yet
+ this.payload.request = jwt
+ }
+ }
+
+ // AuthorizationRequest.assertValidRequestObject(origAuthenticationRequest);
+
+ // We use the orig request for default values, but the JWT payload contains signed request object properties
+ const mergedPayload = { ...this.payload, ...requestObjectPayload }
+ if (opts.state && mergedPayload.state !== opts.state) {
+ throw new Error(`${SIOPErrors.BAD_STATE} payload: ${mergedPayload.state}, supplied: ${opts.state}`)
+ } else if (opts.nonce && mergedPayload.nonce !== opts.nonce) {
+ throw new Error(`${SIOPErrors.BAD_NONCE} payload: ${mergedPayload.nonce}, supplied: ${opts.nonce}`)
+ }
+
+ const registrationPropertyKey = mergedPayload['registration'] || mergedPayload['registration_uri'] ? 'registration' : 'client_metadata'
+ let registrationMetadataPayload: RPRegistrationMetadataPayload
+ if (mergedPayload[registrationPropertyKey] || mergedPayload[`${registrationPropertyKey}_uri`]) {
+ registrationMetadataPayload = await fetchByReferenceOrUseByValue(
+ mergedPayload[`${registrationPropertyKey}_uri`],
+ mergedPayload[registrationPropertyKey],
+ )
+ assertValidRPRegistrationMedataPayload(registrationMetadataPayload)
+ // TODO: We need to do something with the metadata probably
+ }
+ // When the response_uri parameter is present, the redirect_uri Authorization Request parameter MUST NOT be present. If the redirect_uri Authorization Request parameter is present when the Response Mode is direct_post, the Wallet MUST return an invalid_request Authorization Response error.
+ let responseURIType: ResponseURIType
+ let responseURI: string
+ if (mergedPayload.redirect_uri && mergedPayload.response_uri) {
+ throw new Error(`${SIOPErrors.INVALID_REQUEST}, redirect_uri cannot be used together with response_uri`)
+ } else if (mergedPayload.redirect_uri) {
+ responseURIType = 'redirect_uri'
+ responseURI = mergedPayload.redirect_uri
+ } else if (mergedPayload.response_uri) {
+ responseURIType = 'response_uri'
+ responseURI = mergedPayload.response_uri
+ } else if (mergedPayload.client_id_scheme === 'redirect_uri' && mergedPayload.client_id) {
+ responseURIType = 'redirect_uri'
+ responseURI = mergedPayload.client_id
+ } else {
+ throw new Error(`${SIOPErrors.INVALID_REQUEST}, redirect_uri or response_uri is needed`)
+ }
+
+ // TODO: we need to verify somewhere that if response_mode is direct_post, that the response_uri may be present,
+ // BUT not both redirect_uri and response_uri. What is the best place to do this?
+
+ const presentationDefinitions = await PresentationExchange.findValidPresentationDefinitions(mergedPayload, await this.getSupportedVersion())
+ return {
+ jwt,
+ payload: parsedJwt?.payload,
+ issuer: parsedJwt?.payload.iss,
+ responseURIType,
+ responseURI,
+ clientIdScheme: mergedPayload.client_id_scheme,
+ correlationId: opts.correlationId,
+ authorizationRequest: this,
+ verifyOpts: opts,
+ presentationDefinitions,
+ registrationMetadataPayload,
+ requestObject: this.requestObject,
+ authorizationRequestPayload: this.payload,
+ versions: await this.getSupportedVersionsFromPayload(),
+ }
+ }
+
+ static async verify(requestOrUri: string, verifyOpts: VerifyAuthorizationRequestOpts) {
+ assertValidVerifyAuthorizationRequestOpts(verifyOpts)
+ const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestOrUri)
+ return await authorizationRequest.verify(verifyOpts)
+ }
+
+ public async requestObjectJwt(): Promise {
+ return await this.requestObject?.toJwt()
+ }
+
+ private static async fromJwt(jwt: string): Promise {
+ if (!jwt) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const requestObject = await RequestObject.fromJwt(jwt)
+ const payload: AuthorizationRequestPayload = { ...(await requestObject.getPayload()) } as AuthorizationRequestPayload
+ // Although this was a RequestObject we instantiate it as AuthzRequest and then copy in the JWT as the request Object
+ payload.request = jwt
+ return new AuthorizationRequest({ ...payload }, requestObject)
+ }
+
+ private static async fromURI(uri: URI | string): Promise {
+ if (!uri) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const uriObject = typeof uri === 'string' ? await URI.fromUri(uri) : uri
+ const requestObject = await RequestObject.fromJwt(uriObject.requestObjectJwt)
+ return new AuthorizationRequest(uriObject.authorizationRequestPayload, requestObject, undefined, uriObject)
+ }
+
+ public async toStateInfo(): Promise {
+ const requestObject = await this.requestObject.getPayload()
+ return {
+ client_id: this.options.clientMetadata.client_id,
+ iat: requestObject.iat ?? this.payload.iat,
+ nonce: requestObject.nonce ?? this.payload.nonce,
+ state: this.payload.state,
+ }
+ }
+
+ public async containsResponseType(singleType: ResponseType | string): Promise {
+ const responseType: string = await this.getMergedProperty('response_type')
+ return responseType?.includes(singleType) === true
+ }
+
+ public async getMergedProperty(key: string): Promise {
+ const merged = await this.mergedPayloads()
+ return merged[key] as T
+ }
+
+ public async mergedPayloads(): Promise {
+ return { ...this.payload, ...(this.requestObject && (await this.requestObject.getPayload())) }
+ }
+
+ public async getPresentationDefinitions(version?: SupportedVersion): Promise {
+ return await PresentationExchange.findValidPresentationDefinitions(await this.mergedPayloads(), version)
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/Opts.ts b/packages/siop-oid4vp/lib/authorization-request/Opts.ts
new file mode 100644
index 00000000..77182ba0
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/Opts.ts
@@ -0,0 +1,49 @@
+import { assertValidRequestObjectOpts } from '../request-object/Opts'
+import { SIOPErrors, Verification } from '../types'
+
+import { assertValidRequestRegistrationOpts } from './RequestRegistration'
+import { CreateAuthorizationRequestOpts, VerifyAuthorizationRequestOpts } from './types'
+
+export const assertValidVerifyAuthorizationRequestOpts = (opts: VerifyAuthorizationRequestOpts) => {
+ if (!opts || !opts.verification || !opts.verifyJwtCallback) {
+ throw new Error(SIOPErrors.VERIFY_BAD_PARAMS)
+ }
+ if (!opts.correlationId) {
+ throw new Error('No correlation id found')
+ }
+}
+
+export const assertValidAuthorizationRequestOpts = (opts: CreateAuthorizationRequestOpts) => {
+ if (!opts || !opts.requestObject || (!opts.payload && !opts.requestObject.payload) || (opts.payload?.request_uri && !opts.requestObject.payload)) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+ assertValidRequestObjectOpts(opts.requestObject, false)
+ assertValidRequestRegistrationOpts(opts['registration'] ? opts['registration'] : opts.clientMetadata)
+}
+
+export const mergeVerificationOpts = (
+ classOpts: {
+ verification?: Verification
+ },
+ requestOpts: {
+ correlationId: string
+ verification?: Verification
+ },
+) => {
+ const presentationVerificationCallback =
+ requestOpts.verification?.presentationVerificationCallback ?? classOpts.verification?.presentationVerificationCallback
+ const replayRegistry = requestOpts.verification?.replayRegistry ?? classOpts.verification?.replayRegistry
+ return {
+ ...classOpts.verification,
+ ...requestOpts.verification,
+ ...(presentationVerificationCallback && { presentationVerificationCallback }),
+ ...(replayRegistry && { replayRegistry }),
+ revocationOpts: {
+ ...classOpts.verification?.revocationOpts,
+ ...requestOpts.verification?.revocationOpts,
+ revocationVerificationCallback:
+ requestOpts.verification?.revocationOpts?.revocationVerificationCallback ??
+ classOpts?.verification?.revocationOpts?.revocationVerificationCallback,
+ },
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/Payload.ts b/packages/siop-oid4vp/lib/authorization-request/Payload.ts
new file mode 100644
index 00000000..e8f94619
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/Payload.ts
@@ -0,0 +1,85 @@
+import { PEX } from '@sphereon/pex'
+
+import { getNonce, removeNullUndefined } from '../helpers'
+import { RequestObject } from '../request-object'
+import { isTarget, isTargetOrNoTargets } from '../rp/Opts'
+import { RPRegistrationMetadataPayloadSchema } from '../schemas'
+import {
+ AuthorizationRequestPayload,
+ ClaimPayloadVID1,
+ ClientMetadataOpts,
+ PassBy,
+ RPRegistrationMetadataPayload,
+ SIOPErrors,
+ SupportedVersion,
+} from '../types'
+
+import { createRequestRegistration } from './RequestRegistration'
+import { ClaimPayloadOptsVID1, CreateAuthorizationRequestOpts, PropertyTarget } from './types'
+
+export const createPresentationDefinitionClaimsProperties = (opts: ClaimPayloadOptsVID1): ClaimPayloadVID1 => {
+ if (!opts || !opts.vp_token || (!opts.vp_token.presentation_definition && !opts.vp_token.presentation_definition_uri)) {
+ return undefined
+ }
+ const discoveryResult = PEX.definitionVersionDiscovery(opts.vp_token.presentation_definition)
+ if (discoveryResult.error) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID)
+ }
+
+ return {
+ ...(opts.id_token ? { id_token: opts.id_token } : {}),
+ ...((opts.vp_token.presentation_definition || opts.vp_token.presentation_definition_uri) && {
+ vp_token: {
+ ...(!opts.vp_token.presentation_definition_uri && { presentation_definition: opts.vp_token.presentation_definition }),
+ ...(opts.vp_token.presentation_definition_uri && { presentation_definition_uri: opts.vp_token.presentation_definition_uri }),
+ },
+ }),
+ }
+}
+
+export const createAuthorizationRequestPayload = async (
+ opts: CreateAuthorizationRequestOpts,
+ requestObject?: RequestObject,
+): Promise => {
+ const payload = opts.payload
+ const state = payload?.state ?? undefined
+ const nonce = payload?.nonce ? getNonce(state, payload.nonce) : undefined
+ // TODO: if opts['registration] throw Error to get rid of test code using that key
+ const clientMetadata = opts['registration'] ? opts['registration'] : (opts.clientMetadata as ClientMetadataOpts)
+ const registration = await createRequestRegistration(clientMetadata, opts)
+ const claims = opts.version >= SupportedVersion.SIOPv2_ID1 ? opts.payload.claims : createPresentationDefinitionClaimsProperties(opts.payload.claims)
+ const isRequestTarget = isTargetOrNoTargets(PropertyTarget.AUTHORIZATION_REQUEST, opts.requestObject.targets)
+ const isRequestByValue = opts.requestObject.passBy === PassBy.VALUE
+
+ if (isRequestTarget && isRequestByValue && !requestObject) {
+ throw Error(SIOPErrors.NO_JWT)
+ }
+ const request = isRequestByValue ? await requestObject.toJwt() : undefined
+
+ const authRequestPayload = {
+ ...payload,
+ //TODO implement /.well-known/openid-federation support in the OP side to resolve the client_id (URL) and retrieve the metadata
+ ...(isRequestTarget && opts.requestObject.passBy === PassBy.REFERENCE ? { request_uri: opts.requestObject.reference_uri } : {}),
+ ...(isRequestTarget && isRequestByValue && { request }),
+ ...(nonce && { nonce }),
+ ...(state && { state }),
+ ...(registration.payload && isTarget(PropertyTarget.AUTHORIZATION_REQUEST, registration.clientMetadataOpts.targets) ? registration.payload : {}),
+ ...(claims && { claims }),
+ }
+
+ return removeNullUndefined(authRequestPayload)
+}
+
+export const assertValidRPRegistrationMedataPayload = (regObj: RPRegistrationMetadataPayload) => {
+ if (regObj) {
+ const valid = RPRegistrationMetadataPayloadSchema(regObj)
+ if (!valid) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ throw new Error('Registration data validation error: ' + JSON.stringify(RPRegistrationMetadataPayloadSchema.errors))
+ }
+ }
+ if (regObj?.subject_syntax_types_supported && regObj.subject_syntax_types_supported.length == 0) {
+ throw new Error(`${SIOPErrors.VERIFY_BAD_PARAMS}`)
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/RequestRegistration.ts b/packages/siop-oid4vp/lib/authorization-request/RequestRegistration.ts
new file mode 100644
index 00000000..5915201c
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/RequestRegistration.ts
@@ -0,0 +1,101 @@
+import { LanguageTagUtils, removeNullUndefined } from '../helpers'
+import {
+ ClientMetadataOpts,
+ PassBy,
+ RequestClientMetadataPayloadProperties,
+ RequestRegistrationPayloadProperties,
+ RPRegistrationMetadataOpts,
+ RPRegistrationMetadataPayload,
+ SIOPErrors,
+ SupportedVersion,
+} from '../types'
+
+import { CreateAuthorizationRequestOpts } from './types'
+
+/*const ajv = new Ajv({ allowUnionTypes: true, strict: false });
+const validateRPRegistrationMetadata = ajv.compile(RPRegistrationMetadataPayloadSchema);*/
+
+export const assertValidRequestRegistrationOpts = (opts: ClientMetadataOpts) => {
+ if (!opts) {
+ throw new Error(SIOPErrors.REGISTRATION_NOT_SET)
+ } else if (opts.passBy !== PassBy.REFERENCE && opts.passBy !== PassBy.VALUE) {
+ throw new Error(SIOPErrors.REGISTRATION_OBJECT_TYPE_NOT_SET)
+ } else if (opts.passBy === PassBy.REFERENCE && !opts.reference_uri) {
+ throw new Error(SIOPErrors.NO_REFERENCE_URI)
+ }
+}
+
+const createRequestRegistrationPayload = async (
+ opts: ClientMetadataOpts,
+ metadataPayload: RPRegistrationMetadataPayload,
+ version: SupportedVersion,
+): Promise => {
+ assertValidRequestRegistrationOpts(opts)
+
+ if (opts.passBy == PassBy.VALUE) {
+ if (version >= SupportedVersion.SIOPv2_D11.valueOf()) {
+ return { client_metadata: removeNullUndefined(metadataPayload) }
+ } else {
+ return { registration: removeNullUndefined(metadataPayload) }
+ }
+ } else {
+ if (version >= SupportedVersion.SIOPv2_D11.valueOf()) {
+ return {
+ client_metadata_uri: opts.reference_uri,
+ }
+ } else {
+ return {
+ registration_uri: opts.reference_uri,
+ }
+ }
+ }
+}
+
+export const createRequestRegistration = async (
+ clientMetadataOpts: ClientMetadataOpts,
+ createRequestOpts: CreateAuthorizationRequestOpts,
+): Promise<{
+ payload: RequestRegistrationPayloadProperties | RequestClientMetadataPayloadProperties
+ metadata: RPRegistrationMetadataPayload
+ createRequestOpts: CreateAuthorizationRequestOpts
+ clientMetadataOpts: ClientMetadataOpts
+}> => {
+ const metadata = createRPRegistrationMetadataPayload(clientMetadataOpts)
+ const payload = await createRequestRegistrationPayload(clientMetadataOpts, metadata, createRequestOpts.version)
+ return {
+ payload,
+ metadata,
+ createRequestOpts,
+ clientMetadataOpts,
+ }
+}
+
+const createRPRegistrationMetadataPayload = (opts: RPRegistrationMetadataOpts): RPRegistrationMetadataPayload => {
+ const rpRegistrationMetadataPayload = {
+ id_token_signing_alg_values_supported: opts.idTokenSigningAlgValuesSupported,
+ request_object_signing_alg_values_supported: opts.requestObjectSigningAlgValuesSupported,
+ response_types_supported: opts.responseTypesSupported,
+ scopes_supported: opts.scopesSupported,
+ subject_types_supported: opts.subjectTypesSupported,
+ subject_syntax_types_supported: opts.subject_syntax_types_supported || ['did:web:', 'did:ion:'],
+ vp_formats: opts.vpFormatsSupported,
+ client_name: opts.clientName,
+ logo_uri: opts.logo_uri,
+ tos_uri: opts.tos_uri,
+ client_purpose: opts.clientPurpose,
+ client_id: opts.client_id,
+ }
+
+ const languageTagEnabledFieldsNamesMapping = new Map()
+ languageTagEnabledFieldsNamesMapping.set('clientName', 'client_name')
+ languageTagEnabledFieldsNamesMapping.set('clientPurpose', 'client_purpose')
+
+ const languageTaggedFields: Map = LanguageTagUtils.getLanguageTaggedPropertiesMapped(opts, languageTagEnabledFieldsNamesMapping)
+
+ languageTaggedFields.forEach((value: string, key: string) => {
+ const _key = key as keyof typeof rpRegistrationMetadataPayload
+ rpRegistrationMetadataPayload[_key] = value
+ })
+
+ return removeNullUndefined(rpRegistrationMetadataPayload)
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/URI.ts b/packages/siop-oid4vp/lib/authorization-request/URI.ts
new file mode 100644
index 00000000..8e381e94
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/URI.ts
@@ -0,0 +1,273 @@
+import { PresentationExchange } from '../authorization-response/PresentationExchange'
+import { decodeUriAsJson, encodeJsonAsURI, fetchByReferenceOrUseByValue } from '../helpers'
+import { parseJWT } from '../helpers/jwtUtils'
+import { assertValidRequestObjectPayload, RequestObject } from '../request-object'
+import {
+ AuthorizationRequestPayload,
+ AuthorizationRequestURI,
+ ObjectBy,
+ PassBy,
+ RequestObjectJwt,
+ RequestObjectPayload,
+ RPRegistrationMetadataPayload,
+ SIOPErrors,
+ SupportedVersion,
+ UrlEncodingFormat,
+} from '../types'
+
+import { AuthorizationRequest } from './AuthorizationRequest'
+import { assertValidRPRegistrationMedataPayload } from './Payload'
+import { CreateAuthorizationRequestOpts } from './types'
+
+export class URI implements AuthorizationRequestURI {
+ private readonly _scheme: string
+ private readonly _requestObjectJwt: RequestObjectJwt | undefined
+ private readonly _authorizationRequestPayload: AuthorizationRequestPayload
+ private readonly _encodedUri: string // The encoded URI
+ private readonly _encodingFormat: UrlEncodingFormat
+ // private _requestObjectBy: ObjectBy;
+
+ private _registrationMetadataPayload: RPRegistrationMetadataPayload
+
+ private constructor({ scheme, encodedUri, encodingFormat, authorizationRequestPayload, requestObjectJwt }: Partial) {
+ this._scheme = scheme
+ this._encodedUri = encodedUri
+ this._encodingFormat = encodingFormat
+ this._authorizationRequestPayload = authorizationRequestPayload
+ this._requestObjectJwt = requestObjectJwt
+ }
+
+ public static async fromUri(uri: string): Promise {
+ if (!uri) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const { scheme, requestObjectJwt, authorizationRequestPayload, registrationMetadata } = await URI.parseAndResolve(uri)
+ const requestObjectPayload = requestObjectJwt ? (parseJWT(requestObjectJwt).payload as RequestObjectPayload) : undefined
+ if (requestObjectPayload) {
+ assertValidRequestObjectPayload(requestObjectPayload)
+ }
+
+ const result = new URI({
+ scheme,
+ encodingFormat: UrlEncodingFormat.FORM_URL_ENCODED,
+ encodedUri: uri,
+ authorizationRequestPayload,
+ requestObjectJwt,
+ })
+ result._registrationMetadataPayload = registrationMetadata
+ return result
+ }
+
+ /**
+ * Create a signed URL encoded URI with a signed SIOP request token on RP side
+ *
+ * @param opts Request input data to build a SIOP Request Token
+ * @remarks This method is used to generate a SIOP request with info provided by the RP.
+ * First it generates the request payload and then it creates the signed JWT, which is returned as a URI
+ *
+ * Normally you will want to use this method to create the request.
+ */
+ public static async fromOpts(opts: CreateAuthorizationRequestOpts): Promise {
+ if (!opts) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const authorizationRequest = await AuthorizationRequest.fromOpts(opts)
+ return await URI.fromAuthorizationRequest(authorizationRequest)
+ }
+
+ public async toAuthorizationRequest(): Promise {
+ return await AuthorizationRequest.fromUriOrJwt(this)
+ }
+
+ get requestObjectBy(): ObjectBy {
+ if (!this.requestObjectJwt) {
+ return { passBy: PassBy.NONE }
+ }
+ if (this.authorizationRequestPayload.request_uri) {
+ return { passBy: PassBy.REFERENCE, reference_uri: this.authorizationRequestPayload.request_uri }
+ }
+ return { passBy: PassBy.VALUE }
+ }
+
+ get metadataObjectBy(): ObjectBy {
+ if (!this.authorizationRequestPayload.registration_uri && !this.authorizationRequestPayload.registration) {
+ return { passBy: PassBy.NONE }
+ }
+ if (this.authorizationRequestPayload.registration_uri) {
+ return { passBy: PassBy.REFERENCE, reference_uri: this.authorizationRequestPayload.registration_uri }
+ }
+ return { passBy: PassBy.VALUE }
+ }
+
+ /**
+ * Create a URI from the request object, typically you will want to use the createURI version!
+ *
+ * @remarks This method is used to generate a SIOP request Object with info provided by the RP.
+ * First it generates the request object payload, and then it creates the signed JWT.
+ *
+ * Please note that the createURI method allows you to differentiate between OAuth2 and OpenID parameters that become
+ * part of the URI and which become part of the Request Object. If you generate a URI based upon the result of this method,
+ * the URI will be constructed based on the Request Object only!
+ */
+ static async fromRequestObject(requestObject: RequestObject): Promise {
+ if (!requestObject) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ return await URI.fromAuthorizationRequestPayload(requestObject.options, await AuthorizationRequest.fromUriOrJwt(await requestObject.toJwt()))
+ }
+
+ static async fromAuthorizationRequest(authorizationRequest: AuthorizationRequest): Promise {
+ if (!authorizationRequest) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ return await URI.fromAuthorizationRequestPayload(
+ {
+ ...authorizationRequest.options.requestObject,
+ version: authorizationRequest.options.version,
+ uriScheme: authorizationRequest.options.uriScheme,
+ },
+ authorizationRequest.payload,
+ authorizationRequest.requestObject,
+ )
+ }
+
+ /**
+ * Creates an URI Request
+ * @param opts Options to define the Uri Request
+ * @param authorizationRequestPayload
+ *
+ */
+ private static async fromAuthorizationRequestPayload(
+ opts: { uriScheme?: string; passBy: PassBy; reference_uri?: string; version?: SupportedVersion },
+ authorizationRequestPayload: AuthorizationRequestPayload,
+ requestObject?: RequestObject,
+ ): Promise {
+ if (!authorizationRequestPayload) {
+ if (!requestObject || !(await requestObject.getPayload())) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ authorizationRequestPayload = {} // No auth request payload, so the eventual URI will contain a `request_uri` or `request` value only
+ }
+
+ const isJwt = typeof authorizationRequestPayload === 'string'
+ const requestObjectJwt = requestObject
+ ? await requestObject.toJwt()
+ : typeof authorizationRequestPayload === 'string'
+ ? authorizationRequestPayload
+ : authorizationRequestPayload.request
+
+ if (isJwt && (!requestObjectJwt || !requestObjectJwt.startsWith('ey'))) {
+ throw Error(SIOPErrors.NO_JWT)
+ }
+ const requestObjectPayload: RequestObjectPayload = requestObjectJwt ? (parseJWT(requestObjectJwt).payload as RequestObjectPayload) : undefined
+
+ if (requestObjectPayload) {
+ // Only used to validate if the request object contains presentation definition(s)
+ await PresentationExchange.findValidPresentationDefinitions({ ...authorizationRequestPayload, ...requestObjectPayload })
+
+ assertValidRequestObjectPayload(requestObjectPayload)
+ if (requestObjectPayload.registration) {
+ assertValidRPRegistrationMedataPayload(requestObjectPayload.registration)
+ }
+ }
+ const uniformAuthorizationRequestPayload: AuthorizationRequestPayload =
+ typeof authorizationRequestPayload === 'string' ? (requestObjectPayload as AuthorizationRequestPayload) : authorizationRequestPayload
+ if (!uniformAuthorizationRequestPayload) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const type = opts.passBy
+ if (!type) {
+ throw new Error(SIOPErrors.REQUEST_OBJECT_TYPE_NOT_SET)
+ }
+ const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestObjectJwt)
+
+ let scheme
+ if (opts.uriScheme) {
+ scheme = opts.uriScheme.endsWith('://') ? opts.uriScheme : `${opts.uriScheme}://`
+ } else if (opts.version) {
+ if (opts.version === SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1) {
+ scheme = 'openid-vc://'
+ } else {
+ scheme = 'openid://'
+ }
+ } else {
+ try {
+ scheme = (await authorizationRequest.getSupportedVersion()) === SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1 ? 'openid-vc://' : 'openid://'
+ } catch (error: unknown) {
+ scheme = 'openid://'
+ }
+ }
+
+ if (type === PassBy.REFERENCE) {
+ if (!opts.reference_uri) {
+ throw new Error(SIOPErrors.NO_REFERENCE_URI)
+ }
+ uniformAuthorizationRequestPayload.request_uri = opts.reference_uri
+ delete uniformAuthorizationRequestPayload.request
+ } else if (type === PassBy.VALUE) {
+ uniformAuthorizationRequestPayload.request = requestObjectJwt
+ delete uniformAuthorizationRequestPayload.request_uri
+ }
+ return new URI({
+ scheme,
+ encodedUri: `${scheme}?${encodeJsonAsURI(uniformAuthorizationRequestPayload)}`,
+ encodingFormat: UrlEncodingFormat.FORM_URL_ENCODED,
+ // requestObjectBy: opts.requestBy,
+ authorizationRequestPayload: uniformAuthorizationRequestPayload,
+ requestObjectJwt: requestObjectJwt,
+ })
+ }
+
+ /**
+ * Create a Authentication Request Payload from a URI string
+ *
+ * @param uri
+ */
+ public static parse(uri: string): { scheme: string; authorizationRequestPayload: AuthorizationRequestPayload } {
+ if (!uri) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ // We strip the uri scheme before passing it to the decode function
+ const scheme: string = uri.match(/^([a-zA-Z][a-zA-Z0-9-_]*:\/\/)/g)[0]
+ const authorizationRequestPayload = decodeUriAsJson(uri) as AuthorizationRequestPayload
+ return { scheme, authorizationRequestPayload }
+ }
+
+ public static async parseAndResolve(uri: string) {
+ if (!uri) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const { authorizationRequestPayload, scheme } = this.parse(uri)
+ const requestObjectJwt = await fetchByReferenceOrUseByValue(authorizationRequestPayload.request_uri, authorizationRequestPayload.request, true)
+ const registrationMetadata: RPRegistrationMetadataPayload = await fetchByReferenceOrUseByValue(
+ authorizationRequestPayload['client_metadata_uri'] ?? authorizationRequestPayload['registration_uri'],
+ authorizationRequestPayload['client_metadata'] ?? authorizationRequestPayload['registration'],
+ )
+ assertValidRPRegistrationMedataPayload(registrationMetadata)
+ return { scheme, authorizationRequestPayload, requestObjectJwt, registrationMetadata }
+ }
+
+ get encodingFormat(): UrlEncodingFormat {
+ return this._encodingFormat
+ }
+
+ get encodedUri(): string {
+ return this._encodedUri
+ }
+
+ get authorizationRequestPayload(): AuthorizationRequestPayload {
+ return this._authorizationRequestPayload
+ }
+
+ get requestObjectJwt(): RequestObjectJwt | undefined {
+ return this._requestObjectJwt
+ }
+
+ get scheme(): string {
+ return this._scheme
+ }
+
+ get registrationMetadataPayload(): RPRegistrationMetadataPayload {
+ return this._registrationMetadataPayload
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-request/index.ts b/packages/siop-oid4vp/lib/authorization-request/index.ts
new file mode 100644
index 00000000..a1908da9
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/index.ts
@@ -0,0 +1,4 @@
+export * from './AuthorizationRequest'
+export * from './types'
+export * from './Payload'
+export * from './URI'
diff --git a/packages/siop-oid4vp/lib/authorization-request/types.ts b/packages/siop-oid4vp/lib/authorization-request/types.ts
new file mode 100644
index 00000000..e87cc73a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-request/types.ts
@@ -0,0 +1,100 @@
+import { Hasher } from '@sphereon/ssi-types'
+
+import { PresentationDefinitionPayloadOpts } from '../authorization-response'
+import { RequestObjectOpts } from '../request-object'
+import {
+ ClientIdScheme,
+ ClientMetadataOpts,
+ IdTokenClaimPayload,
+ ResponseMode,
+ ResponseType,
+ Schema,
+ Scope,
+ SigningAlgo,
+ SubjectType,
+ SupportedVersion,
+ Verification,
+} from '../types'
+import { VerifyJwtCallback } from '../types/JwtVerifier'
+
+export interface ClaimPayloadOptsVID1 extends ClaimPayloadCommonOpts {
+ id_token?: IdTokenClaimPayload
+ vp_token?: PresentationDefinitionPayloadOpts
+}
+
+export interface ClaimPayloadCommonOpts {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [x: string]: any
+}
+
+export interface AuthorizationRequestPayloadOpts extends Partial> {
+ request_uri?: string // The Request object payload if provided by reference
+ // Note we do not list the request property here, as the lib constructs the value, and we do not want people to pass that value in directly as it will lead to people not understanding why things fail
+}
+export interface RequestObjectPayloadOpts {
+ scope: string // from openid-connect-self-issued-v2-1_0-ID1
+ response_type: string // from openid-connect-self-issued-v2-1_0-ID1
+ client_id: string // from openid-connect-self-issued-v2-1_0-ID1
+ client_id_scheme?: ClientIdScheme
+ redirect_uri?: string // from openid-connect-self-issued-v2-1_0-ID1
+ response_uri?: string // from openid-connect-self-issued-v2-1_0-D18 // either response uri or redirect uri
+ id_token_hint?: string // from openid-connect-self-issued-v2-1_0-ID1
+ claims?: CT // from openid-connect-self-issued-v2-1_0-ID1 look at https://openid.net/specs/openid-connect-core-1_0.html#Claims
+ nonce?: string // An optional nonce, will be generated if not provided
+ state?: string // An optional state, will be generated if not provided
+ authorization_endpoint?: string
+ response_mode?: ResponseMode // How the URI should be returned. This is not being used by the library itself, allows an implementor to make a decision
+ response_types_supported?: ResponseType[] | ResponseType
+ scopes_supported?: Scope[] | Scope
+ subject_types_supported?: SubjectType[] | SubjectType
+ request_object_signing_alg_values_supported?: SigningAlgo[] | SigningAlgo
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [x: string]: any
+}
+
+interface AuthorizationRequestCommonOpts {
+ // Yes, this includes common payload properties both at the payload level as well as in the requestObject.payload property. That is to support OAuth2 with or without a signed OpenID requestObject
+
+ version: SupportedVersion
+ clientMetadata?: ClientMetadataOpts // this maps to 'registration' for older SIOPv2 specs! OPTIONAL. This parameter is used by the RP to provide information about itself to a Self-Issued OP that would normally be provided to an OP during Dynamic RP Registration, as specified in {#rp-registration-parameter}.
+ payload?: AuthorizationRequestPayloadOpts
+ requestObject: RequestObjectOpts
+ uriScheme?: Schema | string // Use a custom scheme for the URI. By default openid:// will be used
+}
+
+export type AuthorizationRequestOptsVID1 = AuthorizationRequestCommonOpts
+
+export interface AuthorizationRequestOptsVD11 extends AuthorizationRequestCommonOpts {
+ idTokenType?: string // OPTIONAL. Space-separated string that specifies the types of ID token the RP wants to obtain, with the values appearing in order of preference. The allowed individual values are subject_signed and attester_signed (see Section 8.2). The default value is attester_signed.
+}
+
+export type CreateAuthorizationRequestOpts = AuthorizationRequestOptsVID1 | AuthorizationRequestOptsVD11
+
+export interface VerifyAuthorizationRequestOpts {
+ correlationId: string
+ verification: Verification
+ verifyJwtCallback: VerifyJwtCallback
+ nonce?: string // If provided the nonce in the request needs to match
+ state?: string // If provided the state in the request needs to match
+ supportedVersions?: SupportedVersion[]
+ hasher?: Hasher
+}
+
+/**
+ * Determines where a property will end up. Methods that support this argument are optional. If you do not provide any value it will default to all targets.
+ */
+export enum PropertyTarget {
+ // The property will end up in the oAuth2 authorization request
+ AUTHORIZATION_REQUEST = 'authorization-request',
+
+ // OpenID Request Object (the JWT)
+ REQUEST_OBJECT = 'request-object',
+}
+
+export type PropertyTargets = PropertyTarget | PropertyTarget[]
+
+export interface RequestPropertyWithTargets {
+ targets?: PropertyTargets
+ propertyValue: T
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts
new file mode 100644
index 00000000..6ff45798
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts
@@ -0,0 +1,227 @@
+import { Hasher } from '@sphereon/ssi-types'
+
+import { AuthorizationRequest, VerifyAuthorizationRequestOpts } from '../authorization-request'
+import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-request/Opts'
+import { IDToken } from '../id-token'
+import { AuthorizationResponsePayload, ResponseType, SIOPErrors, VerifiedAuthorizationRequest, VerifiedAuthorizationResponse } from '../types'
+
+import { assertValidVerifiablePresentations, extractPresentationsFromAuthorizationResponse, verifyPresentations } from './OpenID4VP'
+import { assertValidResponseOpts } from './Opts'
+import { createResponsePayload } from './Payload'
+import { AuthorizationResponseOpts, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from './types'
+
+export class AuthorizationResponse {
+ private readonly _authorizationRequest?: AuthorizationRequest | undefined
+ // private _requestObject?: RequestObject | undefined
+ private readonly _idToken?: IDToken
+ private readonly _payload: AuthorizationResponsePayload
+
+ private readonly _options?: AuthorizationResponseOpts
+
+ private constructor({
+ authorizationResponsePayload,
+ idToken,
+ responseOpts,
+ authorizationRequest,
+ }: {
+ authorizationResponsePayload: AuthorizationResponsePayload
+ idToken?: IDToken
+ responseOpts?: AuthorizationResponseOpts
+ authorizationRequest?: AuthorizationRequest
+ }) {
+ this._authorizationRequest = authorizationRequest
+ this._options = responseOpts
+ this._idToken = idToken
+ this._payload = authorizationResponsePayload
+ }
+
+ /**
+ * Creates a SIOP Response Object
+ *
+ * @param requestObject
+ * @param responseOpts
+ * @param verifyOpts
+ */
+ static async fromRequestObject(
+ requestObject: string,
+ responseOpts: AuthorizationResponseOpts,
+ verifyOpts: VerifyAuthorizationRequestOpts,
+ ): Promise {
+ assertValidVerifyAuthorizationRequestOpts(verifyOpts)
+ assertValidResponseOpts(responseOpts)
+ if (!requestObject || !requestObject.startsWith('ey')) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+ const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestObject)
+ return AuthorizationResponse.fromAuthorizationRequest(authorizationRequest, responseOpts, verifyOpts)
+ }
+
+ static async fromPayload(
+ authorizationResponsePayload: AuthorizationResponsePayload,
+ responseOpts?: AuthorizationResponseOpts,
+ ): Promise {
+ if (!authorizationResponsePayload) {
+ throw new Error(SIOPErrors.NO_RESPONSE)
+ }
+
+ if (responseOpts) {
+ assertValidResponseOpts(responseOpts)
+ }
+ const idToken = authorizationResponsePayload.id_token ? await IDToken.fromIDToken(authorizationResponsePayload.id_token) : undefined
+ return new AuthorizationResponse({
+ authorizationResponsePayload,
+ idToken,
+ responseOpts,
+ })
+ }
+
+ static async fromAuthorizationRequest(
+ authorizationRequest: AuthorizationRequest,
+ responseOpts: AuthorizationResponseOpts,
+ verifyOpts: VerifyAuthorizationRequestOpts,
+ ): Promise {
+ assertValidResponseOpts(responseOpts)
+ if (!authorizationRequest) {
+ throw new Error(SIOPErrors.NO_REQUEST)
+ }
+ const verifiedRequest = await authorizationRequest.verify(verifyOpts)
+ return await AuthorizationResponse.fromVerifiedAuthorizationRequest(verifiedRequest, responseOpts, verifyOpts)
+ }
+
+ static async fromVerifiedAuthorizationRequest(
+ verifiedAuthorizationRequest: VerifiedAuthorizationRequest,
+ responseOpts: AuthorizationResponseOpts,
+ verifyOpts: VerifyAuthorizationRequestOpts,
+ ): Promise {
+ assertValidResponseOpts(responseOpts)
+ if (!verifiedAuthorizationRequest) {
+ throw new Error(SIOPErrors.NO_REQUEST)
+ }
+
+ const authorizationRequest = verifiedAuthorizationRequest.authorizationRequest
+
+ // const merged = verifiedAuthorizationRequest.authorizationRequest.requestObject, verifiedAuthorizationRequest.requestObject);
+ // const presentationDefinitions = await PresentationExchange.findValidPresentationDefinitions(merged, await authorizationRequest.getSupportedVersion());
+ const presentationDefinitions = JSON.parse(
+ JSON.stringify(verifiedAuthorizationRequest.presentationDefinitions),
+ ) as PresentationDefinitionWithLocation[]
+ const wantsIdToken = await authorizationRequest.containsResponseType(ResponseType.ID_TOKEN)
+ const hasVpToken = await authorizationRequest.containsResponseType(ResponseType.VP_TOKEN)
+
+ const idToken = wantsIdToken ? await IDToken.fromVerifiedAuthorizationRequest(verifiedAuthorizationRequest, responseOpts) : undefined
+ const idTokenPayload = idToken ? await idToken.payload() : undefined
+ const authorizationResponsePayload = await createResponsePayload(authorizationRequest, responseOpts, idTokenPayload)
+ const response = new AuthorizationResponse({
+ authorizationResponsePayload,
+ idToken,
+ responseOpts,
+ authorizationRequest,
+ })
+
+ if (hasVpToken) {
+ const wrappedPresentations = await extractPresentationsFromAuthorizationResponse(response, {
+ hasher: verifyOpts.hasher,
+ })
+
+ await assertValidVerifiablePresentations({
+ presentationDefinitions,
+ presentations: wrappedPresentations,
+ verificationCallback: verifyOpts.verification.presentationVerificationCallback,
+ opts: {
+ ...responseOpts.presentationExchange,
+ hasher: verifyOpts.hasher,
+ },
+ })
+ }
+
+ return response
+ }
+
+ public async verify(verifyOpts: VerifyAuthorizationResponseOpts): Promise {
+ // Merge payloads checks for inconsistencies in properties which are present in both the auth request and request object
+ const merged = await this.mergedPayloads({
+ consistencyCheck: true,
+ hasher: verifyOpts.hasher,
+ })
+ if (verifyOpts.state && merged.state !== verifyOpts.state) {
+ throw Error(SIOPErrors.BAD_STATE)
+ }
+
+ const verifiedIdToken = await this.idToken?.verify(verifyOpts)
+ const oid4vp = await verifyPresentations(this, verifyOpts)
+
+ // Gather all nonces
+ const allNonces = new Set()
+ if (oid4vp) allNonces.add(oid4vp.nonce)
+ if (verifiedIdToken) allNonces.add(verifiedIdToken.payload.nonce)
+ if (merged.nonce) allNonces.add(merged.nonce)
+
+ const firstNonce = Array.from(allNonces)[0]
+ if (allNonces.size !== 1 || typeof firstNonce !== 'string') {
+ throw new Error('both id token and VPs in vp token if present must have a nonce, and all nonces must be the same')
+ }
+ if (verifyOpts.nonce && firstNonce !== verifyOpts.nonce) {
+ throw Error(SIOPErrors.BAD_NONCE)
+ }
+
+ const state = merged.state ?? verifiedIdToken?.payload.state
+ if (!state) {
+ throw Error('State is required')
+ }
+
+ return {
+ authorizationResponse: this,
+ verifyOpts,
+ nonce: firstNonce,
+ state,
+ correlationId: verifyOpts.correlationId,
+ ...(this.idToken && { idToken: verifiedIdToken }),
+ ...(oid4vp && { oid4vpSubmission: oid4vp }),
+ }
+ }
+
+ get authorizationRequest(): AuthorizationRequest | undefined {
+ return this._authorizationRequest
+ }
+
+ get payload(): AuthorizationResponsePayload {
+ return this._payload
+ }
+
+ get options(): AuthorizationResponseOpts | undefined {
+ return this._options
+ }
+
+ get idToken(): IDToken | undefined {
+ return this._idToken
+ }
+
+ public async getMergedProperty(key: string, opts?: { consistencyCheck?: boolean; hasher?: Hasher }): Promise {
+ const merged = await this.mergedPayloads(opts)
+ return merged[key] as T
+ }
+
+ public async mergedPayloads(opts?: { consistencyCheck?: boolean; hasher?: Hasher }): Promise {
+ let nonce: string | undefined = this._payload.nonce
+ if (this._payload?.vp_token) {
+ const presentations = await extractPresentationsFromAuthorizationResponse(this, opts)
+ // We do not verify them, as that is done elsewhere. So we simply can take the first nonce
+ if (!nonce) {
+ nonce = presentations[0].decoded.nonce
+ }
+ }
+ const idTokenPayload = await this.idToken?.payload()
+ if (opts?.consistencyCheck !== false && idTokenPayload) {
+ Object.entries(idTokenPayload).forEach((entry) => {
+ if (typeof entry[0] === 'string' && this.payload[entry[0]] && this.payload[entry[0]] !== entry[1]) {
+ throw Error(`Mismatch in Authorization Request and Request object value for ${entry[0]}`)
+ }
+ })
+ }
+ if (!nonce && this._idToken) {
+ nonce = (await this._idToken.payload()).nonce
+ }
+
+ return { ...this.payload, ...idTokenPayload, nonce }
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts
new file mode 100644
index 00000000..043e4b02
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts
@@ -0,0 +1,306 @@
+import { IPresentationDefinition, PEX } from '@sphereon/pex'
+import { Format } from '@sphereon/pex-models'
+import {
+ CredentialMapper,
+ Hasher,
+ IVerifiablePresentation,
+ PresentationSubmission,
+ W3CVerifiablePresentation,
+ WrappedVerifiablePresentation,
+} from '@sphereon/ssi-types'
+
+import { AuthorizationRequest } from '../authorization-request'
+import { verifyRevocation } from '../helpers'
+import { parseJWT } from '../helpers/jwtUtils'
+import {
+ AuthorizationResponsePayload,
+ IDTokenPayload,
+ ResponseType,
+ RevocationVerification,
+ SIOPErrors,
+ SupportedVersion,
+ VerifiedOpenID4VPSubmission,
+} from '../types'
+
+import { AuthorizationResponse } from './AuthorizationResponse'
+import { PresentationExchange } from './PresentationExchange'
+import {
+ AuthorizationResponseOpts,
+ PresentationDefinitionWithLocation,
+ PresentationVerificationCallback,
+ VerifyAuthorizationResponseOpts,
+ VPTokenLocation,
+} from './types'
+
+function extractNonceFromWrappedVerifiablePresentation(wrappedVp: WrappedVerifiablePresentation): string | undefined {
+ // SD-JWT uses kb-jwt for the nonce
+ if (CredentialMapper.isWrappedSdJwtVerifiablePresentation(wrappedVp)) {
+ // TODO: replace this once `kbJwt.payload` is available on the decoded sd-jwt (pr in ssi-sdk)
+ // If it doesn't end with ~, it contains a kbJwt
+ if (!wrappedVp.presentation.compactSdJwtVc.endsWith('~')) {
+ const kbJwt = wrappedVp.presentation.compactSdJwtVc.split('~').pop()
+
+ const { payload } = parseJWT(kbJwt)
+
+ return payload.nonce
+ }
+
+ // No kb-jwt means no nonce (error will be handled later)
+ return undefined
+ }
+
+ if (wrappedVp.format === 'jwt_vp') {
+ return wrappedVp.decoded.nonce
+ }
+
+ // For LDP-VP a challenge is also fine
+ if (wrappedVp.format === 'ldp_vp') {
+ const w3cPresentation = wrappedVp.decoded as IVerifiablePresentation
+ const proof = Array.isArray(w3cPresentation.proof) ? w3cPresentation.proof[0] : w3cPresentation.proof
+
+ return proof.nonce ?? proof.challenge
+ }
+
+ return undefined
+}
+
+export const verifyPresentations = async (
+ authorizationResponse: AuthorizationResponse,
+ verifyOpts: VerifyAuthorizationResponseOpts,
+): Promise => {
+ const presentations = await extractPresentationsFromAuthorizationResponse(authorizationResponse, { hasher: verifyOpts.hasher })
+ const presentationDefinitions = verifyOpts.presentationDefinitions
+ ? Array.isArray(verifyOpts.presentationDefinitions)
+ ? verifyOpts.presentationDefinitions
+ : [verifyOpts.presentationDefinitions]
+ : []
+ let idPayload: IDTokenPayload | undefined
+ if (authorizationResponse.idToken) {
+ idPayload = await authorizationResponse.idToken.payload()
+ }
+ // todo: Probably wise to check against request for the location of the submission_data
+ const presentationSubmission = idPayload?._vp_token?.presentation_submission ?? authorizationResponse.payload.presentation_submission
+
+ await assertValidVerifiablePresentations({
+ presentationDefinitions,
+ presentations,
+ verificationCallback: verifyOpts.verification.presentationVerificationCallback,
+ opts: {
+ presentationSubmission,
+ restrictToFormats: verifyOpts.restrictToFormats,
+ restrictToDIDMethods: verifyOpts.restrictToDIDMethods,
+ hasher: verifyOpts.hasher,
+ },
+ })
+
+ // If there are no presentations, and the `assertValidVerifiablePresentations` did not fail
+ // it means there's no oid4vp response and also not requested
+ if (presentations.length === 0) {
+ return null
+ }
+
+ const nonces = new Set(presentations.map(extractNonceFromWrappedVerifiablePresentation))
+ if (presentations.length > 0 && nonces.size !== 1) {
+ throw Error(`${nonces.size} nonce values found for ${presentations.length}. Should be 1`)
+ }
+
+ // Nonce may be undefined
+ const nonce = Array.from(nonces)[0]
+ if (typeof nonce !== 'string') {
+ throw new Error('Expected all presentations to contain a nonce value')
+ }
+
+ const revocationVerification = verifyOpts.verification?.revocationOpts
+ ? verifyOpts.verification.revocationOpts.revocationVerification
+ : RevocationVerification.IF_PRESENT
+ if (revocationVerification !== RevocationVerification.NEVER) {
+ if (!verifyOpts.verification.revocationOpts?.revocationVerificationCallback) {
+ throw Error(`Please provide a revocation callback as revocation checking of credentials and presentations is not disabled`)
+ }
+ for (const vp of presentations) {
+ await verifyRevocation(vp, verifyOpts.verification.revocationOpts.revocationVerificationCallback, revocationVerification)
+ }
+ }
+ return { nonce, presentations, presentationDefinitions, submissionData: presentationSubmission }
+}
+
+export const extractPresentationsFromAuthorizationResponse = async (
+ response: AuthorizationResponse,
+ opts?: { hasher?: Hasher },
+): Promise => {
+ const wrappedVerifiablePresentations: WrappedVerifiablePresentation[] = []
+ if (response.payload.vp_token) {
+ const presentations = Array.isArray(response.payload.vp_token) ? response.payload.vp_token : [response.payload.vp_token]
+ for (const presentation of presentations) {
+ wrappedVerifiablePresentations.push(CredentialMapper.toWrappedVerifiablePresentation(presentation, { hasher: opts?.hasher }))
+ }
+ }
+ return wrappedVerifiablePresentations
+}
+
+export const createPresentationSubmission = async (
+ verifiablePresentations: W3CVerifiablePresentation[],
+ opts?: { presentationDefinitions: (PresentationDefinitionWithLocation | IPresentationDefinition)[] },
+): Promise => {
+ let submission_data: PresentationSubmission
+ for (const verifiablePresentation of verifiablePresentations) {
+ const wrappedPresentation = CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation)
+
+ let submission: PresentationSubmission | undefined =
+ CredentialMapper.isWrappedW3CVerifiablePresentation(wrappedPresentation) &&
+ (wrappedPresentation.presentation.presentation_submission ??
+ wrappedPresentation.decoded.presentation_submission ??
+ (typeof wrappedPresentation.original !== 'string' && wrappedPresentation.original.presentation_submission))
+ if (typeof submission === 'string') {
+ submission = JSON.parse(submission)
+ }
+ if (!submission && opts?.presentationDefinitions) {
+ console.log(`No submission_data in VPs and not provided. Will try to deduce, but it is better to create the submission data beforehand`)
+ for (const definitionOpt of opts.presentationDefinitions) {
+ const definition = 'definition' in definitionOpt ? definitionOpt.definition : definitionOpt
+ const result = new PEX().evaluatePresentation(definition, wrappedPresentation.original, { generatePresentationSubmission: true })
+ if (result.areRequiredCredentialsPresent) {
+ submission = result.value
+ break
+ }
+ }
+ }
+ if (!submission) {
+ throw Error('Verifiable Presentation has no submission_data, it has not been provided separately, and could also not be deduced')
+ }
+ // let's merge all submission data into one object
+ if (!submission_data) {
+ submission_data = submission
+ } else {
+ // We are pushing multiple descriptors into one submission_data, as it seems this is something which is assumed in OpenID4VP, but not supported in Presentation Exchange (a single VP always has a single submission_data)
+ Array.isArray(submission_data.descriptor_map)
+ ? submission_data.descriptor_map.push(...submission.descriptor_map)
+ : (submission_data.descriptor_map = [...submission.descriptor_map])
+ }
+ }
+ if (typeof submission_data === 'string') {
+ submission_data = JSON.parse(submission_data)
+ }
+ return submission_data
+}
+
+export const putPresentationSubmissionInLocation = async (
+ authorizationRequest: AuthorizationRequest,
+ responsePayload: AuthorizationResponsePayload,
+ resOpts: AuthorizationResponseOpts,
+ idTokenPayload?: IDTokenPayload,
+): Promise => {
+ const version = await authorizationRequest.getSupportedVersion()
+ const idTokenType = await authorizationRequest.containsResponseType(ResponseType.ID_TOKEN)
+ const authResponseType = await authorizationRequest.containsResponseType(ResponseType.VP_TOKEN)
+ // const requestPayload = await authorizationRequest.mergedPayloads();
+ if (!resOpts.presentationExchange) {
+ return
+ } else if (resOpts.presentationExchange.verifiablePresentations.length === 0) {
+ throw Error('Presentation Exchange options set, but no verifiable presentations provided')
+ }
+ if (
+ !resOpts.presentationExchange.presentationSubmission &&
+ (!resOpts.presentationExchange.verifiablePresentations || resOpts.presentationExchange.verifiablePresentations.length === 0)
+ ) {
+ throw Error(`Either a presentationSubmission or verifiable presentations are needed at this point`)
+ }
+ const submissionData =
+ resOpts.presentationExchange.presentationSubmission ??
+ (await createPresentationSubmission(resOpts.presentationExchange.verifiablePresentations, {
+ presentationDefinitions: await authorizationRequest.getPresentationDefinitions(),
+ }))
+
+ const location =
+ resOpts.presentationExchange?.vpTokenLocation ??
+ (idTokenType && version < SupportedVersion.SIOPv2_D11 ? VPTokenLocation.ID_TOKEN : VPTokenLocation.AUTHORIZATION_RESPONSE)
+
+ switch (location) {
+ case VPTokenLocation.TOKEN_RESPONSE: {
+ throw Error('Token response for VP token is not supported yet')
+ }
+ case VPTokenLocation.ID_TOKEN: {
+ if (!idTokenPayload) {
+ throw Error('Cannot place submission data _vp_token in id token if no id token is present')
+ } else if (version >= SupportedVersion.SIOPv2_D11) {
+ throw Error(`This version of the OpenID4VP spec does not allow to store the vp submission data in the ID token`)
+ } else if (!idTokenType) {
+ throw Error(`Cannot place vp token in ID token as the RP didn't provide an "openid" scope in the request`)
+ }
+ if (idTokenPayload._vp_token?.presentation_submission) {
+ if (submissionData !== idTokenPayload._vp_token.presentation_submission) {
+ throw Error('Different submission data was provided as an option, but exising submission data was already present in the id token')
+ }
+ } else {
+ if (!idTokenPayload._vp_token) {
+ idTokenPayload._vp_token = { presentation_submission: submissionData }
+ } else {
+ idTokenPayload._vp_token.presentation_submission = submissionData
+ }
+ }
+ break
+ }
+ case VPTokenLocation.AUTHORIZATION_RESPONSE: {
+ if (!authResponseType) {
+ throw Error('Cannot place vp token in Authorization Response as there is no vp_token scope in the auth request')
+ }
+ if (responsePayload.presentation_submission) {
+ if (submissionData !== responsePayload.presentation_submission) {
+ throw Error(
+ 'Different submission data was provided as an option, but exising submission data was already present in the authorization response',
+ )
+ }
+ } else {
+ responsePayload.presentation_submission = submissionData
+ }
+ }
+ }
+
+ responsePayload.vp_token =
+ resOpts.presentationExchange?.verifiablePresentations.length === 1
+ ? resOpts.presentationExchange.verifiablePresentations[0]
+ : resOpts.presentationExchange?.verifiablePresentations
+}
+
+export const assertValidVerifiablePresentations = async (args: {
+ presentationDefinitions: PresentationDefinitionWithLocation[]
+ presentations: WrappedVerifiablePresentation[]
+ verificationCallback: PresentationVerificationCallback
+ opts?: {
+ limitDisclosureSignatureSuites?: string[]
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+ presentationSubmission?: PresentationSubmission
+ hasher?: Hasher
+ }
+}) => {
+ if (
+ (!args.presentationDefinitions || args.presentationDefinitions.filter((a) => a.definition).length === 0) &&
+ (!args.presentations || (Array.isArray(args.presentations) && args.presentations.filter((vp) => vp.presentation).length === 0))
+ ) {
+ return
+ }
+ PresentationExchange.assertValidPresentationDefinitionWithLocations(args.presentationDefinitions)
+ const presentationsWithFormat = args.presentations
+
+ if (args.presentationDefinitions && args.presentationDefinitions.length && (!presentationsWithFormat || presentationsWithFormat.length === 0)) {
+ throw new Error(SIOPErrors.AUTH_REQUEST_EXPECTS_VP)
+ } else if (
+ (!args.presentationDefinitions || args.presentationDefinitions.length === 0) &&
+ presentationsWithFormat &&
+ presentationsWithFormat.length > 0
+ ) {
+ throw new Error(SIOPErrors.AUTH_REQUEST_DOESNT_EXPECT_VP)
+ } else if (args.presentationDefinitions && presentationsWithFormat && args.presentationDefinitions.length != presentationsWithFormat.length) {
+ throw new Error(SIOPErrors.AUTH_REQUEST_EXPECTS_VP)
+ } else if (args.presentationDefinitions && !args.opts.presentationSubmission) {
+ throw new Error(`No presentation submission present. Please use presentationSubmission opt argument!`)
+ } else if (args.presentationDefinitions && presentationsWithFormat) {
+ await PresentationExchange.validatePresentationsAgainstDefinitions(
+ args.presentationDefinitions,
+ presentationsWithFormat,
+ args.verificationCallback,
+ args.opts,
+ )
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/Opts.ts b/packages/siop-oid4vp/lib/authorization-response/Opts.ts
new file mode 100644
index 00000000..24f62035
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/Opts.ts
@@ -0,0 +1,15 @@
+import { SIOPErrors } from '../types'
+
+import { AuthorizationResponseOpts, VerifyAuthorizationResponseOpts } from './types'
+
+export const assertValidResponseOpts = (opts: AuthorizationResponseOpts) => {
+ if (!opts?.createJwtCallback) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+}
+
+export const assertValidVerifyOpts = (opts: VerifyAuthorizationResponseOpts) => {
+ if (!opts?.verification || !opts.verifyJwtCallback) {
+ throw new Error(SIOPErrors.VERIFY_BAD_PARAMS)
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/Payload.ts b/packages/siop-oid4vp/lib/authorization-response/Payload.ts
new file mode 100644
index 00000000..83997190
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/Payload.ts
@@ -0,0 +1,58 @@
+import { AuthorizationRequest } from '../authorization-request'
+import { IDToken } from '../id-token'
+import { RequestObject } from '../request-object'
+import { AuthorizationRequestPayload, AuthorizationResponsePayload, IDTokenPayload, SIOPErrors } from '../types'
+
+import { putPresentationSubmissionInLocation } from './OpenID4VP'
+import { assertValidResponseOpts } from './Opts'
+import { AuthorizationResponseOpts } from './types'
+
+export const createResponsePayload = async (
+ authorizationRequest: AuthorizationRequest,
+ responseOpts: AuthorizationResponseOpts,
+ idTokenPayload?: IDTokenPayload,
+): Promise => {
+ await assertValidResponseOpts(responseOpts)
+ if (!authorizationRequest) {
+ throw new Error(SIOPErrors.NO_REQUEST)
+ }
+
+ // If state was in request, it must be in response
+ const state: string | undefined = await authorizationRequest.getMergedProperty('state')
+
+ const responsePayload: AuthorizationResponsePayload = {
+ ...(responseOpts.accessToken && { access_token: responseOpts.accessToken }),
+ ...(responseOpts.tokenType && { token_type: responseOpts.tokenType }),
+ ...(responseOpts.refreshToken && { refresh_token: responseOpts.refreshToken }),
+ expires_in: responseOpts.expiresIn || 3600,
+ state,
+ }
+
+ // vp tokens
+ await putPresentationSubmissionInLocation(authorizationRequest, responsePayload, responseOpts, idTokenPayload)
+ if (idTokenPayload) {
+ const idToken = await IDToken.fromIDTokenPayload(idTokenPayload, responseOpts)
+ responsePayload.id_token = await idToken.jwt(responseOpts.jwtIssuer)
+ }
+
+ return responsePayload
+}
+
+/**
+ * Properties can be in oAUth2 and OpenID (JWT) style. If they are in both the OpenID prop takes precedence as they are signed.
+ * @param payload
+ * @param requestObject
+ */
+export const mergeOAuth2AndOpenIdInRequestPayload = async (
+ payload: AuthorizationRequestPayload,
+ requestObject?: RequestObject,
+): Promise => {
+ const payloadCopy = JSON.parse(JSON.stringify(payload))
+
+ const requestObj = requestObject ? requestObject : await RequestObject.fromAuthorizationRequestPayload(payload)
+ if (!requestObj) {
+ return payloadCopy
+ }
+ const requestObjectPayload = await requestObj.getPayload()
+ return { ...payloadCopy, ...requestObjectPayload }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts
new file mode 100644
index 00000000..2aa96631
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts
@@ -0,0 +1,408 @@
+import {
+ EvaluationResults,
+ IPresentationDefinition,
+ KeyEncoding,
+ PEX,
+ PresentationSubmissionLocation,
+ SelectResults,
+ Status,
+ VerifiablePresentationFromOpts,
+ VerifiablePresentationResult,
+} from '@sphereon/pex'
+import { Format, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models'
+import {
+ CredentialMapper,
+ Hasher,
+ IProofPurpose,
+ IProofType,
+ OriginalVerifiableCredential,
+ OriginalVerifiablePresentation,
+ W3CVerifiablePresentation,
+ WrappedVerifiablePresentation,
+} from '@sphereon/ssi-types'
+
+import { extractDataFromPath, getWithUrl } from '../helpers'
+import { AuthorizationRequestPayload, SIOPErrors, SupportedVersion } from '../types'
+
+import {
+ PresentationDefinitionLocation,
+ PresentationDefinitionWithLocation,
+ PresentationSignCallback,
+ PresentationVerificationCallback,
+} from './types'
+
+export class PresentationExchange {
+ readonly pex: PEX
+ readonly allVerifiableCredentials: OriginalVerifiableCredential[]
+ readonly allDIDs
+
+ constructor(opts: { allDIDs?: string[]; allVerifiableCredentials: OriginalVerifiableCredential[]; hasher?: Hasher }) {
+ this.allDIDs = opts.allDIDs
+ this.allVerifiableCredentials = opts.allVerifiableCredentials
+ this.pex = new PEX({ hasher: opts.hasher })
+ }
+
+ /**
+ * Construct presentation submission from selected credentials
+ * @param presentationDefinition payload object received by the OP from the RP
+ * @param selectedCredentials
+ * @param presentationSignCallback
+ * @param options
+ */
+ public async createVerifiablePresentation(
+ presentationDefinition: IPresentationDefinition,
+ selectedCredentials: OriginalVerifiableCredential[],
+ presentationSignCallback: PresentationSignCallback,
+ // options2?: { nonce?: string; domain?: string, proofType?: IProofType, verificationMethod?: string, signatureKeyEncoding?: KeyEncoding },
+ options?: VerifiablePresentationFromOpts,
+ ): Promise {
+ if (!presentationDefinition) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID)
+ }
+
+ const signOptions: VerifiablePresentationFromOpts = {
+ ...options,
+ presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
+ proofOptions: {
+ ...options.proofOptions,
+ proofPurpose: options?.proofOptions?.proofPurpose ?? IProofPurpose.authentication,
+ type: options?.proofOptions?.type ?? IProofType.EcdsaSecp256k1Signature2019,
+ /* challenge: options?.proofOptions?.challenge,
+ domain: options?.proofOptions?.domain,*/
+ },
+ signatureOptions: {
+ ...options.signatureOptions,
+ // verificationMethod: options?.signatureOptions?.verificationMethod,
+ keyEncoding: options?.signatureOptions?.keyEncoding ?? KeyEncoding.Hex,
+ },
+ }
+
+ return await this.pex.verifiablePresentationFrom(presentationDefinition, selectedCredentials, presentationSignCallback, signOptions)
+ }
+
+ /**
+ * This method will be called from the OP when we are certain that we have a
+ * PresentationDefinition object inside our requestPayload
+ * Finds a set of `VerifiableCredential`s from a list supplied to this class during construction,
+ * matching presentationDefinition object found in the requestPayload
+ * if requestPayload doesn't contain any valid presentationDefinition throws an error
+ * if PEX library returns any error in the process, throws the error
+ * returns the SelectResults object if successful
+ * @param presentationDefinition object received by the OP from the RP
+ * @param opts
+ */
+ public async selectVerifiableCredentialsForSubmission(
+ presentationDefinition: IPresentationDefinition,
+ opts?: {
+ holderDIDs?: string[]
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+ },
+ ): Promise {
+ if (!presentationDefinition) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID)
+ } else if (!this.allVerifiableCredentials || this.allVerifiableCredentials.length == 0) {
+ throw new Error(`${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, no VCs were provided`)
+ }
+ const selectResults: SelectResults = this.pex.selectFrom(presentationDefinition, this.allVerifiableCredentials, {
+ ...opts,
+ holderDIDs: opts?.holderDIDs ?? this.allDIDs,
+ // fixme limited disclosure
+ limitDisclosureSignatureSuites: [],
+ })
+ if (selectResults.areRequiredCredentialsPresent === Status.ERROR) {
+ throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(selectResults.errors)}`)
+ }
+ return selectResults
+ }
+
+ /**
+ * validatePresentationAgainstDefinition function is called mainly by the RP
+ * after receiving the VP from the OP
+ * @param presentationDefinition object containing PD
+ * @param verifiablePresentation
+ * @param opts
+ */
+ public static async validatePresentationAgainstDefinition(
+ presentationDefinition: IPresentationDefinition,
+ verifiablePresentation: OriginalVerifiablePresentation | WrappedVerifiablePresentation,
+ opts?: {
+ limitDisclosureSignatureSuites?: string[]
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+ presentationSubmission?: PresentationSubmission
+ hasher?: Hasher
+ },
+ ): Promise {
+ const wvp: WrappedVerifiablePresentation =
+ typeof verifiablePresentation === 'object' && 'original' in verifiablePresentation
+ ? (verifiablePresentation as WrappedVerifiablePresentation)
+ : CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation as OriginalVerifiablePresentation)
+ if (!presentationDefinition) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID)
+ } else if (
+ !wvp ||
+ !wvp.presentation ||
+ (CredentialMapper.isWrappedW3CVerifiablePresentation(wvp) &&
+ (!wvp.presentation.verifiableCredential || wvp.presentation.verifiableCredential.length === 0))
+ ) {
+ throw new Error(SIOPErrors.NO_VERIFIABLE_PRESENTATION_NO_CREDENTIALS)
+ }
+ // console.log(`Presentation (validate): ${JSON.stringify(verifiablePresentation)}`);
+ const evaluationResults: EvaluationResults = new PEX({ hasher: opts?.hasher }).evaluatePresentation(presentationDefinition, wvp.original, opts)
+ if (evaluationResults.errors.length) {
+ throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(evaluationResults.errors)}`)
+ }
+ return evaluationResults
+ }
+
+ public static assertValidPresentationSubmission(presentationSubmission: PresentationSubmission) {
+ const validationResult = PEX.validateSubmission(presentationSubmission)
+ if (validationResult[0].message != 'ok') {
+ throw new Error(`${SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult[0])}`)
+ }
+ }
+
+ /**
+ * Finds a valid PresentationDefinition inside the given AuthenticationRequestPayload
+ * throws exception if the PresentationDefinition is not valid
+ * returns null if no property named "presentation_definition" is found
+ * returns a PresentationDefinition if a valid instance found
+ * @param authorizationRequestPayload object that can have a presentation_definition inside
+ * @param version
+ */
+ public static async findValidPresentationDefinitions(
+ authorizationRequestPayload: AuthorizationRequestPayload,
+ version?: SupportedVersion,
+ ): Promise {
+ const allDefinitions: PresentationDefinitionWithLocation[] = []
+
+ async function extractDefinitionFromVPToken() {
+ const vpTokens: PresentationDefinitionV1[] | PresentationDefinitionV2[] = extractDataFromPath(
+ authorizationRequestPayload,
+ '$..vp_token.presentation_definition',
+ ).map((d) => d.value)
+ const vpTokenRefs = extractDataFromPath(authorizationRequestPayload, '$..vp_token.presentation_definition_uri')
+ if (vpTokens && vpTokens.length && vpTokenRefs && vpTokenRefs.length) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_BY_REF_AND_VALUE_NON_EXCLUSIVE)
+ }
+ if (vpTokens && vpTokens.length) {
+ vpTokens.forEach((vpToken: PresentationDefinitionV1 | PresentationDefinitionV2) => {
+ if (allDefinitions.find((value) => value.definition.id === vpToken.id)) {
+ console.log(
+ `Warning. We encountered presentation definition with id ${vpToken.id}, more then once whilst processing! Make sure your payload is valid!`,
+ )
+ return
+ }
+ PresentationExchange.assertValidPresentationDefinition(vpToken)
+ allDefinitions.push({
+ definition: vpToken,
+ location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN,
+ version,
+ })
+ })
+ } else if (vpTokenRefs && vpTokenRefs.length) {
+ for (const vpTokenRef of vpTokenRefs) {
+ const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = (await getWithUrl(vpTokenRef.value)) as unknown as
+ | PresentationDefinitionV1
+ | PresentationDefinitionV2
+ if (allDefinitions.find((value) => value.definition.id === pd.id)) {
+ console.log(
+ `Warning. We encountered presentation definition with id ${pd.id}, more then once whilst processing! Make sure your payload is valid!`,
+ )
+ return
+ }
+ PresentationExchange.assertValidPresentationDefinition(pd)
+ allDefinitions.push({ definition: pd, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, version })
+ }
+ }
+ }
+
+ function addSingleToplevelPDToPDs(definition: IPresentationDefinition, version?: SupportedVersion): void {
+ if (allDefinitions.find((value) => value.definition.id === definition.id)) {
+ console.log(
+ `Warning. We encountered presentation definition with id ${definition.id}, more then once whilst processing! Make sure your payload is valid!`,
+ )
+ return
+ }
+ PresentationExchange.assertValidPresentationDefinition(definition)
+ allDefinitions.push({
+ definition,
+ location: PresentationDefinitionLocation.TOPLEVEL_PRESENTATION_DEF,
+ version,
+ })
+ }
+
+ async function extractDefinitionFromTopLevelDefinitionProperty(version?: SupportedVersion) {
+ const definitions = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition')
+ const definitionsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition[*]')
+ const definitionRefs = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri')
+ const definitionRefsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri[*]')
+ const hasPD = (definitions && definitions.length > 0) || (definitionsFromList && definitionsFromList.length > 0)
+ const hasPdRef = (definitionRefs && definitionRefs.length > 0) || (definitionRefsFromList && definitionRefsFromList.length > 0)
+ if (hasPD && hasPdRef) {
+ throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_BY_REF_AND_VALUE_NON_EXCLUSIVE)
+ }
+ if (definitions && definitions.length > 0) {
+ definitions.forEach((definition) => {
+ addSingleToplevelPDToPDs(definition.value, version)
+ })
+ } else if (definitionsFromList && definitionsFromList.length > 0) {
+ definitionsFromList.forEach((definition) => {
+ addSingleToplevelPDToPDs(definition.value, version)
+ })
+ } else if (definitionRefs && definitionRefs.length > 0) {
+ for (const definitionRef of definitionRefs) {
+ const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = await getWithUrl(definitionRef.value)
+ addSingleToplevelPDToPDs(pd, version)
+ }
+ } else if (definitionsFromList && definitionRefsFromList.length > 0) {
+ for (const definitionRef of definitionRefsFromList) {
+ const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = await getWithUrl(definitionRef.value)
+ addSingleToplevelPDToPDs(pd, version)
+ }
+ }
+ }
+
+ if (authorizationRequestPayload) {
+ if (!version || version < SupportedVersion.SIOPv2_D11) {
+ await extractDefinitionFromVPToken()
+ }
+ await extractDefinitionFromTopLevelDefinitionProperty()
+ }
+ return allDefinitions
+ }
+
+ public static assertValidPresentationDefinitionWithLocations(definitionsWithLocations: PresentationDefinitionWithLocation[]) {
+ if (definitionsWithLocations && definitionsWithLocations.length > 0) {
+ definitionsWithLocations.forEach((definitionWithLocation) =>
+ PresentationExchange.assertValidPresentationDefinition(definitionWithLocation.definition),
+ )
+ }
+ }
+
+ private static assertValidPresentationDefinition(presentationDefinition: IPresentationDefinition) {
+ const validationResult = PEX.validateDefinition(presentationDefinition)
+ if (validationResult[0].message != 'ok') {
+ throw new Error(`${SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID}`)
+ }
+ }
+
+ static async validatePresentationsAgainstDefinitions(
+ definitions: PresentationDefinitionWithLocation[],
+ vpPayloads: WrappedVerifiablePresentation[],
+ verifyPresentationCallback: PresentationVerificationCallback | undefined,
+ opts?: {
+ limitDisclosureSignatureSuites?: string[]
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+ presentationSubmission?: PresentationSubmission
+ hasher?: Hasher
+ },
+ ) {
+ if (!definitions || !vpPayloads || !definitions.length || definitions.length !== vpPayloads.length) {
+ throw new Error(SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD)
+ }
+ await Promise.all(
+ definitions.map(
+ async (pd) => await PresentationExchange.validatePresentationsAgainstDefinition(pd.definition, vpPayloads, verifyPresentationCallback, opts),
+ ),
+ )
+ }
+
+ private static async validatePresentationsAgainstDefinition(
+ definition: IPresentationDefinition,
+ vpPayloads: WrappedVerifiablePresentation[],
+ verifyPresentationCallback: PresentationVerificationCallback | undefined,
+ opts?: {
+ limitDisclosureSignatureSuites?: string[]
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+ presentationSubmission?: PresentationSubmission
+ hasher?: Hasher
+ },
+ ) {
+ const pex = new PEX({ hasher: opts?.hasher })
+
+ async function filterOutCorrectPresentation() {
+ //TODO: add support for multiple VPs here
+ const matchingVps = vpPayloads.map(async (vpw: WrappedVerifiablePresentation): Promise => {
+ const presentationSubmission =
+ opts?.presentationSubmission ??
+ (CredentialMapper.isWrappedW3CVerifiablePresentation(vpw) ? vpw.presentation.presentation_submission : undefined)
+ const presentation = vpw.presentation
+ if (!definition) {
+ throw new Error(SIOPErrors.NO_PRESENTATION_SUBMISSION)
+ } else if (
+ !vpw.presentation ||
+ (CredentialMapper.isWrappedW3CVerifiablePresentation(vpw) &&
+ (!vpw.presentation.verifiableCredential || vpw.presentation.verifiableCredential.length === 0))
+ ) {
+ throw new Error(SIOPErrors.NO_VERIFIABLE_PRESENTATION_NO_CREDENTIALS)
+ }
+ // The verifyPresentationCallback function is mandatory for RP only,
+ // So the behavior here is to bypass it if not present
+ if (verifyPresentationCallback) {
+ try {
+ const verificationResult = await verifyPresentationCallback(vpw.original as W3CVerifiablePresentation, presentationSubmission)
+ if (!verificationResult.verified) {
+ throw new Error(
+ SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID + verificationResult.reason ? `. ${verificationResult.reason}` : '',
+ )
+ }
+ } catch (error: unknown) {
+ throw new Error(SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID)
+ }
+ }
+ // console.log(`Presentation (filter): ${JSON.stringify(presentation)}`);
+
+ const evaluationResults = pex.evaluatePresentation(definition, vpw.original, {
+ ...opts,
+ presentationSubmission,
+ })
+ const submission = evaluationResults.value
+ if (!presentation || !submission) {
+ throw new Error(SIOPErrors.NO_PRESENTATION_SUBMISSION)
+ }
+
+ // No match
+ if (submission.definition_id !== definition.id) {
+ return undefined
+ }
+
+ return vpw
+ })
+
+ // Wait for all results to finish and filter out undefined (no match) values
+ return (await Promise.all(matchingVps)).filter((vp) => vp !== undefined)
+ }
+
+ const checkedPresentations = await filterOutCorrectPresentation()
+
+ if (checkedPresentations.length !== 1) {
+ throw new Error(`${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}`)
+ }
+ const checkedPresentation = checkedPresentations[0]
+ const presentation = checkedPresentation.presentation
+ // console.log(`Presentation (checked): ${JSON.stringify(checkedPresentation.presentation)}`);
+ if (
+ !checkedPresentation.presentation ||
+ (CredentialMapper.isWrappedW3CVerifiablePresentation(checkedPresentation) &&
+ (!checkedPresentation.presentation.verifiableCredential || checkedPresentation.presentation.verifiableCredential.length === 0))
+ ) {
+ throw new Error(SIOPErrors.NO_VERIFIABLE_PRESENTATION_NO_CREDENTIALS)
+ }
+ const presentationSubmission =
+ opts?.presentationSubmission ?? (CredentialMapper.isW3cPresentation(presentation) ? presentation.presentation_submission : undefined)
+ const evaluationResults = pex.evaluatePresentation(definition, checkedPresentation.original, {
+ ...opts,
+ presentationSubmission,
+ })
+ PresentationExchange.assertValidPresentationSubmission(evaluationResults.value)
+ await PresentationExchange.validatePresentationAgainstDefinition(definition, checkedPresentation, {
+ ...opts,
+ presentationSubmission,
+ hasher: opts?.hasher,
+ })
+ }
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/ResponseRegistration.ts b/packages/siop-oid4vp/lib/authorization-response/ResponseRegistration.ts
new file mode 100644
index 00000000..2959c9d8
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/ResponseRegistration.ts
@@ -0,0 +1,65 @@
+import { LanguageTagUtils, removeNullUndefined } from '../helpers'
+import { DiscoveryMetadataOpts, DiscoveryMetadataPayload, ResponseIss, ResponseType, Schema, Scope, SigningAlgo, SubjectType } from '../types'
+
+export const createDiscoveryMetadataPayload = (opts: DiscoveryMetadataOpts): DiscoveryMetadataPayload => {
+ const discoveryMetadataPayload: DiscoveryMetadataPayload = {
+ authorization_endpoint: opts.authorizationEndpoint || Schema.OPENID,
+ issuer: opts.issuer ?? ResponseIss.SELF_ISSUED_V2,
+ response_types_supported: opts.responseTypesSupported ?? ResponseType.ID_TOKEN,
+ scopes_supported: opts?.scopesSupported || [Scope.OPENID],
+ subject_types_supported: opts?.subjectTypesSupported || [SubjectType.PAIRWISE],
+ id_token_signing_alg_values_supported: opts?.idTokenSigningAlgValuesSupported || [SigningAlgo.ES256K, SigningAlgo.EDDSA],
+ request_object_signing_alg_values_supported: opts.requestObjectSigningAlgValuesSupported || [SigningAlgo.ES256K, SigningAlgo.EDDSA],
+ subject_syntax_types_supported: opts.subject_syntax_types_supported,
+ client_id: opts.client_id,
+ redirect_uris: opts.redirectUris,
+ client_name: opts.clientName,
+ token_endpoint_auth_method: opts.tokenEndpointAuthMethod,
+ application_type: opts.applicationType,
+ response_types: opts.responseTypes,
+ grant_types: opts.grantTypes,
+ vp_formats: opts.vpFormats,
+ token_endpoint: opts.tokenEndpoint,
+ userinfo_endpoint: opts.userinfoEndpoint,
+ jwks_uri: opts.jwksUri,
+ registration_endpoint: opts.registrationEndpoint,
+ response_modes_supported: opts.responseModesSupported,
+ grant_types_supported: opts.grantTypesSupported,
+ acr_values_supported: opts.acrValuesSupported,
+ id_token_encryption_alg_values_supported: opts.idTokenEncryptionAlgValuesSupported,
+ id_token_encryption_enc_values_supported: opts.idTokenEncryptionEncValuesSupported,
+ userinfo_signing_alg_values_supported: opts.userinfoSigningAlgValuesSupported,
+ userinfo_encryption_alg_values_supported: opts.userinfoEncryptionAlgValuesSupported,
+ userinfo_encryption_enc_values_supported: opts.userinfoEncryptionEncValuesSupported,
+ request_object_encryption_alg_values_supported: opts.requestObjectEncryptionAlgValuesSupported,
+ request_object_encryption_enc_values_supported: opts.requestObjectEncryptionEncValuesSupported,
+ token_endpoint_auth_methods_supported: opts.tokenEndpointAuthMethodsSupported,
+ token_endpoint_auth_signing_alg_values_supported: opts.tokenEndpointAuthSigningAlgValuesSupported,
+ display_values_supported: opts.displayValuesSupported,
+ claim_types_supported: opts.claimTypesSupported,
+ claims_supported: opts.claimsSupported,
+ service_documentation: opts.serviceDocumentation,
+ claims_locales_supported: opts.claimsLocalesSupported,
+ ui_locales_supported: opts.uiLocalesSupported,
+ claims_parameter_supported: opts.claimsParameterSupported,
+ request_parameter_supported: opts.requestParameterSupported,
+ request_uri_parameter_supported: opts.requestUriParameterSupported,
+ require_request_uri_registration: opts.requireRequestUriRegistration,
+ op_policy_uri: opts.opPolicyUri,
+ op_tos_uri: opts.opTosUri,
+ logo_uri: opts.logo_uri,
+ client_purpose: opts.clientPurpose,
+ id_token_types_supported: opts.idTokenTypesSupported,
+ }
+
+ const languageTagEnabledFieldsNamesMapping = new Map()
+ languageTagEnabledFieldsNamesMapping.set('clientName', 'client_name')
+ languageTagEnabledFieldsNamesMapping.set('clientPurpose', 'client_purpose')
+
+ const languageTaggedFields: Map = LanguageTagUtils.getLanguageTaggedPropertiesMapped(opts, languageTagEnabledFieldsNamesMapping)
+ languageTaggedFields.forEach((value: string, key: string) => {
+ discoveryMetadataPayload[key] = value
+ })
+
+ return removeNullUndefined(discoveryMetadataPayload)
+}
diff --git a/packages/siop-oid4vp/lib/authorization-response/index.ts b/packages/siop-oid4vp/lib/authorization-response/index.ts
new file mode 100644
index 00000000..02bfafa2
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/index.ts
@@ -0,0 +1,4 @@
+export * from './AuthorizationResponse'
+export * from './types'
+export * from './Payload'
+export * from './ResponseRegistration'
diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts
new file mode 100644
index 00000000..9d557cbd
--- /dev/null
+++ b/packages/siop-oid4vp/lib/authorization-response/types.ts
@@ -0,0 +1,107 @@
+import { IPresentationDefinition, PresentationSignCallBackParams } from '@sphereon/pex'
+import { Format } from '@sphereon/pex-models'
+import { CompactSdJwtVc, Hasher, PresentationSubmission, W3CVerifiablePresentation } from '@sphereon/ssi-types'
+
+import { ResponseMode, ResponseRegistrationOpts, ResponseURIType, SupportedVersion, VerifiablePresentationWithFormat, Verification } from '../types'
+import { CreateJwtCallback, JwtIssuer } from '../types/JwtIssuer'
+import { VerifyJwtCallback } from '../types/JwtVerifier'
+
+import { AuthorizationResponse } from './AuthorizationResponse'
+
+export interface AuthorizationResponseOpts {
+ // redirectUri?: string; // It's typically comes from the request opts as a measure to prevent hijacking.
+ responseURI?: string // This is either the redirect URI or response URI. See also responseURIType. response URI is used when response_mode is `direct_post`
+ responseURIType?: ResponseURIType
+ registration?: ResponseRegistrationOpts
+ version?: SupportedVersion
+ audience?: string
+ createJwtCallback: CreateJwtCallback
+ jwtIssuer?: JwtIssuer
+ responseMode?: ResponseMode
+ // did: string;
+ expiresIn?: number
+ accessToken?: string
+ tokenType?: string
+ refreshToken?: string
+ presentationExchange?: PresentationExchangeResponseOpts
+}
+
+export interface PresentationExchangeResponseOpts {
+ /* presentationSignCallback?: PresentationSignCallback;
+ signOptions?: PresentationSignOptions,
+*/
+ /* credentialsAndDefinitions: {
+ presentationDefinition: IPresentationDefinition,
+ selectedCredentials: W3CVerifiableCredential[]
+ }[],*/
+
+ verifiablePresentations: Array
+ vpTokenLocation?: VPTokenLocation
+ presentationSubmission?: PresentationSubmission
+ restrictToFormats?: Format
+ restrictToDIDMethods?: string[]
+}
+
+export interface PresentationExchangeRequestOpts {
+ presentationVerificationCallback?: PresentationVerificationCallback
+}
+
+export interface PresentationDefinitionPayloadOpts {
+ presentation_definition?: IPresentationDefinition
+ presentation_definition_uri?: string
+}
+
+export interface PresentationDefinitionWithLocation {
+ version?: SupportedVersion
+ location: PresentationDefinitionLocation
+ definition: IPresentationDefinition
+}
+
+export interface VerifiablePresentationWithSubmissionData extends VerifiablePresentationWithFormat {
+ vpTokenLocation: VPTokenLocation
+
+ submissionData: PresentationSubmission
+}
+
+export enum PresentationDefinitionLocation {
+ CLAIMS_VP_TOKEN = 'claims.vp_token',
+ TOPLEVEL_PRESENTATION_DEF = 'presentation_definition',
+}
+
+export enum VPTokenLocation {
+ AUTHORIZATION_RESPONSE = 'authorization_response',
+ ID_TOKEN = 'id_token',
+ TOKEN_RESPONSE = 'token_response',
+}
+
+export type PresentationVerificationResult = { verified: boolean; reason?: string }
+
+export type PresentationVerificationCallback = (
+ args: W3CVerifiablePresentation | CompactSdJwtVc,
+ presentationSubmission: PresentationSubmission,
+) => Promise
+
+export type PresentationSignCallback = (args: PresentationSignCallBackParams) => Promise
+
+export interface VerifyAuthorizationResponseOpts {
+ correlationId: string
+ verification: Verification
+ verifyJwtCallback: VerifyJwtCallback
+ hasher?: Hasher
+ nonce?: string // To verify the response against the supplied nonce
+ state?: string // To verify the response against the supplied state
+ presentationDefinitions?: PresentationDefinitionWithLocation | PresentationDefinitionWithLocation[] // The presentation definitions to match against VPs in the response
+ audience?: string // The audience/redirect_uri
+ restrictToFormats?: Format // Further restrict to certain VC formats, not expressed in the presentation definition
+ restrictToDIDMethods?: string[]
+ // claims?: ClaimPayloadCommonOpts; // The claims, typically the same values used during request creation
+ // verifyCallback?: VerifyCallback;
+ // presentationVerificationCallback?: PresentationVerificationCallback;
+}
+
+export interface AuthorizationResponseWithCorrelationId {
+ // The URI to send the response to. Can be derived from either the redirect_uri or the response_uri
+ responseURI: string
+ response: AuthorizationResponse
+ correlationId: string
+}
diff --git a/packages/siop-oid4vp/lib/helpers/Encodings.ts b/packages/siop-oid4vp/lib/helpers/Encodings.ts
new file mode 100644
index 00000000..075def11
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/Encodings.ts
@@ -0,0 +1,103 @@
+import { InputDescriptorV1 } from '@sphereon/pex-models'
+import { parse, stringify } from 'qs'
+import * as ua8 from 'uint8arrays'
+
+import { SIOPErrors } from '../types'
+
+export function decodeUriAsJson(uri: string) {
+ if (!uri) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+ const queryString = uri.replace(/^([a-zA-Z][a-zA-Z0-9-_]*:\/\/.*[?])/, '')
+ if (!queryString) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+ const parts = parse(queryString, { plainObjects: true, depth: 10, parameterLimit: 5000, ignoreQueryPrefix: true })
+
+ const descriptors = parts?.claims?.['vp_token']?.presentation_definition?.['input_descriptors']
+ if (descriptors && Array.isArray(descriptors)) {
+ // Whenever we have a [{'uri': 'str1'}, 'uri': 'str2'] qs changes this to {uri: ['str1','str2']} which means schema validation fails. So we have to fix that
+ parts.claims['vp_token'].presentation_definition['input_descriptors'] = descriptors.map((descriptor: InputDescriptorV1) => {
+ if (Array.isArray(descriptor.schema)) {
+ descriptor.schema = descriptor.schema.flatMap((val) => {
+ if (typeof val === 'string') {
+ return { uri: val }
+ } else if (typeof val === 'object' && Array.isArray(val.uri)) {
+ return val.uri.map((uri) => ({ uri: uri as string }))
+ }
+ return val
+ })
+ }
+ return descriptor
+ })
+ }
+
+ const json = {}
+ for (const key in parts) {
+ const value = parts[key]
+ if (!value) {
+ continue
+ }
+ const isBool = typeof value == 'boolean'
+ const isNumber = typeof value == 'number'
+ const isString = typeof value == 'string'
+
+ if (isBool || isNumber) {
+ json[decodeURIComponent(key)] = value
+ } else if (isString) {
+ const decoded = decodeURIComponent(value)
+ if (decoded.startsWith('{') && decoded.endsWith('}')) {
+ json[decodeURIComponent(key)] = JSON.parse(decoded)
+ } else {
+ json[decodeURIComponent(key)] = decoded
+ }
+ }
+ }
+ return JSON.parse(JSON.stringify(json))
+}
+
+export function encodeJsonAsURI(json: unknown, _opts?: { arraysWithIndex?: string[] }): string {
+ if (typeof json === 'string') {
+ return encodeJsonAsURI(JSON.parse(json))
+ }
+
+ const results: string[] = []
+
+ function encodeAndStripWhitespace(key: string): string {
+ return encodeURIComponent(key.replace(' ', ''))
+ }
+
+ for (const [key, value] of Object.entries(json)) {
+ if (!value) {
+ continue
+ }
+ const isBool = typeof value == 'boolean'
+ const isNumber = typeof value == 'number'
+ const isString = typeof value == 'string'
+ const isArray = Array.isArray(value)
+ let encoded: string
+ if (isBool || isNumber) {
+ encoded = `${encodeAndStripWhitespace(key)}=${value}`
+ } else if (isString) {
+ encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(value)}`
+ } else if (isArray && _opts?.arraysWithIndex?.includes(key)) {
+ encoded = `${encodeAndStripWhitespace(key)}=${stringify(value, { arrayFormat: 'brackets' })}`
+ } else {
+ encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(JSON.stringify(value))}`
+ }
+ results.push(encoded)
+ }
+ return results.join('&')
+}
+
+export function base64ToHexString(input: string, encoding?: 'base64url' | 'base64'): string {
+ return ua8.toString(ua8.fromString(input, encoding ?? 'base64url'), 'base16')
+}
+
+export function fromBase64(base64: string): string {
+ return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
+}
+
+export function base64urlEncodeBuffer(buf: { toString: (arg0: 'base64') => string }): string {
+ return fromBase64(buf.toString('base64'))
+}
diff --git a/packages/siop-oid4vp/lib/helpers/HttpUtils.ts b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts
new file mode 100644
index 00000000..a88df4ac
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts
@@ -0,0 +1,138 @@
+import { fetch } from 'cross-fetch'
+import Debug from 'debug'
+
+import { ContentType, SIOPErrors, SIOPResonse } from '../types'
+
+const debug = Debug('sphereon:siopv2:http')
+
+export const getJson = async (
+ URL: string,
+ opts?: {
+ bearerToken?: string
+ contentType?: string | ContentType
+ accept?: string
+ customHeaders?: HeadersInit
+ exceptionOnHttpErrorStatus?: boolean
+ },
+): Promise> => {
+ return await siopFetch(URL, undefined, { method: 'GET', ...opts })
+}
+
+export const formPost = async (
+ url: string,
+ body: BodyInit,
+ opts?: {
+ bearerToken?: string
+ contentType?: string | ContentType
+ accept?: string
+ customHeaders?: HeadersInit
+ exceptionOnHttpErrorStatus?: boolean
+ },
+): Promise> => {
+ return await post(url, body, opts?.contentType ? { ...opts } : { contentType: ContentType.FORM_URL_ENCODED, ...opts })
+}
+
+export const post = async (
+ url: string,
+ body?: BodyInit,
+ opts?: {
+ bearerToken?: string
+ contentType?: string | ContentType
+ accept?: string
+ customHeaders?: HeadersInit
+ exceptionOnHttpErrorStatus?: boolean
+ },
+): Promise> => {
+ return await siopFetch(url, body, { method: 'POST', ...opts })
+}
+
+const siopFetch = async (
+ url: string,
+ body?: BodyInit,
+ opts?: {
+ method?: string
+ bearerToken?: string
+ contentType?: string | ContentType
+ accept?: string
+ customHeaders?: HeadersInit
+ exceptionOnHttpErrorStatus?: boolean
+ },
+): Promise> => {
+ if (!url || url.toLowerCase().startsWith('did:')) {
+ throw Error(`Invalid URL supplied. Expected a http(s) URL. Recieved: ${url}`)
+ }
+ const headers = opts?.customHeaders ? opts.customHeaders : {}
+ if (opts?.bearerToken) {
+ headers['Authorization'] = `Bearer ${opts.bearerToken}`
+ }
+ const method = opts?.method ? opts.method : body ? 'POST' : 'GET'
+ const accept = opts?.accept ? opts.accept : 'application/json'
+ headers['Content-Type'] = opts?.contentType ? opts.contentType : method !== 'GET' ? 'application/json' : undefined
+ headers['Accept'] = accept
+
+ const payload: RequestInit = {
+ method,
+ headers,
+ body,
+ }
+
+ debug(`START fetching url: ${url}`)
+ if (body) {
+ debug(`Body:\r\n${JSON.stringify(body)}`)
+ }
+ debug(`Headers:\r\n${JSON.stringify(payload.headers)}`)
+ const origResponse = await fetch(url, payload)
+ const clonedResponse = origResponse.clone()
+ const success = origResponse && origResponse.status >= 200 && origResponse.status < 400
+ const textResponseBody = await clonedResponse.text()
+
+ const isJSONResponse =
+ (accept === 'application/json' || origResponse.headers['Content-Type'] === 'application/json') && textResponseBody.trim().startsWith('{')
+ const responseBody = isJSONResponse ? JSON.parse(textResponseBody) : textResponseBody
+
+ if (success || opts?.exceptionOnHttpErrorStatus) {
+ debug(`${success ? 'success' : 'error'} status: ${clonedResponse.status}, body:\r\n${JSON.stringify(responseBody)}`)
+ } else {
+ console.warn(`${success ? 'success' : 'error'} status: ${clonedResponse.status}, body:\r\n${JSON.stringify(responseBody)}`)
+ }
+
+ if (!success && opts?.exceptionOnHttpErrorStatus) {
+ const error = JSON.stringify(responseBody)
+ throw new Error(error === '{}' ? '{"error": "not found"}' : error)
+ }
+ debug(`END fetching url: ${url}`)
+
+ return {
+ origResponse,
+ successBody: success ? responseBody : undefined,
+ errorBody: !success ? responseBody : undefined,
+ }
+}
+
+export const getWithUrl = async (url: string, textResponse?: boolean): Promise => {
+ // try {
+ const response = await fetch(url)
+ if (response.status >= 400) {
+ return Promise.reject(Error(`${SIOPErrors.RESPONSE_STATUS_UNEXPECTED} ${response.status}:${response.statusText} URL: ${url}`))
+ }
+ if (textResponse === true) {
+ return (await response.text()) as unknown as T
+ }
+ return await response.json()
+ /*} catch (e) {
+ return Promise.reject(Error(`${(e as Error).message}`));
+ }*/
+}
+
+export const fetchByReferenceOrUseByValue = async (referenceURI: string, valueObject: T, textResponse?: boolean): Promise => {
+ let response: T = valueObject
+ if (referenceURI) {
+ try {
+ response = await getWithUrl(referenceURI, textResponse)
+ } catch (e) {
+ console.log(e)
+ throw new Error(`${SIOPErrors.REG_PASS_BY_REFERENCE_INCORRECTLY}: ${e.message}, URL: ${referenceURI}`)
+ }
+ }
+ return response
+}
diff --git a/packages/siop-oid4vp/lib/helpers/Keys.ts b/packages/siop-oid4vp/lib/helpers/Keys.ts
new file mode 100644
index 00000000..45cb05bb
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/Keys.ts
@@ -0,0 +1,150 @@
+// import { keyUtils as ed25519KeyUtils } from '@transmute/did-key-ed25519';
+// import { ec as EC } from 'elliptic';
+import * as u8a from 'uint8arrays'
+
+import { JWK } from '../types'
+
+const ED25519_DID_KEY = 'did:key:z6Mk'
+
+export const isEd25519DidKeyMethod = (did?: string) => {
+ return did && did.includes(ED25519_DID_KEY)
+}
+
+/*
+export const isEd25519JWK = (jwk: JWK): boolean => {
+ return jwk && !!jwk.crv && jwk.crv === KeyCurve.ED25519;
+};
+
+export const getBase58PrivateKeyFromHexPrivateKey = (hexPrivateKey: string): string => {
+ return bs58.encode(Buffer.from(hexPrivateKey, 'hex'));
+};
+
+export const getPublicED25519JWKFromHexPrivateKey = (hexPrivateKey: string, kid?: string): JWK => {
+ const ec = new EC('ed25519');
+ const privKey = ec.keyFromPrivate(hexPrivateKey);
+ const pubPoint = privKey.getPublic();
+
+ return toJWK(kid, KeyCurve.ED25519, pubPoint);
+};
+
+const getPublicSECP256k1JWKFromHexPrivateKey = (hexPrivateKey: string, kid: string) => {
+ const ec = new EC('secp256k1');
+ const privKey = ec.keyFromPrivate(hexPrivateKey.replace('0x', ''), 'hex');
+ const pubPoint = privKey.getPublic();
+ return toJWK(kid, KeyCurve.SECP256k1, pubPoint);
+};
+
+export const getPublicJWKFromHexPrivateKey = (hexPrivateKey: string, kid?: string, did?: string): JWK => {
+ if (isEd25519DidKeyMethod(did)) {
+ return getPublicED25519JWKFromHexPrivateKey(hexPrivateKey, kid);
+ }
+ return getPublicSECP256k1JWKFromHexPrivateKey(hexPrivateKey, kid);
+};
+
+const toJWK = (kid: string, crv: KeyCurve, pubPoint: EC.BN) => {
+ return {
+ kid,
+ kty: KeyType.EC,
+ crv: crv,
+ x: base64url.toBase64(pubPoint.getX().toArrayLike(Buffer)),
+ y: base64url.toBase64(pubPoint.getY().toArrayLike(Buffer))
+ };
+};
+
+// from fingerprintFromPublicKey function in @transmute/Ed25519KeyPair
+const getThumbprintFromJwkDIDKeyImpl = (jwk: JWK): string => {
+ // ed25519 cryptonyms are multicodec encoded values, specifically:
+ // (multicodec ed25519-pub 0xed01 + key bytes)
+ const pubkeyBytes = base64url.toBuffer(jwk.x);
+ const buffer = new Uint8Array(2 + pubkeyBytes.length);
+ buffer[0] = 0xed;
+ buffer[1] = 0x01;
+ buffer.set(pubkeyBytes, 2);
+
+ // prefix with `z` to indicate multi-base encodingFormat
+
+ return base64url.encode(`z${u8a.toString(buffer, 'base58btc')}`);
+};
+
+export const getThumbprintFromJwk = async (jwk: JWK, did: string): Promise => {
+ if (isEd25519DidKeyMethod(did)) {
+ return getThumbprintFromJwkDIDKeyImpl(jwk);
+ } else {
+ return await calculateJwkThumbprint(jwk, 'sha256');
+ }
+};
+
+export const getThumbprint = async (hexPrivateKey: string, did: string): Promise => {
+ return await getThumbprintFromJwk(
+ isEd25519DidKeyMethod(did) ? getPublicED25519JWKFromHexPrivateKey(hexPrivateKey) : getPublicJWKFromHexPrivateKey(hexPrivateKey),
+ did
+ );
+};
+*/
+
+export type DigestAlgorithm = 'sha256' | 'sha384' | 'sha512'
+
+const check = (value, description) => {
+ if (typeof value !== 'string' || !value) {
+ throw Error(`${description} missing or invalid`)
+ }
+}
+
+export async function calculateJwkThumbprint(jwk: JWK, digestAlgorithm?: DigestAlgorithm): Promise {
+ if (!jwk || typeof jwk !== 'object') {
+ throw new TypeError('JWK must be an object')
+ }
+ const algorithm = digestAlgorithm ?? 'sha256'
+ if (algorithm !== 'sha256' && algorithm !== 'sha384' && algorithm !== 'sha512') {
+ throw new TypeError('digestAlgorithm must one of "sha256", "sha384", or "sha512"')
+ }
+ let components
+ switch (jwk.kty) {
+ case 'EC':
+ check(jwk.crv, '"crv" (Curve) Parameter')
+ check(jwk.x, '"x" (X Coordinate) Parameter')
+ check(jwk.y, '"y" (Y Coordinate) Parameter')
+ components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }
+ break
+ case 'OKP':
+ check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter')
+ check(jwk.x, '"x" (Public Key) Parameter')
+ components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }
+ break
+ case 'RSA':
+ check(jwk.e, '"e" (Exponent) Parameter')
+ check(jwk.n, '"n" (Modulus) Parameter')
+ components = { e: jwk.e, kty: jwk.kty, n: jwk.n }
+ break
+ case 'oct':
+ check(jwk.k, '"k" (Key Value) Parameter')
+ components = { k: jwk.k, kty: jwk.kty }
+ break
+ default:
+ throw Error('"kty" (Key Type) Parameter missing or unsupported')
+ }
+ const data = u8a.fromString(JSON.stringify(components), 'utf-8')
+ return u8a.toString(await digest(algorithm, data), 'base64url')
+}
+
+const digest = async (algorithm: DigestAlgorithm, data: Uint8Array) => {
+ const subtleDigest = `SHA-${algorithm.slice(-3)}`
+ return new Uint8Array(await crypto.subtle.digest(subtleDigest, data))
+}
+
+export async function getDigestAlgorithmFromJwkThumbprintUri(uri: string): Promise {
+ const match = uri.match(/^urn:ietf:params:oauth:jwk-thumbprint:sha-(\w+):/)
+ if (!match) {
+ throw new Error(`Invalid JWK thumbprint URI structure ${uri}`)
+ }
+ const algorithm = `sha${match[1]}` as DigestAlgorithm
+ if (algorithm !== 'sha256' && algorithm !== 'sha384' && algorithm !== 'sha512') {
+ throw new Error(`Invalid JWK thumbprint URI digest algorithm ${uri}`)
+ }
+ return algorithm
+}
+
+export async function calculateJwkThumbprintUri(jwk: JWK, digestAlgorithm: DigestAlgorithm = 'sha256'): Promise {
+ const thumbprint = await calculateJwkThumbprint(jwk, digestAlgorithm)
+ return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`
+}
diff --git a/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts
new file mode 100644
index 00000000..434512a4
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts
@@ -0,0 +1,126 @@
+import Tags from 'language-tags'
+
+import { SIOPErrors } from '../types'
+
+import { isStringNullOrEmpty } from './ObjectUtils'
+
+export class LanguageTagUtils {
+ private static readonly LANGUAGE_TAG_SEPARATOR = '#'
+
+ /**
+ * It will give back a fields which are language tag enabled. i.e. all fields with the fields names containing
+ * language tags e.g. fieldName#nl-NL
+ *
+ * @param source is the object from which the language enabled fields and their values will be extracted.
+ */
+ static getAllLanguageTaggedProperties(source: unknown): Map {
+ return this.getLanguageTaggedPropertiesMapped(source, undefined)
+ }
+
+ /**
+ * It will give back a fields which are language tag enabled and are listed in the required fields.
+ *
+ * @param source is the object from which the language enabled fields and their values will be extracted.
+ * @param requiredFieldNames the fields which are supposed to be language enabled. These are the only fields which should be returned.
+ */
+ static getLanguageTaggedProperties(source: unknown, requiredFieldNames: Array): Map {
+ const languageTagEnabledFieldsNamesMapping: Map = new Map()
+ requiredFieldNames.forEach((value) => languageTagEnabledFieldsNamesMapping.set(value, value))
+ return this.getLanguageTaggedPropertiesMapped(source, languageTagEnabledFieldsNamesMapping)
+ }
+
+ /**
+ * It will give back a fields which are language tag enabled and are mapped in the required fields.
+ *
+ * @param source is the object from which the language enabled fields and their values will be extracted.
+ * @param requiredFieldNamesMapping the fields which are supposed to be language enabled. These are the only fields which should be returned. And
+ * the fields names will be transformed as per the mapping provided.
+ */
+ static getLanguageTaggedPropertiesMapped(source: unknown, requiredFieldNamesMapping: Map): Map {
+ this.assertSourceIsWorthChecking(source)
+ this.assertValidTargetFieldNames(requiredFieldNamesMapping)
+
+ const discoveredLanguageTaggedFields: Map = new Map()
+
+ Object.entries(source).forEach(([key, value]) => {
+ const languageTagSeparatorIndexInKey: number = key.indexOf(this.LANGUAGE_TAG_SEPARATOR)
+
+ if (this.isFieldLanguageTagged(languageTagSeparatorIndexInKey)) {
+ this.extractLanguageTaggedField(
+ key,
+ value as string,
+ languageTagSeparatorIndexInKey,
+ requiredFieldNamesMapping,
+ discoveredLanguageTaggedFields,
+ )
+ }
+ })
+
+ return discoveredLanguageTaggedFields
+ }
+
+ private static extractLanguageTaggedField(
+ key: string,
+ value: string,
+ languageTagSeparatorIndexInKey: number,
+ languageTagEnabledFieldsNamesMapping: Map,
+ languageTaggedFields: Map,
+ ): void {
+ const fieldName = this.getFieldName(key, languageTagSeparatorIndexInKey)
+
+ const languageTag = this.getLanguageTag(key, languageTagSeparatorIndexInKey)
+ if (Tags.check(languageTag)) {
+ if (languageTagEnabledFieldsNamesMapping?.size) {
+ if (languageTagEnabledFieldsNamesMapping.has(fieldName)) {
+ languageTaggedFields.set(this.getMappedFieldName(languageTagEnabledFieldsNamesMapping, fieldName, languageTag), value)
+ }
+ } else {
+ languageTaggedFields.set(key, value)
+ }
+ }
+ }
+
+ private static getMappedFieldName(languageTagEnabledFieldsNamesMapping: Map, fieldName: string, languageTag: string): string {
+ return languageTagEnabledFieldsNamesMapping.get(fieldName) + this.LANGUAGE_TAG_SEPARATOR + languageTag
+ }
+
+ private static getLanguageTag(key: string, languageTagSeparatorIndex: number): string {
+ return key.substring(languageTagSeparatorIndex + 1)
+ }
+
+ private static getFieldName(key: string, languageTagSeparatorIndex: number): string {
+ return key.substring(0, languageTagSeparatorIndex)
+ }
+
+ /***
+ * This function checks about the field to be language-tagged.
+ *
+ * @param languageTagSeparatorIndex
+ * @private
+ */
+ private static isFieldLanguageTagged(languageTagSeparatorIndex: number): boolean {
+ return languageTagSeparatorIndex > 0
+ }
+
+ private static assertValidTargetFieldNames(languageTagEnabledFieldsNamesMapping: Map): void {
+ if (languageTagEnabledFieldsNamesMapping) {
+ if (!languageTagEnabledFieldsNamesMapping.size) {
+ throw new Error(SIOPErrors.BAD_PARAMS + ' LanguageTagEnabledFieldsNamesMapping must be non-null or non-empty')
+ } else {
+ for (const entry of languageTagEnabledFieldsNamesMapping.entries()) {
+ const key = entry[0]
+ const value = entry[1]
+ if (isStringNullOrEmpty(key) || isStringNullOrEmpty(value)) {
+ throw new Error(SIOPErrors.BAD_PARAMS + '. languageTagEnabledFieldsName must be non-null or non-empty')
+ }
+ }
+ }
+ }
+ }
+
+ private static assertSourceIsWorthChecking(source: unknown): void {
+ if (!source) {
+ throw new Error(SIOPErrors.BAD_PARAMS + ' Source must be non-null i.e. not-initialized.')
+ }
+ }
+}
diff --git a/packages/siop-oid4vp/lib/helpers/Metadata.ts b/packages/siop-oid4vp/lib/helpers/Metadata.ts
new file mode 100644
index 00000000..632d7c0c
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/Metadata.ts
@@ -0,0 +1,120 @@
+import { Format } from '@sphereon/pex-models'
+
+import {
+ CommonSupportedMetadata,
+ DiscoveryMetadataPayload,
+ RPRegistrationMetadataPayload,
+ SIOPErrors,
+ SubjectSyntaxTypesSupportedValues,
+} from '../types'
+
+export function assertValidMetadata(opMetadata: DiscoveryMetadataPayload, rpMetadata: RPRegistrationMetadataPayload): CommonSupportedMetadata {
+ let subjectSyntaxTypesSupported = []
+ const credentials = supportedCredentialsFormats(rpMetadata.vp_formats, opMetadata.vp_formats)
+ const isValidSubjectSyntax = verifySubjectSyntaxes(rpMetadata.subject_syntax_types_supported)
+ if (isValidSubjectSyntax && rpMetadata.subject_syntax_types_supported) {
+ subjectSyntaxTypesSupported = supportedSubjectSyntaxTypes(rpMetadata.subject_syntax_types_supported, opMetadata.subject_syntax_types_supported)
+ } else if (isValidSubjectSyntax && (!rpMetadata.subject_syntax_types_supported || !rpMetadata.subject_syntax_types_supported.length)) {
+ if (opMetadata.subject_syntax_types_supported || opMetadata.subject_syntax_types_supported.length) {
+ subjectSyntaxTypesSupported = [...opMetadata.subject_syntax_types_supported]
+ }
+ }
+ return { vp_formats: credentials, subject_syntax_types_supported: subjectSyntaxTypesSupported }
+}
+
+function getIntersection(rpMetadata: Array | T, opMetadata: Array | T): Array {
+ let arrayA, arrayB
+ if (!Array.isArray(rpMetadata)) {
+ arrayA = [rpMetadata]
+ } else {
+ arrayA = rpMetadata
+ }
+ if (!Array.isArray(opMetadata)) {
+ arrayB = [opMetadata]
+ } else {
+ arrayB = opMetadata
+ }
+ return arrayA.filter((value) => arrayB.includes(value))
+}
+
+function verifySubjectSyntaxes(subjectSyntaxTypesSupported: string[]): boolean {
+ if (subjectSyntaxTypesSupported.length) {
+ if (Array.isArray(subjectSyntaxTypesSupported)) {
+ if (
+ subjectSyntaxTypesSupported.length ===
+ subjectSyntaxTypesSupported.filter(
+ (sst) =>
+ sst.includes(SubjectSyntaxTypesSupportedValues.DID.valueOf()) || sst === SubjectSyntaxTypesSupportedValues.JWK_THUMBPRINT.valueOf(),
+ ).length
+ ) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+function supportedSubjectSyntaxTypes(rpMethods: string[] | string, opMethods: string[] | string): Array {
+ const rpMethodsList = Array.isArray(rpMethods) ? rpMethods : [rpMethods]
+ const opMethodsList = Array.isArray(opMethods) ? opMethods : [opMethods]
+ const supportedSubjectSyntaxTypes = getIntersection(rpMethodsList, opMethodsList)
+ if (supportedSubjectSyntaxTypes.indexOf(SubjectSyntaxTypesSupportedValues.DID.valueOf()) !== -1) {
+ return [SubjectSyntaxTypesSupportedValues.DID.valueOf()]
+ }
+ if (rpMethodsList.includes(SubjectSyntaxTypesSupportedValues.DID.valueOf())) {
+ const supportedExtendedDids: string[] = opMethodsList.filter((method) => method.startsWith('did:'))
+ if (supportedExtendedDids.length) {
+ return supportedExtendedDids
+ }
+ }
+ if (opMethodsList.includes(SubjectSyntaxTypesSupportedValues.DID.valueOf())) {
+ const supportedExtendedDids: string[] = rpMethodsList.filter((method) => method.startsWith('did:'))
+ if (supportedExtendedDids.length) {
+ return supportedExtendedDids
+ }
+ }
+
+ if (!supportedSubjectSyntaxTypes.length) {
+ throw Error(SIOPErrors.DID_METHODS_NOT_SUPORTED)
+ }
+ const supportedDidMethods = supportedSubjectSyntaxTypes.filter((sst) => sst.includes('did:'))
+ if (supportedDidMethods.length) {
+ return supportedDidMethods
+ }
+ return supportedSubjectSyntaxTypes
+}
+
+function getFormatIntersection(rpFormat: Format, opFormat: Format): Format {
+ const intersectionFormat: Format = {}
+ const supportedCredentials = getIntersection(Object.keys(rpFormat), Object.keys(opFormat))
+ if (!supportedCredentials.length) {
+ throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED)
+ }
+ supportedCredentials.forEach(function (crFormat: string) {
+ const rpAlgs = []
+ const opAlgs = []
+ Object.keys(rpFormat[crFormat]).forEach((k) => rpAlgs.push(...rpFormat[crFormat][k]))
+ Object.keys(opFormat[crFormat]).forEach((k) => opAlgs.push(...opFormat[crFormat][k]))
+ let methodKeyRP = undefined
+ let methodKeyOP = undefined
+ Object.keys(rpFormat[crFormat]).forEach((k) => (methodKeyRP = k))
+ Object.keys(opFormat[crFormat]).forEach((k) => (methodKeyOP = k))
+ if (methodKeyRP !== methodKeyOP) {
+ throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED)
+ }
+ const algs = getIntersection(rpAlgs, opAlgs)
+ if (!algs.length) {
+ throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED)
+ }
+ intersectionFormat[crFormat] = {}
+ intersectionFormat[crFormat][methodKeyOP] = algs
+ })
+ return intersectionFormat
+}
+
+export function supportedCredentialsFormats(rpFormat: Format, opFormat: Format): Format {
+ if (!rpFormat || !opFormat || !Object.keys(rpFormat).length || !Object.keys(opFormat).length) {
+ throw new Error(SIOPErrors.CREDENTIALS_FORMATS_NOT_PROVIDED)
+ }
+ return getFormatIntersection(rpFormat, opFormat)
+}
diff --git a/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts
new file mode 100644
index 00000000..e278902d
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts
@@ -0,0 +1,26 @@
+import { JSONPath as jp } from '@astronautlabs/jsonpath'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function extractDataFromPath(obj: unknown, path: string): { path: string[]; value: any }[] {
+ return jp.nodes(obj, path)
+}
+
+export function isStringNullOrEmpty(key: string) {
+ return !key || !key.length
+}
+
+export function removeNullUndefined(data: unknown) {
+ if (!data) {
+ return data
+ }
+ //transform properties into key-values pairs and filter all the empty-values
+ const entries = Object.entries(data).filter(([, value]) => value != null)
+ //map through all the remaining properties and check if the value is an object.
+ //if value is object, use recursion to remove empty properties
+ const clean = entries.map(([key, v]) => {
+ const value = typeof v === 'object' && !Array.isArray(v) ? removeNullUndefined(v) : v
+ return [key, value]
+ })
+ //transform the key-value pairs back to an object.
+ return Object.fromEntries(clean)
+}
diff --git a/packages/siop-oid4vp/lib/helpers/Revocation.ts b/packages/siop-oid4vp/lib/helpers/Revocation.ts
new file mode 100644
index 00000000..2c38e2de
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/Revocation.ts
@@ -0,0 +1,57 @@
+import { CredentialMapper, W3CVerifiableCredential, WrappedVerifiableCredential, WrappedVerifiablePresentation } from '@sphereon/ssi-types'
+
+import { RevocationStatus, RevocationVerification, RevocationVerificationCallback, VerifiableCredentialTypeFormat } from '../types'
+
+export const verifyRevocation = async (
+ vpToken: WrappedVerifiablePresentation,
+ revocationVerificationCallback: RevocationVerificationCallback,
+ revocationVerification: RevocationVerification,
+): Promise => {
+ if (!vpToken) {
+ throw new Error(`VP token not provided`)
+ }
+ if (!revocationVerificationCallback) {
+ throw new Error(`Revocation callback not provided`)
+ }
+
+ const vcs = CredentialMapper.isWrappedSdJwtVerifiablePresentation(vpToken) ? [vpToken.vcs[0]] : vpToken.presentation.verifiableCredential
+ for (const vc of vcs) {
+ if (
+ revocationVerification === RevocationVerification.ALWAYS ||
+ (revocationVerification === RevocationVerification.IF_PRESENT && credentialHasStatus(vc))
+ ) {
+ const result = await revocationVerificationCallback(
+ vc.original as W3CVerifiableCredential,
+ originalTypeToVerifiableCredentialTypeFormat(vc.format),
+ )
+ if (result.status === RevocationStatus.INVALID) {
+ throw new Error(`Revocation invalid for vc. Error: ${result.error}`)
+ }
+ }
+ }
+}
+
+function originalTypeToVerifiableCredentialTypeFormat(original: WrappedVerifiableCredential['format']): VerifiableCredentialTypeFormat {
+ const mapping: { [T in WrappedVerifiableCredential['format']]: VerifiableCredentialTypeFormat } = {
+ 'vc+sd-jwt': VerifiableCredentialTypeFormat.SD_JWT_VC,
+ jwt: VerifiableCredentialTypeFormat.JWT_VC,
+ jwt_vc: VerifiableCredentialTypeFormat.JWT_VC,
+ ldp: VerifiableCredentialTypeFormat.LDP_VC,
+ ldp_vc: VerifiableCredentialTypeFormat.LDP_VC,
+ }
+
+ return mapping[original]
+}
+
+/**
+ * Checks whether a wrapped verifiable credential has a status in the credential.
+ * For w3c credentials it will check the presence of `credentialStatus` property
+ * For SD-JWT it will check the presence of `status` property
+ */
+function credentialHasStatus(wrappedVerifiableCredential: WrappedVerifiableCredential) {
+ if (CredentialMapper.isWrappedSdJwtVerifiableCredential(wrappedVerifiableCredential)) {
+ return wrappedVerifiableCredential.decoded.status !== undefined
+ } else {
+ return wrappedVerifiableCredential.credential.credentialStatus !== undefined
+ }
+}
diff --git a/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts b/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts
new file mode 100644
index 00000000..cd3c5abb
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts
@@ -0,0 +1,99 @@
+import { AuthorizationRequestPayloadVD11Schema, AuthorizationRequestPayloadVID1Schema } from '../schemas'
+import {
+ AuthorizationRequestPayloadVD12OID4VPD18Schema,
+ AuthorizationRequestPayloadVD12OID4VPD20Schema,
+} from '../schemas/validation/schemaValidation'
+import { AuthorizationRequestPayload, ResponseMode, SupportedVersion } from '../types'
+import errors from '../types/Errors'
+
+const validateJWTVCPresentationProfile = AuthorizationRequestPayloadVID1Schema
+
+function isJWTVC1Payload(authorizationRequest: AuthorizationRequestPayload) {
+ return (
+ authorizationRequest.scope &&
+ authorizationRequest.scope.toLowerCase().includes('openid') &&
+ authorizationRequest.response_type &&
+ authorizationRequest.response_type.toLowerCase().includes('id_token') &&
+ authorizationRequest.response_mode &&
+ authorizationRequest.response_mode.toLowerCase() === 'post' &&
+ authorizationRequest.client_id &&
+ authorizationRequest.client_id.toLowerCase().startsWith('did:') &&
+ authorizationRequest.redirect_uri &&
+ (authorizationRequest.registration_uri || authorizationRequest.registration) &&
+ authorizationRequest.claims &&
+ 'vp_token' in authorizationRequest.claims
+ )
+}
+function isID1Payload(authorizationRequest: AuthorizationRequestPayload) {
+ return (
+ !authorizationRequest.client_metadata_uri &&
+ !authorizationRequest.client_metadata &&
+ !authorizationRequest.presentation_definition &&
+ !authorizationRequest.presentation_definition_uri
+ )
+}
+
+export const authorizationRequestVersionDiscovery = (authorizationRequest: AuthorizationRequestPayload): SupportedVersion[] => {
+ const versions = []
+ const authorizationRequestCopy: AuthorizationRequestPayload = JSON.parse(JSON.stringify(authorizationRequest))
+ const vd13Validation = AuthorizationRequestPayloadVD12OID4VPD20Schema(authorizationRequestCopy)
+ if (vd13Validation) {
+ if (
+ !authorizationRequestCopy.registration_uri &&
+ !authorizationRequestCopy.registration &&
+ !(authorizationRequestCopy.claims && 'vp_token' in authorizationRequestCopy.claims) &&
+ authorizationRequestCopy.response_mode !== ResponseMode.POST // Post has been replaced by direct post
+ ) {
+ versions.push(SupportedVersion.SIOPv2_D12_OID4VP_D20)
+ }
+ }
+
+ // todo: We could use v11 validation for v12 for now, as we do not differentiate in the schema at this point\
+ const vd12Validation = AuthorizationRequestPayloadVD12OID4VPD18Schema(authorizationRequestCopy)
+ if (vd12Validation) {
+ if (
+ !authorizationRequestCopy.registration_uri &&
+ !authorizationRequestCopy.registration &&
+ !(authorizationRequestCopy.claims && 'vp_token' in authorizationRequestCopy.claims) &&
+ authorizationRequestCopy.response_mode !== ResponseMode.POST // Post has been replaced by direct post
+ ) {
+ versions.push(SupportedVersion.SIOPv2_D12_OID4VP_D18)
+ }
+ }
+ const vd11Validation = AuthorizationRequestPayloadVD11Schema(authorizationRequestCopy)
+ if (vd11Validation) {
+ if (
+ !authorizationRequestCopy.registration_uri &&
+ !authorizationRequestCopy.registration &&
+ !(authorizationRequestCopy.claims && 'vp_token' in authorizationRequestCopy.claims) &&
+ !authorizationRequestCopy.client_id_scheme && // introduced after v11
+ !authorizationRequestCopy.response_uri &&
+ authorizationRequestCopy.response_mode !== ResponseMode.DIRECT_POST // Direct post was used before v12 oid4vp18
+ ) {
+ versions.push(SupportedVersion.SIOPv2_D11)
+ }
+ }
+ const jwtVC1Validation = validateJWTVCPresentationProfile(authorizationRequestCopy)
+ if (jwtVC1Validation && isJWTVC1Payload(authorizationRequest)) {
+ versions.push(SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1)
+ }
+ const vid1Validation = AuthorizationRequestPayloadVID1Schema(authorizationRequestCopy)
+ if (vid1Validation && isID1Payload(authorizationRequest)) {
+ versions.push(SupportedVersion.SIOPv2_ID1)
+ }
+ if (versions.length === 0) {
+ throw new Error(errors.SIOP_VERSION_NOT_SUPPORTED)
+ }
+ return versions
+}
+
+export const checkSIOPSpecVersionSupported = async (
+ payload: AuthorizationRequestPayload,
+ supportedVersions: SupportedVersion[],
+): Promise => {
+ const versions: SupportedVersion[] = authorizationRequestVersionDiscovery(payload)
+ if (!supportedVersions || supportedVersions.length === 0) {
+ return versions
+ }
+ return supportedVersions.filter((version) => versions.includes(version))
+}
diff --git a/packages/siop-oid4vp/lib/helpers/State.ts b/packages/siop-oid4vp/lib/helpers/State.ts
new file mode 100644
index 00000000..b67c357f
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/State.ts
@@ -0,0 +1,21 @@
+import SHA from 'sha.js'
+import { v4 as uuidv4 } from 'uuid'
+
+import { base64urlEncodeBuffer } from './Encodings'
+
+export function getNonce(state: string, nonce?: string) {
+ return nonce || toNonce(state)
+}
+
+export function toNonce(input: string): string {
+ const buff = SHA('sha256').update(input).digest()
+ return base64urlEncodeBuffer(buff)
+}
+
+export function getState(state?: string) {
+ return state || createState()
+}
+
+export function createState(): string {
+ return uuidv4()
+}
diff --git a/packages/siop-oid4vp/lib/helpers/index.ts b/packages/siop-oid4vp/lib/helpers/index.ts
new file mode 100644
index 00000000..c15804a7
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/index.ts
@@ -0,0 +1,8 @@
+export * from './Metadata'
+export * from './Encodings'
+export * from './HttpUtils'
+export * from './Keys'
+export * from './ObjectUtils'
+export * from './Revocation'
+export * from './State'
+export * from './LanguageTagUtils'
diff --git a/packages/siop-oid4vp/lib/helpers/jwtUtils.ts b/packages/siop-oid4vp/lib/helpers/jwtUtils.ts
new file mode 100644
index 00000000..eb8776de
--- /dev/null
+++ b/packages/siop-oid4vp/lib/helpers/jwtUtils.ts
@@ -0,0 +1,17 @@
+import { jwtDecode } from 'jwt-decode'
+
+import { JwtHeader, JwtPayload, SIOPErrors } from '../types'
+
+export type JwtType = 'id-token' | 'request-object' | 'verifier-attestation'
+
+export type JwtProtectionMethod = 'did' | 'x5c' | 'jwk' | 'custom'
+
+export function parseJWT(jwt: string) {
+ const header = jwtDecode(jwt, { header: true })
+ const payload = jwtDecode(jwt, { header: false })
+
+ if (!payload || !header) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+ return { header, payload }
+}
diff --git a/packages/siop-oid4vp/lib/id-token/IDToken.ts b/packages/siop-oid4vp/lib/id-token/IDToken.ts
new file mode 100644
index 00000000..a5916d91
--- /dev/null
+++ b/packages/siop-oid4vp/lib/id-token/IDToken.ts
@@ -0,0 +1,233 @@
+import { AuthorizationResponseOpts, VerifyAuthorizationResponseOpts } from '../authorization-response'
+import { assertValidVerifyOpts } from '../authorization-response/Opts'
+import { parseJWT } from '../helpers/jwtUtils'
+import {
+ getJwtVerifierWithContext,
+ IDTokenJwt,
+ IDTokenPayload,
+ JWK,
+ JwtHeader,
+ JWTPayload,
+ ResponseIss,
+ SIOPErrors,
+ VerifiedAuthorizationRequest,
+ VerifiedIDToken,
+} from '../types'
+import { JwtIssuer, JwtIssuerWithContext } from '../types/JwtIssuer'
+
+import { calculateJwkThumbprintUri } from './../helpers/Keys'
+import { createIDTokenPayload } from './Payload'
+
+export class IDToken {
+ private _header?: JwtHeader
+ private _payload?: IDTokenPayload
+ private _jwt?: IDTokenJwt
+ private readonly _responseOpts: AuthorizationResponseOpts
+
+ private constructor(jwt?: IDTokenJwt, payload?: IDTokenPayload, responseOpts?: AuthorizationResponseOpts) {
+ this._jwt = jwt
+ this._payload = payload
+ this._responseOpts = responseOpts
+ }
+
+ public static async fromVerifiedAuthorizationRequest(
+ verifiedAuthorizationRequest: VerifiedAuthorizationRequest,
+ responseOpts: AuthorizationResponseOpts,
+ verifyOpts?: VerifyAuthorizationResponseOpts,
+ ) {
+ const authorizationRequestPayload = verifiedAuthorizationRequest.authorizationRequestPayload
+ if (!authorizationRequestPayload) {
+ throw new Error(SIOPErrors.NO_REQUEST)
+ }
+ const idToken = new IDToken(null, await createIDTokenPayload(verifiedAuthorizationRequest, responseOpts), responseOpts)
+ if (verifyOpts) {
+ await idToken.verify(verifyOpts)
+ }
+ return idToken
+ }
+
+ public static async fromIDToken(idTokenJwt: IDTokenJwt, verifyOpts?: VerifyAuthorizationResponseOpts) {
+ if (!idTokenJwt) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+ const idToken = new IDToken(idTokenJwt, undefined)
+ if (verifyOpts) {
+ await idToken.verify(verifyOpts)
+ }
+ return idToken
+ }
+
+ public static async fromIDTokenPayload(
+ idTokenPayload: IDTokenPayload,
+ responseOpts: AuthorizationResponseOpts,
+ verifyOpts?: VerifyAuthorizationResponseOpts,
+ ) {
+ if (!idTokenPayload) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+ const idToken = new IDToken(null, idTokenPayload, responseOpts)
+ if (verifyOpts) {
+ await idToken.verify(verifyOpts)
+ }
+ return idToken
+ }
+
+ public async payload(): Promise {
+ if (!this._payload) {
+ if (!this._jwt) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+ const { header, payload } = this.parseAndVerifyJwt()
+ this._header = header
+ this._payload = payload
+ }
+ return this._payload
+ }
+
+ public async jwt(_jwtIssuer: JwtIssuer): Promise {
+ if (!this._jwt) {
+ if (!this.responseOpts) {
+ throw Error(SIOPErrors.BAD_IDTOKEN_RESPONSE_OPTS)
+ }
+
+ const jwtIssuer: JwtIssuerWithContext = _jwtIssuer
+ ? { ..._jwtIssuer, type: 'id-token', authorizationResponseOpts: this.responseOpts }
+ : { method: 'custom', type: 'id-token', authorizationResponseOpts: this.responseOpts }
+
+ if (jwtIssuer.method === 'custom') {
+ this._jwt = await this.responseOpts.createJwtCallback(jwtIssuer, { header: {}, payload: this._payload })
+ } else if (jwtIssuer.method === 'did') {
+ const did = jwtIssuer.didUrl.split('#')[0]
+ this._payload.sub = did
+
+ const issuer = this._responseOpts.registration.issuer || this._payload.iss
+ if (!issuer || !(issuer.includes(ResponseIss.SELF_ISSUED_V2) || issuer === this._payload.sub)) {
+ throw new Error(SIOPErrors.NO_SELF_ISSUED_ISS)
+ }
+ if (!this._payload.iss) {
+ this._payload.iss = issuer
+ }
+
+ const header = { kid: jwtIssuer.didUrl, alg: jwtIssuer.alg, typ: 'JWT' }
+ this._jwt = await this.responseOpts.createJwtCallback({ ...jwtIssuer, type: 'id-token' }, { header, payload: this._payload })
+ } else if (jwtIssuer.method === 'x5c') {
+ this._payload.iss = jwtIssuer.issuer
+ this._payload.sub = jwtIssuer.issuer
+
+ const header = { x5c: jwtIssuer.x5c, typ: 'JWT' }
+ this._jwt = await this._responseOpts.createJwtCallback(jwtIssuer, { header, payload: this._payload })
+ } else if (jwtIssuer.method === 'jwk') {
+ const jwkThumbprintUri = await calculateJwkThumbprintUri(jwtIssuer.jwk as JWK)
+ this._payload.sub = jwkThumbprintUri
+ this._payload.iss = jwkThumbprintUri
+ this._payload.sub_jwk = jwtIssuer.jwk
+
+ const header = { jwk: jwtIssuer.jwk, alg: jwtIssuer.jwk.alg, typ: 'JWT' }
+ this._jwt = await this._responseOpts.createJwtCallback(jwtIssuer, { header, payload: this._payload })
+ } else {
+ throw new Error(`JwtIssuer method '${(jwtIssuer as JwtIssuer).method}' not implemented`)
+ }
+
+ const { header, payload } = this.parseAndVerifyJwt()
+ this._header = header
+ this._payload = payload
+ }
+ return this._jwt
+ }
+
+ private parseAndVerifyJwt(): { header: JwtHeader; payload: IDTokenPayload } {
+ const { header, payload } = parseJWT(this._jwt)
+ this.assertValidResponseJWT({ header, payload })
+ const idTokenPayload = payload as IDTokenPayload
+ return { header, payload: idTokenPayload }
+ }
+
+ /**
+ * Verifies a SIOP ID Response JWT on the RP Side
+ *
+ * @param idToken ID token to be validated
+ * @param verifyOpts
+ */
+ public async verify(verifyOpts: VerifyAuthorizationResponseOpts): Promise {
+ assertValidVerifyOpts(verifyOpts)
+
+ if (!this._jwt) {
+ throw new Error(SIOPErrors.NO_JWT)
+ }
+
+ const parsedJwt = parseJWT(this._jwt)
+ this.assertValidResponseJWT(parsedJwt)
+ const idTokenPayload = parsedJwt.payload as IDTokenPayload
+
+ const jwtVerifier = await getJwtVerifierWithContext(parsedJwt, { type: 'id-token' })
+ const verificationResult = await verifyOpts.verifyJwtCallback(jwtVerifier, { ...parsedJwt, raw: this._jwt })
+ if (!verificationResult) {
+ throw Error(SIOPErrors.ERROR_VERIFYING_SIGNATURE)
+ }
+
+ this.assertValidResponseJWT({ header: parsedJwt.header, verPayload: idTokenPayload, audience: verifyOpts.audience })
+ // Enforces verifyPresentationCallback function on the RP side,
+ if (!verifyOpts?.verification.presentationVerificationCallback) {
+ throw new Error(SIOPErrors.VERIFIABLE_PRESENTATION_VERIFICATION_FUNCTION_MISSING)
+ }
+ return {
+ jwt: this._jwt,
+ payload: { ...idTokenPayload },
+ verifyOpts,
+ }
+ }
+
+ static async verify(idTokenJwt: IDTokenJwt, verifyOpts: VerifyAuthorizationResponseOpts): Promise {
+ const idToken = await IDToken.fromIDToken(idTokenJwt, verifyOpts)
+ const verifiedIdToken = await idToken.verify(verifyOpts)
+
+ return {
+ ...verifiedIdToken,
+ }
+ }
+
+ private assertValidResponseJWT(opts: { header: JwtHeader; payload?: JWTPayload; verPayload?: IDTokenPayload; audience?: string; nonce?: string }) {
+ if (!opts.header) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+ if (opts.payload) {
+ if (!opts.payload.iss || !(opts.payload.iss.includes(ResponseIss.SELF_ISSUED_V2) || opts.payload.iss.startsWith('did:'))) {
+ throw new Error(`${SIOPErrors.NO_SELF_ISSUED_ISS}, got: ${opts.payload.iss}`)
+ }
+ }
+
+ if (opts.verPayload) {
+ if (!opts.verPayload.nonce) {
+ throw Error(SIOPErrors.NO_NONCE)
+ // No need for our own expiration check. DID jwt already does that
+ /*} else if (!opts.verPayload.exp || opts.verPayload.exp < Date.now() / 1000) {
+ throw Error(SIOPErrors.EXPIRED);
+ /!*} else if (!opts.verPayload.iat || opts.verPayload.iat > (Date.now() / 1000)) {
+ throw Error(SIOPErrors.EXPIRED);*!/
+ // todo: Add iat check
+
+ */
+ }
+ if ((opts.verPayload.aud && !opts.audience) || (!opts.verPayload.aud && opts.audience)) {
+ throw Error(SIOPErrors.NO_AUDIENCE)
+ } else if (opts.audience && opts.audience != opts.verPayload.aud) {
+ throw Error(SIOPErrors.INVALID_AUDIENCE)
+ } else if (opts.nonce && opts.nonce != opts.verPayload.nonce) {
+ throw Error(SIOPErrors.BAD_NONCE)
+ }
+ }
+ }
+
+ get header(): JwtHeader {
+ return this._header
+ }
+
+ get responseOpts(): AuthorizationResponseOpts {
+ return this._responseOpts
+ }
+
+ public async isSelfIssued(): Promise {
+ const payload = await this.payload()
+ return payload.iss === ResponseIss.SELF_ISSUED_V2 || (payload.sub !== undefined && payload.sub === payload.iss)
+ }
+}
diff --git a/packages/siop-oid4vp/lib/id-token/Payload.ts b/packages/siop-oid4vp/lib/id-token/Payload.ts
new file mode 100644
index 00000000..379e930b
--- /dev/null
+++ b/packages/siop-oid4vp/lib/id-token/Payload.ts
@@ -0,0 +1,46 @@
+import { AuthorizationResponseOpts, mergeOAuth2AndOpenIdInRequestPayload } from '../authorization-response'
+import { assertValidResponseOpts } from '../authorization-response/Opts'
+import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion'
+import { IDTokenPayload, ResponseIss, SIOPErrors, SupportedVersion, VerifiedAuthorizationRequest } from '../types'
+
+export const createIDTokenPayload = async (
+ verifiedAuthorizationRequest: VerifiedAuthorizationRequest,
+ responseOpts: AuthorizationResponseOpts,
+): Promise => {
+ await assertValidResponseOpts(responseOpts)
+ const authorizationRequestPayload = await verifiedAuthorizationRequest.authorizationRequest.mergedPayloads()
+ const requestObject = verifiedAuthorizationRequest.requestObject
+ if (!authorizationRequestPayload) {
+ throw new Error(SIOPErrors.VERIFY_BAD_PARAMS)
+ }
+ const payload = await mergeOAuth2AndOpenIdInRequestPayload(authorizationRequestPayload, requestObject)
+
+ const state = payload.state
+ const nonce = payload.nonce
+ const SEC_IN_MS = 1000
+
+ const rpSupportedVersions = authorizationRequestVersionDiscovery(payload)
+ const maxRPVersion = rpSupportedVersions.reduce(
+ (previous, current) => (current.valueOf() > previous.valueOf() ? current : previous),
+ SupportedVersion.SIOPv2_D12_OID4VP_D18,
+ )
+ if (responseOpts.version && rpSupportedVersions.length > 0 && !rpSupportedVersions.includes(responseOpts.version)) {
+ throw Error(`RP does not support spec version ${responseOpts.version}, supported versions: ${rpSupportedVersions.toString()}`)
+ }
+ const opVersion = responseOpts.version ?? maxRPVersion
+
+ const idToken: IDTokenPayload = {
+ // fixme: ID11 does not use this static value anymore
+ iss:
+ responseOpts?.registration?.issuer ??
+ (opVersion === SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1 ? ResponseIss.JWT_VC_PRESENTATION_V1 : ResponseIss.SELF_ISSUED_V2),
+ aud: responseOpts.audience || payload.client_id,
+ iat: Math.round(Date.now() / SEC_IN_MS - 60 * SEC_IN_MS),
+ exp: Math.round(Date.now() / SEC_IN_MS + (responseOpts.expiresIn || 600)),
+ ...(payload.auth_time && { auth_time: payload.auth_time }),
+ nonce,
+ state,
+ // ...(responseOpts.presentationExchange?._vp_token ? { _vp_token: responseOpts.presentationExchange._vp_token } : {}),
+ }
+ return idToken
+}
diff --git a/packages/siop-oid4vp/lib/id-token/index.ts b/packages/siop-oid4vp/lib/id-token/index.ts
new file mode 100644
index 00000000..53343294
--- /dev/null
+++ b/packages/siop-oid4vp/lib/id-token/index.ts
@@ -0,0 +1,2 @@
+export * from './IDToken'
+export * from './Payload'
diff --git a/packages/siop-oid4vp/lib/index.ts b/packages/siop-oid4vp/lib/index.ts
new file mode 100644
index 00000000..3829b607
--- /dev/null
+++ b/packages/siop-oid4vp/lib/index.ts
@@ -0,0 +1,12 @@
+import * as RPRegistrationMetadata from './authorization-request/RequestRegistration'
+import { PresentationExchange } from './authorization-response/PresentationExchange'
+
+export * from './helpers'
+export * from './types'
+export * from './authorization-request'
+export * from './authorization-response'
+export * from './id-token'
+export * from './request-object'
+export * from './rp'
+export * from './op'
+export { PresentationExchange, RPRegistrationMetadata }
diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts
new file mode 100644
index 00000000..b300dd5a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/op/OP.ts
@@ -0,0 +1,294 @@
+import { EventEmitter } from 'events'
+
+import { IIssuerId } from '@sphereon/ssi-types'
+import { v4 as uuidv4 } from 'uuid'
+
+import { AuthorizationRequest, URI, VerifyAuthorizationRequestOpts } from '../authorization-request'
+import { mergeVerificationOpts } from '../authorization-request/Opts'
+import {
+ AuthorizationResponse,
+ AuthorizationResponseOpts,
+ AuthorizationResponseWithCorrelationId,
+ PresentationExchangeResponseOpts,
+} from '../authorization-response'
+import { encodeJsonAsURI, post } from '../helpers'
+import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion'
+import {
+ AuthorizationEvent,
+ AuthorizationEvents,
+ ContentType,
+ JwtIssuer,
+ ParsedAuthorizationRequestURI,
+ RegisterEventListener,
+ ResponseIss,
+ ResponseMode,
+ SIOPErrors,
+ SIOPResonse,
+ SupportedVersion,
+ UrlEncodingFormat,
+ Verification,
+ VerifiedAuthorizationRequest,
+} from '../types'
+
+import { OPBuilder } from './OPBuilder'
+import { createResponseOptsFromBuilderOrExistingOpts, createVerifyRequestOptsFromBuilderOrExistingOpts } from './Opts'
+
+// The OP publishes the formats it supports using the vp_formats_supported metadata parameter as defined above in its "openid-configuration".
+export class OP {
+ private readonly _createResponseOptions: AuthorizationResponseOpts
+ private readonly _verifyRequestOptions: Partial
+ private readonly _eventEmitter?: EventEmitter
+
+ private constructor(opts: { builder?: OPBuilder; responseOpts?: AuthorizationResponseOpts; verifyOpts?: VerifyAuthorizationRequestOpts }) {
+ this._createResponseOptions = { ...createResponseOptsFromBuilderOrExistingOpts(opts) }
+ this._verifyRequestOptions = { ...createVerifyRequestOptsFromBuilderOrExistingOpts(opts) }
+ this._eventEmitter = opts.builder?.eventEmitter
+ }
+
+ /**
+ * This method tries to infer the SIOP specs version based on the request payload.
+ * If the version cannot be inferred or is not supported it throws an exception.
+ * This method needs to be called to ensure the OP can handle the request
+ * @param requestJwtOrUri
+ * @param requestOpts
+ */
+
+ public async verifyAuthorizationRequest(
+ requestJwtOrUri: string | URI,
+ requestOpts?: { correlationId?: string; verification?: Verification },
+ ): Promise {
+ const correlationId = requestOpts?.correlationId || uuidv4()
+ const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestJwtOrUri)
+ .then((result: AuthorizationRequest) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_REQUEST_RECEIVED_SUCCESS, { correlationId, subject: result })
+ return result
+ })
+ .catch((error: Error) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_REQUEST_RECEIVED_FAILED, {
+ correlationId,
+ subject: requestJwtOrUri,
+ error,
+ })
+ throw error
+ })
+
+ return authorizationRequest
+ .verify(this.newVerifyAuthorizationRequestOpts({ ...requestOpts, correlationId }))
+ .then((verifiedAuthorizationRequest: VerifiedAuthorizationRequest) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_REQUEST_VERIFIED_SUCCESS, {
+ correlationId,
+ subject: verifiedAuthorizationRequest.authorizationRequest,
+ })
+ return verifiedAuthorizationRequest
+ })
+ .catch((error) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_REQUEST_VERIFIED_FAILED, {
+ correlationId,
+ subject: authorizationRequest,
+ error,
+ })
+ throw error
+ })
+ }
+
+ public async createAuthorizationResponse(
+ verifiedAuthorizationRequest: VerifiedAuthorizationRequest,
+ responseOpts: {
+ jwtIssuer?: JwtIssuer
+ version?: SupportedVersion
+ correlationId?: string
+ audience?: string
+ issuer?: ResponseIss | string
+ verification?: Verification
+ presentationExchange?: PresentationExchangeResponseOpts
+ },
+ ): Promise {
+ if (
+ verifiedAuthorizationRequest.correlationId &&
+ responseOpts?.correlationId &&
+ verifiedAuthorizationRequest.correlationId !== responseOpts.correlationId
+ ) {
+ throw new Error(
+ `Request correlation id ${verifiedAuthorizationRequest.correlationId} is different from option correlation id ${responseOpts.correlationId}`,
+ )
+ }
+ let version = responseOpts?.version
+ const rpSupportedVersions = authorizationRequestVersionDiscovery(await verifiedAuthorizationRequest.authorizationRequest.mergedPayloads())
+ if (version && rpSupportedVersions.length > 0 && !rpSupportedVersions.includes(version)) {
+ throw Error(`RP does not support spec version ${version}, supported versions: ${rpSupportedVersions.toString()}`)
+ } else if (!version) {
+ version = rpSupportedVersions.reduce(
+ (previous, current) => (current.valueOf() > previous.valueOf() ? current : previous),
+ SupportedVersion.SIOPv2_ID1,
+ )
+ }
+ const correlationId = responseOpts?.correlationId ?? verifiedAuthorizationRequest.correlationId ?? uuidv4()
+ try {
+ // IF using DIRECT_POST, the response_uri takes precedence over the redirect_uri
+ let responseUri = verifiedAuthorizationRequest.responseURI
+ if (verifiedAuthorizationRequest.authorizationRequestPayload.response_mode === ResponseMode.DIRECT_POST) {
+ responseUri = verifiedAuthorizationRequest.authorizationRequestPayload.response_uri ?? responseUri
+ }
+
+ const response = await AuthorizationResponse.fromVerifiedAuthorizationRequest(
+ verifiedAuthorizationRequest,
+ this.newAuthorizationResponseOpts({
+ ...responseOpts,
+ version,
+ correlationId,
+ }),
+ verifiedAuthorizationRequest.verifyOpts,
+ )
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_CREATE_SUCCESS, {
+ correlationId,
+ subject: response,
+ })
+ return { correlationId, response, responseURI: responseUri }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_CREATE_FAILED, {
+ correlationId,
+ subject: verifiedAuthorizationRequest.authorizationRequest,
+ error,
+ })
+ throw error
+ }
+ }
+
+ // TODO SK Can you please put some documentation on it?
+ public async submitAuthorizationResponse(authorizationResponse: AuthorizationResponseWithCorrelationId): Promise {
+ const { correlationId, response } = authorizationResponse
+ if (!correlationId) {
+ throw Error('No correlation Id provided')
+ }
+ if (
+ !response ||
+ (response.options?.responseMode &&
+ !(
+ response.options.responseMode === ResponseMode.POST ||
+ response.options.responseMode === ResponseMode.FORM_POST ||
+ response.options.responseMode === ResponseMode.DIRECT_POST
+ ))
+ ) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ }
+
+ const payload = response.payload
+ const idToken = await response.idToken?.payload()
+ const responseUri = authorizationResponse.responseURI ?? idToken?.aud
+ if (!responseUri) {
+ throw Error('No response URI present')
+ }
+ const authResponseAsURI = encodeJsonAsURI(payload, { arraysWithIndex: ['presentation_submission', 'vp_token'] })
+ return post(responseUri, authResponseAsURI, { contentType: ContentType.FORM_URL_ENCODED, exceptionOnHttpErrorStatus: true })
+ .then((result: SIOPResonse) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_SUCCESS, { correlationId, subject: response })
+ return result.origResponse
+ })
+ .catch((error: Error) => {
+ void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_FAILED, { correlationId, subject: response, error })
+ throw error
+ })
+ }
+
+ /**
+ * Create an Authentication Request Payload from a URI string
+ *
+ * @param encodedUri
+ */
+ public async parseAuthorizationRequestURI(encodedUri: string): Promise {
+ const { scheme, requestObjectJwt, authorizationRequestPayload, registrationMetadata } = await URI.parseAndResolve(encodedUri)
+
+ return {
+ encodedUri,
+ encodingFormat: UrlEncodingFormat.FORM_URL_ENCODED,
+ scheme: scheme,
+ requestObjectJwt,
+ authorizationRequestPayload,
+ registration: registrationMetadata,
+ }
+ }
+
+ private newAuthorizationResponseOpts(opts: {
+ correlationId: string
+ version?: SupportedVersion
+ issuer?: IIssuerId | ResponseIss
+ audience?: string
+ presentationExchange?: PresentationExchangeResponseOpts
+ }): AuthorizationResponseOpts {
+ const version = opts.version ?? this._createResponseOptions.version
+ let issuer = opts.issuer ?? this._createResponseOptions?.registration?.issuer
+ if (version === SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1) {
+ issuer = ResponseIss.JWT_VC_PRESENTATION_V1
+ } else if (version === SupportedVersion.SIOPv2_ID1) {
+ issuer = ResponseIss.SELF_ISSUED_V2
+ }
+
+ if (!issuer) {
+ throw Error(`No issuer value present. Either use IDv1, JWT VC Presentation profile version, or provide a DID as issuer value`)
+ }
+ // We are taking the whole presentationExchange object from a certain location
+ const presentationExchange = opts.presentationExchange ?? this._createResponseOptions.presentationExchange
+ const responseURI = opts.audience ?? this._createResponseOptions.responseURI
+ return {
+ ...this._createResponseOptions,
+ ...opts,
+ ...(presentationExchange && { presentationExchange }),
+ registration: { ...this._createResponseOptions?.registration, issuer },
+ responseURI,
+ responseURIType:
+ this._createResponseOptions.responseURIType ?? (version < SupportedVersion.SIOPv2_D12_OID4VP_D18 && responseURI ? 'redirect_uri' : undefined),
+ }
+ }
+
+ private newVerifyAuthorizationRequestOpts(requestOpts: { correlationId: string; verification?: Verification }): VerifyAuthorizationRequestOpts {
+ const verification: VerifyAuthorizationRequestOpts = {
+ ...this._verifyRequestOptions,
+ verifyJwtCallback: this._verifyRequestOptions.verifyJwtCallback,
+ ...requestOpts,
+ verification: mergeVerificationOpts(this._verifyRequestOptions, requestOpts),
+ correlationId: requestOpts.correlationId,
+ }
+
+ return verification
+ }
+
+ private async emitEvent(
+ type: AuthorizationEvents,
+ payload: {
+ correlationId: string
+ subject: AuthorizationRequest | AuthorizationResponse | string | URI
+ error?: Error
+ },
+ ): Promise {
+ if (this._eventEmitter) {
+ this._eventEmitter.emit(type, new AuthorizationEvent(payload))
+ }
+ }
+
+ public addEventListener(register: RegisterEventListener) {
+ if (!this._eventEmitter) {
+ throw Error('Cannot add listeners if no event emitter is available')
+ }
+ const events = Array.isArray(register.event) ? register.event : [register.event]
+ for (const event of events) {
+ this._eventEmitter.addListener(event, register.listener)
+ }
+ }
+
+ public static fromOpts(responseOpts: AuthorizationResponseOpts, verifyOpts: VerifyAuthorizationRequestOpts): OP {
+ return new OP({ responseOpts, verifyOpts })
+ }
+
+ public static builder() {
+ return new OPBuilder()
+ }
+
+ get createResponseOptions(): AuthorizationResponseOpts {
+ return this._createResponseOptions
+ }
+
+ get verifyRequestOptions(): Partial {
+ return this._verifyRequestOptions
+ }
+}
diff --git a/packages/siop-oid4vp/lib/op/OPBuilder.ts b/packages/siop-oid4vp/lib/op/OPBuilder.ts
new file mode 100644
index 00000000..c27ab259
--- /dev/null
+++ b/packages/siop-oid4vp/lib/op/OPBuilder.ts
@@ -0,0 +1,115 @@
+import { EventEmitter } from 'events'
+
+import { Hasher, IIssuerId } from '@sphereon/ssi-types'
+
+import { PropertyTargets } from '../authorization-request'
+import { PresentationSignCallback } from '../authorization-response'
+import { ResponseIss, ResponseMode, ResponseRegistrationOpts, SupportedVersion, VerifyJwtCallback } from '../types'
+import { CreateJwtCallback } from '../types/JwtIssuer'
+
+import { OP } from './OP'
+
+export class OPBuilder {
+ expiresIn?: number
+ issuer?: IIssuerId | ResponseIss
+ responseMode?: ResponseMode = ResponseMode.DIRECT_POST
+ responseRegistration?: Partial = {}
+ createJwtCallback?: CreateJwtCallback
+ verifyJwtCallback?: VerifyJwtCallback
+ presentationSignCallback?: PresentationSignCallback
+ supportedVersions?: SupportedVersion[]
+ eventEmitter?: EventEmitter
+
+ hasher?: Hasher
+
+ withHasher(hasher: Hasher): OPBuilder {
+ this.hasher = hasher
+
+ return this
+ }
+
+ withIssuer(issuer: ResponseIss | string): OPBuilder {
+ this.issuer = issuer
+ return this
+ }
+
+ withExpiresIn(expiresIn: number): OPBuilder {
+ this.expiresIn = expiresIn
+ return this
+ }
+
+ withResponseMode(responseMode: ResponseMode): OPBuilder {
+ this.responseMode = responseMode
+ return this
+ }
+
+ withRegistration(responseRegistration: ResponseRegistrationOpts, targets?: PropertyTargets): OPBuilder {
+ this.responseRegistration = {
+ targets,
+ ...responseRegistration,
+ }
+ return this
+ }
+
+ /*//TODO registration object creation
+ authorizationEndpoint?: Schema.OPENID | string;
+ scopesSupported?: Scope[] | Scope;
+ subjectTypesSupported?: SubjectType[] | SubjectType;
+ idTokenSigningAlgValuesSupported?: SigningAlgo[] | SigningAlgo;
+ requestObjectSigningAlgValuesSupported?: SigningAlgo[] | SigningAlgo;
+*/
+
+ withCreateJwtCallback(createJwtCallback: CreateJwtCallback): OPBuilder {
+ this.createJwtCallback = createJwtCallback
+ return this
+ }
+
+ withVerifyJwtCallback(verifyJwtCallback: VerifyJwtCallback): OPBuilder {
+ this.verifyJwtCallback = verifyJwtCallback
+ return this
+ }
+
+ withSupportedVersions(supportedVersions: SupportedVersion[] | SupportedVersion | string[] | string): OPBuilder {
+ const versions = Array.isArray(supportedVersions) ? supportedVersions : [supportedVersions]
+ for (const version of versions) {
+ this.addSupportedVersion(version)
+ }
+ return this
+ }
+
+ addSupportedVersion(supportedVersion: string | SupportedVersion): OPBuilder {
+ if (!this.supportedVersions) {
+ this.supportedVersions = []
+ }
+ if (typeof supportedVersion === 'string') {
+ this.supportedVersions.push(SupportedVersion[supportedVersion])
+ } else {
+ this.supportedVersions.push(supportedVersion)
+ }
+ return this
+ }
+
+ withPresentationSignCallback(presentationSignCallback: PresentationSignCallback): OPBuilder {
+ this.presentationSignCallback = presentationSignCallback
+ return this
+ }
+
+ withEventEmitter(eventEmitter?: EventEmitter): OPBuilder {
+ this.eventEmitter = eventEmitter ?? new EventEmitter()
+ return this
+ }
+
+ build(): OP {
+ /*if (!this.responseRegistration) {
+ throw Error('You need to provide response registrations values')
+ } else */ /*if (!this.withSignature) {
+ throw Error('You need to supply withSignature values');
+ } else */ if (!this.supportedVersions || this.supportedVersions.length === 0) {
+ this.supportedVersions = [SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_ID1, SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1]
+ }
+ // We ignore the private visibility, as we don't want others to use the OP directly
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return new OP({ builder: this })
+ }
+}
diff --git a/packages/siop-oid4vp/lib/op/Opts.ts b/packages/siop-oid4vp/lib/op/Opts.ts
new file mode 100644
index 00000000..7ea19624
--- /dev/null
+++ b/packages/siop-oid4vp/lib/op/Opts.ts
@@ -0,0 +1,72 @@
+import { VerifyAuthorizationRequestOpts } from '../authorization-request'
+import { AuthorizationResponseOpts } from '../authorization-response'
+import { LanguageTagUtils } from '../helpers'
+import { AuthorizationResponseOptsSchema } from '../schemas'
+import { PassBy, ResponseRegistrationOpts } from '../types'
+
+import { OPBuilder } from './OPBuilder'
+
+export const createResponseOptsFromBuilderOrExistingOpts = (opts: {
+ builder?: OPBuilder
+ responseOpts?: AuthorizationResponseOpts
+}): AuthorizationResponseOpts => {
+ let responseOpts: AuthorizationResponseOpts
+ if (opts.builder) {
+ responseOpts = {
+ registration: {
+ issuer: opts.builder.issuer,
+ ...(opts.builder.responseRegistration as ResponseRegistrationOpts),
+ },
+ expiresIn: opts.builder.expiresIn,
+ jwtIssuer: responseOpts?.jwtIssuer,
+ createJwtCallback: opts.builder.createJwtCallback,
+ responseMode: opts.builder.responseMode,
+ ...(responseOpts?.version
+ ? { version: responseOpts.version }
+ : Array.isArray(opts.builder.supportedVersions) && opts.builder.supportedVersions.length > 0
+ ? { version: opts.builder.supportedVersions[0] }
+ : {}),
+ }
+
+ if (!responseOpts.registration.passBy) {
+ responseOpts.registration.passBy = PassBy.VALUE
+ }
+ const languageTagEnabledFieldsNames = ['clientName', 'clientPurpose']
+ const languageTaggedFields: Map = LanguageTagUtils.getLanguageTaggedProperties(
+ opts.builder.responseRegistration,
+ languageTagEnabledFieldsNames,
+ )
+
+ languageTaggedFields.forEach((value: string, key: string) => {
+ responseOpts.registration[key] = value
+ })
+ } else {
+ responseOpts = {
+ ...opts.responseOpts,
+ }
+ }
+
+ const valid = AuthorizationResponseOptsSchema(responseOpts)
+ if (!valid) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ throw new Error('OP builder validation error: ' + JSON.stringify(AuthorizationResponseOptsSchema.errors))
+ }
+
+ return responseOpts
+}
+
+export const createVerifyRequestOptsFromBuilderOrExistingOpts = (opts: {
+ builder?: OPBuilder
+ verifyOpts?: VerifyAuthorizationRequestOpts
+}): VerifyAuthorizationRequestOpts => {
+ return opts.builder
+ ? {
+ verifyJwtCallback: opts.builder.verifyJwtCallback,
+ hasher: opts.builder.hasher,
+ verification: {},
+ supportedVersions: opts.builder.supportedVersions,
+ correlationId: undefined,
+ }
+ : opts.verifyOpts
+}
diff --git a/packages/siop-oid4vp/lib/op/index.ts b/packages/siop-oid4vp/lib/op/index.ts
new file mode 100644
index 00000000..c3a5427a
--- /dev/null
+++ b/packages/siop-oid4vp/lib/op/index.ts
@@ -0,0 +1,2 @@
+export * from './OP'
+export * from './OPBuilder'
diff --git a/packages/siop-oid4vp/lib/request-object/Opts.ts b/packages/siop-oid4vp/lib/request-object/Opts.ts
new file mode 100644
index 00000000..465b5201
--- /dev/null
+++ b/packages/siop-oid4vp/lib/request-object/Opts.ts
@@ -0,0 +1,22 @@
+import { ClaimPayloadCommonOpts } from '../authorization-request'
+import { PassBy, SIOPErrors } from '../types'
+
+import { RequestObjectOpts } from './types'
+
+export const assertValidRequestObjectOpts = (opts: RequestObjectOpts, checkRequestObject: boolean) => {
+ if (!opts) {
+ throw new Error(SIOPErrors.BAD_PARAMS)
+ } else if (opts.passBy !== PassBy.REFERENCE && opts.passBy !== PassBy.VALUE) {
+ throw new Error(SIOPErrors.REQUEST_OBJECT_TYPE_NOT_SET)
+ } else if (opts.passBy === PassBy.REFERENCE && !opts.reference_uri) {
+ throw new Error(SIOPErrors.NO_REFERENCE_URI)
+ } else if (!opts.payload) {
+ if (opts.reference_uri) {
+ // reference URI, but no actual payload to host there!
+ throw Error(SIOPErrors.REFERENCE_URI_NO_PAYLOAD)
+ } else if (checkRequestObject) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ }
+ // assertValidRequestRegistrationOpts(opts['registration'] ? opts['registration'] : opts['clientMetadata']);
+}
diff --git a/packages/siop-oid4vp/lib/request-object/Payload.ts b/packages/siop-oid4vp/lib/request-object/Payload.ts
new file mode 100644
index 00000000..13d88776
--- /dev/null
+++ b/packages/siop-oid4vp/lib/request-object/Payload.ts
@@ -0,0 +1,61 @@
+import { v4 as uuidv4 } from 'uuid'
+
+import { CreateAuthorizationRequestOpts, createPresentationDefinitionClaimsProperties } from '../authorization-request'
+import { createRequestRegistration } from '../authorization-request/RequestRegistration'
+import { getNonce, getState, removeNullUndefined } from '../helpers'
+import { RequestObjectPayload, ResponseMode, ResponseType, Scope, SIOPErrors, SupportedVersion } from '../types'
+
+import { assertValidRequestObjectOpts } from './Opts'
+
+export const createRequestObjectPayload = async (opts: CreateAuthorizationRequestOpts): Promise => {
+ assertValidRequestObjectOpts(opts.requestObject, false)
+ if (!opts.requestObject?.payload) {
+ return undefined // No request object apparently
+ }
+ assertValidRequestObjectOpts(opts.requestObject, true)
+
+ const payload = opts.requestObject.payload
+
+ const state = getState(payload.state)
+ const registration = await createRequestRegistration(opts.clientMetadata, opts)
+ const claims = createPresentationDefinitionClaimsProperties(payload.claims)
+
+ const metadataKey = opts.version >= SupportedVersion.SIOPv2_D11.valueOf() ? 'client_metadata' : 'registration'
+ const clientId = payload.client_id ?? registration.payload[metadataKey]?.client_id
+
+ const now = Math.round(new Date().getTime() / 1000)
+ const validInSec = 120 // todo config/option
+ const iat = payload.iat ?? now
+ const nbf = payload.nbf ?? iat
+ const exp = payload.exp ?? iat + validInSec
+ const jti = payload.jti ?? uuidv4()
+
+ return removeNullUndefined({
+ response_type: payload.response_type ?? ResponseType.ID_TOKEN,
+ scope: payload.scope ?? Scope.OPENID,
+ //TODO implement /.well-known/openid-federation support in the OP side to resolve the client_id (URL) and retrieve the metadata
+ client_id: clientId,
+ client_id_scheme: opts.requestObject.payload.client_id_scheme,
+ ...(payload.redirect_uri && { redirect_uri: payload.redirect_uri }),
+ ...(payload.response_uri && { response_uri: payload.response_uri }),
+ response_mode: payload.response_mode ?? ResponseMode.DIRECT_POST,
+ ...(payload.id_token_hint && { id_token_hint: payload.id_token_hint }),
+ registration_uri: registration.clientMetadataOpts.reference_uri,
+ nonce: getNonce(state, payload.nonce),
+ state,
+ ...registration.payload,
+ claims,
+ presentation_definition_uri: payload.presentation_definition_uri,
+ presentation_definition: payload.presentation_definition,
+ iat,
+ nbf,
+ exp,
+ jti,
+ })
+}
+
+export const assertValidRequestObjectPayload = (verPayload: RequestObjectPayload): void => {
+ if (verPayload['registration_uri'] && verPayload['registration']) {
+ throw new Error(`${SIOPErrors.REG_OBJ_N_REG_URI_CANT_BE_SET_SIMULTANEOUSLY}`)
+ }
+}
diff --git a/packages/siop-oid4vp/lib/request-object/RequestObject.ts b/packages/siop-oid4vp/lib/request-object/RequestObject.ts
new file mode 100644
index 00000000..da606bbb
--- /dev/null
+++ b/packages/siop-oid4vp/lib/request-object/RequestObject.ts
@@ -0,0 +1,170 @@
+import { ClaimPayloadCommonOpts, ClaimPayloadOptsVID1, CreateAuthorizationRequestOpts } from '../authorization-request'
+import { assertValidAuthorizationRequestOpts } from '../authorization-request/Opts'
+import { fetchByReferenceOrUseByValue, removeNullUndefined } from '../helpers'
+import { parseJWT } from '../helpers/jwtUtils'
+import { AuthorizationRequestPayload, JwtIssuer, JwtIssuerWithContext, RequestObjectJwt, RequestObjectPayload, SIOPErrors } from '../types'
+
+import { assertValidRequestObjectOpts } from './Opts'
+import { assertValidRequestObjectPayload, createRequestObjectPayload } from './Payload'
+import { RequestObjectOpts } from './types'
+
+export class RequestObject {
+ private payload: RequestObjectPayload
+ private jwt?: RequestObjectJwt
+ private readonly opts: RequestObjectOpts
+
+ private constructor(
+ opts?: CreateAuthorizationRequestOpts | RequestObjectOpts,
+ payload?: RequestObjectPayload,
+ jwt?: string,
+ ) {
+ this.opts = opts ? RequestObject.mergeOAuth2AndOpenIdProperties(opts) : undefined
+ this.payload = payload
+ this.jwt = jwt
+ }
+
+ /**
+ * Create a request object that typically is used as a JWT on RP side, typically this method is called automatically when creating an Authorization Request, but you could use it directly!
+ *
+ * @param authorizationRequestOpts Request Object options to build a Request Object
+ * @remarks This method is used to generate a SIOP request Object.
+ * First it generates the request object payload, and then it a signed JWT can be accessed on request.
+ *
+ * Normally you will want to use the Authorization Request class. That class creates a URI that includes the JWT from this class in the URI
+ * If you do use this class directly, you can call the `convertRequestObjectToURI` afterwards to get the URI.
+ * Please note that the Authorization Request allows you to differentiate between OAuth2 and OpenID parameters that become
+ * part of the URI and which become part of the Request Object. If you generate a URI based upon the result of this class,
+ * the URI will be constructed based on the Request Object only!
+ */
+ public static async fromOpts(authorizationRequestOpts: CreateAuthorizationRequestOpts) {
+ assertValidAuthorizationRequestOpts(authorizationRequestOpts)
+ const createJwtCallback = authorizationRequestOpts.requestObject.createJwtCallback // We copy the signature separately as it can contain a function, which would be removed in the merge function below
+ const jwtIssuer = authorizationRequestOpts.requestObject.jwtIssuer // We copy the signature separately as it can contain a function, which would be removed in the merge function below
+ const requestObjectOpts = RequestObject.mergeOAuth2AndOpenIdProperties(authorizationRequestOpts)
+ const mergedOpts = {
+ ...authorizationRequestOpts,
+ requestObject: { ...authorizationRequestOpts.requestObject, ...requestObjectOpts, createJwtCallback, jwtIssuer },
+ }
+ return new RequestObject(mergedOpts, await createRequestObjectPayload(mergedOpts))
+ }
+
+ public static async fromJwt(requestObjectJwt: RequestObjectJwt) {
+ return requestObjectJwt ? new RequestObject(undefined, undefined, requestObjectJwt) : undefined
+ }
+
+ public static async fromPayload(requestObjectPayload: RequestObjectPayload, authorizationRequestOpts: CreateAuthorizationRequestOpts) {
+ return new RequestObject(authorizationRequestOpts, requestObjectPayload)
+ }
+
+ public static async fromAuthorizationRequestPayload(payload: AuthorizationRequestPayload): Promise {
+ const requestObjectJwt =
+ payload.request || payload.request_uri ? await fetchByReferenceOrUseByValue(payload.request_uri, payload.request, true) : undefined
+ return requestObjectJwt ? await RequestObject.fromJwt(requestObjectJwt) : undefined
+ }
+
+ public async toJwt(): Promise {
+ if (!this.jwt) {
+ if (!this.opts) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ } else if (!this.payload) {
+ return undefined
+ }
+ this.removeRequestProperties()
+ if (this.payload.registration_uri) {
+ delete this.payload.registration
+ }
+ assertValidRequestObjectPayload(this.payload)
+
+ const jwtIssuer: JwtIssuerWithContext = this.opts.jwtIssuer
+ ? { ...this.opts.jwtIssuer, type: 'request-object' }
+ : { method: 'custom', type: 'request-object' }
+
+ if (jwtIssuer.method === 'custom') {
+ this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header: {}, payload: this.payload })
+ } else if (jwtIssuer.method === 'did') {
+ const did = jwtIssuer.didUrl.split('#')[0]
+ this.payload.iss = this.payload.iss ?? did
+ this.payload.sub = this.payload.sub ?? did
+ this.payload.client_id = this.payload.client_id ?? did
+
+ const header = { kid: jwtIssuer.didUrl, alg: jwtIssuer.alg, typ: 'JWT' }
+ this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload })
+ } else if (jwtIssuer.method === 'x5c') {
+ this.payload.iss = jwtIssuer.issuer
+ this.payload.client_id = jwtIssuer.issuer
+ this.payload.redirect_uri = jwtIssuer.issuer
+ this.payload.client_id_scheme = jwtIssuer.clientIdScheme
+
+ const header = { x5c: jwtIssuer.x5c, typ: 'JWT' }
+ this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload })
+ } else if (jwtIssuer.method === 'jwk') {
+ if (!this.payload.client_id) {
+ throw new Error('Please provide a client_id for the RP')
+ }
+
+ const header = { jwk: jwtIssuer.jwk, typ: 'JWT', alg: jwtIssuer.jwk.alg as string }
+ this.jwt = await this.opts.createJwtCallback(jwtIssuer, { header, payload: this.payload })
+ } else {
+ throw new Error(`JwtIssuer method '${(jwtIssuer as JwtIssuer).method}' not implemented`)
+ }
+ }
+ return this.jwt
+ }
+
+ public async getPayload(): Promise {
+ if (!this.payload) {
+ if (!this.jwt) {
+ return undefined
+ }
+ this.payload = removeNullUndefined(parseJWT(this.jwt).payload) as RequestObjectPayload
+ this.removeRequestProperties()
+ if (this.payload.registration_uri) {
+ delete this.payload.registration
+ } else if (this.payload.registration) {
+ delete this.payload.registration_uri
+ }
+ }
+ assertValidRequestObjectPayload(this.payload)
+ return this.payload
+ }
+
+ public async assertValid(): Promise {
+ if (this.options) {
+ assertValidRequestObjectOpts(this.options, false)
+ }
+ assertValidRequestObjectPayload(await this.getPayload())
+ }
+
+ public get options(): RequestObjectOpts | undefined {
+ return this.opts
+ }
+
+ private removeRequestProperties(): void {
+ if (this.payload) {
+ // https://openid.net/specs/openid-connect-core-1_0.html#RequestObject
+ // request and request_uri parameters MUST NOT be included in Request Objects.
+ delete this.payload.request
+ delete this.payload.request_uri
+ }
+ }
+
+ private static mergeOAuth2AndOpenIdProperties(
+ opts: CreateAuthorizationRequestOpts | RequestObjectOpts,
+ ): RequestObjectOpts {
+ if (!opts) {
+ throw Error(SIOPErrors.BAD_PARAMS)
+ }
+ const isAuthReq = opts['requestObject'] !== undefined
+ const mergedOpts = JSON.parse(JSON.stringify(opts))
+ const createJwtCallback = opts['requestObject']?.createJwtCallback
+ if (createJwtCallback) {
+ mergedOpts.requestObject.createJwtCallback = createJwtCallback
+ }
+ const jwtIssuer = opts['requestObject']?.jwtIssuer
+ if (createJwtCallback) {
+ mergedOpts.requestObject.jwtIssuer = jwtIssuer
+ }
+ delete mergedOpts?.request?.requestObject
+ return isAuthReq ? mergedOpts.requestObject : mergedOpts
+ }
+}
diff --git a/packages/siop-oid4vp/lib/request-object/index.ts b/packages/siop-oid4vp/lib/request-object/index.ts
new file mode 100644
index 00000000..1ac61c7e
--- /dev/null
+++ b/packages/siop-oid4vp/lib/request-object/index.ts
@@ -0,0 +1,3 @@
+export * from './RequestObject'
+export * from './types'
+export * from './Payload'
diff --git a/packages/siop-oid4vp/lib/request-object/types.ts b/packages/siop-oid4vp/lib/request-object/types.ts
new file mode 100644
index 00000000..2283b19f
--- /dev/null
+++ b/packages/siop-oid4vp/lib/request-object/types.ts
@@ -0,0 +1,9 @@
+import { ClaimPayloadCommonOpts, RequestObjectPayloadOpts } from '../authorization-request'
+import { ObjectBy } from '../types'
+import { CreateJwtCallback, JwtIssuer } from '../types/JwtIssuer'
+
+export interface RequestObjectOpts extends ObjectBy {
+ payload?: RequestObjectPayloadOpts // for pass by value
+ createJwtCallback: CreateJwtCallback
+ jwtIssuer: JwtIssuer
+}
diff --git a/packages/siop-oid4vp/lib/rp/InMemoryRPSessionManager.ts b/packages/siop-oid4vp/lib/rp/InMemoryRPSessionManager.ts
new file mode 100644
index 00000000..5c3bd170
--- /dev/null
+++ b/packages/siop-oid4vp/lib/rp/InMemoryRPSessionManager.ts
@@ -0,0 +1,263 @@
+import { EventEmitter } from 'events'
+
+import { AuthorizationRequest } from '../authorization-request'
+import { AuthorizationResponse } from '../authorization-response'
+import {
+ AuthorizationEvent,
+ AuthorizationEvents,
+ AuthorizationRequestState,
+ AuthorizationRequestStateStatus,
+ AuthorizationResponseState,
+ AuthorizationResponseStateStatus,
+} from '../types'
+
+import { IRPSessionManager } from './types'
+
+/**
+ * Please note that this session manager is not really meant to be used in large production settings, as it stores everything in memory!
+ * It also doesn't do scheduled cleanups. It runs a cleanup whenever a request or response is received. In a high-volume production setting you will want scheduled cleanups running in the background
+ * Since this is a low level library we have not created a full-fledged implementation.
+ * We suggest to create your own implementation using the event system of the library
+ */
+export class InMemoryRPSessionManager implements IRPSessionManager {
+ private readonly authorizationRequests: Record = {}
+ private readonly authorizationResponses: Record = {}
+
+ // stored by hashcode
+ private readonly nonceMapping: Record = {}
+ // stored by hashcode
+ private readonly stateMapping: Record = {}
+ private readonly maxAgeInSeconds: number
+
+ private static getKeysForCorrelationId(mapping: Record, correlationId: string): number[] {
+ return Object.entries(mapping)
+ .filter((entry) => entry[1] === correlationId)
+ .map((filtered) => Number.parseInt(filtered[0]))
+ }
+
+ public constructor(eventEmitter: EventEmitter, opts?: { maxAgeInSeconds?: number }) {
+ if (!eventEmitter) {
+ throw Error('RP Session manager depends on an event emitter in the application')
+ }
+ this.maxAgeInSeconds = opts?.maxAgeInSeconds ?? 5 * 60
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, this.onAuthorizationRequestCreatedSuccess.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, this.onAuthorizationRequestCreatedFailed.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, this.onAuthorizationRequestSentFailed.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, this.onAuthorizationResponseReceivedSuccess.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, this.onAuthorizationResponseReceivedFailed.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, this.onAuthorizationResponseVerifiedSuccess.bind(this))
+ eventEmitter.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, this.onAuthorizationResponseVerifiedFailed.bind(this))
+ }
+
+ async getRequestStateByCorrelationId(correlationId: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('correlationId', correlationId, this.authorizationRequests, errorOnNotFound)
+ }
+
+ async getRequestStateByNonce(nonce: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('nonce', nonce, this.authorizationRequests, errorOnNotFound)
+ }
+
+ async getRequestStateByState(state: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('state', state, this.authorizationRequests, errorOnNotFound)
+ }
+
+ async getResponseStateByCorrelationId(correlationId: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('correlationId', correlationId, this.authorizationResponses, errorOnNotFound)
+ }
+
+ async getResponseStateByNonce(nonce: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('nonce', nonce, this.authorizationResponses, errorOnNotFound)
+ }
+
+ async getResponseStateByState(state: string, errorOnNotFound?: boolean): Promise {
+ return await this.getFromMapping('state', state, this.authorizationResponses, errorOnNotFound)
+ }
+
+ private async getFromMapping(
+ type: 'nonce' | 'state' | 'correlationId',
+ value: string,
+ mapping: Record,
+ errorOnNotFound?: boolean,
+ ): Promise {
+ const correlationId = await this.getCorrelationIdImpl(type, value, errorOnNotFound)
+ const result = mapping[correlationId] as T
+ if (!result && errorOnNotFound) {
+ throw Error(`Could not find ${type} from correlation id ${correlationId}`)
+ }
+ return result
+ }
+
+ private async onAuthorizationRequestCreatedSuccess(event: AuthorizationEvent): Promise {
+ this.cleanup().catch((error) => console.log(JSON.stringify(error)))
+ this.updateState('request', event, AuthorizationRequestStateStatus.CREATED).catch((error) => console.log(JSON.stringify(error)))
+ }
+
+ private async onAuthorizationRequestCreatedFailed(event: AuthorizationEvent): Promise {
+ this.cleanup().catch((error) => console.log(JSON.stringify(error)))
+ this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => console.log(JSON.stringify(error)))
+ }
+
+ private async onAuthorizationRequestSentSuccess(event: AuthorizationEvent): Promise {
+ this.cleanup().catch((error) => console.log(JSON.stringify(error)))
+ this.updateState('request', event, AuthorizationRequestStateStatus.SENT).catch((error) => console.log(JSON.stringify(error)))
+ }
+
+ private async onAuthorizationRequestSentFailed(event: AuthorizationEvent): Promise {
+ this.cleanup().catch((error) => console.log(JSON.stringify(error)))
+ this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => console.log(JSON.stringify(error)))
+ }
+
+ private async onAuthorizationResponseReceivedSuccess(event: AuthorizationEvent): Promise {
+ this.cleanup().catch((error) => console.log(JSON.stringify(error)))
+ await this.updateState('response', event, AuthorizationResponseStateStatus.RECEIVED)
+ }
+
+ private async onAuthorizationResponseReceivedFailed(event: AuthorizationEvent): Promise