├── .changeset
├── afraid-falcons-work.md
├── afraid-waves-post.md
├── chatty-buttons-sip.md
├── commit.js
├── config.json
├── cruel-pants-search.md
├── fair-hairs-bathe.md
├── funny-actors-kiss.md
├── healthy-items-push.md
├── lemon-buses-pump.md
├── lovely-schools-brush.md
├── mighty-cougars-work.md
├── perfect-bees-clean.md
├── rare-pillows-taste.md
├── red-ladybugs-perform.md
├── selfish-dolls-fix.md
├── tame-plants-kneel.md
├── three-clouds-return.md
├── twelve-kangaroos-help.md
└── twenty-shirts-love.md
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── README.md
├── biome.json
├── package.json
├── packages
├── oauth2
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── Oauth2AuthorizationServer.ts
│ │ ├── Oauth2Client.ts
│ │ ├── Oauth2ResourceServer.ts
│ │ ├── __tests__
│ │ │ ├── Oauth2AuthorizationServer.test.mts
│ │ │ └── Oauth2ResourceServer.test.mts
│ │ ├── access-token
│ │ │ ├── __tests__
│ │ │ │ ├── parse-access-token-request.test.mts
│ │ │ │ └── verify-access-token-request.test.mts
│ │ │ ├── create-access-token-response.ts
│ │ │ ├── create-access-token.ts
│ │ │ ├── introspect-token.ts
│ │ │ ├── parse-access-token-request.ts
│ │ │ ├── retrieve-access-token.ts
│ │ │ ├── verify-access-token-request.ts
│ │ │ ├── verify-access-token.ts
│ │ │ ├── z-access-token-jwt.ts
│ │ │ ├── z-access-token.ts
│ │ │ └── z-token-introspection.ts
│ │ ├── authorization-challenge
│ │ │ ├── create-authorization-challenge-response.ts
│ │ │ ├── parse-authorization-challenge-request.ts
│ │ │ ├── send-authorization-challenge.ts
│ │ │ ├── verify-authorization-challenge-request.ts
│ │ │ └── z-authorization-challenge.ts
│ │ ├── authorization-request
│ │ │ ├── create-authorization-request.ts
│ │ │ ├── create-pushed-authorization-response.ts
│ │ │ ├── parse-authorization-request.ts
│ │ │ ├── parse-pushed-authorization-request.ts
│ │ │ ├── verify-authorization-request.ts
│ │ │ ├── verify-pushed-authorization-request.ts
│ │ │ └── z-authorization-request.ts
│ │ ├── callbacks.ts
│ │ ├── client-attestation
│ │ │ ├── clent-attestation.ts
│ │ │ ├── client-attestation-pop.ts
│ │ │ └── z-client-attestation.ts
│ │ ├── client-authentication.ts
│ │ ├── common
│ │ │ ├── jwk
│ │ │ │ ├── jwk-thumbprint.ts
│ │ │ │ ├── jwks.ts
│ │ │ │ └── z-jwk.ts
│ │ │ ├── jwt
│ │ │ │ ├── __tests__
│ │ │ │ │ └── decode-jwt.test.mts
│ │ │ │ ├── decode-jwt-header.ts
│ │ │ │ ├── decode-jwt.ts
│ │ │ │ ├── verify-jwt.ts
│ │ │ │ ├── z-jwe.ts
│ │ │ │ └── z-jwt.ts
│ │ │ ├── z-common.ts
│ │ │ └── z-oauth2-error.ts
│ │ ├── dpop
│ │ │ ├── dpop-retry.ts
│ │ │ ├── dpop.ts
│ │ │ └── z-dpop.ts
│ │ ├── error
│ │ │ ├── Oauth2ClientAuthorizationChallengeError.ts
│ │ │ ├── Oauth2ClientErrorResponseError.ts
│ │ │ ├── Oauth2Error.ts
│ │ │ ├── Oauth2InvalidFetchResponseError.ts
│ │ │ ├── Oauth2JwtParseError.ts
│ │ │ ├── Oauth2JwtVerificationError.ts
│ │ │ ├── Oauth2ResourceUnauthorizedError.ts
│ │ │ └── Oauth2ServerErrorResponseError.ts
│ │ ├── index.ts
│ │ ├── metadata
│ │ │ ├── authorization-server
│ │ │ │ ├── __tests__
│ │ │ │ │ └── z-authorization-server-metadata.test.mts
│ │ │ │ ├── authorization-server-metadata.ts
│ │ │ │ └── z-authorization-server-metadata.ts
│ │ │ ├── fetch-jwks-uri.ts
│ │ │ └── fetch-well-known-metadata.ts
│ │ ├── pkce.ts
│ │ ├── resource-request
│ │ │ ├── make-resource-request.ts
│ │ │ └── verify-resource-request.ts
│ │ └── z-grant-type.ts
│ ├── tests
│ │ └── util.mts
│ └── tsconfig.json
├── openid4vci
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── Openid4vciClient.ts
│ │ ├── Openid4vciIssuer.ts
│ │ ├── Openid4vciWalletProvider.ts
│ │ ├── __tests__
│ │ │ ├── Openid4vciClient.test.mts
│ │ │ ├── Openid4vciIssuer.test.mts
│ │ │ └── __fixtures__
│ │ │ │ ├── bdr.ts
│ │ │ │ ├── paradym.ts
│ │ │ │ └── presentationDuringIssuance.ts
│ │ ├── credential-offer
│ │ │ ├── __tests__
│ │ │ │ ├── credential-offer.test.mts
│ │ │ │ └── z-credential-offer.test.mts
│ │ │ ├── credential-offer.ts
│ │ │ └── z-credential-offer.ts
│ │ ├── credential-request
│ │ │ ├── __tests__
│ │ │ │ ├── parse-credential-request.test.mts
│ │ │ │ └── z-credential-request.test.mts
│ │ │ ├── credential-request-configurations.ts
│ │ │ ├── credential-response.ts
│ │ │ ├── format-payload.ts
│ │ │ ├── parse-credential-request.ts
│ │ │ ├── retrieve-credentials.ts
│ │ │ ├── z-credential-request-common.ts
│ │ │ ├── z-credential-request.ts
│ │ │ └── z-credential-response.ts
│ │ ├── error
│ │ │ ├── Openid4vciError.ts
│ │ │ ├── Openid4vciRetrieveCredentialsError.ts
│ │ │ └── Openid4vciSendNotificationError.ts
│ │ ├── formats
│ │ │ ├── credential
│ │ │ │ ├── index.ts
│ │ │ │ ├── mso-mdoc
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ └── z-mso-mdoc.test.mts
│ │ │ │ │ └── z-mso-mdoc.ts
│ │ │ │ ├── sd-jwt-dc
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ └── z-sd-jwt-dc.test.mts
│ │ │ │ │ └── z-sd-jwt-dc.ts
│ │ │ │ ├── sd-jwt-vc
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ └── z-sd-jwt-vc.test.mts
│ │ │ │ │ └── z-sd-jwt-vc.ts
│ │ │ │ └── w3c-vc
│ │ │ │ │ ├── z-w3c-jwt-vc-json-ld.ts
│ │ │ │ │ ├── z-w3c-jwt-vc-json.ts
│ │ │ │ │ ├── z-w3c-ldp-vc.ts
│ │ │ │ │ └── z-w3c-vc-common.ts
│ │ │ └── proof-type
│ │ │ │ ├── attestation
│ │ │ │ ├── attestation-proof-type.ts
│ │ │ │ └── z-attestation-proof-type.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── jwt
│ │ │ │ ├── jwt-proof-type.ts
│ │ │ │ └── z-jwt-proof-type.ts
│ │ ├── index.ts
│ │ ├── key-attestation
│ │ │ ├── key-attestation.ts
│ │ │ └── z-key-attestation.ts
│ │ ├── metadata
│ │ │ ├── credential-issuer
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── credential-configurations.test.mts
│ │ │ │ │ ├── z-credential-configurations.test.mts
│ │ │ │ │ └── z-credential-issuer-metadata.test.mts
│ │ │ │ ├── credential-configurations.ts
│ │ │ │ ├── credential-issuer-metadata.ts
│ │ │ │ ├── z-claims-description.ts
│ │ │ │ ├── z-credential-configuration-supported-common.ts
│ │ │ │ └── z-credential-issuer-metadata.ts
│ │ │ └── fetch-issuer-metadata.ts
│ │ ├── nonce
│ │ │ ├── nonce-request.ts
│ │ │ └── z-nonce.ts
│ │ ├── notification
│ │ │ ├── notification.ts
│ │ │ └── z-notification.ts
│ │ └── version.ts
│ ├── tests
│ │ └── full-flow.test.mts
│ └── tsconfig.json
├── openid4vp
│ ├── Readme.md
│ ├── package.json
│ ├── src
│ │ ├── Openid4vpClient.ts
│ │ ├── Openid4vpVerifier.ts
│ │ ├── authorization-request
│ │ │ ├── __tests__
│ │ │ │ └── parse-authorization-request-params.test.mts
│ │ │ ├── create-authorization-request.ts
│ │ │ ├── parse-authorization-request-params.ts
│ │ │ ├── resolve-authorization-request.ts
│ │ │ ├── validate-authorization-request-dc-api.ts
│ │ │ ├── validate-authorization-request.ts
│ │ │ ├── z-authorization-request-dc-api.ts
│ │ │ └── z-authorization-request.ts
│ │ ├── authorization-response
│ │ │ ├── create-authorization-response.ts
│ │ │ ├── parse-authorization-response-payload.ts
│ │ │ ├── parse-authorization-response.ts
│ │ │ ├── parse-jarm-authorization-response.ts
│ │ │ ├── submit-authorization-response.ts
│ │ │ ├── validate-authorization-response-result.ts
│ │ │ ├── validate-authorization-response.ts
│ │ │ └── z-authorization-response.ts
│ │ ├── client-identifier-scheme
│ │ │ ├── parse-client-identifier-scheme.ts
│ │ │ ├── validate-verifier-attestation-jwt.ts
│ │ │ └── z-client-id-scheme.ts
│ │ ├── fetch-client-metadata.ts
│ │ ├── index.ts
│ │ ├── jar
│ │ │ ├── create-jar-authorization-request.ts
│ │ │ ├── handle-jar-request
│ │ │ │ ├── validate-jar-request-against-session.ts
│ │ │ │ └── verify-jar-request.ts
│ │ │ ├── jar-request-object
│ │ │ │ ├── fetch-jar-request-object.ts
│ │ │ │ └── z-jar-request-object.ts
│ │ │ ├── z-jar-authorization-request.ts
│ │ │ ├── z-jar-authorization-server-metadata.ts
│ │ │ └── z-jar-client-metadata.ts
│ │ ├── jarm
│ │ │ ├── jarm-authorizatino-response-send.ts
│ │ │ ├── jarm-authorization-response-create.ts
│ │ │ ├── jarm-authorization-response
│ │ │ │ ├── jarm-validate-authorization-response.ts
│ │ │ │ ├── verify-jarm-authorization-response.ts
│ │ │ │ └── z-jarm-authorization-response.ts
│ │ │ ├── jarm-extract-jwks.ts
│ │ │ ├── jarm-response-mode.ts
│ │ │ ├── metadata
│ │ │ │ ├── jarm-assert-metadata-supported.ts
│ │ │ │ ├── z-jarm-authorization-server-metadata.ts
│ │ │ │ └── z-jarm-client-metadata.ts
│ │ │ └── parse-jarm-authorization-response-direct-post-jwt.ts
│ │ ├── models
│ │ │ ├── z-client-metadata.ts
│ │ │ ├── z-credential-formats.ts
│ │ │ ├── z-pex.ts
│ │ │ ├── z-proof-formats.ts
│ │ │ ├── z-verifier-attestations.ts
│ │ │ ├── z-vp-formats-supported.ts
│ │ │ └── z-wallet-metadata.ts
│ │ ├── transaction-data
│ │ │ ├── parse-transaction-data.ts
│ │ │ ├── verify-transaction-data.test.mts
│ │ │ ├── verify-transaction-data.ts
│ │ │ └── z-transaction-data.ts
│ │ ├── version.ts
│ │ └── vp-token
│ │ │ ├── parse-vp-token.ts
│ │ │ └── z-vp-token.ts
│ ├── tests
│ │ ├── full-flow.test.mts
│ │ ├── parse-client-identifier-scheme.test.mts
│ │ └── version-inference.test.mts
│ └── tsconfig.json
└── utils
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── __tests__
│ │ ├── validation-error.test.mts
│ │ └── www-authenticate.test.mts
│ ├── array.ts
│ ├── config.ts
│ ├── content-type.ts
│ ├── date.ts
│ ├── encoding.ts
│ ├── error
│ │ ├── FetchError.ts
│ │ ├── InvalidFetchResponseError.ts
│ │ ├── JsonParseError.ts
│ │ └── ValidationError.ts
│ ├── fetcher.ts
│ ├── globals.ts
│ ├── index.ts
│ ├── object.ts
│ ├── parse.ts
│ ├── path.ts
│ ├── type.ts
│ ├── url.ts
│ ├── validation.ts
│ ├── www-authenticate.ts
│ └── zod-error.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.json
└── vite.config.js
/.changeset/afraid-falcons-work.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": minor
3 | "@openid4vc/oauth2": minor
4 | "@openid4vc/utils": minor
5 | ---
6 |
7 | Add support for OpenID4VCI draft 15. It also includes improved support for client (wallet) attestations, and better support for server side verification.
8 |
9 | Due to the changes between Draft 14 and Draft 15 and it's up to the caller of this library to handle the difference between the versions. Draft 11 is still supported based on Draft 14 syntax (and thus will be automatically converted).
10 |
--------------------------------------------------------------------------------
/.changeset/afraid-waves-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": patch
3 | ---
4 |
5 | Added `verifier_attestations` to DC Api type
6 |
--------------------------------------------------------------------------------
/.changeset/chatty-buttons-sip.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": minor
3 | ---
4 |
5 | feat: initial version of openid4vp
6 |
--------------------------------------------------------------------------------
/.changeset/commit.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('node:child_process')
2 |
3 | const getSignedOffBy = () => {
4 | const gitUserName = execSync('git config user.name').toString('utf-8').trim()
5 | const gitEmail = execSync('git config user.email').toString('utf-8').trim()
6 |
7 | return `Signed-off-by: ${gitUserName} <${gitEmail}>`
8 | }
9 |
10 | const getAddMessage = async (changeset) => {
11 | return `docs(changeset): ${changeset.summary}\n\n${getSignedOffBy()}\n`
12 | }
13 |
14 | const getVersionMessage = async () => {
15 | return `chore(release): new version\n\n${getSignedOffBy()}\n`
16 | }
17 |
18 | module.exports = {
19 | getAddMessage,
20 | getVersionMessage,
21 | }
22 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": "./commit",
5 | "privatePackages": false,
6 | "fixed": [["@openid4vc/*"]],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "snapshot": {
11 | "useCalculatedVersion": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.changeset/cruel-pants-search.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": minor
3 | "@openid4vc/oauth2": minor
4 | ---
5 |
6 | apu and apv in JWE encryptor are now base64 encoded values, to align with JOSE
7 |
--------------------------------------------------------------------------------
/.changeset/fair-hairs-bathe.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/oauth2": patch
3 | ---
4 |
5 | fix: make key_ops array of strings instead of string in jwk
6 |
--------------------------------------------------------------------------------
/.changeset/funny-actors-kiss.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": minor
3 | "@openid4vc/oauth2": minor
4 | "@openid4vc/utils": minor
5 | ---
6 |
7 | fix typo in param from authorizationServerMetata to authorizationServerMetadata
8 |
--------------------------------------------------------------------------------
/.changeset/healthy-items-push.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": minor
3 | ---
4 |
5 | renamed oid4vci to openid4vci. This includes the package name, but also the class names and oid4vpRequestUrl to openid4vpRequestUrl
6 |
--------------------------------------------------------------------------------
/.changeset/lemon-buses-pump.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": minor
3 | "@openid4vc/oauth2": minor
4 | ---
5 |
6 | replace the `dpopJwk` return value with `dpop` object with `jwk` key. It now also returns the `jwkThumbprint`
7 |
--------------------------------------------------------------------------------
/.changeset/lovely-schools-brush.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": patch
3 | ---
4 |
5 | support key-attestation+jwt in addition to keyattestation+jwt when verifying key attestation
6 |
--------------------------------------------------------------------------------
/.changeset/mighty-cougars-work.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": patch
3 | "@openid4vc/oauth2": patch
4 | ---
5 |
6 | fix: path where oauth2 authorization server metadata is retrieved from.
7 |
8 | For OAuth you need to put `.well-known/oauth-authorization-server` between the origin and path (so `https://funke.animo.id/provider` becomes `https://funke.animo.id/.well-known/oauth-authorization-server/provider`). We were putting the well known path after the full issuer url.
9 |
10 | It will now first check the correct path, and fall back to the invalid path.
11 |
--------------------------------------------------------------------------------
/.changeset/perfect-bees-clean.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vci": minor
3 | "@openid4vc/oauth2": minor
4 | "@openid4vc/utils": minor
5 | ---
6 |
7 | Before this PR, all packages used Valibot for data validation.
8 | We have now fully transitioned to Zod. This introduces obvious breaking changes for some packages that re-exported Valibot types or schemas for example.
9 |
--------------------------------------------------------------------------------
/.changeset/rare-pillows-taste.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": patch
3 | ---
4 |
5 | Exposed types and schema for verifier attestations
6 |
--------------------------------------------------------------------------------
/.changeset/red-ladybugs-perform.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": minor
3 | "@openid4vc/oauth2": minor
4 | "@openid4vc/openid4vci": minor
5 | ---
6 |
7 | refactor: change the jwt signer method 'trustChain' to 'federation' and make 'trustChain' variable optional.
8 |
--------------------------------------------------------------------------------
/.changeset/selfish-dolls-fix.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": patch
3 | ---
4 |
5 | fix: export JarmMode enum
6 |
--------------------------------------------------------------------------------
/.changeset/tame-plants-kneel.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": patch
3 | ---
4 |
5 | - Added `verifier_attestations` parsing
6 |
--------------------------------------------------------------------------------
/.changeset/three-clouds-return.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/oauth2": patch
3 | ---
4 |
5 | feat: add `kid` to the JwtSigner interface
6 |
--------------------------------------------------------------------------------
/.changeset/twelve-kangaroos-help.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/oauth2": minor
3 | "@openid4vc/openid4vci": minor
4 | ---
5 |
6 | add support for sha-384, sha-512, rename SHA-256 to sha-256 to align with IANA hash algorithm names (https://www.iana.org/assignments/named-information/named-information.xhtml)
7 |
--------------------------------------------------------------------------------
/.changeset/twenty-shirts-love.md:
--------------------------------------------------------------------------------
1 | ---
2 | "@openid4vc/openid4vp": patch
3 | "@openid4vc/oauth2": patch
4 | "@openid4vc/utils": patch
5 | "@openid4vc/openid4vci": patch
6 | ---
7 |
8 | fix: create fetch wrapper that always calls toString on URLSearchParams as React Native does not encode this correctly while Node.JS does
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | pull_request_review:
5 |
6 | pull_request:
7 | branches:
8 | - main
9 | push:
10 | branches:
11 | - main
12 |
13 | jobs:
14 | validate:
15 | if: github.event_name != 'pull_request_review' || github.event.pull_request.head.ref == 'changeset-release/main'
16 | runs-on: ubuntu-24.04
17 | name: Validate
18 | steps:
19 | - name: Checkout Repo
20 | uses: actions/checkout@v4
21 |
22 | - uses: pnpm/action-setup@v4
23 | - name: Setup NodeJS
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: 20
27 | cache: "pnpm"
28 |
29 | - name: Install dependencies
30 | run: pnpm install --frozen-lockfile
31 |
32 | - name: Check Style
33 | run: pnpm style:check
34 |
35 | - name: Check Types
36 | run: pnpm types:check
37 |
38 | - name: Compile
39 | run: pnpm build
40 |
41 | tests:
42 | runs-on: ubuntu-24.04
43 | name: Tests
44 | if: github.event_name != 'pull_request_review' || github.event.pull_request.head.ref == 'changeset-release/main'
45 |
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | node-version: [18, 20]
50 |
51 | steps:
52 | - name: Checkout Repo
53 | uses: actions/checkout@v4
54 |
55 | - uses: pnpm/action-setup@v4
56 | - name: Setup NodeJS
57 | id: setup-node
58 | uses: actions/setup-node@v4
59 | with:
60 | node-version: ${{ matrix.node-version }}
61 |
62 | # See https://github.com/actions/setup-node/issues/641#issuecomment-1358859686
63 | - name: pnpm cache path
64 | id: pnpm-cache-path
65 | run: |
66 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
67 |
68 | - name: pnpm cache
69 | uses: actions/cache@v3
70 | with:
71 | path: ${{ steps.pnpm-cache-path.outputs.STORE_PATH }}
72 | key: ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
73 | restore-keys: |
74 | ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store-
75 |
76 | - name: Install dependencies
77 | run: pnpm install --frozen-lockfile
78 |
79 | - name: Run tests
80 | run: pnpm test
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | .tgz
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "files": {
4 | "maxSize": 10000000
5 | },
6 | "formatter": {
7 | "lineWidth": 120,
8 | "indentStyle": "space"
9 | },
10 | "javascript": {
11 | "parser": {
12 | "unsafeParameterDecoratorsEnabled": true
13 | },
14 | "formatter": {
15 | "semicolons": "asNeeded",
16 | "quoteStyle": "single",
17 | "trailingCommas": "es5",
18 | "lineWidth": 120,
19 | "indentStyle": "space"
20 | }
21 | },
22 | "json": {
23 | "parser": {
24 | "allowComments": true
25 | }
26 | },
27 | "organizeImports": {
28 | "enabled": true
29 | },
30 | "linter": {
31 | "enabled": true,
32 | "rules": {
33 | "recommended": true,
34 | "style": {
35 | "noRestrictedGlobals": {
36 | "level": "error",
37 | "options": {
38 | "deniedGlobals": ["URL", "URLSearchParams", "fetch", "Response", "Headers"]
39 | }
40 | }
41 | },
42 | "correctness": {
43 | "recommended": true,
44 | "noUnusedImports": "error"
45 | }
46 | }
47 | },
48 | "vcs": {
49 | "useIgnoreFile": true,
50 | "clientKind": "git",
51 | "enabled": true
52 | },
53 | "overrides": [
54 | {
55 | "include": ["*.test.ts", "**/tests/*", "*.test.mts"],
56 | "linter": {
57 | "rules": {
58 | "style": {
59 | "noRestrictedGlobals": {
60 | "level": "info",
61 | "options": {}
62 | }
63 | }
64 | }
65 | }
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oid4vc-ts",
3 | "private": true,
4 | "description": "Environment agnostic TypeScript implementation of OpenID4VC specifications",
5 | "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
6 | "repository": {
7 | "url": "https://github.com/openwallet-foundation-labs/oid4vc-ts",
8 | "type": "git"
9 | },
10 | "author": "Animo Solutions",
11 | "license": "Apache-2.0",
12 | "scripts": {
13 | "types:check": "tsc --noEmit",
14 | "style:check": "biome check --unsafe",
15 | "style:fix": "biome check --write --unsafe",
16 | "build": "pnpm -r build",
17 | "test": "vitest",
18 | "release": "pnpm build && pnpm changeset publish --no-git-tag",
19 | "changeset-version": "pnpm changeset version && pnpm style:fix"
20 | },
21 | "devDependencies": {
22 | "@biomejs/biome": "^1.9.4",
23 | "@changesets/cli": "^2.28.1",
24 | "msw": "^2.7.3",
25 | "tsup": "^8.4.0",
26 | "typescript": "^5.8.2",
27 | "vitest": "^3.0.9"
28 | },
29 | "engines": {
30 | "node": ">=18"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/oauth2/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openid4vc/oauth2
2 |
3 | ## 0.2.0
4 |
5 | ### Minor Changes
6 |
7 | - 0f60387: feat: add client attestations
8 | - 3f6d360: change order of fetching authorization server metadata. First `oauth-authorization-server` metadata is fetched now. If that returns a 404, the `openid-configuration` will be fetched.
9 |
10 | ### Patch Changes
11 |
12 | - @openid4vc/utils@0.2.0
13 |
14 | ## 0.1.4
15 |
16 | ### Patch Changes
17 |
18 | - 12a517a: feat: add refresh_token grant type
19 | - @openid4vc/utils@0.1.4
20 |
21 | ## 0.1.3
22 |
23 | ### Patch Changes
24 |
25 | - d4b9279: chore: create github release
26 | - Updated dependencies [d4b9279]
27 | - @openid4vc/utils@0.1.3
28 |
29 | ## 0.1.2
30 |
31 | ### Patch Changes
32 |
33 | - 1de27e5: chore: correct formatting for publishing
34 | - Updated dependencies [1de27e5]
35 | - @openid4vc/utils@0.1.2
36 |
37 | ## 0.1.1
38 |
39 | ### Patch Changes
40 |
41 | - 6434781: docs: add readme
42 | - Updated dependencies [6434781]
43 | - @openid4vc/utils@0.1.1
44 |
45 | ## 0.1.0
46 |
47 | ### Minor Changes
48 |
49 | - 71326c8: feat: initial release
50 |
51 | ### Patch Changes
52 |
53 | - Updated dependencies [71326c8]
54 | - @openid4vc/utils@0.1.0
55 |
--------------------------------------------------------------------------------
/packages/oauth2/README.md:
--------------------------------------------------------------------------------
1 |
OpenID for Verifiable Credentials - OAuth2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Check out the documentation in the [Github repository](https://github.com/openwallet-foundation-labs/oid4vc-ts).
10 |
--------------------------------------------------------------------------------
/packages/oauth2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openid4vc/oauth2",
3 | "version": "0.2.0",
4 | "files": ["dist"],
5 | "license": "Apache-2.0",
6 | "exports": "./src/index.ts",
7 | "homepage": "https://github.com/openwallet-foundation-labs/oid4vc-ts/tree/main/packages/oauth2",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/openwallet-foundation-labs/oid4vc-ts",
11 | "directory": "packages/oauth2"
12 | },
13 | "publishConfig": {
14 | "main": "./dist/index.js",
15 | "module": "./dist/index.mjs",
16 | "types": "./dist/index.d.ts",
17 | "exports": {
18 | ".": {
19 | "import": "./dist/index.mjs",
20 | "require": "./dist/index.js",
21 | "types": "./dist/index.d.ts"
22 | },
23 | "./package.json": "./package.json"
24 | }
25 | },
26 | "scripts": {
27 | "build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap"
28 | },
29 | "dependencies": {
30 | "@openid4vc/utils": "workspace:*",
31 | "zod": "catalog:"
32 | },
33 | "devDependencies": {
34 | "jose": "catalog:"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/oauth2/src/Oauth2ResourceServer.ts:
--------------------------------------------------------------------------------
1 | import { type VerifyResourceRequestOptions, verifyResourceRequest } from '.'
2 | import type { CallbackContext } from './callbacks'
3 |
4 | export interface Oauth2ResourceServerOptions {
5 | /**
6 | * Callbacks required for the oauth2 resource server
7 | */
8 | callbacks: Pick
9 | }
10 |
11 | export class Oauth2ResourceServer {
12 | public constructor(private options: Oauth2ResourceServerOptions) {}
13 |
14 | public async verifyResourceRequest(options: Omit) {
15 | return verifyResourceRequest({
16 | callbacks: this.options.callbacks,
17 | ...options,
18 | })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/create-access-token-response.ts:
--------------------------------------------------------------------------------
1 | import { parseWithErrorHandling } from '@openid4vc/utils'
2 | import type { CallbackContext } from '../callbacks'
3 | import { type AccessTokenResponse, zAccessTokenResponse } from './z-access-token'
4 |
5 | export interface CreateAccessTokenResponseOptions {
6 | callbacks: Pick
7 |
8 | /**
9 | * The access token
10 | */
11 | accessToken: string
12 |
13 | /**
14 | * The type of token. Should be DPoP if the access token
15 | * is bound to a dpop key
16 | */
17 | tokenType: 'DPoP' | 'Bearer' | (string & {})
18 |
19 | /**
20 | * Number of seconds after which the access tokens expires.
21 | */
22 | expiresInSeconds: number
23 |
24 | /**
25 | * New cNonce value
26 | */
27 | cNonce?: string
28 | cNonceExpiresIn?: number
29 |
30 | /**
31 | * Additional payload to include in the access token response.
32 | *
33 | * Will be applied after default payload to allow overriding over values, but be careful.
34 | */
35 | additionalPayload?: Record
36 | }
37 |
38 | export async function createAccessTokenResponse(options: CreateAccessTokenResponseOptions) {
39 | const accessTokenResponse = parseWithErrorHandling(zAccessTokenResponse, {
40 | access_token: options.accessToken,
41 | token_type: options.tokenType,
42 | expires_in: options.expiresInSeconds,
43 | c_nonce: options.cNonce,
44 | c_nonce_expires_in: options.cNonceExpiresIn,
45 | ...options.additionalPayload,
46 | } satisfies AccessTokenResponse)
47 |
48 | return accessTokenResponse
49 | }
50 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/introspect-token.ts:
--------------------------------------------------------------------------------
1 | import { ContentType, createZodFetcher, objectToQueryParams, parseWithErrorHandling } from '@openid4vc/utils'
2 | import { InvalidFetchResponseError } from '@openid4vc/utils'
3 | import { Oauth2Error } from '../error/Oauth2Error'
4 | import type { AuthorizationServerMetadata } from '../metadata/authorization-server/z-authorization-server-metadata'
5 |
6 | import { Headers } from '@openid4vc/utils'
7 | import type { CallbackContext } from '../callbacks'
8 | import {
9 | type TokenIntrospectionRequest,
10 | zTokenIntrospectionRequest,
11 | zTokenIntrospectionResponse,
12 | } from './z-token-introspection'
13 |
14 | export interface IntrospectTokenOptions {
15 | /**
16 | * Metadata of the authorization server. Must contain an `introspection_endpoint`
17 | */
18 | authorizationServerMetadata: AuthorizationServerMetadata
19 |
20 | /**
21 | * The provided acccess token
22 | */
23 | token: string
24 |
25 | /**
26 | * The scheme of the access token, will be sent along with the token
27 | * as a hint.
28 | */
29 | tokenTypeHint?: string
30 |
31 | /**
32 | * Additional payload to include in the introspection equest. Items will be encoded and sent
33 | * using x-www-form-urlencoded format. Nested items (JSON) will be stringified and url encoded.
34 | */
35 | additionalPayload?: Record
36 |
37 | callbacks: Pick
38 | }
39 |
40 | export async function introspectToken(options: IntrospectTokenOptions) {
41 | const fetchWithZod = createZodFetcher(options.callbacks.fetch)
42 |
43 | const introspectionRequest = parseWithErrorHandling(zTokenIntrospectionRequest, {
44 | token: options.token,
45 | token_type_hint: options.tokenTypeHint,
46 | ...options.additionalPayload,
47 | } satisfies TokenIntrospectionRequest)
48 |
49 | const introspectionEndpoint = options.authorizationServerMetadata.introspection_endpoint
50 | if (!introspectionEndpoint) {
51 | throw new Oauth2Error(`Missing required 'introspection_endpoint' parameter in authorization server metadata`)
52 | }
53 |
54 | const headers = new Headers({
55 | 'Content-Type': ContentType.XWwwFormUrlencoded,
56 | })
57 |
58 | // Apply client authentication
59 | await options.callbacks.clientAuthentication({
60 | url: introspectionEndpoint,
61 | method: 'POST',
62 | authorizationServerMetadata: options.authorizationServerMetadata,
63 | body: introspectionRequest,
64 | contentType: ContentType.XWwwFormUrlencoded,
65 | headers,
66 | })
67 |
68 | const { result, response } = await fetchWithZod(
69 | zTokenIntrospectionResponse,
70 | ContentType.Json,
71 | introspectionEndpoint,
72 | {
73 | body: objectToQueryParams(introspectionRequest).toString(),
74 | method: 'POST',
75 | headers,
76 | }
77 | )
78 |
79 | // TODO: better error handling (error response?)
80 | if (!response.ok || !result?.success) {
81 | throw new InvalidFetchResponseError(
82 | `Unable to introspect token from '${introspectionEndpoint}'. Received response with status ${response.status}`,
83 | await response.clone().text(),
84 | response
85 | )
86 | }
87 |
88 | return result.data
89 | }
90 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/verify-access-token.ts:
--------------------------------------------------------------------------------
1 | import type { CallbackContext } from '../callbacks'
2 | import { extractJwkFromJwksForJwt } from '../common/jwk/jwks'
3 | import { decodeJwt } from '../common/jwt/decode-jwt'
4 | import { verifyJwt } from '../common/jwt/verify-jwt'
5 | import { Oauth2Error } from '../error/Oauth2Error'
6 | import type { AuthorizationServerMetadata } from '../metadata/authorization-server/z-authorization-server-metadata'
7 | import { fetchJwks } from '../metadata/fetch-jwks-uri'
8 | import { zAccessTokenProfileJwtHeader, zAccessTokenProfileJwtPayload } from './z-access-token-jwt'
9 |
10 | export enum SupportedAuthenticationScheme {
11 | Bearer = 'Bearer',
12 | DPoP = 'DPoP',
13 | }
14 |
15 | export interface VerifyJwtProfileAccessTokenOptions {
16 | /**
17 | * The access token
18 | */
19 | accessToken: string
20 |
21 | /**
22 | * Callbacks used for verifying the access token
23 | */
24 | callbacks: Pick
25 |
26 | /**
27 | * If not provided current time will be used
28 | */
29 | now?: Date
30 |
31 | /**
32 | * Identifier of the resource server
33 | */
34 | resourceServer: string
35 |
36 | /**
37 | * List of authorization servers that this resource endpoint supports
38 | */
39 | authorizationServers: AuthorizationServerMetadata[]
40 | }
41 |
42 | /**
43 | * Verify an access token as a JWT Profile access token.
44 | *
45 | * @throws {@link ValidationError} if the JWT header or payload does not align with JWT Profile rules
46 | * @throws {@link Oauth2JwtParseError} if the jwt is not a valid jwt format, or the jwt header/payload cannot be parsed as JSON
47 | * @throws {@link Oauth2JwtVerificationError} if the JWT verification fails (signature or nbf/exp)
48 | * @throws {@link Oauth2JwtVerificationError} if the JWT verification fails (signature or nbf/exp)
49 | */
50 | export async function verifyJwtProfileAccessToken(options: VerifyJwtProfileAccessTokenOptions) {
51 | const decodedJwt = decodeJwt({
52 | jwt: options.accessToken,
53 | headerSchema: zAccessTokenProfileJwtHeader,
54 | payloadSchema: zAccessTokenProfileJwtPayload,
55 | })
56 |
57 | const authorizationServer = options.authorizationServers.find(({ issuer }) => decodedJwt.payload.iss === issuer)
58 | if (!authorizationServer) {
59 | // Authorization server not found
60 | throw new Oauth2Error(
61 | `Access token jwt contains unrecognized authorization server 'iss' value of '${decodedJwt.payload.iss}'`
62 | )
63 | }
64 |
65 | const jwksUrl = authorizationServer.jwks_uri
66 | if (!jwksUrl) {
67 | throw new Oauth2Error(
68 | `Authorization server '${authorizationServer.issuer}' does not have a 'jwks_uri' parameter to fetch JWKs.`
69 | )
70 | }
71 |
72 | const jwks = await fetchJwks(jwksUrl, options.callbacks.fetch)
73 | const publicJwk = extractJwkFromJwksForJwt({
74 | kid: decodedJwt.header.kid,
75 | jwks,
76 | use: 'sig',
77 | })
78 |
79 | await verifyJwt({
80 | compact: options.accessToken,
81 | header: decodedJwt.header,
82 | payload: decodedJwt.payload,
83 | signer: { method: 'jwk', publicJwk, alg: decodedJwt.header.alg },
84 | verifyJwtCallback: options.callbacks.verifyJwt,
85 | errorMessage: 'Error during verification of access token jwt.',
86 | now: options.now,
87 | expectedAudience: options.resourceServer,
88 | })
89 |
90 | return {
91 | header: decodedJwt.header,
92 | payload: decodedJwt.payload,
93 | authorizationServer,
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/z-access-token-jwt.ts:
--------------------------------------------------------------------------------
1 | import { zInteger } from '@openid4vc/utils'
2 | import z from 'zod'
3 | import { zJwtHeader, zJwtPayload } from '../common/jwt/z-jwt'
4 |
5 | export const zAccessTokenProfileJwtHeader = z
6 | .object({
7 | ...zJwtHeader.shape,
8 | typ: z.enum(['application/at+jwt', 'at+jwt']),
9 | })
10 | .passthrough()
11 | export type AccessTokenProfileJwtHeader = z.infer
12 |
13 | export const zAccessTokenProfileJwtPayload = z
14 | .object({
15 | ...zJwtPayload.shape,
16 | iss: z.string(),
17 | exp: zInteger,
18 | iat: zInteger,
19 | aud: z.string(),
20 | sub: z.string(),
21 |
22 | // REQUIRED according to RFC 9068, but OpenID4VCI allows anonymous access
23 | client_id: z.optional(z.string()),
24 | jti: z.string(),
25 |
26 | // SHOULD be included in the authorization request contained it
27 | scope: z.optional(z.string()),
28 | })
29 | .passthrough()
30 |
31 | export type AccessTokenProfileJwtPayload = z.infer
32 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/z-access-token.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 |
3 | import { zHttpsUrl } from '@openid4vc/utils'
4 | import { zOauth2ErrorResponse } from '../common/z-oauth2-error'
5 | import {
6 | zAuthorizationCodeGrantIdentifier,
7 | zPreAuthorizedCodeGrantIdentifier,
8 | zRefreshTokenGrantIdentifier,
9 | } from '../z-grant-type'
10 |
11 | export const zAccessTokenRequest = z.intersection(
12 | z
13 | .object({
14 | // Pre authorized code flow
15 | 'pre-authorized_code': z.optional(z.string()),
16 |
17 | // Authorization code flow
18 | code: z.optional(z.string()),
19 | redirect_uri: z.string().url().optional(),
20 |
21 | // Refresh token grant
22 | refresh_token: z.optional(z.string()),
23 |
24 | resource: z.optional(zHttpsUrl),
25 | code_verifier: z.optional(z.string()),
26 |
27 | grant_type: z.union([
28 | zPreAuthorizedCodeGrantIdentifier,
29 | zAuthorizationCodeGrantIdentifier,
30 | zRefreshTokenGrantIdentifier,
31 | // string makes the previous ones unessary, but it does help with error messages
32 | z.string(),
33 | ]),
34 | })
35 | .passthrough(),
36 | z
37 | .object({
38 | tx_code: z.optional(z.string()),
39 | // user_pin is from OpenID4VCI draft 11
40 | user_pin: z.optional(z.string()),
41 | })
42 | .passthrough()
43 | .refine(({ tx_code, user_pin }) => !tx_code || !user_pin || user_pin === tx_code, {
44 | message: `If both 'tx_code' and 'user_pin' are present they must match`,
45 | })
46 | .transform(({ tx_code, user_pin, ...rest }) => {
47 | return {
48 | ...rest,
49 | ...((tx_code ?? user_pin) ? { tx_code: tx_code ?? user_pin } : {}),
50 | }
51 | })
52 | )
53 | export type AccessTokenRequest = z.infer
54 |
55 | export const zAccessTokenResponse = z
56 | .object({
57 | access_token: z.string(),
58 | token_type: z.string(),
59 |
60 | expires_in: z.optional(z.number().int()),
61 | scope: z.optional(z.string()),
62 | state: z.optional(z.string()),
63 |
64 | refresh_token: z.optional(z.string()),
65 |
66 | // OpenID4VCI specific parameters
67 | c_nonce: z.optional(z.string()),
68 | c_nonce_expires_in: z.optional(z.number().int()),
69 |
70 | // TODO: add additional params
71 | authorization_details: z
72 | .array(
73 | z
74 | .object({
75 | // required when type is openid_credential (so we probably need a discriminator)
76 | // credential_identifiers: z.array(z.string()),
77 | })
78 | .passthrough()
79 | )
80 | .optional(),
81 | })
82 | .passthrough()
83 |
84 | export type AccessTokenResponse = z.infer
85 |
86 | export const zAccessTokenErrorResponse = zOauth2ErrorResponse
87 | export type AccessTokenErrorResponse = z.infer
88 |
--------------------------------------------------------------------------------
/packages/oauth2/src/access-token/z-token-introspection.ts:
--------------------------------------------------------------------------------
1 | import { zInteger } from '@openid4vc/utils'
2 | import z from 'zod'
3 | import { zJwtConfirmationPayload } from '../common/jwt/z-jwt'
4 |
5 | export const zTokenIntrospectionRequest = z
6 | .object({
7 | token: z.string(),
8 | token_type_hint: z.optional(z.string()),
9 | })
10 | .passthrough()
11 |
12 | export type TokenIntrospectionRequest = z.infer
13 |
14 | export const zTokenIntrospectionResponse = z
15 | .object({
16 | active: z.boolean(),
17 | scope: z.optional(z.string()),
18 | client_id: z.optional(z.string()),
19 | username: z.optional(z.string()),
20 | token_type: z.optional(z.string()),
21 |
22 | exp: z.optional(zInteger),
23 | iat: z.optional(zInteger),
24 | nbf: z.optional(zInteger),
25 |
26 | sub: z.optional(z.string()),
27 | aud: z.optional(z.string()),
28 |
29 | iss: z.optional(z.string()),
30 | jti: z.optional(z.string()),
31 |
32 | cnf: z.optional(zJwtConfirmationPayload),
33 | })
34 | .passthrough()
35 |
36 | export type TokenIntrospectionResponse = z.infer
37 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-challenge/parse-authorization-challenge-request.ts:
--------------------------------------------------------------------------------
1 | import { formatZodError } from '@openid4vc/utils'
2 | import {
3 | type ParseAuthorizationRequestResult,
4 | parseAuthorizationRequest,
5 | } from '../authorization-request/parse-authorization-request'
6 | import type { RequestLike } from '../common/z-common'
7 | import { Oauth2ErrorCodes } from '../common/z-oauth2-error'
8 | import { Oauth2ServerErrorResponseError } from '../error/Oauth2ServerErrorResponseError'
9 | import { type AuthorizationChallengeRequest, zAuthorizationChallengeRequest } from './z-authorization-challenge'
10 |
11 | export interface ParseAuthorizationChallengeRequestOptions {
12 | request: RequestLike
13 |
14 | authorizationChallengeRequest: unknown
15 | }
16 |
17 | export interface ParseAuthorizationChallengeRequestResult extends ParseAuthorizationRequestResult {
18 | authorizationChallengeRequest: AuthorizationChallengeRequest
19 | }
20 |
21 | /**
22 | * Parse an authorization challenge request.
23 | *
24 | * @throws {Oauth2ServerErrorResponseError}
25 | */
26 | export function parseAuthorizationChallengeRequest(
27 | options: ParseAuthorizationChallengeRequestOptions
28 | ): ParseAuthorizationChallengeRequestResult {
29 | const parsedAuthorizationChallengeRequest = zAuthorizationChallengeRequest.safeParse(
30 | options.authorizationChallengeRequest
31 | )
32 | if (!parsedAuthorizationChallengeRequest.success) {
33 | throw new Oauth2ServerErrorResponseError({
34 | error: Oauth2ErrorCodes.InvalidRequest,
35 | error_description: `Error occured during validation of authorization challenge request.\n${formatZodError(parsedAuthorizationChallengeRequest.error)}`,
36 | })
37 | }
38 |
39 | const authorizationChallengeRequest = parsedAuthorizationChallengeRequest.data
40 | const { clientAttestation, dpop } = parseAuthorizationRequest({
41 | authorizationRequest: authorizationChallengeRequest,
42 | request: options.request,
43 | })
44 |
45 | return {
46 | authorizationChallengeRequest: parsedAuthorizationChallengeRequest.data,
47 |
48 | dpop,
49 | clientAttestation,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-challenge/verify-authorization-challenge-request.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type VerifyAuthorizationRequestOptions,
3 | type VerifyAuthorizationRequestReturn,
4 | verifyAuthorizationRequest,
5 | } from '../authorization-request/verify-authorization-request'
6 | import type { AuthorizationChallengeRequest } from './z-authorization-challenge'
7 |
8 | export type VerifyAuthorizationChallengeRequestReturn = VerifyAuthorizationRequestReturn
9 | export interface VerifyAuthorizationChallengeRequestOptions
10 | extends Omit {
11 | authorizationChallengeRequest: AuthorizationChallengeRequest
12 | }
13 |
14 | export async function verifyAuthorizationChallengeRequest(
15 | options: VerifyAuthorizationChallengeRequestOptions
16 | ): Promise {
17 | const { clientAttestation, dpop } = await verifyAuthorizationRequest({
18 | ...options,
19 | authorizationRequest: options.authorizationChallengeRequest,
20 | })
21 |
22 | return {
23 | dpop,
24 | clientAttestation,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-challenge/z-authorization-challenge.ts:
--------------------------------------------------------------------------------
1 | import { zInteger } from '@openid4vc/utils'
2 |
3 | import z from 'zod'
4 | import { zAuthorizationRequest } from '../authorization-request/z-authorization-request'
5 | import { zOauth2ErrorResponse } from '../common/z-oauth2-error'
6 |
7 | export const zAuthorizationChallengeRequest = z
8 | .object({
9 | // authorization challenge request can include same parameters as an authorization request
10 | // except for response_type (always `code`), and `client_id` is optional (becase
11 | // it's possible to do client authentication using different methods)
12 | ...zAuthorizationRequest.omit({ response_type: true, client_id: true }).shape,
13 | client_id: z.optional(zAuthorizationRequest.shape.client_id),
14 |
15 | auth_session: z.optional(z.string()),
16 |
17 | // DRAFT presentation during issuance
18 | presentation_during_issuance_session: z.optional(z.string()),
19 | })
20 | .passthrough()
21 | export type AuthorizationChallengeRequest = z.infer
22 |
23 | export const zAuthorizationChallengeResponse = z
24 | .object({
25 | authorization_code: z.string(),
26 | })
27 | .passthrough()
28 | export type AuthorizationChallengeResponse = z.infer
29 |
30 | export const zAuthorizationChallengeErrorResponse = z
31 | .object({
32 | ...zOauth2ErrorResponse.shape,
33 | auth_session: z.optional(z.string()),
34 | request_uri: z.optional(z.string()),
35 | expires_in: z.optional(zInteger),
36 |
37 | // DRAFT: presentation during issuance
38 | presentation: z.optional(z.string()),
39 | })
40 | .passthrough()
41 | export type AuthorizationChallengeErrorResponse = z.infer
42 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-request/create-pushed-authorization-response.ts:
--------------------------------------------------------------------------------
1 | import { type StringWithAutoCompletion, parseWithErrorHandling } from '@openid4vc/utils'
2 | import { zAccessTokenErrorResponse } from '../access-token/z-access-token'
3 | import type { Oauth2ErrorCodes } from '../common/z-oauth2-error'
4 | import {
5 | type PushedAuthorizationErrorResponse,
6 | type PushedAuthorizationResponse,
7 | zPushedAuthorizationResponse,
8 | } from './z-authorization-request'
9 |
10 | export interface CreatePushedAuthorizationResponseOptions {
11 | /**
12 | * The request uri where the client should redirect to
13 | */
14 | requestUri: string
15 |
16 | /**
17 | * Number of seconds after which the `requestUri` will expire.
18 | */
19 | expiresInSeconds: number
20 |
21 | /**
22 | * Additional payload to include in the pushed authorization response.
23 | */
24 | additionalPayload?: Record
25 | }
26 |
27 | /**
28 | * Create an pushed authorization response
29 | *
30 | * @throws {ValidationError} if an error occured during verification of the {@link PushedAuthorizationResponse}
31 | */
32 | export function createPushedAuthorizationResponse(options: CreatePushedAuthorizationResponseOptions) {
33 | const pushedAuthorizationResponse = parseWithErrorHandling(zPushedAuthorizationResponse, {
34 | ...options.additionalPayload,
35 | expires_in: options.expiresInSeconds,
36 | request_uri: options.requestUri,
37 | } satisfies PushedAuthorizationResponse)
38 |
39 | return { pushedAuthorizationResponse }
40 | }
41 |
42 | export interface CreatePushedAuthorizationErrorResponseOptions {
43 | /**
44 | * The pushed authorization error
45 | */
46 | error: StringWithAutoCompletion
47 |
48 | /**
49 | * Optional error description
50 | */
51 | errorDescription?: string
52 |
53 | /**
54 | * Additional payload to include in the pushed authorization error response.
55 | */
56 | additionalPayload?: Record
57 | }
58 |
59 | /**
60 | * Create a pushed authorization error response
61 | *
62 | * @throws {ValidationError} if an error occured during validation of the {@link PushedAuthorizationErrorResponse}
63 | */
64 | export function createPushedAuthorizationErrorResponse(options: CreatePushedAuthorizationErrorResponseOptions) {
65 | const pushedAuthorizationErrorResponse = parseWithErrorHandling(zAccessTokenErrorResponse, {
66 | ...options.additionalPayload,
67 | error: options.error,
68 | error_description: options.errorDescription,
69 | } satisfies PushedAuthorizationErrorResponse)
70 |
71 | return pushedAuthorizationErrorResponse
72 | }
73 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-request/parse-pushed-authorization-request.ts:
--------------------------------------------------------------------------------
1 | import { formatZodError } from '@openid4vc/utils'
2 | import type { RequestLike } from '../common/z-common'
3 | import { Oauth2ErrorCodes } from '../common/z-oauth2-error'
4 | import { Oauth2ServerErrorResponseError } from '../error/Oauth2ServerErrorResponseError'
5 | import { type ParseAuthorizationRequestResult, parseAuthorizationRequest } from './parse-authorization-request'
6 | import { type AuthorizationRequest, zAuthorizationRequest } from './z-authorization-request'
7 |
8 | export interface ParsePushedAuthorizationRequestOptions {
9 | request: RequestLike
10 | authorizationRequest: unknown
11 | }
12 | export interface ParsePushedAuthorizationRequestResult extends ParseAuthorizationRequestResult {
13 | authorizationRequest: AuthorizationRequest
14 | }
15 |
16 | /**
17 | * Parse an pushed authorization request.
18 | *
19 | * @throws {Oauth2ServerErrorResponseError}
20 | */
21 | export function parsePushedAuthorizationRequest(
22 | options: ParsePushedAuthorizationRequestOptions
23 | ): ParsePushedAuthorizationRequestResult {
24 | const parsedAuthorizationRequest = zAuthorizationRequest.safeParse(options.authorizationRequest)
25 | if (!parsedAuthorizationRequest.success) {
26 | throw new Oauth2ServerErrorResponseError({
27 | error: Oauth2ErrorCodes.InvalidRequest,
28 | error_description: `Error occured during validation of pushed authorization request.\n${formatZodError(parsedAuthorizationRequest.error)}`,
29 | })
30 | }
31 |
32 | const authorizationRequest = parsedAuthorizationRequest.data
33 | const { clientAttestation, dpop } = parseAuthorizationRequest({
34 | authorizationRequest,
35 | request: options.request,
36 | })
37 |
38 | return {
39 | authorizationRequest,
40 |
41 | dpop,
42 | clientAttestation,
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-request/verify-pushed-authorization-request.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type VerifyAuthorizationRequestOptions,
3 | type VerifyAuthorizationRequestReturn,
4 | verifyAuthorizationRequest,
5 | } from './verify-authorization-request'
6 | import type { AuthorizationRequest } from './z-authorization-request'
7 |
8 | export type VerifyPushedAuthorizationRequestReturn = VerifyAuthorizationRequestReturn
9 | export interface VerifyPushedAuthorizationRequestOptions extends VerifyAuthorizationRequestOptions {
10 | authorizationRequest: AuthorizationRequest
11 | }
12 |
13 | export async function verifyPushedAuthorizationRequest(
14 | options: VerifyPushedAuthorizationRequestOptions
15 | ): Promise {
16 | const { clientAttestation, dpop } = await verifyAuthorizationRequest(options)
17 |
18 | return {
19 | dpop,
20 | clientAttestation,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/oauth2/src/authorization-request/z-authorization-request.ts:
--------------------------------------------------------------------------------
1 | import { zHttpsUrl } from '@openid4vc/utils'
2 | import z from 'zod'
3 | import { zOauth2ErrorResponse } from '../common/z-oauth2-error'
4 |
5 | // TODO: should create different request validations for different
6 | // response types. Currently we basically only support `code`
7 | export const zAuthorizationRequest = z
8 | .object({
9 | response_type: z.string(),
10 | client_id: z.string(),
11 |
12 | issuer_state: z.optional(z.string()),
13 | redirect_uri: z.string().url().optional(),
14 | resource: z.optional(zHttpsUrl),
15 | scope: z.optional(z.string()),
16 |
17 | // DPoP jwk thumbprint
18 | dpop_jkt: z.optional(z.string().base64url()),
19 |
20 | code_challenge: z.optional(z.string()),
21 | code_challenge_method: z.optional(z.string()),
22 | })
23 | .passthrough()
24 | export type AuthorizationRequest = z.infer
25 |
26 | export const zPushedAuthorizationRequest = z
27 | .object({
28 | request_uri: z.string(),
29 | client_id: z.string(),
30 | })
31 | .passthrough()
32 | export type PushedAuthorizationRequest = z.infer
33 |
34 | export const zPushedAuthorizationResponse = z
35 | .object({
36 | request_uri: z.string(),
37 | expires_in: z.number().int(),
38 | })
39 | .passthrough()
40 | export type PushedAuthorizationResponse = z.infer
41 |
42 | export const zPushedAuthorizationErrorResponse = zOauth2ErrorResponse
43 | export type PushedAuthorizationErrorResponse = z.infer
44 |
--------------------------------------------------------------------------------
/packages/oauth2/src/client-attestation/z-client-attestation.ts:
--------------------------------------------------------------------------------
1 | import { zJwtHeader, zJwtPayload } from '../common/jwt/z-jwt'
2 |
3 | import { zHttpsUrl, zInteger } from '@openid4vc/utils'
4 | import z from 'zod'
5 | import { zJwk } from '../common/jwk/z-jwk'
6 |
7 | export const zOauthClientAttestationHeader = z.literal('OAuth-Client-Attestation')
8 | export const oauthClientAttestationHeader = zOauthClientAttestationHeader.value
9 |
10 | export const zClientAttestationJwtPayload = z
11 | .object({
12 | ...zJwtPayload.shape,
13 | iss: z.string(),
14 | sub: z.string(),
15 | exp: zInteger,
16 | cnf: z
17 | .object({
18 | jwk: zJwk,
19 | })
20 | .passthrough(),
21 |
22 | // OID4VCI Wallet Attestation Extensions
23 | wallet_name: z.string().optional(),
24 | wallet_link: z.string().url().optional(),
25 | })
26 | .passthrough()
27 | export type ClientAttestationJwtPayload = z.infer
28 |
29 | export const zClientAttestationJwtHeader = z
30 | .object({
31 | ...zJwtHeader.shape,
32 | typ: z.literal('oauth-client-attestation+jwt'),
33 | })
34 | .passthrough()
35 |
36 | export type ClientAttestationJwtHeader = z.infer
37 |
38 | export const zOauthClientAttestationPopHeader = z.literal('OAuth-Client-Attestation-PoP')
39 | export const oauthClientAttestationPopHeader = zOauthClientAttestationPopHeader.value
40 |
41 | export const zClientAttestationPopJwtPayload = z
42 | .object({
43 | ...zJwtPayload.shape,
44 | iss: z.string(),
45 | exp: zInteger,
46 | aud: zHttpsUrl,
47 |
48 | jti: z.string(),
49 | nonce: z.optional(z.string()),
50 | })
51 | .passthrough()
52 | export type ClientAttestationPopJwtPayload = z.infer
53 |
54 | export const zClientAttestationPopJwtHeader = z
55 | .object({
56 | ...zJwtHeader.shape,
57 | typ: z.literal('oauth-client-attestation-pop+jwt'),
58 | })
59 | .passthrough()
60 | export type ClientAttestationPopJwtHeader = z.infer
61 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/jwk/jwk-thumbprint.ts:
--------------------------------------------------------------------------------
1 | import type { HashAlgorithm, HashCallback } from '../../callbacks'
2 |
3 | import { decodeUtf8String, encodeToBase64Url, parseWithErrorHandling } from '@openid4vc/utils'
4 | import z from 'zod'
5 | import type { Jwk } from './z-jwk'
6 |
7 | export const zJwkThumbprintComponents = z
8 | .discriminatedUnion('kty', [
9 | z.object({
10 | kty: z.literal('EC'),
11 | crv: z.string(),
12 | x: z.string(),
13 | y: z.string(),
14 | }),
15 | z.object({
16 | kty: z.literal('OKP'),
17 | crv: z.string(),
18 | x: z.string(),
19 | }),
20 | z.object({
21 | kty: z.literal('RSA'),
22 | e: z.string(),
23 | n: z.string(),
24 | }),
25 | z.object({
26 | kty: z.literal('oct'),
27 | k: z.string(),
28 | }),
29 | ])
30 | .transform((data) => {
31 | if (data.kty === 'EC') {
32 | return { crv: data.crv, kty: data.kty, x: data.x, y: data.y }
33 | }
34 |
35 | if (data.kty === 'OKP') {
36 | return { crv: data.crv, kty: data.kty, x: data.x }
37 | }
38 |
39 | if (data.kty === 'RSA') {
40 | return { e: data.e, kty: data.kty, n: data.n }
41 | }
42 |
43 | if (data.kty === 'oct') {
44 | return { k: data.k, kty: data.kty }
45 | }
46 |
47 | throw new Error('Unsupported kty')
48 | })
49 |
50 | export interface CalculateJwkThumbprintOptions {
51 | /**
52 | * The jwk to calcualte the thumbprint for.
53 | */
54 | jwk: Jwk
55 |
56 | /**
57 | * The hashing algorithm to use for calculating the thumbprint
58 | */
59 | hashAlgorithm: HashAlgorithm
60 |
61 | /**
62 | * The hash callback to calculate the digest
63 | */
64 | hashCallback: HashCallback
65 | }
66 |
67 | export async function calculateJwkThumbprint(options: CalculateJwkThumbprintOptions): Promise {
68 | const jwkThumbprintComponents = parseWithErrorHandling(
69 | zJwkThumbprintComponents,
70 | options.jwk,
71 | `Provided jwk does not match a supported jwk structure. Either the 'kty' is not supported, or required values are missing.`
72 | )
73 |
74 | const thumbprint = encodeToBase64Url(
75 | await options.hashCallback(decodeUtf8String(JSON.stringify(jwkThumbprintComponents)), options.hashAlgorithm)
76 | )
77 | return thumbprint
78 | }
79 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/jwk/jwks.ts:
--------------------------------------------------------------------------------
1 | import { type CallbackContext, HashAlgorithm } from '../../callbacks'
2 | import { Oauth2Error } from '../../error/Oauth2Error'
3 | import { calculateJwkThumbprint } from './jwk-thumbprint'
4 | import type { Jwk, JwkSet } from './z-jwk'
5 |
6 | interface ExtractJwkFromJwksForJwtOptions {
7 | kid?: string
8 | use: 'enc' | 'sig'
9 |
10 | /**
11 | * The JWKs
12 | */
13 | jwks: JwkSet
14 | }
15 |
16 | /**
17 | *
18 | * @param header
19 | * @param jwks
20 | */
21 | export function extractJwkFromJwksForJwt(options: ExtractJwkFromJwksForJwtOptions) {
22 | const jwksForUse = options.jwks.keys.filter(({ use }) => !use || use === options.use)
23 | const jwkForKid = options.kid ? jwksForUse.find(({ kid }) => kid === options.kid) : undefined
24 |
25 | if (jwkForKid) {
26 | return jwkForKid
27 | }
28 |
29 | if (jwksForUse.length === 1) {
30 | return jwksForUse[0]
31 | }
32 |
33 | throw new Oauth2Error(
34 | `Unable to extract jwk from jwks for use '${options.use}'${options.kid ? `with kid '${options.kid}'.` : '. No kid provided and more than jwk.'}`
35 | )
36 | }
37 |
38 | export async function isJwkInSet({
39 | jwk,
40 | jwks,
41 | callbacks,
42 | }: {
43 | jwk: Jwk
44 | jwks: Jwk[]
45 | callbacks: Pick
46 | }) {
47 | const jwkThumbprint = await calculateJwkThumbprint({
48 | hashAlgorithm: HashAlgorithm.Sha256,
49 | hashCallback: callbacks.hash,
50 | jwk,
51 | })
52 |
53 | for (const jwkFromSet of jwks) {
54 | const jwkFromSetThumbprint = await calculateJwkThumbprint({
55 | hashAlgorithm: HashAlgorithm.Sha256,
56 | hashCallback: callbacks.hash,
57 | jwk: jwkFromSet,
58 | })
59 |
60 | if (jwkFromSetThumbprint === jwkThumbprint) return true
61 | }
62 |
63 | return false
64 | }
65 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/jwk/z-jwk.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 |
3 | export const zJwk = z
4 | .object({
5 | kty: z.string(),
6 | crv: z.optional(z.string()),
7 | x: z.optional(z.string()),
8 | y: z.optional(z.string()),
9 | e: z.optional(z.string()),
10 | n: z.optional(z.string()),
11 | alg: z.optional(z.string()),
12 | d: z.optional(z.string()),
13 | dp: z.optional(z.string()),
14 | dq: z.optional(z.string()),
15 | ext: z.optional(z.boolean()),
16 | k: z.optional(z.string()),
17 | key_ops: z.optional(z.array(z.string())),
18 | kid: z.optional(z.string()),
19 | oth: z.optional(
20 | z.array(
21 | z
22 | .object({
23 | d: z.optional(z.string()),
24 | r: z.optional(z.string()),
25 | t: z.optional(z.string()),
26 | })
27 | .passthrough()
28 | )
29 | ),
30 | p: z.optional(z.string()),
31 | q: z.optional(z.string()),
32 | qi: z.optional(z.string()),
33 | use: z.optional(z.string()),
34 | x5c: z.optional(z.array(z.string())),
35 | x5t: z.optional(z.string()),
36 | 'x5t#S256': z.optional(z.string()),
37 | x5u: z.optional(z.string()),
38 | })
39 | .passthrough()
40 |
41 | export type Jwk = z.infer
42 |
43 | export const zJwkSet = z.object({ keys: z.array(zJwk) }).passthrough()
44 |
45 | export type JwkSet = z.infer
46 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/jwt/decode-jwt-header.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type BaseSchema,
3 | decodeBase64,
4 | encodeToUtf8String,
5 | parseWithErrorHandling,
6 | stringToJsonWithErrorHandling,
7 | } from '@openid4vc/utils'
8 | import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError'
9 | import type { InferSchemaOrDefaultOutput } from './decode-jwt'
10 | import { zJwtHeader } from './z-jwt'
11 |
12 | export interface DecodeJwtHeaderOptions {
13 | /**
14 | * The comapct encoded jwt
15 | */
16 | jwt: string
17 |
18 | /**
19 | * Schema to use for validating the header. If not provided the
20 | * default `vJwtHeader` schema will be used
21 | */
22 | headerSchema?: HeaderSchema
23 | }
24 |
25 | export type DecodeJwtHeaderResult = {
26 | header: InferSchemaOrDefaultOutput
27 | }
28 |
29 | export function decodeJwtHeader(
30 | options: DecodeJwtHeaderOptions
31 | ): DecodeJwtHeaderResult {
32 | const jwtParts = options.jwt.split('.')
33 | if (jwtParts.length <= 2) {
34 | throw new Oauth2JwtParseError('Jwt is not a valid jwt, unable to decode')
35 | }
36 |
37 | let headerJson: Record
38 | try {
39 | headerJson = stringToJsonWithErrorHandling(
40 | encodeToUtf8String(decodeBase64(jwtParts[0])),
41 | 'Unable to parse jwt header to JSON'
42 | )
43 | } catch (error) {
44 | throw new Oauth2JwtParseError(`Error parsing JWT. ${error instanceof Error ? error.message : ''}`)
45 | }
46 |
47 | const header = parseWithErrorHandling(options.headerSchema ?? zJwtHeader, headerJson) as InferSchemaOrDefaultOutput<
48 | HeaderSchema,
49 | typeof zJwtHeader
50 | >
51 |
52 | return {
53 | header,
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/jwt/z-jwe.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const zCompactJwe = z
4 | .string()
5 | .regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, {
6 | message: 'Not a valid compact jwe',
7 | })
8 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/z-common.ts:
--------------------------------------------------------------------------------
1 | import type { FetchHeaders, HttpMethod } from '@openid4vc/utils'
2 | import z from 'zod'
3 |
4 | export const zAlgValueNotNone = z.string().refine((alg) => alg !== 'none', { message: `alg value may not be 'none'` })
5 |
6 | export interface RequestLike {
7 | headers: FetchHeaders
8 | method: HttpMethod
9 | url: string
10 | }
11 |
--------------------------------------------------------------------------------
/packages/oauth2/src/common/z-oauth2-error.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 |
3 | export enum Oauth2ErrorCodes {
4 | ServerError = 'server_error',
5 |
6 | // Resource Indicators
7 | InvalidTarget = 'invalid_target',
8 |
9 | // Oauth2
10 | InvalidRequest = 'invalid_request',
11 | InvalidToken = 'invalid_token',
12 | InsufficientScope = 'insufficient_scope',
13 | InvalidGrant = 'invalid_grant',
14 | InvalidClient = 'invalid_client',
15 | UnauthorizedClient = 'unauthorized_client',
16 | UnsupportedGrantType = 'unsupported_grant_type',
17 | InvalidScope = 'invalid_scope',
18 |
19 | // DPoP
20 | InvalidDpopProof = 'invalid_dpop_proof',
21 | UseDpopNonce = 'use_dpop_nonce',
22 |
23 | // FiPA
24 | RedirectToWeb = 'redirect_to_web',
25 | InvalidSession = 'invalid_session',
26 | InsufficientAuthorization = 'insufficient_authorization',
27 |
28 | // OpenID4VCI
29 | InvalidCredentialRequest = 'invalid_credential_request',
30 | CredentialRequestDenied = 'credential_request_denied',
31 | UnsupportedCredentialType = 'unsupported_credential_type',
32 | UnsupportedCredentialFormat = 'unsupported_credential_format',
33 | InvalidProof = 'invalid_proof',
34 | InvalidNonce = 'invalid_nonce',
35 | InvalidEncryptionParameters = 'invalid_encryption_parameters',
36 |
37 | // Jar
38 | InvalidRequestUri = 'invalid_request_uri',
39 | InvalidRequestObject = 'invalid_request_object',
40 | RequestNotSupported = 'request_not_supported',
41 | RequestUriNotSupported = 'request_uri_not_supported',
42 |
43 | // OpenID4VP
44 | VpFormatsNotSupported = 'vp_formats_not_supported',
45 | AccessDenied = 'access_denied',
46 | InvalidPresentationDefinitionUri = 'invalid_presentation_definition_uri',
47 | InvalidPresentationDefinitionReference = 'invalid_presentation_definition_reference',
48 | InvalidRequestUriMethod = 'invalid_request_uri_method',
49 | InvalidTransactionData = 'invalid_transaction_data',
50 | WalletUnavailable = 'wallet_unavailable',
51 | }
52 |
53 | export const zOauth2ErrorResponse = z
54 | .object({
55 | error: z.union([z.nativeEnum(Oauth2ErrorCodes), z.string()]),
56 | error_description: z.string().optional(),
57 | error_uri: z.string().optional(),
58 | })
59 | .passthrough()
60 |
61 | export type Oauth2ErrorResponse = z.infer
62 |
--------------------------------------------------------------------------------
/packages/oauth2/src/dpop/z-dpop.ts:
--------------------------------------------------------------------------------
1 | import { zJwtHeader, zJwtPayload } from '../common/jwt/z-jwt'
2 |
3 | import { zHttpMethod, zHttpsUrl, zInteger } from '@openid4vc/utils'
4 | import z from 'zod'
5 | import { zJwk } from '../common/jwk/z-jwk'
6 |
7 | export const zDpopJwtPayload = z
8 | .object({
9 | ...zJwtPayload.shape,
10 | iat: zInteger,
11 | htu: zHttpsUrl,
12 | htm: zHttpMethod,
13 | jti: z.string(),
14 |
15 | // Only required when presenting in combination with access token
16 | ath: z.optional(z.string()),
17 | })
18 | .passthrough()
19 | export type DpopJwtPayload = z.infer
20 |
21 | export const zDpopJwtHeader = z
22 | .object({
23 | ...zJwtHeader.shape,
24 | typ: z.literal('dpop+jwt'),
25 | jwk: zJwk,
26 | })
27 | .passthrough()
28 | export type DpopJwtHeader = z.infer
29 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2ClientAuthorizationChallengeError.ts:
--------------------------------------------------------------------------------
1 | import type { FetchResponse } from '@openid4vc/utils'
2 | import type { AuthorizationChallengeErrorResponse } from '../authorization-challenge/z-authorization-challenge'
3 | import { Oauth2ClientErrorResponseError } from './Oauth2ClientErrorResponseError'
4 |
5 | export class Oauth2ClientAuthorizationChallengeError extends Oauth2ClientErrorResponseError {
6 | public constructor(
7 | message: string,
8 | public readonly errorResponse: AuthorizationChallengeErrorResponse,
9 | response: FetchResponse
10 | ) {
11 | super(message, errorResponse, response)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2ClientErrorResponseError.ts:
--------------------------------------------------------------------------------
1 | import type { FetchResponse } from '@openid4vc/utils'
2 | import type { Oauth2ErrorResponse } from '../common/z-oauth2-error'
3 | import { Oauth2Error } from './Oauth2Error'
4 |
5 | export class Oauth2ClientErrorResponseError extends Oauth2Error {
6 | public readonly response: FetchResponse
7 |
8 | public constructor(
9 | message: string,
10 | public readonly errorResponse: Oauth2ErrorResponse,
11 | response: FetchResponse
12 | ) {
13 | super(`${message}\n${JSON.stringify(errorResponse, null, 2)}`)
14 | this.response = response.clone()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2Error.ts:
--------------------------------------------------------------------------------
1 | export interface Oauth2ErrorOptions {
2 | cause?: unknown
3 | }
4 |
5 | export class Oauth2Error extends Error {
6 | public readonly cause?: unknown
7 |
8 | public constructor(message?: string, options?: Oauth2ErrorOptions) {
9 | const errorMessage = message ?? 'Unknown error occured.'
10 | const causeMessage =
11 | options?.cause instanceof Error ? ` ${options.cause.message}` : options?.cause ? ` ${options?.cause}` : ''
12 |
13 | super(`${errorMessage}${causeMessage}`)
14 | this.cause = options?.cause
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2InvalidFetchResponseError.ts:
--------------------------------------------------------------------------------
1 | import type { FetchResponse } from '@openid4vc/utils'
2 | import { Oauth2Error } from './Oauth2Error'
3 |
4 | export class InvalidFetchResponseError extends Oauth2Error {
5 | public readonly response: FetchResponse
6 |
7 | public constructor(
8 | message: string,
9 | public readonly textResponse: string,
10 | response: FetchResponse
11 | ) {
12 | super(`${message}\n${textResponse}`)
13 | this.response = response.clone()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2JwtParseError.ts:
--------------------------------------------------------------------------------
1 | import { Oauth2Error } from './Oauth2Error'
2 |
3 | export class Oauth2JwtParseError extends Oauth2Error {
4 | public constructor(message?: string) {
5 | const errorMessage = message ?? 'Error parsing jwt'
6 |
7 | super(errorMessage)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2JwtVerificationError.ts:
--------------------------------------------------------------------------------
1 | import { Oauth2Error, type Oauth2ErrorOptions } from './Oauth2Error'
2 |
3 | export class Oauth2JwtVerificationError extends Oauth2Error {
4 | public constructor(message?: string, options?: Oauth2ErrorOptions) {
5 | const errorMessage = message ?? 'Error verifiying jwt.'
6 |
7 | super(errorMessage, options)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2ResourceUnauthorizedError.ts:
--------------------------------------------------------------------------------
1 | import { encodeWwwAuthenticateHeader, parseWwwAuthenticateHeader } from '@openid4vc/utils'
2 | import type { SupportedAuthenticationScheme } from '../access-token/verify-access-token'
3 | import type { Oauth2ErrorCodes } from '../common/z-oauth2-error'
4 | import { Oauth2Error } from './Oauth2Error'
5 |
6 | export interface WwwAuthenticateHeaderChallenge {
7 | scheme: SupportedAuthenticationScheme | (string & {})
8 |
9 | /**
10 | * Space delimited scope value that lists scopes required
11 | * to access this resource.
12 | */
13 | scope?: string
14 |
15 | /**
16 | * Error should only be undefined if no access token was provided at all
17 | */
18 | error?: Oauth2ErrorCodes | string
19 | error_description?: string
20 |
21 | /**
22 | * Additional payload items to include in the Www-Authenticate
23 | * header response.
24 | */
25 | additionalPayload?: Record
26 | }
27 |
28 | export class Oauth2ResourceUnauthorizedError extends Oauth2Error {
29 | public readonly wwwAuthenticateHeaders: WwwAuthenticateHeaderChallenge[]
30 |
31 | public constructor(
32 | internalMessage: string | undefined,
33 | wwwAuthenticateHeaders: WwwAuthenticateHeaderChallenge | Array
34 | ) {
35 | super(`${internalMessage}\n${JSON.stringify(wwwAuthenticateHeaders, null, 2)}`)
36 | this.wwwAuthenticateHeaders = Array.isArray(wwwAuthenticateHeaders)
37 | ? wwwAuthenticateHeaders
38 | : [wwwAuthenticateHeaders]
39 | }
40 |
41 | static fromHeaderValue(value: string) {
42 | const headers = parseWwwAuthenticateHeader(value)
43 | return new Oauth2ResourceUnauthorizedError(
44 | undefined,
45 | headers.map(
46 | ({ scheme, payload: { error, error_description, scope, ...additionalPayload } }) =>
47 | ({
48 | scheme,
49 | error: Array.isArray(error) ? error.join(',') : (error ?? undefined),
50 | error_description: Array.isArray(error_description)
51 | ? error_description.join(',')
52 | : (error_description ?? undefined),
53 | scope: Array.isArray(scope) ? scope.join(',') : (scope ?? undefined),
54 | ...additionalPayload,
55 | }) satisfies WwwAuthenticateHeaderChallenge
56 | )
57 | )
58 | }
59 |
60 | public toHeaderValue() {
61 | return encodeWwwAuthenticateHeader(
62 | this.wwwAuthenticateHeaders.map((header) => ({
63 | scheme: header.scheme,
64 | payload: {
65 | error: header.error ?? null,
66 | error_description: header.error_description ?? null,
67 | scope: header.scope ?? null,
68 | ...header.additionalPayload,
69 | },
70 | }))
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/packages/oauth2/src/error/Oauth2ServerErrorResponseError.ts:
--------------------------------------------------------------------------------
1 | import type { Oauth2ErrorResponse } from '../common/z-oauth2-error'
2 | import type { Oauth2ErrorOptions } from '../error/Oauth2Error'
3 | import { Oauth2Error } from './Oauth2Error'
4 |
5 | interface Oauth2ServerErrorResponseErrorOptions extends Oauth2ErrorOptions {
6 | internalMessage?: string
7 |
8 | /**
9 | * @default 400
10 | */
11 | status?: number
12 | }
13 |
14 | export class Oauth2ServerErrorResponseError extends Oauth2Error {
15 | public readonly status: number
16 |
17 | public constructor(
18 | public readonly errorResponse: Oauth2ErrorResponse,
19 | options?: Oauth2ServerErrorResponseErrorOptions
20 | ) {
21 | super(
22 | `${options?.internalMessage ?? errorResponse.error_description}\n${JSON.stringify(errorResponse, null, 2)}`,
23 | options
24 | )
25 | this.status = options?.status ?? 400
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/oauth2/src/metadata/authorization-server/__tests__/z-authorization-server-metadata.test.mts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import { zAuthorizationServerMetadata } from '../z-authorization-server-metadata.js'
3 |
4 | test('should parse authorization server metadata', () => {
5 | // Correct
6 | const r1 = zAuthorizationServerMetadata.safeParse({
7 | issuer: 'https://authorization.com',
8 | token_endpoint: 'https://authorization.com/token',
9 | })
10 |
11 | expect(r1.success).toBe(true)
12 |
13 | // Incorrect
14 | const r2 = zAuthorizationServerMetadata.safeParse({
15 | issuer: 'uri:not-valid',
16 | })
17 | expect(r2.success).toBe(false)
18 | expect(r2.error?.issues).toBeInstanceOf(Array)
19 | })
20 |
--------------------------------------------------------------------------------
/packages/oauth2/src/metadata/authorization-server/z-authorization-server-metadata.ts:
--------------------------------------------------------------------------------
1 | import { zHttpsUrl } from '@openid4vc/utils'
2 | import z from 'zod'
3 | import { zAlgValueNotNone } from '../../common/z-common'
4 |
5 | const knownClientAuthenticationMethod = z.enum([
6 | 'client_secret_basic',
7 | 'client_secret_post',
8 | 'attest_jwt_client_auth',
9 | 'client_secret_jwt',
10 | 'private_key_jwt',
11 | ])
12 |
13 | export const zAuthorizationServerMetadata = z
14 | .object({
15 | issuer: zHttpsUrl,
16 | token_endpoint: zHttpsUrl,
17 | token_endpoint_auth_methods_supported: z.optional(z.array(z.union([knownClientAuthenticationMethod, z.string()]))),
18 | authorization_endpoint: z.optional(zHttpsUrl),
19 | jwks_uri: z.optional(zHttpsUrl),
20 |
21 | // RFC7636
22 | code_challenge_methods_supported: z.optional(z.array(z.string())),
23 |
24 | // RFC9449
25 | dpop_signing_alg_values_supported: z.optional(z.array(z.string())),
26 |
27 | // RFC9126
28 | require_pushed_authorization_requests: z.optional(z.boolean()),
29 | pushed_authorization_request_endpoint: z.optional(zHttpsUrl),
30 |
31 | // RFC9068
32 | introspection_endpoint: z.optional(zHttpsUrl),
33 | introspection_endpoint_auth_methods_supported: z.optional(
34 | z.array(z.union([knownClientAuthenticationMethod, z.string()]))
35 | ),
36 | introspection_endpoint_auth_signing_alg_values_supported: z.optional(z.array(zAlgValueNotNone)),
37 |
38 | // FiPA (no RFC yet)
39 | authorization_challenge_endpoint: z.optional(zHttpsUrl),
40 |
41 | // From OpenID4VCI specification
42 | 'pre-authorized_grant_anonymous_access_supported': z.optional(z.boolean()),
43 |
44 | // Attestation Based Client Auth (draft 5)
45 | client_attestation_pop_nonce_required: z.boolean().optional(),
46 | })
47 | .passthrough()
48 | .refine(
49 | ({
50 | introspection_endpoint_auth_methods_supported: methodsSupported,
51 | introspection_endpoint_auth_signing_alg_values_supported: algValuesSupported,
52 | }) => {
53 | if (!methodsSupported) return true
54 | if (!methodsSupported.includes('private_key_jwt') && !methodsSupported.includes('client_secret_jwt')) return true
55 |
56 | return algValuesSupported !== undefined && algValuesSupported.length > 0
57 | },
58 | `Metadata value 'introspection_endpoint_auth_signing_alg_values_supported' must be defined if metadata 'introspection_endpoint_auth_methods_supported' value contains values 'private_key_jwt' or 'client_secret_jwt'`
59 | )
60 |
61 | export type AuthorizationServerMetadata = z.infer
62 |
--------------------------------------------------------------------------------
/packages/oauth2/src/metadata/fetch-jwks-uri.ts:
--------------------------------------------------------------------------------
1 | import { ContentType, type Fetch, createZodFetcher } from '@openid4vc/utils'
2 | import { InvalidFetchResponseError } from '@openid4vc/utils'
3 | import { ValidationError } from '../../../utils/src/error/ValidationError'
4 | import { type JwkSet, zJwkSet } from '../common/jwk/z-jwk'
5 |
6 | /**
7 | * Fetch JWKs from a provided JWKs URI.
8 | *
9 | * Returns validated metadata if successfull response
10 | * Throws error otherwise
11 | *
12 | * @throws {ValidationError} if successfull response but validation of response failed
13 | * @throws {InvalidFetchResponseError} if unsuccesful response
14 | */
15 | export async function fetchJwks(jwksUrl: string, fetch?: Fetch): Promise {
16 | const fetcher = createZodFetcher(fetch)
17 |
18 | const { result, response } = await fetcher(zJwkSet, ContentType.JwkSet, jwksUrl)
19 | if (!response.ok) {
20 | throw new InvalidFetchResponseError(
21 | `Fetching JWKs from jwks_uri '${jwksUrl}' resulted in an unsuccessfull response with status code '${response.status}'.`,
22 | await response.clone().text(),
23 | response
24 | )
25 | }
26 |
27 | if (!result?.success) {
28 | throw new ValidationError(`Validation of JWKs from jwks_uri '${jwksUrl}' failed`, result?.error)
29 | }
30 |
31 | return result.data
32 | }
33 |
--------------------------------------------------------------------------------
/packages/oauth2/src/metadata/fetch-well-known-metadata.ts:
--------------------------------------------------------------------------------
1 | import { type BaseSchema, ContentType, type Fetch, createZodFetcher } from '@openid4vc/utils'
2 | import { InvalidFetchResponseError } from '@openid4vc/utils'
3 | import type z from 'zod'
4 | import { ValidationError } from '../../../utils/src/error/ValidationError'
5 |
6 | /**
7 | * Fetch well known metadata and validate the response.
8 | *
9 | * Returns null if 404 is returned
10 | * Returns validated metadata if successfull response
11 | * Throws error otherwise
12 | *
13 | * @throws {ValidationError} if successfull response but validation of response failed
14 | * @throws {InvalidFetchResponseError} if no successfull or 404 response
15 | * @throws {Error} if parsing json from response fails
16 | */
17 | export async function fetchWellKnownMetadata(
18 | wellKnownMetadataUrl: string,
19 | schema: Schema,
20 | fetch?: Fetch
21 | ): Promise | null> {
22 | const fetcher = createZodFetcher(fetch)
23 |
24 | const { result, response } = await fetcher(schema, ContentType.Json, wellKnownMetadataUrl)
25 | if (response.status === 404) {
26 | return null
27 | }
28 |
29 | if (!response.ok) {
30 | throw new InvalidFetchResponseError(
31 | `Fetching well known metadata from '${wellKnownMetadataUrl}' resulted in an unsuccessfull response with status '${response.status}'.`,
32 | await response.clone().text(),
33 | response
34 | )
35 | }
36 |
37 | if (!result?.success) {
38 | throw new ValidationError(`Validation of metadata from '${wellKnownMetadataUrl}' failed`, result?.error)
39 | }
40 |
41 | return result.data
42 | }
43 |
--------------------------------------------------------------------------------
/packages/oauth2/src/pkce.ts:
--------------------------------------------------------------------------------
1 | import { decodeUtf8String, encodeToBase64Url } from '@openid4vc/utils'
2 | import { type CallbackContext, HashAlgorithm, type HashCallback } from './callbacks'
3 | import { Oauth2Error } from './error/Oauth2Error'
4 |
5 | export enum PkceCodeChallengeMethod {
6 | Plain = 'plain',
7 | S256 = 'S256',
8 | }
9 |
10 | export interface CreatePkceOptions {
11 | /**
12 | * Also allows string values so it can be directly passed from the
13 | * 'code_challenge_methods_supported' metadata parameter
14 | */
15 | allowedCodeChallengeMethods?: Array
16 |
17 | /**
18 | * Code verifier to use. If not provided a value will be generated.
19 | */
20 | codeVerifier?: string
21 |
22 | callbacks: Pick
23 | }
24 |
25 | export interface CreatePkceReturn {
26 | codeVerifier: string
27 | codeChallenge: string
28 | codeChallengeMethod: PkceCodeChallengeMethod
29 | }
30 |
31 | export async function createPkce(options: CreatePkceOptions): Promise {
32 | const allowedCodeChallengeMethods = options.allowedCodeChallengeMethods ?? [
33 | PkceCodeChallengeMethod.S256,
34 | PkceCodeChallengeMethod.Plain,
35 | ]
36 |
37 | if (allowedCodeChallengeMethods.length === 0) {
38 | throw new Oauth2Error(`Unable to create PKCE code verifier. 'allowedCodeChallengeMethods' is an empty array.`)
39 | }
40 |
41 | const codeChallengeMethod = allowedCodeChallengeMethods.includes(PkceCodeChallengeMethod.S256)
42 | ? PkceCodeChallengeMethod.S256
43 | : PkceCodeChallengeMethod.Plain
44 |
45 | const codeVerifier = options.codeVerifier ?? encodeToBase64Url(await options.callbacks.generateRandom(64))
46 | return {
47 | codeVerifier,
48 | codeChallenge: await calculateCodeChallenge({
49 | codeChallengeMethod,
50 | codeVerifier,
51 | hashCallback: options.callbacks.hash,
52 | }),
53 | codeChallengeMethod,
54 | }
55 | }
56 |
57 | export interface VerifyPkceOptions {
58 | /**
59 | * secure random code verifier
60 | */
61 | codeVerifier: string
62 |
63 | codeChallenge: string
64 | codeChallengeMethod: PkceCodeChallengeMethod
65 |
66 | callbacks: Pick
67 | }
68 |
69 | export async function verifyPkce(options: VerifyPkceOptions) {
70 | const calculatedCodeChallenge = await calculateCodeChallenge({
71 | codeChallengeMethod: options.codeChallengeMethod,
72 | codeVerifier: options.codeVerifier,
73 | hashCallback: options.callbacks.hash,
74 | })
75 |
76 | if (options.codeChallenge !== calculatedCodeChallenge) {
77 | throw new Oauth2Error(
78 | `Derived code challenge '${calculatedCodeChallenge}' from code_verifier '${options.codeVerifier}' using code challenge method '${options.codeChallengeMethod}' does not match the expected code challenge.`
79 | )
80 | }
81 | }
82 |
83 | async function calculateCodeChallenge(options: {
84 | codeVerifier: string
85 | codeChallengeMethod: PkceCodeChallengeMethod
86 | hashCallback: HashCallback
87 | }) {
88 | if (options.codeChallengeMethod === PkceCodeChallengeMethod.Plain) {
89 | return options.codeVerifier
90 | }
91 |
92 | if (options.codeChallengeMethod === PkceCodeChallengeMethod.S256) {
93 | return encodeToBase64Url(await options.hashCallback(decodeUtf8String(options.codeVerifier), HashAlgorithm.Sha256))
94 | }
95 |
96 | throw new Oauth2Error(`Unsupported code challenge method ${options.codeChallengeMethod}`)
97 | }
98 |
--------------------------------------------------------------------------------
/packages/oauth2/src/z-grant-type.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 |
3 | export const zPreAuthorizedCodeGrantIdentifier = z.literal('urn:ietf:params:oauth:grant-type:pre-authorized_code')
4 | export const preAuthorizedCodeGrantIdentifier = zPreAuthorizedCodeGrantIdentifier.value
5 | export type PreAuthorizedCodeGrantIdentifier = z.infer
6 |
7 | export const zAuthorizationCodeGrantIdentifier = z.literal('authorization_code')
8 | export const authorizationCodeGrantIdentifier = zAuthorizationCodeGrantIdentifier.value
9 | export type AuthorizationCodeGrantIdentifier = z.infer
10 |
11 | export const zRefreshTokenGrantIdentifier = z.literal('refresh_token')
12 | export const refreshTokenGrantIdentifier = zRefreshTokenGrantIdentifier.value
13 | export type RefreshTokenGrantIdentifier = z.infer
14 |
--------------------------------------------------------------------------------
/packages/oauth2/tests/util.mts:
--------------------------------------------------------------------------------
1 | import crypto, { webcrypto } from 'node:crypto'
2 | import { decodeBase64, encodeToUtf8String } from '@openid4vc/utils'
3 | import * as jose from 'jose'
4 | import { type CallbackContext, HashAlgorithm, type SignJwtCallback } from '../src/callbacks.js'
5 | import { clientAuthenticationNone } from '../src/client-authentication.js'
6 | import { calculateJwkThumbprint } from '../src/common/jwk/jwk-thumbprint.js'
7 | import type { Jwk } from '../src/common/jwk/z-jwk.js'
8 |
9 | // Needed for Node 18 support with jose6. We can soon drop node18 support.
10 | if (process.versions.node.startsWith('18.')) {
11 | // @ts-ignore
12 | globalThis.crypto = webcrypto
13 | }
14 |
15 | export function parseXwwwFormUrlEncoded(text: string) {
16 | return Object.fromEntries(Array.from(new URLSearchParams(text).entries()))
17 | }
18 |
19 | export const callbacks = {
20 | hash: (data, alg) => crypto.createHash(alg.replace('-', '').toLowerCase()).update(data).digest(),
21 | generateRandom: (bytes) => crypto.randomBytes(bytes),
22 | clientAuthentication: clientAuthenticationNone({
23 | clientId: 'some-random-client-id',
24 | }),
25 | verifyJwt: async (signer, { compact, payload }) => {
26 | let jwk: Jwk
27 | if (signer.method === 'did') {
28 | jwk = JSON.parse(encodeToUtf8String(decodeBase64(signer.didUrl.split('#')[0].replace('did:jwk:', ''))))
29 | } else if (signer.method === 'jwk') {
30 | jwk = signer.publicJwk
31 | } else {
32 | throw new Error('Signer method not supported')
33 | }
34 |
35 | const josePublicKey = await jose.importJWK(jwk as jose.JWK, signer.alg)
36 | try {
37 | await jose.jwtVerify(compact, josePublicKey, {
38 | currentDate: payload.exp ? new Date((payload.exp - 300) * 1000) : undefined,
39 | })
40 | return {
41 | verified: true,
42 | signerJwk: jwk,
43 | }
44 | } catch (error) {
45 | return {
46 | verified: false,
47 | }
48 | }
49 | },
50 | } as const satisfies Partial
51 |
52 | export const getSignJwtCallback = (privateJwks: Jwk[]): SignJwtCallback => {
53 | return async (signer, { header, payload }) => {
54 | let jwk: Jwk
55 | if (signer.method === 'did') {
56 | jwk = JSON.parse(encodeToUtf8String(decodeBase64(signer.didUrl.split('#')[0].replace('did:jwk:', ''))))
57 | } else if (signer.method === 'jwk') {
58 | jwk = signer.publicJwk
59 | } else {
60 | throw new Error('Signer method not supported')
61 | }
62 |
63 | const jwkThumprint = await calculateJwkThumbprint({
64 | jwk,
65 | hashAlgorithm: HashAlgorithm.Sha256,
66 | hashCallback: callbacks.hash,
67 | })
68 |
69 | const privateJwk = await Promise.all(
70 | privateJwks.map(async (jwk) =>
71 | (await calculateJwkThumbprint({
72 | hashAlgorithm: HashAlgorithm.Sha256,
73 | hashCallback: callbacks.hash,
74 | jwk,
75 | })) === jwkThumprint
76 | ? jwk
77 | : undefined
78 | )
79 | ).then((jwks) => jwks.find((jwk) => jwk !== undefined))
80 |
81 | if (!privateJwk) {
82 | throw new Error(`No private key available for public jwk \n${JSON.stringify(jwk, null, 2)}`)
83 | }
84 |
85 | const josePrivateKey = await jose.importJWK(privateJwk as jose.JWK, signer.alg)
86 | const jwt = await new jose.SignJWT(payload).setProtectedHeader(header).sign(josePrivateKey)
87 |
88 | return {
89 | jwt: jwt,
90 | signerJwk: jwk,
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/oauth2/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/openid4vci/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openid4vc/openid4vci
2 |
3 | ## 0.2.0
4 |
5 | ### Minor Changes
6 |
7 | - 0f60387: feat: add key attestations
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies [0f60387]
12 | - Updated dependencies [3f6d360]
13 | - @openid4vc/oauth2@0.2.0
14 | - @openid4vc/utils@0.2.0
15 |
16 | ## 0.1.4
17 |
18 | ### Patch Changes
19 |
20 | - Updated dependencies [12a517a]
21 | - @openid4vc/oauth2@0.1.4
22 | - @openid4vc/utils@0.1.4
23 |
24 | ## 0.1.3
25 |
26 | ### Patch Changes
27 |
28 | - d4b9279: chore: create github release
29 | - Updated dependencies [d4b9279]
30 | - @openid4vc/oauth2@0.1.3
31 | - @openid4vc/utils@0.1.3
32 |
33 | ## 0.1.2
34 |
35 | ### Patch Changes
36 |
37 | - 1de27e5: chore: correct formatting for publishing
38 | - Updated dependencies [1de27e5]
39 | - @openid4vc/oauth2@0.1.2
40 | - @openid4vc/utils@0.1.2
41 |
42 | ## 0.1.1
43 |
44 | ### Patch Changes
45 |
46 | - 6434781: docs: add readme
47 | - Updated dependencies [6434781]
48 | - @openid4vc/oauth2@0.1.1
49 | - @openid4vc/utils@0.1.1
50 |
51 | ## 0.1.0
52 |
53 | ### Minor Changes
54 |
55 | - 71326c8: feat: initial release
56 |
57 | ### Patch Changes
58 |
59 | - 3bcbd08: export additional types
60 | - Updated dependencies [71326c8]
61 | - @openid4vc/oauth2@0.1.0
62 | - @openid4vc/utils@0.1.0
63 |
--------------------------------------------------------------------------------
/packages/openid4vci/README.md:
--------------------------------------------------------------------------------
1 | OpenID for Verifiable Credentials - OpenID4VCI
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Check out the documentation in the [Github repository](https://github.com/openwallet-foundation-labs/oid4vc-ts).
10 |
--------------------------------------------------------------------------------
/packages/openid4vci/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openid4vc/openid4vci",
3 | "version": "0.2.0",
4 | "files": ["dist"],
5 | "license": "Apache-2.0",
6 | "exports": "./src/index.ts",
7 | "homepage": "https://github.com/openwallet-foundation-labs/oid4vc-ts/tree/main/packages/openid4vci",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/openwallet-foundation-labs/oid4vc-ts",
11 | "directory": "packages/openid4vci"
12 | },
13 | "publishConfig": {
14 | "main": "./dist/index.js",
15 | "module": "./dist/index.mjs",
16 | "types": "./dist/index.d.ts",
17 | "exports": {
18 | ".": {
19 | "import": "./dist/index.mjs",
20 | "require": "./dist/index.js",
21 | "types": "./dist/index.d.ts"
22 | },
23 | "./package.json": "./package.json"
24 | }
25 | },
26 | "scripts": {
27 | "build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap"
28 | },
29 | "dependencies": {
30 | "@openid4vc/oauth2": "workspace:*",
31 | "@openid4vc/utils": "workspace:*",
32 | "zod": "catalog:"
33 | },
34 | "devDependencies": {
35 | "jose": "catalog:"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/Openid4vciWalletProvider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type CallbackContext,
3 | type CreateClientAttestationJwtOptions,
4 | createClientAttestationJwt,
5 | } from '@openid4vc/oauth2'
6 | import { type CreateKeyAttestationJwtOptions, createKeyAttestationJwt } from './key-attestation/key-attestation'
7 |
8 | export interface Openid4vciWalletProviderOptions {
9 | /**
10 | * Callbacks required for the openid4vc issuer
11 | */
12 | callbacks: Pick
13 | }
14 |
15 | export class Openid4vciWalletProvider {
16 | public constructor(private options: Openid4vciWalletProviderOptions) {}
17 |
18 | public async createWalletAttestationJwt(
19 | options: Omit & { walletName?: string; walletLink?: string }
20 | ) {
21 | const additionalPayload = options.additionalPayload
22 | ? {
23 | wallet_name: options.walletName,
24 | wallet_link: options.walletLink,
25 | ...options.additionalPayload,
26 | }
27 | : {
28 | wallet_name: options.walletName,
29 | wallet_link: options.walletLink,
30 | }
31 |
32 | return await createClientAttestationJwt({
33 | ...options,
34 | callbacks: this.options.callbacks,
35 | additionalPayload,
36 | })
37 | }
38 |
39 | public async createKeyAttestationJwt(options: Omit) {
40 | return await createKeyAttestationJwt({
41 | callbacks: this.options.callbacks,
42 | ...options,
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/credential-request/credential-request-configurations.ts:
--------------------------------------------------------------------------------
1 | import { arrayEqualsIgnoreOrder } from '@openid4vc/utils'
2 | import { extractKnownCredentialConfigurationSupportedFormats } from '../metadata/credential-issuer/credential-issuer-metadata'
3 | import type {
4 | CredentialConfigurationsSupported,
5 | CredentialConfigurationsSupportedWithFormats,
6 | } from '../metadata/credential-issuer/z-credential-issuer-metadata'
7 | import type { CredentialRequestFormatSpecific } from './z-credential-request'
8 |
9 | export interface GetCredentialConfigurationsMatchingRequestFormatOptions {
10 | requestFormat: CredentialRequestFormatSpecific
11 | credentialConfigurations: CredentialConfigurationsSupported
12 | }
13 |
14 | export function getCredentialConfigurationsMatchingRequestFormat({
15 | requestFormat,
16 | credentialConfigurations,
17 | }: GetCredentialConfigurationsMatchingRequestFormatOptions): CredentialConfigurationsSupportedWithFormats {
18 | // credential request format will only contain known formats
19 | const knownCredentialConfigurations = extractKnownCredentialConfigurationSupportedFormats(credentialConfigurations)
20 |
21 | return Object.fromEntries(
22 | Object.entries(knownCredentialConfigurations).filter(([, credentialConfiguration]) => {
23 | if (credentialConfiguration.format !== requestFormat.format) return false
24 |
25 | const r = requestFormat
26 | const c = credentialConfiguration
27 |
28 | if ((c.format === 'ldp_vc' || c.format === 'jwt_vc_json-ld') && r.format === c.format) {
29 | return (
30 | arrayEqualsIgnoreOrder(r.credential_definition.type, c.credential_definition.type) &&
31 | arrayEqualsIgnoreOrder(r.credential_definition['@context'], c.credential_definition['@context'])
32 | )
33 | }
34 |
35 | if (c.format === 'jwt_vc_json' && r.format === c.format) {
36 | return arrayEqualsIgnoreOrder(r.credential_definition.type, c.credential_definition.type)
37 | }
38 |
39 | if (c.format === 'vc+sd-jwt' && r.format === c.format) {
40 | return r.vct === c.vct
41 | }
42 |
43 | if (c.format === 'mso_mdoc' && r.format === c.format) {
44 | return r.doctype === c.doctype
45 | }
46 |
47 | return false
48 | })
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/credential-request/credential-response.ts:
--------------------------------------------------------------------------------
1 | import { parseWithErrorHandling } from '@openid4vc/utils'
2 | import type { ParseCredentialRequestReturn } from './parse-credential-request'
3 | import { type CredentialResponse, zCredentialResponse } from './z-credential-response'
4 |
5 | export interface CreateCredentialResponseOptions {
6 | credentialRequest: ParseCredentialRequestReturn
7 |
8 | credential?: CredentialResponse['credential']
9 | credentials?: CredentialResponse['credentials']
10 |
11 | cNonce?: string
12 | cNonceExpiresInSeconds?: number
13 |
14 | notificationId?: string
15 |
16 | /**
17 | * Additional payload to include in the credential response
18 | */
19 | additionalPayload?: Record
20 | }
21 |
22 | export function createCredentialResponse(options: CreateCredentialResponseOptions) {
23 | const credentialResponse = parseWithErrorHandling(zCredentialResponse, {
24 | c_nonce: options.cNonce,
25 | c_nonce_expires_in: options.cNonceExpiresInSeconds,
26 | credential: options.credential,
27 | credentials: options.credentials,
28 | notification_id: options.notificationId,
29 |
30 | // NOTE `format` is removed in draft 13. For now if a format was requested
31 | // we just always return it in the response as well.
32 | format: options.credentialRequest.format?.format,
33 | ...options.additionalPayload,
34 | } satisfies CredentialResponse)
35 |
36 | return credentialResponse
37 | }
38 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/credential-request/z-credential-request-common.ts:
--------------------------------------------------------------------------------
1 | import { zJwk } from '@openid4vc/oauth2'
2 | import type { InferOutputUnion, Simplify } from '@openid4vc/utils'
3 | import z from 'zod'
4 | import {
5 | zAttestationProofTypeIdentifier,
6 | zCredentialRequestProofAttestation,
7 | zCredentialRequestProofJwt,
8 | zJwtProofTypeIdentifier,
9 | } from '../formats/proof-type'
10 |
11 | const zCredentialRequestProofCommon = z
12 | .object({
13 | proof_type: z.string(),
14 | })
15 | .passthrough()
16 |
17 | export const allCredentialRequestProofs = [zCredentialRequestProofJwt, zCredentialRequestProofAttestation] as const
18 |
19 | export const zCredentialRequestProof = z.union([
20 | zCredentialRequestProofCommon,
21 | z.discriminatedUnion('proof_type', allCredentialRequestProofs),
22 | ])
23 |
24 | const zCredentialRequestProofsCommon = z.record(z.string(), z.array(z.unknown()))
25 | export const zCredentialRequestProofs = z.object({
26 | [zJwtProofTypeIdentifier.value]: z.optional(z.array(zCredentialRequestProofJwt.shape.jwt)),
27 | [zAttestationProofTypeIdentifier.value]: z.optional(z.array(zCredentialRequestProofAttestation.shape.attestation)),
28 | })
29 |
30 | type CredentialRequestProofCommon = z.infer
31 | export type CredentialRequestProofFormatSpecific = InferOutputUnion
32 | export type CredentialRequestProofWithFormats = Simplify<
33 | CredentialRequestProofCommon & CredentialRequestProofFormatSpecific
34 | >
35 | export type CredentialRequestProof = z.infer
36 |
37 | export type CredentialRequestProofsCommon = z.infer
38 | export type CredentialRequestProofsFormatSpecific = z.infer
39 | export type CredentialRequestProofsWithFormat = CredentialRequestProofsCommon & CredentialRequestProofsFormatSpecific
40 | export type CredentialRequestProofs = z.infer
41 |
42 | export const zCredentialRequestCommon = z
43 | .object({
44 | proof: zCredentialRequestProof.optional(),
45 | proofs: z.optional(
46 | z
47 | .intersection(zCredentialRequestProofsCommon, zCredentialRequestProofs)
48 | .refine((proofs) => Object.values(proofs).length === 1, {
49 | message: `The 'proofs' object in a credential request should contain exactly one attribute`,
50 | })
51 | ),
52 |
53 | credential_response_encryption: z
54 | .object({
55 | jwk: zJwk,
56 | alg: z.string(),
57 | enc: z.string(),
58 | })
59 | .passthrough()
60 | .optional(),
61 | })
62 | .passthrough()
63 | // It's not allowed to provide both proof and proofs
64 | .refine(({ proof, proofs }) => !(proof !== undefined && proofs !== undefined), {
65 | message: `Both 'proof' and 'proofs' are defined. Only one is allowed`,
66 | })
67 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/credential-request/z-credential-response.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 | import { zOauth2ErrorResponse } from '../../../oauth2/src/common/z-oauth2-error'
3 |
4 | const zCredentialEncoding = z.union([z.string(), z.record(z.string(), z.any())])
5 |
6 | export const zCredentialResponse = z
7 | .object({
8 | credential: z.optional(zCredentialEncoding),
9 | credentials: z.optional(z.array(zCredentialEncoding)),
10 |
11 | transaction_id: z.string().optional(),
12 |
13 | c_nonce: z.string().optional(),
14 | c_nonce_expires_in: z.number().int().optional(),
15 |
16 | notification_id: z.string().optional(),
17 | })
18 | .passthrough()
19 | .refine(
20 | (value) => {
21 | const { credential, credentials, transaction_id } = value
22 | return [credential, credentials, transaction_id].filter((i) => i !== undefined).length === 1
23 | },
24 | {
25 | message: `Exactly one of 'credential', 'credentials', or 'transaction_id' MUST be defined.`,
26 | }
27 | )
28 |
29 | export type CredentialResponse = z.infer
30 |
31 | export const zCredentialErrorResponse = z
32 | .object({
33 | ...zOauth2ErrorResponse.shape,
34 |
35 | c_nonce: z.string().optional(),
36 | c_nonce_expires_in: z.number().int().optional(),
37 | })
38 | .passthrough()
39 |
40 | export type CredentialErrorResponse = z.infer
41 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/error/Openid4vciError.ts:
--------------------------------------------------------------------------------
1 | export interface Openid4vciErrorOptions {
2 | cause?: unknown
3 | }
4 |
5 | export class Openid4vciError extends Error {
6 | public readonly cause?: unknown
7 |
8 | public constructor(message?: string, options?: Openid4vciErrorOptions) {
9 | const errorMessage = message ?? 'Unknown error occured.'
10 | const causeMessage =
11 | options?.cause instanceof Error ? ` ${options.cause.message}` : options?.cause ? ` ${options?.cause}` : ''
12 |
13 | super(`${errorMessage}${causeMessage}`)
14 | this.cause = options?.cause
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/error/Openid4vciRetrieveCredentialsError.ts:
--------------------------------------------------------------------------------
1 | import type { RetrieveCredentialsResponseNotOk } from '../credential-request/retrieve-credentials'
2 | import { Openid4vciError } from './Openid4vciError'
3 |
4 | export class Openid4vciRetrieveCredentialsError extends Openid4vciError {
5 | public constructor(
6 | message: string,
7 | public response: RetrieveCredentialsResponseNotOk,
8 | responseText: string
9 | ) {
10 | super(
11 | `${message}\n${JSON.stringify(response.credentialResponseResult?.data ?? response.credentialErrorResponseResult?.data ?? responseText, null, 2)}`
12 | )
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/error/Openid4vciSendNotificationError.ts:
--------------------------------------------------------------------------------
1 | import type { SendNotificationResponseNotOk } from '../notification/notification'
2 | import { Openid4vciError } from './Openid4vciError'
3 |
4 | export class Openid4vciSendNotificationError extends Openid4vciError {
5 | public constructor(
6 | message: string,
7 | public response: SendNotificationResponseNotOk
8 | ) {
9 | super(message)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/index.ts:
--------------------------------------------------------------------------------
1 | import type { MsoMdocFormatIdentifier } from './mso-mdoc/z-mso-mdoc'
2 | import type { SdJwtDcFormatIdentifier } from './sd-jwt-dc/z-sd-jwt-dc'
3 | import type { SdJwtVcFormatIdentifier } from './sd-jwt-vc/z-sd-jwt-vc'
4 | import type { JwtVcJsonFormatIdentifier } from './w3c-vc/z-w3c-jwt-vc-json'
5 | import type { JwtVcJsonLdFormatIdentifier } from './w3c-vc/z-w3c-jwt-vc-json-ld'
6 | import type { LdpVcFormatIdentifier } from './w3c-vc/z-w3c-ldp-vc'
7 |
8 | // mso_mdoc
9 | export {
10 | type MsoMdocFormatIdentifier,
11 | zMsoMdocCredentialIssuerMetadata,
12 | zMsoMdocCredentialIssuerMetadataDraft14,
13 | zMsoMdocCredentialRequestFormatDraft14,
14 | zMsoMdocFormatIdentifier,
15 | } from './mso-mdoc/z-mso-mdoc'
16 |
17 | // vc+sd-jwt
18 | export {
19 | type SdJwtVcFormatIdentifier,
20 | zSdJwtVcCredentialIssuerMetadataDraft14,
21 | zSdJwtVcCredentialRequestFormatDraft14,
22 | zSdJwtVcFormatIdentifier,
23 | } from './sd-jwt-vc/z-sd-jwt-vc'
24 |
25 | // dc+sd-jwt
26 | export {
27 | type SdJwtDcFormatIdentifier,
28 | zSdJwtDcCredentialIssuerMetadata,
29 | zSdJwtDcFormatIdentifier,
30 | } from './sd-jwt-dc/z-sd-jwt-dc'
31 |
32 | // ldp_vc
33 | export {
34 | type LdpVcFormatIdentifier,
35 | zLdpVcCredentialIssuerMetadata,
36 | zLdpVcCredentialIssuerMetadataDraft14,
37 | zLdpVcCredentialIssuerMetadataDraft11,
38 | zLdpVcCredentialIssuerMetadataDraft11To14,
39 | zLdpVcCredentialIssuerMetadataDraft14To11,
40 | zLdpVcCredentialRequestFormatDraft14,
41 | zLdpVcCredentialRequestDraft14To11,
42 | zLdpVcFormatIdentifier,
43 | } from './w3c-vc/z-w3c-ldp-vc'
44 |
45 | // jwt_vc_json-ld
46 | export {
47 | type JwtVcJsonLdFormatIdentifier,
48 | zJwtVcJsonLdCredentialIssuerMetadata,
49 | zJwtVcJsonLdCredentialIssuerMetadataDraft14,
50 | zJwtVcJsonLdCredentialIssuerMetadataDraft11,
51 | zJwtVcJsonLdCredentialIssuerMetadataDraft11To14,
52 | zJwtVcJsonLdCredentialIssuerMetadataDraft14To11,
53 | zJwtVcJsonLdCredentialRequestFormatDraft14,
54 | zJwtVcJsonLdCredentialRequestDraft14To11,
55 | zJwtVcJsonLdFormatIdentifier,
56 | } from './w3c-vc/z-w3c-jwt-vc-json-ld'
57 |
58 | // jwt_vc_json
59 | export {
60 | type JwtVcJsonFormatIdentifier,
61 | zJwtVcJsonCredentialIssuerMetadata,
62 | zJwtVcJsonCredentialIssuerMetadataDraft14,
63 | zJwtVcJsonCredentialIssuerMetadataDraft11,
64 | zJwtVcJsonCredentialIssuerMetadataDraft11To14,
65 | zJwtVcJsonCredentialIssuerMetadataDraft14To11,
66 | zJwtVcJsonCredentialRequestDraft14To11,
67 | zJwtVcJsonCredentialRequestFormatDraft14,
68 | zJwtVcJsonFormatIdentifier,
69 | } from './w3c-vc/z-w3c-jwt-vc-json'
70 |
71 | export type CredentialFormatIdentifier =
72 | | MsoMdocFormatIdentifier
73 | | SdJwtVcFormatIdentifier
74 | | SdJwtDcFormatIdentifier
75 | | LdpVcFormatIdentifier
76 | | JwtVcJsonLdFormatIdentifier
77 | | JwtVcJsonFormatIdentifier
78 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/mso-mdoc/z-mso-mdoc.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 | import {
3 | zCredentialConfigurationSupportedClaimsDraft14,
4 | zMsoMdocIssuerMetadataClaimsDescription,
5 | } from '../../../metadata/credential-issuer/z-claims-description'
6 |
7 | export const zMsoMdocFormatIdentifier = z.literal('mso_mdoc')
8 | export type MsoMdocFormatIdentifier = z.infer
9 |
10 | export const zMsoMdocCredentialIssuerMetadata = z.object({
11 | format: zMsoMdocFormatIdentifier,
12 | doctype: z.string(),
13 | claims: z.array(zMsoMdocIssuerMetadataClaimsDescription).optional(),
14 | })
15 |
16 | export const zMsoMdocCredentialIssuerMetadataDraft14 = z.object({
17 | format: zMsoMdocFormatIdentifier,
18 | doctype: z.string(),
19 | claims: zCredentialConfigurationSupportedClaimsDraft14.optional(),
20 | order: z.optional(z.array(z.string())),
21 | })
22 |
23 | export const zMsoMdocCredentialRequestFormatDraft14 = z.object({
24 | format: zMsoMdocFormatIdentifier,
25 | doctype: z.string(),
26 | // Format based request is removed in Draft 15, so only old claims syntax supported.
27 | claims: z.optional(zCredentialConfigurationSupportedClaimsDraft14),
28 | })
29 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/sd-jwt-dc/__tests__/z-sd-jwt-dc.test.mts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import { zSdJwtDcCredentialIssuerMetadata, zSdJwtDcFormatIdentifier } from '../z-sd-jwt-dc.js'
3 |
4 | test('should parse sd-jwt-dc format identifier', () => {
5 | expect(zSdJwtDcFormatIdentifier.safeParse('dc+sd-jwt')).toStrictEqual({
6 | data: 'dc+sd-jwt',
7 | success: true,
8 | })
9 |
10 | expect(zSdJwtDcFormatIdentifier.safeParse('dc+sd-jwt2')).toStrictEqual({
11 | error: expect.any(Error),
12 | success: false,
13 | })
14 | })
15 |
16 | test('should parse sd-jwt-dc credential issuer metadata', () => {
17 | expect(
18 | zSdJwtDcCredentialIssuerMetadata.safeParse({
19 | format: 'dc+sd-jwt',
20 | scope: 'SD_JWT_DC_example_in_OpenID4VCI',
21 | cryptographic_binding_methods_supported: ['jwk'],
22 | credential_signing_alg_values_supported: ['ES256'],
23 | display: [
24 | {
25 | name: 'IdentityCredential',
26 | logo: {
27 | uri: 'https://university.example.edu/public/logo.png',
28 | alt_text: 'a square logo of a university',
29 | },
30 | locale: 'en-US',
31 | background_color: '#12107c',
32 | text_color: '#FFFFFF',
33 | },
34 | ],
35 | proof_types_supported: {
36 | jwt: {
37 | proof_signing_alg_values_supported: ['ES256'],
38 | },
39 | },
40 | vct: 'SD_JWT_DC_example_in_OpenID4VCI',
41 | claims: [
42 | {
43 | path: ['given_name'],
44 | display: [
45 | {
46 | name: 'Given Name',
47 | locale: 'en-US',
48 | },
49 | {
50 | name: 'Vorname',
51 | locale: 'de-DE',
52 | },
53 | ],
54 | },
55 | {
56 | path: ['family_name'],
57 | display: [
58 | {
59 | name: 'Surname',
60 | locale: 'en-US',
61 | },
62 | {
63 | name: 'Nachname',
64 | locale: 'de-DE',
65 | },
66 | ],
67 | },
68 | {
69 | path: ['email'],
70 | },
71 | ],
72 | })
73 | ).toStrictEqual({
74 | data: expect.objectContaining({}),
75 | success: true,
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/sd-jwt-dc/z-sd-jwt-dc.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 | import { zIssuerMetadataClaimsDescription } from '../../../metadata/credential-issuer/z-claims-description'
3 |
4 | export const zSdJwtDcFormatIdentifier = z.literal('dc+sd-jwt')
5 | export type SdJwtDcFormatIdentifier = z.infer
6 |
7 | export const zSdJwtDcCredentialIssuerMetadata = z.object({
8 | vct: z.string(),
9 | format: zSdJwtDcFormatIdentifier,
10 | claims: z.array(zIssuerMetadataClaimsDescription).optional(),
11 | })
12 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/sd-jwt-vc/__tests__/z-sd-jwt-vc.test.mts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import { zSdJwtVcCredentialIssuerMetadataDraft14, zSdJwtVcFormatIdentifier } from '../z-sd-jwt-vc.js'
3 |
4 | test('should parse sd-jwt-vc format identifier', () => {
5 | expect(zSdJwtVcFormatIdentifier.safeParse('vc+sd-jwt')).toStrictEqual({
6 | data: 'vc+sd-jwt',
7 | success: true,
8 | })
9 |
10 | expect(zSdJwtVcFormatIdentifier.safeParse('vc+sd-jwt2')).toStrictEqual({
11 | error: expect.any(Error),
12 | success: false,
13 | })
14 | })
15 |
16 | test('should parse sd-jwt-vc credential issuer metadata', () => {
17 | expect(
18 | zSdJwtVcCredentialIssuerMetadataDraft14.safeParse({
19 | format: 'vc+sd-jwt',
20 | scope: 'SD_JWT_VC_example_in_OpenID4VCI',
21 | cryptographic_binding_methods_supported: ['jwk'],
22 | credential_signing_alg_values_supported: ['ES256'],
23 | display: [
24 | {
25 | name: 'IdentityCredential',
26 | logo: {
27 | uri: 'https://university.example.edu/public/logo.png',
28 | alt_text: 'a square logo of a university',
29 | },
30 | locale: 'en-US',
31 | background_color: '#12107c',
32 | text_color: '#FFFFFF',
33 | },
34 | ],
35 | proof_types_supported: {
36 | jwt: {
37 | proof_signing_alg_values_supported: ['ES256'],
38 | },
39 | },
40 | vct: 'SD_JWT_VC_example_in_OpenID4VCI',
41 | claims: {
42 | given_name: {
43 | display: [
44 | {
45 | name: 'Given Name',
46 | locale: 'en-US',
47 | },
48 | {
49 | name: 'Vorname',
50 | locale: 'de-DE',
51 | },
52 | ],
53 | },
54 | family_name: {
55 | display: [
56 | {
57 | name: 'Surname',
58 | locale: 'en-US',
59 | },
60 | {
61 | name: 'Nachname',
62 | locale: 'de-DE',
63 | },
64 | ],
65 | },
66 | email: {},
67 | phone_number: {},
68 | address: {
69 | street_address: {},
70 | locality: {},
71 | region: {},
72 | country: {},
73 | },
74 | birthdate: {},
75 | is_over_18: {},
76 | is_over_21: {},
77 | is_over_65: {},
78 | },
79 | })
80 | ).toStrictEqual({
81 | data: expect.objectContaining({}),
82 | success: true,
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/sd-jwt-vc/z-sd-jwt-vc.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 | import { zCredentialConfigurationSupportedClaimsDraft14 } from '../../../metadata/credential-issuer/z-claims-description'
3 |
4 | export const zSdJwtVcFormatIdentifier = z.literal('vc+sd-jwt')
5 | export type SdJwtVcFormatIdentifier = z.infer
6 |
7 | export const zSdJwtVcCredentialIssuerMetadataDraft14 = z.object({
8 | vct: z.string(),
9 | format: zSdJwtVcFormatIdentifier,
10 | claims: z.optional(zCredentialConfigurationSupportedClaimsDraft14),
11 | order: z.optional(z.array(z.string())),
12 | })
13 |
14 | export const zSdJwtVcCredentialRequestFormatDraft14 = z.object({
15 | format: zSdJwtVcFormatIdentifier,
16 | vct: z.string(),
17 | claims: z.optional(zCredentialConfigurationSupportedClaimsDraft14),
18 | })
19 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/credential/w3c-vc/z-w3c-vc-common.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod'
2 |
3 | const zCredentialSubjectLeafTypeDraft14 = z
4 | .object({
5 | mandatory: z.boolean().optional(),
6 | value_type: z.string().optional(),
7 | display: z
8 | .array(
9 | z
10 | .object({
11 | name: z.string().optional(),
12 | locale: z.string().optional(),
13 | })
14 | .passthrough()
15 | )
16 | .optional(),
17 | })
18 | .passthrough()
19 |
20 | const zClaimValueSchemaDraft14 = z.union([
21 | z.array(z.any()),
22 | z.record(z.string(), z.any()),
23 | zCredentialSubjectLeafTypeDraft14,
24 | ])
25 |
26 | export const zW3cVcCredentialSubjectDraft14 = z.record(z.string(), zClaimValueSchemaDraft14)
27 |
28 | export const zW3cVcJsonLdCredentialDefinition = z
29 | .object({
30 | '@context': z.array(z.string()),
31 | type: z.array(z.string()),
32 | })
33 | .passthrough()
34 |
35 | export const zW3cVcJsonLdCredentialDefinitionDraft14 = zW3cVcJsonLdCredentialDefinition.extend({
36 | credentialSubject: zW3cVcCredentialSubjectDraft14.optional(),
37 | })
38 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/proof-type/attestation/attestation-proof-type.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type CreateKeyAttestationJwtOptions,
3 | type VerifyKeyAttestationJwtOptions,
4 | createKeyAttestationJwt,
5 | verifyKeyAttestationJwt,
6 | } from '../../../key-attestation/key-attestation'
7 |
8 | export interface CreateCredentialRequestAttestationProofOptions extends Omit {
9 | /**
10 | * Nonce to use in the attestation. Should be derived from the c_nonce
11 | *
12 | * Required because the attestation is created for 'attestation' proof types
13 | */
14 | nonce: string
15 |
16 | /**
17 | * The date when the key attestation will expire.
18 | */
19 | expiresAt: Date
20 | }
21 |
22 | export async function createCredentialRequestAttestationProof(
23 | options: CreateCredentialRequestAttestationProofOptions
24 | ): Promise {
25 | return createKeyAttestationJwt({
26 | ...options,
27 | use: 'proof_type.attestation',
28 | })
29 | }
30 |
31 | export interface VerifyCredentialRequestAttestationProofOptions extends Omit {}
32 | export async function verifyCredentialRequestAttestationProof(options: VerifyCredentialRequestAttestationProofOptions) {
33 | const verificationResult = await verifyKeyAttestationJwt({
34 | ...options,
35 | use: 'proof_type.attestation',
36 | })
37 |
38 | return verificationResult
39 | }
40 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/proof-type/attestation/z-attestation-proof-type.ts:
--------------------------------------------------------------------------------
1 | import { zCompactJwt } from '@openid4vc/oauth2'
2 | import z from 'zod'
3 | import {
4 | type KeyAttestationJwtHeader,
5 | zKeyAttestationJwtHeader,
6 | zKeyAttestationJwtPayloadForUse,
7 | } from '../../../key-attestation/z-key-attestation'
8 |
9 | export const zAttestationProofTypeIdentifier = z.literal('attestation')
10 | export const attestationProofTypeIdentifier = zAttestationProofTypeIdentifier.value
11 | export type AttestationProofTypeIdentifier = z.infer
12 |
13 | export const zCredentialRequestProofAttestation = z.object({
14 | proof_type: zAttestationProofTypeIdentifier,
15 | attestation: zCompactJwt,
16 | })
17 |
18 | export const zCredentialRequestAttestationProofTypeHeader = zKeyAttestationJwtHeader
19 | export type CredentialRequestAttestationProofTypeHeader = KeyAttestationJwtHeader
20 |
21 | export const zCredentialRequestAttestationProofTypePayload = zKeyAttestationJwtPayloadForUse('proof_type.attestation')
22 | export type CredentialRequestAttestationProofTypePayload = z.infer
23 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/proof-type/index.ts:
--------------------------------------------------------------------------------
1 | import type { AttestationProofTypeIdentifier } from './attestation/z-attestation-proof-type'
2 | import type { JwtProofTypeIdentifier } from './jwt/z-jwt-proof-type'
3 |
4 | // jwt
5 | export {
6 | type JwtProofTypeIdentifier,
7 | zCredentialRequestProofJwt,
8 | zJwtProofTypeIdentifier,
9 | } from './jwt/z-jwt-proof-type'
10 |
11 | // attestation
12 | export {
13 | type AttestationProofTypeIdentifier,
14 | zCredentialRequestProofAttestation,
15 | zAttestationProofTypeIdentifier,
16 | } from './attestation/z-attestation-proof-type'
17 |
18 | export type ProofTypeIdentifier = JwtProofTypeIdentifier | AttestationProofTypeIdentifier
19 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/formats/proof-type/jwt/z-jwt-proof-type.ts:
--------------------------------------------------------------------------------
1 | import { zCompactJwt, zJwtHeader, zJwtPayload } from '@openid4vc/oauth2'
2 | import { zHttpsUrl, zInteger } from '@openid4vc/utils'
3 | import z from 'zod'
4 |
5 | export const zJwtProofTypeIdentifier = z.literal('jwt')
6 | export const jwtProofTypeIdentifier = zJwtProofTypeIdentifier.value
7 | export type JwtProofTypeIdentifier = z.infer
8 |
9 | export const zCredentialRequestProofJwt = z.object({
10 | proof_type: zJwtProofTypeIdentifier,
11 | jwt: zCompactJwt,
12 | })
13 |
14 | export const zCredentialRequestJwtProofTypeHeader = zJwtHeader
15 | .merge(
16 | z.object({
17 | key_attestation: z.optional(zCompactJwt),
18 | typ: z.literal('openid4vci-proof+jwt'),
19 | })
20 | )
21 | .passthrough()
22 | .refine(({ kid, jwk }) => jwk === undefined || kid === undefined, {
23 | message: `Both 'jwk' and 'kid' are defined. Only one is allowed`,
24 | })
25 | .refine(({ trust_chain, kid }) => !trust_chain || !kid, {
26 | message: `When 'trust_chain' is provided, 'kid' is required`,
27 | })
28 |
29 | export type CredentialRequestJwtProofTypeHeader = z.infer
30 |
31 | export const zCredentialRequestJwtProofTypePayload = z
32 | .object({
33 | ...zJwtPayload.shape,
34 | aud: zHttpsUrl,
35 | iat: zInteger,
36 | })
37 | .passthrough()
38 |
39 | export type CredentialRequestJwtProofTypePayload = z.infer
40 |
--------------------------------------------------------------------------------
/packages/openid4vci/src/key-attestation/z-key-attestation.ts:
--------------------------------------------------------------------------------
1 | import { zJwk, zJwtHeader, zJwtPayload } from '@openid4vc/oauth2'
2 | import { zInteger } from '@openid4vc/utils'
3 | import z from 'zod'
4 |
5 | export type KeyAttestationJwtUse = 'proof_type.jwt' | 'proof_type.attestation'
6 |
7 | export const zKeyAttestationJwtHeader = z
8 | .object({
9 | ...zJwtHeader.shape,
10 | typ: z
11 | // Draft 15
12 | .literal('keyattestation+jwt')
13 | .or(
14 | // Draft 16
15 | z.literal('key-attestation+jwt')
16 | ),
17 | })
18 | .passthrough()
19 | .refine(({ kid, jwk }) => jwk === undefined || kid === undefined, {
20 | message: `Both 'jwk' and 'kid' are defined. Only one is allowed`,
21 | })
22 | .refine(({ trust_chain, kid }) => !trust_chain || !kid, {
23 | message: `When 'trust_chain' is provided, 'kid' is required`,
24 | })
25 |
26 | export type KeyAttestationJwtHeader = z.infer
27 |
28 | export const zIso18045 = z.enum(['iso_18045_high', 'iso_18045_moderate', 'iso_18045_enhanced-basic', 'iso_18045_basic'])
29 |
30 | export type Iso18045 = z.infer
31 | export const zIso18045OrStringArray = z.array(z.union([zIso18045, z.string()]))
32 |
33 | export const zKeyAttestationJwtPayload = z
34 | .object({
35 | ...zJwtPayload.shape,
36 | iat: zInteger,
37 |
38 | attested_keys: z.array(zJwk),
39 | key_storage: z.optional(zIso18045OrStringArray),
40 | user_authentication: z.optional(zIso18045OrStringArray),
41 | certification: z.optional(z.string().url()),
42 | })
43 | .passthrough()
44 |
45 | export const zKeyAttestationJwtPayloadForUse =