diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index 18fb74ca..d88905a3 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -156,7 +156,7 @@ export class AccessTokenClient { return request as AccessTokenRequest; } - + throw new Error('Credential offer request follows neither pre-authorized code nor authorization code flow requirements.'); } diff --git a/packages/common/lib/jwt/jwtUtils.ts b/packages/common/lib/jwt/jwtUtils.ts index 966fc985..b2f96b0a 100644 --- a/packages/common/lib/jwt/jwtUtils.ts +++ b/packages/common/lib/jwt/jwtUtils.ts @@ -41,3 +41,31 @@ export function getNowSkewed(now?: number, skewTime?: number) { export function epochTime() { return Math.floor(Date.now() / 1000); } + +export const BASE64_URL_REGEX = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; + +export const isJws = (jws: string) => { + const jwsParts = jws.split('.'); + return jwsParts.length === 3 && jwsParts.every((part) => BASE64_URL_REGEX.test(part)); +}; +export const isJwe = (jwe: string) => { + const jweParts = jwe.split('.'); + return jweParts.length === 5 && jweParts.every((part) => BASE64_URL_REGEX.test(part)); +}; + +export const decodeProtectedHeader = (jwt: string) => { + return jwtDecode(jwt, { header: true }); +}; + +export const decodeJwt = (jwt: string): JwtPayload => { + return jwtDecode(jwt, { header: false }); +}; + +export const checkExp = (input: { + exp: number; + now?: number; // The number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC). + clockSkew?: number; +}) => { + const { exp, now, clockSkew } = input; + return exp < (now ?? Date.now() / 1000) - (clockSkew ?? 120); +}; diff --git a/packages/jarm/CHANGELOG.md b/packages/jarm/CHANGELOG.md new file mode 100644 index 00000000..6ee7ff6e --- /dev/null +++ b/packages/jarm/CHANGELOG.md @@ -0,0 +1,193 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.16.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.15.1...v0.16.0) (2024-08-02) + +### Bug Fixes + +- add some dpop unit tests ([c24a898](https://github.com/Sphereon-Opensource/OID4VCI/commit/c24a8985b8c788c5947b9493c1d74185a419d7f9)) +- jwk thumprint using crypto.subtle ([56a291c](https://github.com/Sphereon-Opensource/OID4VCI/commit/56a291c2a59c2966fdf428d7cf7e2e69389fd38b)) +- nits ([1a54e69](https://github.com/Sphereon-Opensource/OID4VCI/commit/1a54e6966da62e4796640dd73393fd0fdc5c76b4)) +- prettier + eslint ([57c7592](https://github.com/Sphereon-Opensource/OID4VCI/commit/57c7592f1cd787321d8ded8c89013076b428a9c8)) +- rename common to oid4vc-common ([d89ac4f](https://github.com/Sphereon-Opensource/OID4VCI/commit/d89ac4f4956e69dad5274b197912485665aeb97c)) +- some last nits ([3c71599](https://github.com/Sphereon-Opensource/OID4VCI/commit/3c715992fe8c52e32147c3bc0aaf7c2ea8fb9741)) + +### Features + +- add additional dpop retry mechanisms ([a102854](https://github.com/Sphereon-Opensource/OID4VCI/commit/a1028540432115f26677a860bf6bac10e630a1d9)) +- address feedback part 2 ([01f6d4d](https://github.com/Sphereon-Opensource/OID4VCI/commit/01f6d4d7884c7f49f4395f7ec9ba12ee9b0a8668)) +- create common package ([d5b4b75](https://github.com/Sphereon-Opensource/OID4VCI/commit/d5b4b75f036edcf8082e062214c036c9be934071)) +- dpop support ([9202667](https://github.com/Sphereon-Opensource/OID4VCI/commit/92026678c745b770957f5bae290ae7b456601fd2)) +- incorporate feedback and fix tests ([c7c6af4](https://github.com/Sphereon-Opensource/OID4VCI/commit/c7c6af464d9fda53b86c3095feca5705df9e92cc)) +- incorporate feedback part1 ([f30475a](https://github.com/Sphereon-Opensource/OID4VCI/commit/f30475a8c98f869ffe82e67f59231a4faf182a98)) +- rename common to oid4vci-common ([9efbf32](https://github.com/Sphereon-Opensource/OID4VCI/commit/9efbf32a68ae8b9b91be23c2fb07138181fe5af4)) + +## [0.15.1](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.15.0...v0.15.1) (2024-07-23) + +### Bug Fixes + +- oid4vci draft 13 typing ([6d0bfc9](https://github.com/Sphereon-Opensource/OID4VCI/commit/6d0bfc9227b1120913b773904ef991757cb9282a)) + +# [0.15.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.14.0...v0.15.0) (2024-07-15) + +### Features + +- did-auth-siop-adapter ([32ec2fc](https://github.com/Sphereon-Opensource/OID4VCI/commit/32ec2fc27a22cd069dc12fe011debef7f870cf5d)) + +# [0.14.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.13.0...v0.14.0) (2024-07-06) + +### Features + +- Enable tx_code support for the issuer, and properly handle both the old userPin and tx_code on the client side. fixes [#117](https://github.com/Sphereon-Opensource/OID4VCI/issues/117) ([e54071c](https://github.com/Sphereon-Opensource/OID4VCI/commit/e54071c65b00ef921acafa2c2c73707a3bc33a44)) + +# [0.13.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.12.0...v0.13.0) (2024-07-03) + +### Bug Fixes + +- Make sure we use 'JWT' as typ instead of the lower case version as suggested in the JWT RFC. ([1ff4e40](https://github.com/Sphereon-Opensource/OID4VCI/commit/1ff4e40cefb183072951e3ede3f8b3a5842d645a)) + +### Features + +- add get types from offer function to get the types from multiple versions of credential offers ([b966d8c](https://github.com/Sphereon-Opensource/OID4VCI/commit/b966d8c75bb3df36e816706b961e749b86ae1586)) +- Add support for jwt-bearer client assertions in access token ([ab4905c](https://github.com/Sphereon-Opensource/OID4VCI/commit/ab4905ce7b4465b0c8adce6140209fb2c39f1469)) +- added a facade for CredentialRequestClientBuilder and adjusted the tests ([30cddd3](https://github.com/Sphereon-Opensource/OID4VCI/commit/30cddd3af544e97047d27f48d1d76ce16f80a79b)) +- added x5c support and made sure that we support request-responses without dids ([27bc1d9](https://github.com/Sphereon-Opensource/OID4VCI/commit/27bc1d9522fa74d8016dced63fa415efb6c4eebc)) +- Allow to pass in custom access token request params ([1a469f9](https://github.com/Sphereon-Opensource/OID4VCI/commit/1a469f9f1f07dc54facf831b3336eb706cb0fe7a)) + +# [0.12.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.10.3...v0.12.0) (2024-06-19) + +### Bug Fixes + +- (WIP) fixed all the build errors ([e522a3d](https://github.com/Sphereon-Opensource/OID4VCI/commit/e522a3dd5821fb710211e35c8871f89772b672a0)) +- (WIP) refactored and fixed build. still have to fix 33 test cases that are failing ([ff88a64](https://github.com/Sphereon-Opensource/OID4VCI/commit/ff88a647574baa9813939c296342cc112d00237f)) +- (WIP) refactored and fixed build. still have to fix 8 test cases that are failing ([d8c2c4f](https://github.com/Sphereon-Opensource/OID4VCI/commit/d8c2c4fa8d73ea14a0faa823a394cde23733db8f)) +- (WIP) refactored and fixed parts of the logic for v1_0_13. ([06117c0](https://github.com/Sphereon-Opensource/OID4VCI/commit/06117c0fd9a06170284ce5a89075d5b12fcd7d7b)) +- added back optional vct to CredentialConfigurationSupportedV1_0_13 for sd-jwt ([88341ef](https://github.com/Sphereon-Opensource/OID4VCI/commit/88341ef186c5c2842bf16729ab5c02fae9f22999)) +- added back the isEbsi function to the new version's OpenID4VCIClient ([479bea7](https://github.com/Sphereon-Opensource/OID4VCI/commit/479bea791e2d82a1e564e08a569f4caf205e1cc1)) +- added generic union types for frequently used types ([72474d6](https://github.com/Sphereon-Opensource/OID4VCI/commit/72474d6b95d58914d31ee36875feace8f0432942)) +- added generic union types for frequently used types ([f10d0b2](https://github.com/Sphereon-Opensource/OID4VCI/commit/f10d0b22c4a1c4f6d57fe21d5a7d659f35a3fc27)) +- Ensure we have a single client that handles both v13 and v11 and lower ([eadbba0](https://github.com/Sphereon-Opensource/OID4VCI/commit/eadbba03ddb6e9e32b69bb3a4d9eb9ca8ac2d260)) +- fixed some issue in the IssuerMetadataUtils ([8a6c16f](https://github.com/Sphereon-Opensource/OID4VCI/commit/8a6c16f39fdee838d935edbc46c6842b628f08b7)) +- fixed some issue in the IssuerMetadataUtils plus added some unittests for it ([d348641](https://github.com/Sphereon-Opensource/OID4VCI/commit/d348641523d786d354fff3dfe75dbdda18e2d550)) +- fixed type mismatch in some files ([a2b3c22](https://github.com/Sphereon-Opensource/OID4VCI/commit/a2b3c2294331bceea8c39228b9b3da1c385d01cd)) +- fixes after merge with CWALL-199 ([af967a9](https://github.com/Sphereon-Opensource/OID4VCI/commit/af967a96370f6dce8b9afad296fc2ff1c557dd84)) +- fixes for PAR. Several things were missing, wrong. Higly likely this is a problem for non PAR flows as well ([9ed5064](https://github.com/Sphereon-Opensource/OID4VCI/commit/9ed506466413b6fdb5df7bff50accf3a7a1ad874)) +- MetadataClient for version 13 and added better type distinction. added credential_definition to credential metadata of v13 ([e39bf71](https://github.com/Sphereon-Opensource/OID4VCI/commit/e39bf71625c2a66821061ece7625f0b08f1c0ad2)) +- set client_id on authorization url ([04e7cb8](https://github.com/Sphereon-Opensource/OID4VCI/commit/04e7cb8d60bddca7cea7d8ec04f3072ef989a2c3)) + +### Features + +- Add wallet signing support to VCI and notification support ([c4d3483](https://github.com/Sphereon-Opensource/OID4VCI/commit/c4d34836fb4923c98e7743221978c902c8427f2a)) +- created special type for CredentialRequest v1_0_13 and fixed the tests for it ([25a6051](https://github.com/Sphereon-Opensource/OID4VCI/commit/25a6051ed0bb096c2249f24cd054c1a7aec97f61)) +- expose functions for experimental subject issuer support ([c4adecc](https://github.com/Sphereon-Opensource/OID4VCI/commit/c4adeccdbde6b42a7df85dfbdcb821f2fab8b819)) +- Unify how we get types from different spec versions ([449364b](https://github.com/Sphereon-Opensource/OID4VCI/commit/449364b49db4eaf5b847d5124687f9a3cd4bbc40)) + +## [0.10.3](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.10.2...v0.10.3) (2024-04-25) + +**Note:** Version bump only for package @sphereon/oid4vci-common + +## [0.10.1](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.10.0...v0.10.1) (2024-03-12) + +### Bug Fixes + +- type for cred request ldp ([dbbe447](https://github.com/Sphereon-Opensource/OID4VCI/commit/dbbe44784f60234897c1b9ccdac09259a1226066)) + +# [0.10.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.9.0...v0.10.0) (2024-02-29) + +### Bug Fixes + +- enum type ([c39d8e1](https://github.com/Sphereon-Opensource/OID4VCI/commit/c39d8e1d0b10f6f683dbd229c14e6299a9163e1c)) +- Extend Alg enum to allow for more algorithms. refs [#88](https://github.com/Sphereon-Opensource/OID4VCI/issues/88) ([6e76f57](https://github.com/Sphereon-Opensource/OID4VCI/commit/6e76f5759d2cf989f246ed8a4d45e6c5bd2cb06f)) + +### Features + +- Open the signing algorithm list in the credential issuance process, refs [#88](https://github.com/Sphereon-Opensource/OID4VCI/issues/88) ([d9b17af](https://github.com/Sphereon-Opensource/OID4VCI/commit/d9b17af8098f55b688891de5e536fa95560ef8af)) + +# [0.9.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.8.1...v0.9.0) (2024-02-16) + +### Bug Fixes + +- Add back jwt_vc format support for older versions ([9f06ab1](https://github.com/Sphereon-Opensource/OID4VCI/commit/9f06ab1e0efef89848fb6e6a2b80ed874717e580)) +- Do not set a default redirect_uri, unless no authorization request options are set at all ([6c96089](https://github.com/Sphereon-Opensource/OID4VCI/commit/6c96089f1d328c60cd040f34a3d06ae3b0df392b)) +- Do not set default client_id ([7a1afbc](https://github.com/Sphereon-Opensource/OID4VCI/commit/7a1afbcee3de7c7b0dbe3e32330f0a96e1dcfa1e)) +- Fix uri to json conversion when no required params are provided ([36a70ca](https://github.com/Sphereon-Opensource/OID4VCI/commit/36a70ca634c1caf92555745108ea07c35570b423)) + +### Features + +- Add deferred support ([99dc87d](https://github.com/Sphereon-Opensource/OID4VCI/commit/99dc87d3748cb1f71aa67237b28b6c4bb667eb29)) +- add sd-jwt support ([a37ef06](https://github.com/Sphereon-Opensource/OID4VCI/commit/a37ef06d38fdc7a6d5acc372cd2da8935b4c414e)) +- Add support to get a client id from an offer, and from state JWTs. EBSI for instance is using this ([f089116](https://github.com/Sphereon-Opensource/OID4VCI/commit/f0891164a7a6863940c264afa386144a1e4ac19a)) +- Allow to create an authorization request URL when initiating the OID4VCI client ([84ea215](https://github.com/Sphereon-Opensource/OID4VCI/commit/84ea215c10da042417dabc1d30b2e3898b635bab)) +- PAR improvements ([99f55c2](https://github.com/Sphereon-Opensource/OID4VCI/commit/99f55c23e907022954b0eb169e276f3ef9ffb8ae)) +- PKCE support improvements. ([5d5cb06](https://github.com/Sphereon-Opensource/OID4VCI/commit/5d5cb060fda0790641c1b0d8d513af16ac041970)) +- Support sd-jwt 0.2.0 library ([77c9c24](https://github.com/Sphereon-Opensource/OID4VCI/commit/77c9c246ac994dff1b0ca80eb42819bf9bb1844a)) + +## [0.8.1](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.3...v0.8.1) (2023-10-14) + +**Note:** Version bump only for package @sphereon/oid4vci-common + +## [0.7.3](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.2...v0.7.3) (2023-09-30) + +**Note:** Version bump only for package @sphereon/oid4vci-common + +## [0.7.2](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.1...v0.7.2) (2023-09-28) + +### Bug Fixes + +- id lookup against server metadata not working ([592ec4b](https://github.com/Sphereon-Opensource/OID4VCI/commit/592ec4b837898eb3022d19479d79b6065e7a0d9e)) + +## [0.7.1](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.0...v0.7.1) (2023-09-28) + +### Bug Fixes + +- Better match credential offer types and formats onto issuer metadata ([4044c21](https://github.com/Sphereon-Opensource/OID4VCI/commit/4044c2175b4cbee16f44c8bb5499bba249ca4993)) +- Fix credential offer matching against metadata ([3c23bab](https://github.com/Sphereon-Opensource/OID4VCI/commit/3c23bab83569e04a4b5846fed83ce00d68e8ddce)) +- Fix credential offer matching against metadata ([b79027f](https://github.com/Sphereon-Opensource/OID4VCI/commit/b79027fe601ecccb1373ba399419e14f5ec2d7ff)) +- relax auth_endpoint handling. Doesn't have to be available when doing pre-auth flow. Client handles errors anyway in case of auth/par flow ([cb5f9c1](https://github.com/Sphereon-Opensource/OID4VCI/commit/cb5f9c1c12285508c6d403814d032e8883a59e7d)) + +# [0.7.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.6.0...v0.7.0) (2023-08-19) + +### Bug Fixes + +- fix credential request properties ([0037025](https://github.com/Sphereon-Opensource/OID4VCI/commit/0037025ef27d3a1fa7c68954b1f87e660ef0c82c)) +- Revise well-known metadata retrieval for OID4VCI, OAuth 2.0 and OIDC. fixes [#62](https://github.com/Sphereon-Opensource/OID4VCI/issues/62) ([a750cc7](https://github.com/Sphereon-Opensource/OID4VCI/commit/a750cc76e084f12aeb58f2b1ac44b1bb5e69b5ae)) + +### Features + +- Integrate ssi-express-support to allow for future authn/authz. Also moved endpoints to functions, so solutions can include their own set of endpoints ([c749aba](https://github.com/Sphereon-Opensource/OID4VCI/commit/c749ababd4bec567d6aeeda49b76f195ec792201)) + +# [0.6.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.4.0...v0.6.0) (2023-06-24) + +### Bug Fixes + +- added a couple of todos for handling v11, plus changed the getIssuer method to throw exception if nothing is found, and some other pr notes ([091786e](https://github.com/Sphereon-Opensource/OID4VCI/commit/091786e31246da16f6c9385fc13e7fd3e01664dc)) +- added disable eslint comments in three places ([0e3ffdb](https://github.com/Sphereon-Opensource/OID4VCI/commit/0e3ffdb3a434e142d3bd8d0e04ca0b2b0f8f73e3)) +- made v1_0.09 types strict and added a few utility methods to it for ease of access ([9391f31](https://github.com/Sphereon-Opensource/OID4VCI/commit/9391f317ee41068b823901036c3ac7d4b33ce6dd)) +- Many v11 fixes on server and client side ([08be1ed](https://github.com/Sphereon-Opensource/OID4VCI/commit/08be1ed009fb80e910cffa2e4cf376758798b27e)) +- PAR objects where in the wrong locations and one had a wrong name ([24f98e7](https://github.com/Sphereon-Opensource/OID4VCI/commit/24f98e75137cf70595753cbcf77159584d7ebe08)) +- prettier, plus some type casting in test/mock files for v9 ([162af38](https://github.com/Sphereon-Opensource/OID4VCI/commit/162af3828b3dc826dc3cd5adffe3dab61925ad33)) +- removed type support for mso_mdoc ([867073c](https://github.com/Sphereon-Opensource/OID4VCI/commit/867073ccf3612e6ad869dbc662c791b292fe06ca)) +- rename jwt_vc_json_ld to jwt_vc_json-ld ([a366bef](https://github.com/Sphereon-Opensource/OID4VCI/commit/a366bef5a7bda052de6ffa201186e02b70447a79)) + +### Features + +- Add status support to sessions ([02c7eaf](https://github.com/Sphereon-Opensource/OID4VCI/commit/02c7eaf69af441e15c6302a9c0f2874d54066b32)) +- Add support for alg, kid, did, did document to Jwt Verification callback so we can ensure to set proper values in the resulting VC. ([62dd947](https://github.com/Sphereon-Opensource/OID4VCI/commit/62dd947d0e09360719e6f704db33d766dff2363a)) +- Add support for background_image for credentials ([a3c2561](https://github.com/Sphereon-Opensource/OID4VCI/commit/a3c2561c7596ad7303467528d92cdaa033c7af94)) +- Add supported flow type detection ([100f9e6](https://github.com/Sphereon-Opensource/OID4VCI/commit/100f9e6ccd7c53353f2876be81df4d6e3f7efde4)) +- Add VCI Issuer ([5cab075](https://github.com/Sphereon-Opensource/OID4VCI/commit/5cab07534e7a8b340f7a05343f56fbf091d64738)) +- added better support (and distinction) for types v1.0.09 and v1.0.11 ([f311258](https://github.com/Sphereon-Opensource/OID4VCI/commit/f31125865a3d63ce7719f790fc5ac74fea7f9ade)) +- added callback function for issuing credentials ([c478788](https://github.com/Sphereon-Opensource/OID4VCI/commit/c478788d3d3d2414073eedddd9d43cc3d593ee1b)) +- added error code invalid_scope ([e7864d9](https://github.com/Sphereon-Opensource/OID4VCI/commit/e7864d96476ae8ff21867646c0943975b773d7d5)) +- Added new mock data from actual issuers, fixed a small bug with v1_0_08 types, updated v1_0_08 types to support data from jff issuers ([a6b1eea](https://github.com/Sphereon-Opensource/OID4VCI/commit/a6b1eeaabc0f34cc13a79cf967a8c35a6d8dc7f5)) +- Added new tests for CredentialRequestClient plus fixed a problem with CredentialOfferUtil. a CredentialRequest can have no issuer field ([50f2292](https://github.com/Sphereon-Opensource/OID4VCI/commit/50f22928426761cc3bf5d973d1f15fea407a9175)) +- added support for creating offer deeplink from object and test it. plus some refactors ([a87dcb1](https://github.com/Sphereon-Opensource/OID4VCI/commit/a87dcb1ec10ea26a221d61ec0ffd4b4e098a594f)) +- added support for v8 in our types (partially) to make old logics work ([4b5abf1](https://github.com/Sphereon-Opensource/OID4VCI/commit/4b5abf16507bcde0d696ea3948f816d9a2de13c4)) +- added utility method for recognizing v1.0.11 objects ([ed6436e](https://github.com/Sphereon-Opensource/OID4VCI/commit/ed6436e3bd22307fe9f7b4411ff3c8086ddb940c)) +- added VcIssuer and builders related to that ([c2592a8](https://github.com/Sphereon-Opensource/OID4VCI/commit/c2592a8846061c5791050a76e522f50e21f617de)) +- Ass support to provide credential input data to the issuer whilst creating the offer to be used with a credential data supplier ([03d3e46](https://github.com/Sphereon-Opensource/OID4VCI/commit/03d3e46ab44b2e924320b6aed213c88d2ad161db)) +- Issuer credential offer and more fixes/features ([0bbe17c](https://github.com/Sphereon-Opensource/OID4VCI/commit/0bbe17c13de4df95e2fd79b3470a746cc7a5374a)) +- Support data supplier callback ([1c49cc8](https://github.com/Sphereon-Opensource/OID4VCI/commit/1c49cc80cfd83115956c7e9a040e12e814724e72)) +- Translate v8 credentials_supported to v11 ([b06fa22](https://github.com/Sphereon-Opensource/OID4VCI/commit/b06fa221bed33e69aa76ae0234779f80314f2887)) diff --git a/packages/jarm/LICENSE b/packages/jarm/LICENSE new file mode 100644 index 00000000..5f0d873b --- /dev/null +++ b/packages/jarm/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/jarm/lib/index.ts b/packages/jarm/lib/index.ts new file mode 100644 index 00000000..da75c97f --- /dev/null +++ b/packages/jarm/lib/index.ts @@ -0,0 +1,3 @@ +export * from './jarm-auth-response-send/index.js'; +export * from './jarm-auth-response/index.js'; +export * from './metadata/index.js'; diff --git a/packages/jarm/lib/jarm-auth-response-send/index.ts b/packages/jarm/lib/jarm-auth-response-send/index.ts new file mode 100644 index 00000000..5de821a6 --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response-send/index.ts @@ -0,0 +1 @@ +export * from './jarm-auth-response-send.js'; diff --git a/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts b/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts new file mode 100644 index 00000000..07f0d10f --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts @@ -0,0 +1,76 @@ +import { appendFragmentParams, appendQueryParams } from '../utils.js'; +import type { JarmResponseMode, Openid4vpJarmResponseMode } from '../v-response-mode-registry.js'; +import { getJarmDefaultResponseMode, validateResponseMode } from '../v-response-mode-registry.js'; +import type { ResponseTypeOut } from '../v-response-type-registry.js'; + +interface JarmAuthResponseSendInput { + authRequestParams: { + response_mode?: JarmResponseMode | Openid4vpJarmResponseMode; + response_type: ResponseTypeOut; + } & ( + | { + response_uri: string; + } + | { + redirect_uri: string; + } + ); + + authResponse: string; +} + +export const jarmAuthResponseSend = async (input: JarmAuthResponseSendInput): Promise => { + const { authRequestParams, authResponse } = input; + + const responseEndpoint = 'response_uri' in authRequestParams ? new URL(authRequestParams.response_uri) : new URL(authRequestParams.redirect_uri); + + const responseMode = + authRequestParams.response_mode && authRequestParams.response_mode !== 'jwt' + ? authRequestParams.response_mode + : getJarmDefaultResponseMode(authRequestParams); + + validateResponseMode({ + response_type: authRequestParams.response_type, + response_mode: responseMode, + }); + + switch (responseMode) { + case 'direct_post.jwt': + return handleDirectPostJwt(responseEndpoint, authResponse); + case 'query.jwt': + return handleQueryJwt(responseEndpoint, authResponse); + case 'fragment.jwt': + return handleFragmentJwt(responseEndpoint, authResponse); + case 'form_post.jwt': + throw new Error('Not implemented'); + } +}; + +async function handleDirectPostJwt(responseEndpoint: URL, responseJwt: string) { + const response = await fetch(responseEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `response=${responseJwt}`, + }); + + return response; +} + +async function handleQueryJwt(responseEndpoint: URL, responseJwt: string) { + const responseUrl = appendQueryParams({ + url: responseEndpoint, + params: { response: responseJwt }, + }); + + const response = await fetch(responseUrl, { method: 'POST' }); + return response; +} + +async function handleFragmentJwt(responseEndpoint: URL, responseJwt: string) { + const responseUrl = appendFragmentParams({ + url: responseEndpoint, + fragments: { response: responseJwt }, + }); + const response = await fetch(responseUrl, { method: 'POST' }); + return response; +} diff --git a/packages/jarm/lib/jarm-auth-response/c-jarm-auth-response.ts b/packages/jarm/lib/jarm-auth-response/c-jarm-auth-response.ts new file mode 100644 index 00000000..7c2795ed --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response/c-jarm-auth-response.ts @@ -0,0 +1,41 @@ +import * as v from 'valibot'; + +import { vJarmResponseMode, vOpenid4vpJarmResponseMode } from '../v-response-mode-registry.js'; +import { vResponseType } from '../v-response-type-registry.js'; + +import type { JarmAuthResponseParams } from './v-jarm-auth-response-params.js'; +import type { JarmDirectPostJwtResponseParams } from './v-jarm-direct-post-jwt-auth-response-params.js'; + +export const vAuthRequestParams = v.looseObject({ + state: v.optional(v.string()), + response_mode: v.optional(v.union([vJarmResponseMode, vOpenid4vpJarmResponseMode])), + client_id: v.string(), + response_type: vResponseType, + client_metadata: v.looseObject({ + jwks: v.optional( + v.object({ + keys: v.array(v.looseObject({ kid: v.optional(v.string()), kty: v.string() })), + }), + ), + jwks_uri: v.optional(v.string()), + }), +}); + +export type AuthRequestParams = v.InferInput; + +export const vOAuthAuthRequestGetParamsOut = v.object({ + authRequestParams: vAuthRequestParams, +}); + +export type OAuthAuthRequestGetParamsOut = v.InferOutput; + +export interface JarmDirectPostJwtAuthResponseValidationContext { + openid4vp: { + authRequest: { + getParams: (input: JarmAuthResponseParams | JarmDirectPostJwtResponseParams) => Promise; + }; + }; + jwe: { + decryptCompact: (input: { jwe: string; jwk: { kid: string } }) => Promise<{ plaintext: string }>; + }; +} diff --git a/packages/jarm/lib/jarm-auth-response/index.ts b/packages/jarm/lib/jarm-auth-response/index.ts new file mode 100644 index 00000000..26f460a8 --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response/index.ts @@ -0,0 +1,4 @@ +export * from './c-jarm-auth-response.js'; +export * from './jarm-auth-response.js'; +export * from './v-jarm-auth-response-params.js'; +export * from './v-jarm-direct-post-jwt-auth-response-params.js'; diff --git a/packages/jarm/lib/jarm-auth-response/jarm-auth-response.ts b/packages/jarm/lib/jarm-auth-response/jarm-auth-response.ts new file mode 100644 index 00000000..7564a5d2 --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response/jarm-auth-response.ts @@ -0,0 +1,106 @@ +import { decodeProtectedHeader, isJwe, isJws } from '@sphereon/oid4vc-common'; +import * as v from 'valibot'; + +import type { JarmDirectPostJwtResponseParams } from '../index.js'; + +import type { AuthRequestParams, JarmDirectPostJwtAuthResponseValidationContext } from './c-jarm-auth-response.js'; +import { vJarmAuthResponseErrorParams } from './v-jarm-auth-response-params.js'; +import { jarmAuthResponseDirectPostValidateParams, vJarmDirectPostJwtParams } from './v-jarm-direct-post-jwt-auth-response-params.js'; + +export interface JarmDirectPostJwtAuthResponseValidation { + /** + * The JARM response parameter conveyed either as url query param, fragment param, or application/x-www-form-urlencoded in the body of a post request + */ + response: string; +} + +const parseJarmAuthResponseParams = >>( + schema: Schema, + responseParams: unknown, +) => { + if (v.is(vJarmAuthResponseErrorParams, responseParams)) { + const errorResponseJson = JSON.stringify(responseParams, undefined, 2); + throw new Error(`Received error response from authorization server. '${errorResponseJson}'`); + } + + return v.parse(schema, responseParams); +}; + +const decryptJarmAuthResponse = async (input: { response: string }, ctx: JarmDirectPostJwtAuthResponseValidationContext) => { + const { response } = input; + + const responseProtectedHeader = decodeProtectedHeader(response); + if (!responseProtectedHeader.kid) { + throw new Error(`Jarm JWE is missing the protected header field 'kid'.`); + } + + const { plaintext } = await ctx.jwe.decryptCompact({ + jwe: response, + jwk: { kid: responseProtectedHeader.kid }, + }); + + return plaintext; +}; + +/** + * Validate a JARM direct_post.jwt compliant authentication response + * * The decryption key should be resolvable using the the protected header's 'kid' field + * * The signature verification jwk should be resolvable using the jws protected header's 'kid' field and the payload's 'iss' field. + */ +export const jarmAuthResponseDirectPostJwtValidate = async ( + input: JarmDirectPostJwtAuthResponseValidation, + ctx: JarmDirectPostJwtAuthResponseValidationContext, +) => { + const { response } = input; + + const responseIsEncrypted = isJwe(response); + const decryptedResponse = responseIsEncrypted ? await decryptJarmAuthResponse(input, ctx) : response; + + const responseIsSigned = isJws(decryptedResponse); + if (!responseIsEncrypted && !responseIsSigned) { + throw new Error('Jarm Auth Response must be either encrypted, signed, or signed and encrypted.'); + } + + let authResponseParams: JarmDirectPostJwtResponseParams; + let authRequestParams: AuthRequestParams; + + if (responseIsSigned) { + throw new Error('Signed JARM responses are not supported.'); + //const jwsProtectedHeader = decodeProtectedHeader(decryptedResponse); + //const jwsPayload = decodeJwt(decryptedResponse); + + //const schema = v.required(vJarmDirectPostJwtParams, ['iss', 'aud', 'exp']); + //const responseParams = parseJarmAuthResponseParams(schema, jwsPayload); + //({ authRequestParams } = await ctx.openid4vp.authRequest.getParams(responseParams)); + + //if (!jwsProtectedHeader.kid) { + //throw new Error(`Jarm JWS is missing the protected header field 'kid'.`); + //} + + //await ctx.jose.jws.verifyJwt({ + //jws: decryptedResponse, + //jwk: { kid: jwsProtectedHeader.kid, kty: 'auto' }, + //}); + //authResponseParams = responseParams; + } else { + const jsonResponse: unknown = JSON.parse(decryptedResponse); + authResponseParams = parseJarmAuthResponseParams(vJarmDirectPostJwtParams, jsonResponse); + ({ authRequestParams } = await ctx.openid4vp.authRequest.getParams(authResponseParams)); + } + + jarmAuthResponseDirectPostValidateParams({ + authRequestParams, + authResponseParams, + }); + + let type: 'signed encrypted' | 'encrypted' | 'signed'; + if (responseIsSigned && responseIsEncrypted) type = 'signed encrypted'; + else if (responseIsEncrypted) type = 'encrypted'; + else type = 'signed'; + + return { + authRequestParams, + authResponseParams, + type, + }; +}; diff --git a/packages/jarm/lib/jarm-auth-response/v-jarm-auth-response-params.ts b/packages/jarm/lib/jarm-auth-response/v-jarm-auth-response-params.ts new file mode 100644 index 00000000..1cbb90de --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response/v-jarm-auth-response-params.ts @@ -0,0 +1,62 @@ +import { checkExp } from '@sphereon/oid4vc-common'; +import * as v from 'valibot'; + +export const vJarmAuthResponseErrorParams = v.looseObject({ + error: v.string(), + state: v.optional(v.string()), + + error_description: v.pipe( + v.optional(v.string()), + v.description('Text providing additional information, used to assist the client developer in understanding the error that occurred.'), + ), + + error_uri: v.pipe( + v.optional(v.pipe(v.string(), v.url())), + v.description( + 'A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error', + ), + ), +}); + +export const vJarmAuthResponseParams = v.looseObject({ + state: v.optional(v.string()), + + /** + * The issuer URL of the authorization server that created the response + */ + iss: v.string(), + + /** + * The client_id of the client the response is intended for + */ + exp: v.number(), + + /** + * Expiration of the JWT + */ + aud: v.string(), +}); + +export type JarmAuthResponseParams = v.InferInput; + +export const validateJarmAuthResponseParams = (input: { + authRequestParams: { client_id: string; state?: string }; + authResponseParams: JarmAuthResponseParams; +}) => { + const { authRequestParams, authResponseParams } = input; + // 2. The client obtains the state parameter from the JWT and checks its binding to the user agent. If the check fails, the client MUST abort processing and refuse the response. + if (authRequestParams.state !== authResponseParams.state) { + throw new Error(`State missmatch in jarm-auth-response. Expected '${authRequestParams.state}' received '${authRequestParams.state}'.`); + } + + // 4. The client obtains the aud element from the JWT and checks whether it matches the client id the client used to identify itself in the corresponding authorization request. If the check fails, the client MUST abort processing and refuse the response. + if (authRequestParams.client_id !== authResponseParams.client_id) { + throw new Error(`Invalid audience in jarm-auth-response. Expected '${authRequestParams.client_id}' received '${authResponseParams.aud}'.`); + } + + // 5. The client checks the JWT's exp element to determine if the JWT is still valid. If the check fails, the client MUST abort processing and refuse the response. + // 120 seconds clock skew + if (checkExp({ exp: authResponseParams.exp })) { + throw new Error(`The '${authRequestParams.state}' and the jarm-auth-response.`); + } +}; diff --git a/packages/jarm/lib/jarm-auth-response/v-jarm-direct-post-jwt-auth-response-params.ts b/packages/jarm/lib/jarm-auth-response/v-jarm-direct-post-jwt-auth-response-params.ts new file mode 100644 index 00000000..0530655e --- /dev/null +++ b/packages/jarm/lib/jarm-auth-response/v-jarm-direct-post-jwt-auth-response-params.ts @@ -0,0 +1,26 @@ +import * as v from 'valibot'; + +import { vJarmAuthResponseParams } from './v-jarm-auth-response-params.js'; + +export const vJarmDirectPostJwtParams = v.looseObject({ + ...v.omit(vJarmAuthResponseParams, ['iss', 'aud', 'exp']).entries, + ...v.partial(v.pick(vJarmAuthResponseParams, ['iss', 'aud', 'exp'])).entries, + + vp_token: v.string(), + presentation_submission: v.unknown(), + nonce: v.optional(v.string()), +}); + +export type JarmDirectPostJwtResponseParams = v.InferInput; + +export const jarmAuthResponseDirectPostValidateParams = (input: { + authRequestParams: { state?: string }; + authResponseParams: JarmDirectPostJwtResponseParams; +}) => { + const { authRequestParams, authResponseParams } = input; + + // 2. The client obtains the state parameter from the JWT and checks its binding to the user agent. If the check fails, the client MUST abort processing and refuse the response. + if (authRequestParams.state !== authResponseParams.state) { + throw new Error(`State missmatch between auth request '${authRequestParams.state}' and the jarm-auth-response.`); + } +}; diff --git a/packages/jarm/lib/metadata/index.ts b/packages/jarm/lib/metadata/index.ts new file mode 100644 index 00000000..aaf86a43 --- /dev/null +++ b/packages/jarm/lib/metadata/index.ts @@ -0,0 +1,3 @@ +export * from './v-jarm-client-metadata.js'; +export * from './v-jarm-server-metadata.js'; +export * from './jarm-validate-metadata.js'; diff --git a/packages/jarm/lib/metadata/jarm-validate-metadata.ts b/packages/jarm/lib/metadata/jarm-validate-metadata.ts new file mode 100644 index 00000000..32363d60 --- /dev/null +++ b/packages/jarm/lib/metadata/jarm-validate-metadata.ts @@ -0,0 +1,80 @@ +import * as v from 'valibot'; + +import { + vJarmClientMetadata, + vJarmClientMetadataEncrypt, + vJarmClientMetadataSign, + vJarmClientMetadataSignEncrypt, +} from '../metadata/v-jarm-client-metadata.js'; +import { vJarmServerMetadata } from '../metadata/v-jarm-server-metadata.js'; +import { assertValueSupported } from '../utils.js'; + +export const vJarmAuthResponseValidateMetadataInput = v.object({ + client_metadata: vJarmClientMetadata, + server_metadata: v.partial(vJarmServerMetadata), +}); +export type JarmMetadataValidate = v.InferInput; + +export const vJarmMetadataValidateOut = v.variant('type', [ + v.object({ + type: v.literal('signed'), + client_metadata: vJarmClientMetadataSign, + }), + v.object({ + type: v.literal('encrypted'), + client_metadata: vJarmClientMetadataEncrypt, + }), + v.object({ + type: v.literal('signed encrypted'), + client_metadata: vJarmClientMetadataSignEncrypt, + }), +]); + +export const jarmMetadataValidate = (vJarmMetadataValidate: JarmMetadataValidate): v.InferOutput => { + const { client_metadata, server_metadata } = vJarmMetadataValidate; + const { authorization_encrypted_response_alg, authorization_encrypted_response_enc, authorization_signed_response_alg } = client_metadata; + + assertValueSupported({ + supported: server_metadata.authorization_signing_alg_values_supported ?? [], + actual: authorization_signed_response_alg, + required: !!authorization_signed_response_alg, + error: new Error('Invalid authorization_signed_response_alg'), + }); + + assertValueSupported({ + supported: server_metadata.authorization_encryption_alg_values_supported ?? [], + actual: authorization_encrypted_response_alg, + required: !!authorization_encrypted_response_alg, + error: new Error('Invalid authorization_encrypted_response_alg'), + }); + + assertValueSupported({ + supported: server_metadata.authorization_encryption_enc_values_supported ?? [], + actual: authorization_encrypted_response_enc, + required: !!authorization_encrypted_response_enc, + error: new Error('Invalid authorization_encrypted_response_enc'), + }); + + if (authorization_signed_response_alg && authorization_encrypted_response_alg && authorization_encrypted_response_enc) { + return { + type: 'signed encrypted', + client_metadata: { + authorization_signed_response_alg, + authorization_encrypted_response_alg, + authorization_encrypted_response_enc, + }, + }; + } else if (authorization_signed_response_alg && !authorization_encrypted_response_alg && !authorization_encrypted_response_enc) { + return { + type: 'signed', + client_metadata: { authorization_signed_response_alg }, + }; + } else if (!authorization_signed_response_alg && authorization_encrypted_response_alg && authorization_encrypted_response_enc) { + return { + type: 'encrypted', + client_metadata: { authorization_encrypted_response_alg, authorization_encrypted_response_enc }, + }; + } else { + throw new Error(`Invalid jarm client_metadata combination`); + } +}; diff --git a/packages/jarm/lib/metadata/v-jarm-client-metadata.ts b/packages/jarm/lib/metadata/v-jarm-client-metadata.ts new file mode 100644 index 00000000..5addf710 --- /dev/null +++ b/packages/jarm/lib/metadata/v-jarm-client-metadata.ts @@ -0,0 +1,42 @@ +import * as v from 'valibot'; + +export const vJarmClientMetadataSign = v.object({ + authorization_signed_response_alg: v.pipe( + v.optional(v.string()), // @default 'RS256' This makes no sense with openid4vp if just encrypted can be specified + v.description( + 'JWA. If this is specified, the response will be signed using JWS and the configured algorithm. The algorithm none is not allowed.', + ), + ), + + authorization_encrypted_response_alg: v.optional(v.never()), + authorization_encrypted_response_enc: v.optional(v.never()), +}); + +export const vJarmClientMetadataEncrypt = v.object({ + authorization_signed_response_alg: v.optional(v.never()), + authorization_encrypted_response_alg: v.pipe( + v.string(), + v.description( + 'JWE alg algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.', + ), + ), + + authorization_encrypted_response_enc: v.pipe( + v.optional(v.string(), 'A128CBC-HS256'), + v.description( + 'JWE enc algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.', + ), + ), +}); + +export const vJarmClientMetadataSignEncrypt = v.object({ + ...v.pick(vJarmClientMetadataSign, ['authorization_signed_response_alg']).entries, + ...v.pick(vJarmClientMetadataEncrypt, ['authorization_encrypted_response_alg', 'authorization_encrypted_response_enc']).entries, +}); + +/** + * Clients may register their public encryption keys using the jwks_uri or jwks metadata parameters. + */ +export const vJarmClientMetadata = v.union([vJarmClientMetadataSign, vJarmClientMetadataEncrypt, vJarmClientMetadataSignEncrypt]); + +export type JarmClientMetadata = v.InferInput; diff --git a/packages/jarm/lib/metadata/v-jarm-server-metadata.ts b/packages/jarm/lib/metadata/v-jarm-server-metadata.ts new file mode 100644 index 00000000..deca18bb --- /dev/null +++ b/packages/jarm/lib/metadata/v-jarm-server-metadata.ts @@ -0,0 +1,29 @@ +import * as v from 'valibot'; + +/** + * Authorization servers SHOULD publish the supported algorithms for signing and encrypting the JWT of an authorization response by utilizing OAuth 2.0 Authorization Server Metadata [RFC8414] parameters. + */ +export const vJarmServerMetadata = v.object({ + authorization_signing_alg_values_supported: v.pipe( + v.array(v.string()), + v.description( + 'JSON array containing a list of the JWS [RFC7515] signing algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to sign the response.', + ), + ), + + authorization_encryption_alg_values_supported: v.pipe( + v.array(v.string()), + v.description( + 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.', + ), + ), + + authorization_encryption_enc_values_supported: v.pipe( + v.array(v.string()), + v.description( + 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (enc values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.', + ), + ), +}); + +export type JarmServerMetadata = v.InferInput; diff --git a/packages/jarm/lib/utils.ts b/packages/jarm/lib/utils.ts new file mode 100644 index 00000000..067b0f19 --- /dev/null +++ b/packages/jarm/lib/utils.ts @@ -0,0 +1,42 @@ +export function appendQueryParams(input: { url: URL; params: Record }) { + const { url, params } = input; + + // Append the new query parameters from the params object + for (const [key, value] of Object.entries(params)) { + url.searchParams.append(key, encodeURIComponent(value)); + } + + return url; +} + +export function appendFragmentParams(input: { url: URL; fragments: Record }) { + const { url, fragments } = input; + + // Convert existing fragment to an object and remove the leading '#' + const fragmentParams = new URLSearchParams(url.hash.slice(1)); // Remove the leading '#' from the fragment + + // Append the new fragments from the fragments object + for (const [key, value] of Object.entries(fragments)) { + fragmentParams.append(key, encodeURIComponent(value)); + } + + // Rebuild the fragment string and assign it to the URL + url.hash = fragmentParams.toString(); + + return url; +} + +interface AssertValueSupported { + supported: T[]; + actual: T; + error: Error; + required: boolean; +} + +export function assertValueSupported(input: AssertValueSupported): T | undefined { + const { required, error, supported, actual } = input; + const intersection = supported.find((value) => value === actual); + + if (required && !intersection) throw error; + return intersection; +} diff --git a/packages/jarm/lib/v-response-mode-registry.ts b/packages/jarm/lib/v-response-mode-registry.ts new file mode 100644 index 00000000..162db546 --- /dev/null +++ b/packages/jarm/lib/v-response-mode-registry.ts @@ -0,0 +1,81 @@ +import * as v from 'valibot'; + +import type { ResponseTypeOut } from './v-response-type-registry.js'; + +export const vJarmResponseMode = v.picklist(['jwt', 'query.jwt', 'fragment.jwt', 'form_post.jwt']); +export type JarmResponseMode = v.InferInput; + +export const vOpenid4vpResponseMode = v.picklist(['direct_post']); +export type Openid4vpResponseMode = v.InferInput; + +/** + * * 'direct_post.jwt' The response is send as HTTP POST request using the application/x-www-form-urlencoded content type. The body contains a single parameter response which is the JWT encoded Response as defined in JARM 4.1 + */ +export const vOpenid4vpJarmResponseMode = v.picklist(['direct_post.jwt']); +export type Openid4vpJarmResponseMode = v.InferInput; + +/** + * The use of this parameter is NOT RECOMMENDED when the Response Mode that would be requested is the default mode specified for the Response Type. + * * 'query' In this mode, Authorization Response parameters are encoded in the query string added to the redirect_uri when redirecting back to the Client. + * * 'fragment' In this mode, Authorization Response parameters are encoded in the fragment added to the redirect_uri when redirecting back to the Client. + * * 'direct_post' the Authorization Response is send to an endpoint controlled by the Verifier via an HTTP POST request. + */ +export const vResponseMode = v.pipe( + v.picklist(['query', 'fragment', ...vOpenid4vpResponseMode.options, ...vJarmResponseMode.options, ...vOpenid4vpJarmResponseMode.options]), + v.description('Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint.'), +); +export type ResponseMode = v.InferInput; + +const getDisAllowedResponseModes = (input: { response_type: ResponseTypeOut }): [ResponseMode, ...ResponseMode[]] | undefined => { + const { response_type } = input; + + switch (response_type) { + case 'code token': + return ['query']; + case 'code id_token': + return ['query']; + case 'id_token token': + return ['query']; + case 'code id_token token': + return ['query']; + } + return undefined; +}; + +export const getDefaultResponseMode = (input: { response_type: ResponseTypeOut }): 'query' | 'fragment' => { + const { response_type } = input; + + switch (response_type) { + case 'code': + case 'none': + return 'query'; + case 'token': + case 'id_token': + case 'code token': + case 'code id_token': + case 'id_token token': + case 'code id_token token': + case 'vp_token': + case 'id_token vp_token': + return 'fragment'; + } +}; + +export const getJarmDefaultResponseMode = (input: { response_type: ResponseTypeOut }): 'query.jwt' | 'fragment.jwt' => { + const responseMode = getDefaultResponseMode(input); + + switch (responseMode) { + case 'query': + return 'query.jwt'; + case 'fragment': + return 'fragment.jwt'; + } +}; + +export const validateResponseMode = (input: { response_type: ResponseTypeOut; response_mode: ResponseMode }) => { + const disallowedResponseModes = getDisAllowedResponseModes(input); + + if (disallowedResponseModes?.includes(input.response_mode)) { + throw new Error(`Response_type '${input.response_type}' is not compatible with response_mode '${input.response_mode}'.`); + } +}; diff --git a/packages/jarm/lib/v-response-type-registry.ts b/packages/jarm/lib/v-response-type-registry.ts new file mode 100644 index 00000000..1f792379 --- /dev/null +++ b/packages/jarm/lib/v-response-type-registry.ts @@ -0,0 +1,23 @@ +import * as v from 'valibot'; + +export const oAuthResponseTypes = v.picklist(['code', 'token']); + +// NOTE: MAKE SURE THAT THE RESPONSE TYPES ARE SORTED CORRECTLY +export const oAuthMRTEPResponseTypes = v.picklist(['none', 'id_token', 'code token', 'code id_token', 'id_token token', 'code id_token token']); + +export const openid4vpResponseTypes = v.picklist(['vp_token', 'id_token vp_token']); + +export const vTransformedResponseTypes = v.picklist([ + ...openid4vpResponseTypes.options, + ...oAuthResponseTypes.options, + ...oAuthMRTEPResponseTypes.options, +]); + +export const vResponseType = v.pipe( + v.string(), + v.transform((val) => val.split(' ').sort().join(' ')), + vTransformedResponseTypes, +); + +export type ResponseType = v.InferInput; +export type ResponseTypeOut = v.InferOutput; diff --git a/packages/jarm/package.json b/packages/jarm/package.json new file mode 100644 index 00000000..28179973 --- /dev/null +++ b/packages/jarm/package.json @@ -0,0 +1,43 @@ +{ + "name": "@sphereon/jarm", + "version": "0.16.0", + "description": "Sphereon JARM", + "source": "lib/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "build:clean": "tsc --build --clean && tsc --build" + }, + "dependencies": { + "valibot": "^0.42.1", + "@sphereon/oid4vc-common": "workspace:*" + }, + "engines": { + "node": ">=18" + }, + "files": [ + "lib/**/*", + "dist/**/*" + ], + "prettier": { + "singleQuote": true, + "printWidth": 150 + }, + "keywords": [ + "Sphereon", + "Verifiable Credentials", + "OpenID", + "OpenID for Verifiable Credential Issuance", + "OAuth2", + "SSI", + "JARM", + "OpenId for Verifiable Presentations" + ], + "author": "Sphereon", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/jarm/tsconfig.json b/packages/jarm/tsconfig.json new file mode 100644 index 00000000..30cdb4c1 --- /dev/null +++ b/packages/jarm/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "rootDir": "./lib", + "outDir": "./dist", + "declarationDir": "./dist", + "esModuleInterop": true, + "moduleResolution": "Node" + } +} diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index bc16c5a4..10a510c8 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -315,7 +315,7 @@ export interface AuthorizationRequestOpts { redirectUri?: string; scope?: string; requestObjectOpts?: RequestObjectOpts; - holderPreferredAuthzFlowTypeOrder?: AuthzFlowType[] + holderPreferredAuthzFlowTypeOrder?: AuthzFlowType[]; } export interface AuthorizationResponse { diff --git a/packages/siop-oid4vp/lib/__tests__/TestUtils.ts b/packages/siop-oid4vp/lib/__tests__/TestUtils.ts index b53cd9e2..bf073abe 100644 --- a/packages/siop-oid4vp/lib/__tests__/TestUtils.ts +++ b/packages/siop-oid4vp/lib/__tests__/TestUtils.ts @@ -3,7 +3,7 @@ import crypto, { createHash } from 'crypto' import { JwtPayload, parseJWT, SigningAlgo, uuidv4 } from '@sphereon/oid4vc-common' -import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib'; +import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib' import { IProofType } from '@sphereon/ssi-types' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts b/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts index 5a8368e2..a8f523a7 100644 --- a/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts +++ b/packages/siop-oid4vp/lib/__tests__/e2e/mattr.launchpad.spec.ts @@ -135,7 +135,7 @@ describe('OID4VCI-Client using Mattr issuer should', () => { const correlationId = 'test' - const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(offer.authorizeRequestUri); + const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(offer.authorizeRequestUri) const verifiedAuthRequest = await authorizationRequest.verify({ correlationId, verifyJwtCallback: getVerifyJwtCallback(getResolver()), diff --git a/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts b/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts index e0a5a4db..db2deb40 100644 --- a/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts +++ b/packages/siop-oid4vp/lib/__tests__/functions/LanguageTagUtils.spec.ts @@ -196,13 +196,19 @@ describe('Language tag util should', () => { const allLanguageTaggedProperties = LanguageTagUtils.getLanguageTaggedPropertiesMapped(source, languageTagEnabledFieldsNamesMapping) expect(allLanguageTaggedProperties).toEqual(expectedTaggedFields) }) - + 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('return empty if list is given but not effective', async () => { + expect.assertions(1) + const result = await LanguageTagUtils.getLanguageTaggedProperties({}, []) + expect(result).toEqual(new Map()) + }) + it('throw error if list is given but no proper field names', async () => { expect.assertions(1) await expect(() => LanguageTagUtils.getLanguageTaggedProperties({}, [''])).toThrowError() @@ -212,7 +218,13 @@ describe('Language tag util should', () => { expect.assertions(1) expect(LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, null as any)).toEqual(new Map()) }) - + + it('return empty map if mapping is given but not effective', async () => { + expect.assertions(1) + const result = await LanguageTagUtils.getLanguageTaggedPropertiesMapped({}, new Map()) + expect(result).toEqual(new Map()) + }) + it('throw error if mapping is given but no proper names', async () => { expect.assertions(1) const languageTagEnabledFieldsNamesMapping: Map = new Map() diff --git a/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts index 18196e8b..9904fd9d 100644 --- a/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts +++ b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts @@ -224,19 +224,21 @@ export class AuthorizationResponse { if (this._payload?.vp_token) { const presentations = await extractPresentationsFromAuthorizationResponse(this, opts) const presentationsArray = Array.isArray(presentations) ? presentations : [presentations] - + // We do not verify them, as that is done elsewhere. So we simply can take the first nonce + nonce = presentationsArray // FIXME toWrappedVerifiablePresentation() does not extract the nonce yet from mdocs. // Either it's not availble or we or not reading the SessionTranscript yet - .filter(presentation => !CredentialMapper.isWrappedMdocPresentation(presentation)) + .filter(presentation => !CredentialMapper.isWrappedMdocPresentation(presentation)) .map(extractNonceFromWrappedVerifiablePresentation) .find(nonce => nonce !== undefined); - + if(!nonce && !this._idToken && presentationsArray.some(presentation => CredentialMapper.isWrappedMdocPresentation(presentation))) { nonce = 'mdoc' // FIXME toWrappedVerifiablePresentation() does not extract the nonce yet from mdocs. } } + const idTokenPayload = await this.idToken?.payload() if (opts?.consistencyCheck !== false && idTokenPayload) { Object.entries(idTokenPayload).forEach((entry) => { diff --git a/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts index ae75c14b..d1840067 100644 --- a/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts +++ b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts @@ -35,6 +35,7 @@ import { export const extractNonceFromWrappedVerifiablePresentation = (wrappedVp: WrappedVerifiablePresentation): string | undefined => { // SD-JWT uses kb-jwt for the nonce if (CredentialMapper.isWrappedSdJwtVerifiablePresentation(wrappedVp)) { + // SD-JWT uses kb-jwt for the nonce // 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('~')) { @@ -143,7 +144,6 @@ export const extractPresentationsFromAuthorizationResponse = async ( return CredentialMapper.toWrappedVerifiablePresentation(response.payload.vp_token, { hasher: opts?.hasher }) } - export const createPresentationSubmission = async ( verifiablePresentations: W3CVerifiablePresentation[], opts?: { presentationDefinitions: (PresentationDefinitionWithLocation | IPresentationDefinition)[] }, diff --git a/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts index ec97a9b0..2f646351 100644 --- a/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts +++ b/packages/siop-oid4vp/lib/authorization-response/PresentationExchange.ts @@ -7,7 +7,7 @@ import { Status, Validated, VerifiablePresentationFromOpts, - VerifiablePresentationResult + VerifiablePresentationResult, } from '@sphereon/pex'; import { PresentationEvaluationResults } from '@sphereon/pex/dist/main/lib/evaluation'; import { @@ -173,9 +173,11 @@ export class PresentationExchange { } public static assertValidPresentationSubmission(presentationSubmission: PresentationSubmission) { - const validationResult:Validated = PEX.validateSubmission(presentationSubmission) - if (Array.isArray(validationResult) && validationResult[0].message != 'ok' - || !Array.isArray(validationResult) && validationResult.message != 'ok') { + const validationResult: Validated = PEX.validateSubmission(presentationSubmission) + if ( + (Array.isArray(validationResult) && validationResult[0].message != 'ok') || + (!Array.isArray(validationResult) && validationResult.message != 'ok') + ) { throw new Error(`${SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult)}`) } } @@ -300,8 +302,10 @@ export class PresentationExchange { private static assertValidPresentationDefinition(presentationDefinition: IPresentationDefinition) { const validationResult = PEX.validateDefinition(presentationDefinition) - if (Array.isArray(validationResult) && validationResult[0].message != 'ok' - || !Array.isArray(validationResult) && validationResult.message != 'ok') { + if ( + (Array.isArray(validationResult) && validationResult[0].message != 'ok') || + (!Array.isArray(validationResult) && validationResult.message != 'ok') + ) { throw new Error(`${SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID}`) } } diff --git a/packages/siop-oid4vp/lib/authorization-response/index.ts b/packages/siop-oid4vp/lib/authorization-response/index.ts index 02bfafa2..c8ae3c97 100644 --- a/packages/siop-oid4vp/lib/authorization-response/index.ts +++ b/packages/siop-oid4vp/lib/authorization-response/index.ts @@ -2,3 +2,4 @@ export * from './AuthorizationResponse' export * from './types' export * from './Payload' export * from './ResponseRegistration' +export * from './OpenID4VP' diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts index d3e30fb7..259b03fb 100644 --- a/packages/siop-oid4vp/lib/authorization-response/types.ts +++ b/packages/siop-oid4vp/lib/authorization-response/types.ts @@ -3,7 +3,15 @@ import { IPresentationDefinition, PresentationSignCallBackParams } from '@sphere 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 { + ResponseMode, + ResponseRegistrationOpts, + ResponseType, + ResponseURIType, + SupportedVersion, + VerifiablePresentationWithFormat, + Verification, +} from '../types' import { CreateJwtCallback } from '../types/VpJwtIssuer' import { VerifyJwtCallback } from '../types/VpJwtVerifier' @@ -19,6 +27,7 @@ export interface AuthorizationResponseOpts { createJwtCallback: CreateJwtCallback jwtIssuer?: JwtIssuer responseMode?: ResponseMode + responseType?: [ResponseType] // did: string; expiresIn?: number accessToken?: string diff --git a/packages/siop-oid4vp/lib/helpers/Encodings.ts b/packages/siop-oid4vp/lib/helpers/Encodings.ts index c7a56aa3..27de1636 100644 --- a/packages/siop-oid4vp/lib/helpers/Encodings.ts +++ b/packages/siop-oid4vp/lib/helpers/Encodings.ts @@ -14,8 +14,8 @@ export function decodeUriAsJson(uri: string) { } const parts = parse(queryString, { plainObjects: true, depth: 10, parameterLimit: 5000, ignoreQueryPrefix: true }) - const vpToken = (parts?.claims as { [key: string]: any })?.['vp_token']; - const descriptors = vpToken?.presentation_definition?.['input_descriptors']; // FIXME? + const vpToken = (parts?.claims as { [key: string]: any })?.['vp_token'] + const descriptors = vpToken?.presentation_definition?.['input_descriptors'] // FIXME? 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 vpToken.presentation_definition['input_descriptors'] = descriptors.map((descriptor: InputDescriptorV1) => { @@ -33,7 +33,7 @@ export function decodeUriAsJson(uri: string) { }) } - const json:Record = {} + const json: Record = {} for (const key in parts) { const value = parts[key] if (!value) { diff --git a/packages/siop-oid4vp/lib/helpers/HttpUtils.ts b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts index 70ba748c..cbc932ad 100644 --- a/packages/siop-oid4vp/lib/helpers/HttpUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/HttpUtils.ts @@ -61,7 +61,7 @@ const siopFetch = async ( if (!url || url.toLowerCase().startsWith('did:')) { throw Error(`Invalid URL supplied. Expected a http(s) URL. Recieved: ${url}`) } - const headers:Record = opts?.customHeaders ? opts.customHeaders : {} + const headers: Record = opts?.customHeaders ? opts.customHeaders : {} if (opts?.bearerToken) { headers['Authorization'] = `Bearer ${opts.bearerToken}` } diff --git a/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts index eb85e6c1..b8fd0dd7 100644 --- a/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/LanguageTagUtils.ts @@ -14,7 +14,7 @@ export class LanguageTagUtils { * @param source is the object from which the language enabled fields and their values will be extracted. */ static getAllLanguageTaggedProperties(source: object): Map { - return this.getLanguageTaggedPropertiesMapped(source, new Map() ) + return this.getLanguageTaggedPropertiesMapped(source, new Map()) } /** diff --git a/packages/siop-oid4vp/lib/helpers/Metadata.ts b/packages/siop-oid4vp/lib/helpers/Metadata.ts index aae1b70f..8e1739a1 100644 --- a/packages/siop-oid4vp/lib/helpers/Metadata.ts +++ b/packages/siop-oid4vp/lib/helpers/Metadata.ts @@ -13,7 +13,10 @@ export function assertValidMetadata(opMetadata: DiscoveryMetadataPayload, rpMeta 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 as string[]) + subjectSyntaxTypesSupported = supportedSubjectSyntaxTypes( + rpMetadata.subject_syntax_types_supported, + opMetadata.subject_syntax_types_supported as string[], + ) } else if (isValidSubjectSyntax && (!rpMetadata.subject_syntax_types_supported || !rpMetadata.subject_syntax_types_supported.length)) { if (opMetadata.subject_syntax_types_supported) { subjectSyntaxTypesSupported = [...opMetadata.subject_syntax_types_supported] @@ -122,7 +125,7 @@ function getFormatIntersection(rpFormat: Format, opFormat: Format): Format { throw new Error(SIOPErrors.CREDENTIAL_FORMATS_NOT_SUPPORTED) } intersectionFormat[crFormat] = {} - if(methodKeyOP !== undefined) { + if (methodKeyOP !== undefined) { intersectionFormat[crFormat][methodKeyOP] = algs } }) diff --git a/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts index 1e4e803b..c0b877e6 100644 --- a/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/ObjectUtils.ts @@ -9,7 +9,7 @@ export function isStringNullOrEmpty(key: string) { return !key || !key.length } -export function removeNullUndefined(data: T) : T { +export function removeNullUndefined(data: T): T { if (!data) { return data } diff --git a/packages/siop-oid4vp/lib/helpers/Revocation.ts b/packages/siop-oid4vp/lib/helpers/Revocation.ts index adbb2a11..6005baae 100644 --- a/packages/siop-oid4vp/lib/helpers/Revocation.ts +++ b/packages/siop-oid4vp/lib/helpers/Revocation.ts @@ -21,8 +21,10 @@ export const verifyRevocation = async ( throw new Error(`Revocation callback not provided`) } - const vcs = (CredentialMapper.isWrappedSdJwtVerifiablePresentation(vpToken) || CredentialMapper.isWrappedMdocPresentation(vpToken)) - ? [vpToken.vcs[0]] : vpToken.presentation.verifiableCredential + const vcs = + CredentialMapper.isWrappedSdJwtVerifiablePresentation(vpToken) || CredentialMapper.isWrappedMdocPresentation(vpToken) + ? [vpToken.vcs[0]] + : vpToken.presentation.verifiableCredential for (const vc of vcs) { if ( revocationVerification === RevocationVerification.ALWAYS || @@ -47,7 +49,7 @@ function originalTypeToVerifiableCredentialTypeFormat(original: WrappedVerifiabl jwt_vc: VerifiableCredentialTypeFormat.JWT_VC, ldp: VerifiableCredentialTypeFormat.LDP_VC, ldp_vc: VerifiableCredentialTypeFormat.LDP_VC, - mso_mdoc: VerifiableCredentialTypeFormat.MSO_MDOC + mso_mdoc: VerifiableCredentialTypeFormat.MSO_MDOC, } return mapping[original] diff --git a/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts b/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts index cd3c5abb..2e0d9d77 100644 --- a/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts +++ b/packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts @@ -37,6 +37,7 @@ export const authorizationRequestVersionDiscovery = (authorizationRequest: Autho const versions = [] const authorizationRequestCopy: AuthorizationRequestPayload = JSON.parse(JSON.stringify(authorizationRequest)) const vd13Validation = AuthorizationRequestPayloadVD12OID4VPD20Schema(authorizationRequestCopy) + if (vd13Validation) { if ( !authorizationRequestCopy.registration_uri && diff --git a/packages/siop-oid4vp/lib/helpers/extract-jwks.ts b/packages/siop-oid4vp/lib/helpers/extract-jwks.ts new file mode 100644 index 00000000..f48a6a00 --- /dev/null +++ b/packages/siop-oid4vp/lib/helpers/extract-jwks.ts @@ -0,0 +1,43 @@ +import { JWK } from '../types' + +import { getJson } from './HttpUtils' + +export type Jwks = { + keys: JWK[] +} + +export type JwksMetadataParams = { + jwks?: Jwks + jwks_uri?: string +} + +/** + * Fetches a JSON Web Key Set (JWKS) from the specified URI. + * + * @param jwksUri - The URI of the JWKS endpoint. + * @returns A Promise that resolves to the JWKS object. + * @throws Will throw an error if the fetch fails or if the response is not valid JSON. + */ +export async function fetchJwks(jwksUri: string): Promise { + const res = await getJson(jwksUri) + return res.successBody ?? undefined +} + +/** + * Extracts JSON Web Key Set (JWKS) from the provided metadata. + * If a jwks field is provided, the JWKS will be extracted from the field. + * If a jwks_uri is provided, the JWKS will be fetched from the URI. + * + * @param input - The metadata input to be validated and parsed. + * @returns A promise that resolves to the extracted JWKS or undefined. + * @throws {JoseJwksExtractionError} If the metadata format is invalid or no decryption key is found. + */ +export const extractJwksFromJwksMetadata = async (metadata: JwksMetadataParams) => { + let jwks: Jwks | undefined = metadata.jwks?.keys[0] ? metadata.jwks : undefined + + if (!jwks && metadata.jwks_uri) { + jwks = await fetchJwks(metadata.jwks_uri) + } + + return jwks +} diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 32c701c7..544d00c1 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; +import { jarmAuthResponseSend, JarmClientMetadata, jarmMetadataValidate, JarmServerMetadata } from '@sphereon/jarm' import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common'; import { IIssuerId } from '@sphereon/ssi-types'; @@ -9,16 +10,19 @@ import { AuthorizationResponse, AuthorizationResponseOpts, AuthorizationResponseWithCorrelationId, - PresentationExchangeResponseOpts -} from '../authorization-response'; -import { encodeJsonAsURI, post } from '../helpers'; -import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion'; + PresentationExchangeResponseOpts, +} from '../authorization-response' +import { encodeJsonAsURI, post } from '../helpers' +import { authorizationRequestVersionDiscovery } from '../helpers/SIOPSpecVersion' +import { extractJwksFromJwksMetadata, JwksMetadataParams } from '../helpers/extract-jwks' import { AuthorizationEvent, AuthorizationEvents, + AuthorizationResponsePayload, ContentType, ParsedAuthorizationRequestURI, RegisterEventListener, + RequestObjectPayload, ResponseIss, ResponseMode, SIOPErrors, @@ -56,7 +60,7 @@ export class OP { requestOpts?: { correlationId?: string; verification?: Verification }, ): Promise { const correlationId = requestOpts?.correlationId || uuidv4() - + let authorizationRequest: AuthorizationRequest try { authorizationRequest = await AuthorizationRequest.fromUriOrJwt(requestJwtOrUri) @@ -127,7 +131,7 @@ export class OP { 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) { + if (verifiedAuthorizationRequest.payload?.response_mode === ResponseMode.DIRECT_POST) { responseUri = verifiedAuthorizationRequest.authorizationRequestPayload.response_uri ?? responseUri } @@ -156,19 +160,47 @@ export class OP { } } + public static async extractEncJwksFromClientMetadata(clientMetadata: JwksMetadataParams) { + // The client metadata will be parsed in the joseExtractJWKS function + const jwks = await extractJwksFromJwksMetadata(clientMetadata) + const encryptionJwk = jwks?.keys.find((key) => key.use === 'enc') + if (!encryptionJwk) { + throw new Error('No encryption jwk could be extracted from the client metadata.') + } + + return encryptionJwk + } + // TODO SK Can you please put some documentation on it? - public async submitAuthorizationResponse(authorizationResponse: AuthorizationResponseWithCorrelationId): Promise { + public async submitAuthorizationResponse( + authorizationResponse: AuthorizationResponseWithCorrelationId, + createJarmResponse?: (opts: { + authorizationResponsePayload: AuthorizationResponsePayload + requestObjectPayload: RequestObjectPayload + }) => Promise<{ + response: string + }>, + ): Promise { const { correlationId, response } = authorizationResponse if (!correlationId) { throw Error('No correlation Id provided') } + + const isJarmResponseMode = (responseMode: string): responseMode is 'jwt' | 'direct_post.jwt' | 'query.jwt' | 'fragment.jwt' => { + return responseMode === ResponseMode.DIRECT_POST_JWT || responseMode === ResponseMode.QUERY_JWT || responseMode === ResponseMode.FRAGMENT_JWT + } + + const requestObjectPayload = await response.authorizationRequest.requestObject?.getPayload() + const responseMode = requestObjectPayload?.response_mode ?? response.options?.responseMode + if ( !response || (response.options?.responseMode && !( - response.options.responseMode === ResponseMode.POST || - response.options.responseMode === ResponseMode.FORM_POST || - response.options.responseMode === ResponseMode.DIRECT_POST + responseMode === ResponseMode.POST || + responseMode === ResponseMode.FORM_POST || + responseMode === ResponseMode.DIRECT_POST || + isJarmResponseMode(responseMode) )) ) { throw new Error(SIOPErrors.BAD_PARAMS) @@ -180,6 +212,45 @@ export class OP { if (!responseUri) { throw Error('No response URI present') } + + if (isJarmResponseMode(responseMode)) { + if (responseMode !== ResponseMode.DIRECT_POST_JWT) { + throw new Error('Only direct_post.jwt response mode is supported for JARM at the moment.') + } + let responseType: 'id_token' | 'id_token vp_token' | 'vp_token' + if (idToken && payload.vp_token) { + responseType = 'id_token vp_token' + } else if (idToken) { + responseType = 'id_token' + } else if (payload.vp_token) { + responseType = 'vp_token' + } else { + throw new Error('No id_token or vp_token present in the response payload') + } + + const { response } = await createJarmResponse({ + requestObjectPayload, + authorizationResponsePayload: payload, + }) + + return jarmAuthResponseSend({ + authRequestParams: { + response_uri: responseUri, + response_mode: responseMode, + response_type: responseType, + }, + authResponse: response, + }) + .then((result) => { + void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_SUCCESS, { correlationId, subject: response }) + return result + }) + .catch((error: Error) => { + void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_FAILED, { correlationId, subject: response, error }) + throw error + }) + } + const authResponseAsURI = encodeJsonAsURI(payload, { arraysWithIndex: ['presentation_submission'] }) try { const result = await post(responseUri, authResponseAsURI, { contentType: ContentType.FORM_URL_ENCODED, exceptionOnHttpErrorStatus: true }) @@ -291,4 +362,8 @@ export class OP { get verifyRequestOptions(): Partial { return this._verifyRequestOptions } + + public static validateJarmMetadata(input: { client_metadata: JarmClientMetadata; server_metadata: Partial }) { + return jarmMetadataValidate(input) + } } diff --git a/packages/siop-oid4vp/lib/request-object/Payload.ts b/packages/siop-oid4vp/lib/request-object/Payload.ts index 738cdb67..59991031 100644 --- a/packages/siop-oid4vp/lib/request-object/Payload.ts +++ b/packages/siop-oid4vp/lib/request-object/Payload.ts @@ -48,6 +48,7 @@ export const createRequestObjectPayload = async (opts: CreateAuthorizationReques claims, presentation_definition_uri: payload.presentation_definition_uri, presentation_definition: payload.presentation_definition, + client_metadata: payload.client_metadata, iat, nbf, exp, diff --git a/packages/siop-oid4vp/lib/request-object/RequestObject.ts b/packages/siop-oid4vp/lib/request-object/RequestObject.ts index bd9ecbae..4cc0a178 100644 --- a/packages/siop-oid4vp/lib/request-object/RequestObject.ts +++ b/packages/siop-oid4vp/lib/request-object/RequestObject.ts @@ -53,13 +53,16 @@ export class RequestObject { return requestObjectJwt ? new RequestObject(undefined, undefined, requestObjectJwt) : undefined } - public static async fromPayload(requestObjectPayload: RequestObjectPayload, authorizationRequestOpts: CreateAuthorizationRequestOpts): Promise { + public static async fromPayload( + requestObjectPayload: RequestObjectPayload, + authorizationRequestOpts: CreateAuthorizationRequestOpts, + ): Promise { return new RequestObject(authorizationRequestOpts, requestObjectPayload) } public static async fromAuthorizationRequestPayload(payload: AuthorizationRequestPayload): Promise { const requestObjectJwt = - payload.request ?? payload.request_uri ? await fetchByReferenceOrUseByValue(payload.request_uri as string, payload.request, true) : undefined + (payload.request ?? payload.request_uri) ? await fetchByReferenceOrUseByValue(payload.request_uri as string, payload.request, true) : undefined return requestObjectJwt ? await RequestObject.fromJwt(requestObjectJwt) : undefined } diff --git a/packages/siop-oid4vp/lib/rp/Opts.ts b/packages/siop-oid4vp/lib/rp/Opts.ts index 5519b76d..ae2f4a46 100644 --- a/packages/siop-oid4vp/lib/rp/Opts.ts +++ b/packages/siop-oid4vp/lib/rp/Opts.ts @@ -53,7 +53,10 @@ export const createRequestOptsFromBuilderOrExistingOpts = (opts: { builder?: RPB return createRequestOpts } -export const createVerifyResponseOptsFromBuilderOrExistingOpts = (opts: { builder?: RPBuilder; verifyOpts?: VerifyAuthorizationResponseOpts }): Partial => { +export const createVerifyResponseOptsFromBuilderOrExistingOpts = (opts: { + builder?: RPBuilder + verifyOpts?: VerifyAuthorizationResponseOpts +}): Partial => { return opts.builder ? { hasher: opts.builder.hasher ?? defaultHasher, diff --git a/packages/siop-oid4vp/lib/rp/RP.ts b/packages/siop-oid4vp/lib/rp/RP.ts index 8ff7e371..e3bf844a 100644 --- a/packages/siop-oid4vp/lib/rp/RP.ts +++ b/packages/siop-oid4vp/lib/rp/RP.ts @@ -1,5 +1,11 @@ import { EventEmitter } from 'events' +import { + jarmAuthResponseDirectPostJwtValidate, + JarmAuthResponseParams, + JarmDirectPostJwtAuthResponseValidationContext, + JarmDirectPostJwtResponseParams, +} from '@sphereon/jarm' import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common' import { Hasher } from '@sphereon/ssi-types' @@ -15,12 +21,14 @@ import { import { mergeVerificationOpts } from '../authorization-request/Opts' import { AuthorizationResponse, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from '../authorization-response' import { getNonce, getState } from '../helpers' -import { PassBy } from '../types' import { AuthorizationEvent, AuthorizationEvents, AuthorizationResponsePayload, + DecryptCompact, + PassBy, RegisterEventListener, + RequestObjectPayload, ResponseURIType, SIOPErrors, SupportedVersion, @@ -133,6 +141,28 @@ export class RP { }) } + static async processJarmAuthorizationResponse( + response: string, + opts: { + decryptCompact: DecryptCompact + getAuthRequestPayload: (input: JarmDirectPostJwtResponseParams | JarmAuthResponseParams) => Promise<{ authRequestParams: RequestObjectPayload }> + }, + ) { + const { decryptCompact, getAuthRequestPayload } = opts + + const getParams = getAuthRequestPayload as JarmDirectPostJwtAuthResponseValidationContext['openid4vp']['authRequest']['getParams'] + + const validatedResponse = await jarmAuthResponseDirectPostJwtValidate( + { response }, + { + openid4vp: { authRequest: { getParams } }, + jwe: { decryptCompact }, + }, + ) + + return validatedResponse + } + public async verifyAuthorizationResponse( authorizationResponsePayload: AuthorizationResponsePayload, opts?: { diff --git a/packages/siop-oid4vp/lib/rp/RPBuilder.ts b/packages/siop-oid4vp/lib/rp/RPBuilder.ts index 608cc10d..7861c333 100644 --- a/packages/siop-oid4vp/lib/rp/RPBuilder.ts +++ b/packages/siop-oid4vp/lib/rp/RPBuilder.ts @@ -133,7 +133,7 @@ export class RPBuilder { return this } - withResponsetUri(redirectUri: string, targets?: PropertyTargets): RPBuilder { + withResponseUri(redirectUri: string, targets?: PropertyTargets): RPBuilder { this._authorizationRequestPayload.response_uri = assignIfAuth({ propertyValue: redirectUri, targets }, false) this._requestObjectPayload.response_uri = assignIfRequestObject({ propertyValue: redirectUri, targets }, true) return this diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD11.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD11.schema.ts index f0e09d23..f96cfebe 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD11.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD11.schema.ts @@ -407,7 +407,10 @@ export const AuthorizationRequestPayloadVD11SchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "ClaimPayloadCommon": { diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts index 0a5925b3..a0476c1b 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts @@ -413,7 +413,10 @@ export const AuthorizationRequestPayloadVD12OID4VPD18SchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "ClaimPayloadCommon": { diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD20.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD20.schema.ts index d52d61fd..ca386902 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD20.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVD12OID4VPD20.schema.ts @@ -413,7 +413,10 @@ export const AuthorizationRequestPayloadVD12OID4VPD20SchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "ClaimPayloadCommon": { diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVID1.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVID1.schema.ts index 21f39348..d8d3ff89 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVID1.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationRequestPayloadVID1.schema.ts @@ -379,7 +379,10 @@ export const AuthorizationRequestPayloadVID1SchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "ClaimPayloadVID1": { diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts index 2ba2e074..cdf66763 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts @@ -30,6 +30,14 @@ export const AuthorizationResponseOptsSchemaObj = { "responseMode": { "$ref": "#/definitions/ResponseMode" }, + "responseType": { + "type": "array", + "items": { + "$ref": "#/definitions/ResponseType" + }, + "minItems": 1, + "maxItems": 1 + }, "expiresIn": { "type": "number" }, @@ -1350,7 +1358,10 @@ export const AuthorizationResponseOptsSchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "GrantType": { diff --git a/packages/siop-oid4vp/lib/schemas/DiscoveryMetadataPayload.schema.ts b/packages/siop-oid4vp/lib/schemas/DiscoveryMetadataPayload.schema.ts index e7d14ba6..a675aad4 100644 --- a/packages/siop-oid4vp/lib/schemas/DiscoveryMetadataPayload.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/DiscoveryMetadataPayload.schema.ts @@ -1196,7 +1196,10 @@ export const DiscoveryMetadataPayloadSchemaObj = { "form_post", "post", "direct_post", - "query" + "query", + "direct_post.jwt", + "query.jwt", + "fragment.jwt" ] }, "GrantType": { diff --git a/packages/siop-oid4vp/lib/types/JWT.types.ts b/packages/siop-oid4vp/lib/types/JWT.types.ts index eb92f1e4..d562aff2 100644 --- a/packages/siop-oid4vp/lib/types/JWT.types.ts +++ b/packages/siop-oid4vp/lib/types/JWT.types.ts @@ -72,3 +72,8 @@ export interface JWK { } // export declare type ECCurve = 'P-256' | 'secp256k1' | 'P-384' | 'P-521'; + +export type DecryptCompact = (input: { + jwk: { kid: string } + jwe: string +}) => Promise<{ plaintext: string; protectedHeader: Record & { alg: string; enc: string } }> diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 40af582d..b44484cf 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -1,5 +1,5 @@ // noinspection JSUnusedGlobalSymbols - +import { JarmClientMetadata } from '@sphereon/jarm' import { SigningAlgo } from '@sphereon/oid4vc-common' import { Format, PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' import { @@ -21,6 +21,7 @@ import { PresentationVerificationCallback, VerifyAuthorizationResponseOpts, } from '../authorization-response' +import { JwksMetadataParams } from '../helpers/extract-jwks' import { RequestObject, RequestObjectOpts } from '../request-object' import { IRPSessionManager } from '../rp' @@ -34,6 +35,7 @@ export interface RequestObjectPayload extends RequestCommonPayload, JWTPayload { response_type: ResponseType | string // REQUIRED. Constant string value id_token. client_id: string // REQUIRED. RP's identifier at the Self-Issued OP. client_id_scheme?: ClientIdScheme // The client_id_scheme enables deployments of this specification to use different mechanisms to obtain and validate metadata of the Verifier beyond the scope of [RFC6749]. The term client_id_scheme is used since the Verifier is acting as an OAuth 2.0 Client. + client_metadata: ClientMetadataOpts redirect_uri?: string // REQUIRED before OID4VP v18, now optional because of response_uri. URI to which the Self-Issued OP Response will be sent response_uri?: string // New since OID4VP18 OPTIONAL. The Response URI to which the Wallet MUST send the Authorization Response using an HTTPS POST request as defined by the Response Mode direct_post. The Response URI receives all Authorization Response parameters as defined by the respective Response Type. 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. nonce: string @@ -374,7 +376,7 @@ export type DiscoveryMetadataPayload = DiscoveryMetadataPayloadVID1 | JWT_VCDisc export type DiscoveryMetadataOpts = (JWT_VCDiscoveryMetadataOpts | DiscoveryMetadataOptsVID1 | DiscoveryMetadataOptsVD11) & DiscoveryMetadataCommonOpts -export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties +export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties & JarmClientMetadata & JwksMetadataParams export type ResponseRegistrationOpts = DiscoveryMetadataOpts & ClientMetadataProperties @@ -534,6 +536,10 @@ export enum ResponseMode { // See https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-mode-direct_post DIRECT_POST = 'direct_post', QUERY = 'query', + + DIRECT_POST_JWT = 'direct_post.jwt', + QUERY_JWT = 'query.jwt', + FRAGMENT_JWT = 'fragment.jwt', } export enum ProtocolFlow { @@ -706,3 +712,5 @@ export enum ContentType { FORM_URL_ENCODED = 'application/x-www-form-urlencoded', UTF_8 = 'UTF-8', } + +export { JarmClientMetadata } diff --git a/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts b/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts index b31f991b..0e06e486 100644 --- a/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts +++ b/packages/siop-oid4vp/lib/types/VpJwtVerifier.ts @@ -126,8 +126,8 @@ export const getRequestObjectJwtVerifier = async ( typeof attestationPayload.exp !== 'number' || typeof attestationPayload.cnf !== 'object' || !attestationPayload.cnf || - (!('jwk' in attestationPayload.cnf) - || typeof attestationPayload.cnf['jwk'] !== 'object') + !('jwk' in attestationPayload.cnf) || + typeof attestationPayload.cnf['jwk'] !== 'object' ) { throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION) } diff --git a/packages/siop-oid4vp/package.json b/packages/siop-oid4vp/package.json index 7e1c186e..b06ff15d 100644 --- a/packages/siop-oid4vp/package.json +++ b/packages/siop-oid4vp/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", + "@sphereon/jarm": "workspace:*", "@sphereon/did-uni-client": "^0.6.2", "@sphereon/oid4vc-common": "workspace:*", "@sphereon/pex": "5.0.0-unstable.10",