Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/big-kiwis-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openid4vc/openid4vp": minor
---

feat: add support for `x509_hash` client id scheme.

With support for this new client id scheme the `hash` callback is now required in the `Openid4vpClient`, and the `validateOpenid4vpClientId` method is now asynchronous.
5 changes: 5 additions & 0 deletions .changeset/cold-sheep-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": patch
---

feat: support verifier_attestation in addition to verifier_info
5 changes: 5 additions & 0 deletions .changeset/fruity-hoops-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": patch
---

feat: correctly extract jwk from jarm kid if defined
7 changes: 7 additions & 0 deletions .changeset/hungry-mails-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openid4vc/openid4vp": minor
---

refactor: client id scheme to client id prefix.

All parameters have been changed to use prefix, so .e.g. `scheme` has become `prefix`. Only the parameters referring to the legacy separate `client_id_scheme` are still called scheme.
5 changes: 5 additions & 0 deletions .changeset/lazy-jobs-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": patch
---

deprecate the `x509_san_uri` client id scheme for draft 25+
5 changes: 5 additions & 0 deletions .changeset/major-yaks-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": patch
---

feat: add support for client_id_prefixes in addition to client_id_schemes
7 changes: 7 additions & 0 deletions .changeset/old-beds-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openid4vc/openid4vp": minor
---

feat: support the new `origin:` client id prefix in addition to `web-origin:` for the DC API.

NOTE that for unsigned requests over the DC API, the `client_id` should be omitted, and you need to calculate the effective client id. Up to draft 25 this was `web-origin:<origin>` and after draft 25 it's `origin:<origin>`. It's not always possible to detect which prefix needs to be used, so if you're a verifier that wants to support both draft versions with the DC API, make sure to allow both prefixes for the session binding of presentations.
7 changes: 7 additions & 0 deletions .changeset/orange-seas-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openid4vc/openid4vp": minor
---

add support for response encryption without leveraging JARM.

Both the JARM-based response encryption, and the new OID4VP-based response encryption methods are supported. Both methods are used to determine which alg and enc values to use, and you should provide the same `jarm` configuration options. Once support for pre-1.0 drafts will be removed, the JARM options will also be replaced with a more OID4VP aligned API.
5 changes: 5 additions & 0 deletions .changeset/polite-pens-take.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": minor
---

feat: add support for draft 27 vp_formats_supported
7 changes: 7 additions & 0 deletions .changeset/salty-geese-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openid4vc/openid4vp": minor
---

feat: add support for the new `decentralized_identifier` and `openid_federation` client id schemes.

The client information is also updated to return the `decentralized_identifier` and `openid_federation` scheme. The `effective` client is the value that should be used for comparison.
5 changes: 5 additions & 0 deletions .changeset/sharp-women-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vp": patch
---

feat: add `version` to the `resolveOpenid4vpAuthorizationRequest` return value, indicating the highest supported draft version for the authorization request
2 changes: 1 addition & 1 deletion packages/openid4vp/src/Openid4vpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface Openid4vpClientOptions {
/**
* Callbacks required for the openid4vp client
*/
callbacks: Omit<CallbackContext, 'hash' | 'generateRandom' | 'clientAuthentication'>
callbacks: Omit<CallbackContext, 'generateRandom' | 'clientAuthentication'>
}

export class Openid4vpClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('Parse Authorization Request Params', () => {
test('parse authorization request uri and transforms string JSON fields to JSON', () => {
expect(
parseOpenid4vpAuthorizationRequest({
authorizationRequest: `openid4vp://?client_id=test&presentation_definition=${encodeURIComponent(JSON.stringify({ id: 'something' }))}&dcql_query=${encodeURIComponent(JSON.stringify({ id: 'something' }))}&client_metadata=${encodeURIComponent(JSON.stringify({ my: 'metadata' }))}&transaction_data=${encodeURIComponent(JSON.stringify(['something']))}&verifier_attestations=${encodeURIComponent(JSON.stringify([{ format: 'custom', data: 'none', credential_ids: ['my-id'] }]))}`,
authorizationRequest: `openid4vp://?client_id=test&presentation_definition=${encodeURIComponent(JSON.stringify({ id: 'something' }))}&dcql_query=${encodeURIComponent(JSON.stringify({ id: 'something' }))}&client_metadata=${encodeURIComponent(JSON.stringify({ my: 'metadata' }))}&transaction_data=${encodeURIComponent(JSON.stringify(['something']))}&verifier_attestations=${encodeURIComponent(JSON.stringify([{ format: 'custom', data: 'none', credential_ids: ['my-id'] }]))}&verifier_info=${encodeURIComponent(JSON.stringify([{ format: 'custom', data: 'none', credential_ids: ['my-id'] }]))}`,
})
).toEqual({
type: 'openid4vp',
Expand All @@ -23,6 +23,13 @@ describe('Parse Authorization Request Params', () => {
credential_ids: ['my-id'],
},
],
verifier_info: [
{
format: 'custom',
data: 'none',
credential_ids: ['my-id'],
},
],
},
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import z from 'zod'
import {
type ParsedClientIdentifier,
validateOpenid4vpClientId,
} from '../client-identifier-scheme/parse-client-identifier-scheme'
} from '../client-identifier-prefix/parse-client-identifier-prefix'
import { fetchClientMetadata } from '../fetch-client-metadata'
import { type VerifiedJarRequest, verifyJarRequest } from '../jar/handle-jar-request/verify-jar-request'
import {
Expand All @@ -14,6 +14,7 @@ import {
} from '../jar/z-jar-authorization-request'
import type { PexPresentationDefinition } from '../models/z-pex'
import { type ParsedTransactionDataEntry, parseTransactionData } from '../transaction-data/parse-transaction-data'
import { type Openid4vpDraftVersionNumber, parseAuthorizationRequestVersion } from '../version'
import {
type WalletVerificationOptions,
validateOpenid4vpAuthorizationRequestPayload,
Expand All @@ -34,7 +35,7 @@ export interface ResolveOpenid4vpAuthorizationRequestOptions {
wallet?: WalletVerificationOptions
origin?: string
disableOriginValidation?: boolean
callbacks: Pick<CallbackContext, 'verifyJwt' | 'decryptJwe' | 'getX509CertificateMetadata' | 'fetch'>
callbacks: Pick<CallbackContext, 'verifyJwt' | 'decryptJwe' | 'getX509CertificateMetadata' | 'fetch' | 'hash'>
}

export type ResolvedOpenid4vpAuthorizationRequest = {
Expand All @@ -47,6 +48,11 @@ export type ResolvedOpenid4vpAuthorizationRequest = {
presentation_definition_uri?: string
}
dcql?: { query: unknown } | undefined

/**
* The highest possible draft version number based on draft-specific version checks done on the request.
*/
version: Openid4vpDraftVersionNumber
}
export async function resolveOpenid4vpAuthorizationRequest(
options: ResolveOpenid4vpAuthorizationRequestOptions
Expand Down Expand Up @@ -99,7 +105,7 @@ export async function resolveOpenid4vpAuthorizationRequest(
clientMetadata = await fetchClientMetadata({ clientMetadataUri: authorizationRequestPayload.client_metadata_uri })
}

const clientMeta = validateOpenid4vpClientId({
const clientMeta = await validateOpenid4vpClientId({
authorizationRequestPayload: {
...authorizationRequestPayload,
client_metadata: clientMetadata,
Expand Down Expand Up @@ -141,6 +147,7 @@ export async function resolveOpenid4vpAuthorizationRequest(
client: clientMeta,
pex,
dcql,
version: parseAuthorizationRequestVersion(authorizationRequestPayload),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export const validateOpenid4vpAuthorizationRequestPayload = (
})
}

if (params.client_id.startsWith('web-origin:')) {
if (params.client_id.startsWith('web-origin:') || params.client_id.startsWith('origin:')) {
throw new Oauth2ServerErrorResponseError({
error: Oauth2ErrorCodes.InvalidRequest,
error_description: `The 'client_id' parameter MUST NOT use client identifier scheme 'web-origin' when not using the dc_api response mode. Current: ${params.client_id}`,
error_description: `The 'client_id' parameter MUST NOT use client identifier scheme '${params.client_id.split(':')[0]}' when not using the dc_api response mode. Current: ${params.client_id}`,
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const zOpenid4vpAuthorizationRequestDcApi = zOpenid4vpAuthorizationReques
trust_chain: true,
state: true,
verifier_attestations: true,
verifier_info: true,
})
.extend({
client_id: z.optional(z.string()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ export const zOpenid4vpAuthorizationRequest = z
'verifier_attestation',
'x509_san_dns',
'x509_san_uri',
'x509_hash',
])
.optional(),
verifier_attestations: zVerifierAttestations.optional(),
verifier_info: zVerifierAttestations.optional(),
})
.passthrough()

Expand All @@ -59,6 +61,7 @@ export const zOpenid4vpAuthorizationRequestFromUriParams = z
dcql_query: zStringToJson.optional(),
transaction_data: zStringToJson.optional(),
verifier_attestations: zStringToJson.optional(),
verifier_info: zStringToJson.optional(),
})
.passthrough()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { dateToSeconds, encodeToBase64Url } from '@openid4vc/utils'
import { addSecondsToDate } from '../../../utils/src/date'
import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request'
import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api'
import { getOpenid4vpClientId } from '../client-identifier-scheme/parse-client-identifier-scheme'
import { getOpenid4vpClientId } from '../client-identifier-prefix/parse-client-identifier-prefix'
import { createJarmAuthorizationResponse } from '../jarm/jarm-authorization-response-create'
import { extractJwksFromClientMetadata } from '../jarm/jarm-extract-jwks'
import { extractJwkFromJwks } from '../jarm/jarm-extract-jwks'
import { isJarmResponseMode } from '../jarm/jarm-response-mode'
import { jarmAssertMetadataSupported } from '../jarm/metadata/jarm-assert-metadata-supported'
import { assertValueSupported, jarmAssertMetadataSupported } from '../jarm/metadata/jarm-assert-metadata-supported'
import type { JarmServerMetadata } from '../jarm/metadata/z-jarm-authorization-server-metadata'
import type { ClientMetadata } from '../models/z-client-metadata'
import type { Openid4vpAuthorizationResponse } from './z-authorization-response'
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function createOpenid4vpAuthorizationResponse(
state: authorizationRequestPayload.state,
} satisfies Openid4vpAuthorizationResponse

const { clientIdScheme } = getOpenid4vpClientId({
const { clientIdPrefix } = getOpenid4vpClientId({
responseMode: authorizationRequestPayload.response_mode,
clientId: authorizationRequestPayload.client_id,
legacyClientIdScheme: authorizationRequestPayload.client_id_scheme,
Expand All @@ -85,9 +85,9 @@ export async function createOpenid4vpAuthorizationResponse(
}

// When using OpenID Federation, we must not rely on the client metadata from the request
if (clientIdScheme === 'https' && !options.clientMetadata) {
if (clientIdPrefix === 'openid_federation' && !options.clientMetadata) {
throw new Oauth2Error(
"When OpenID Federation is used as the client id scheme (https), passing externally fetched and verified 'clientMetadata' to the 'createOpenid4vpAuthorizationResponse' is required."
"When OpenID Federation is used as the client id scheme (https/openid_federation), passing externally fetched and verified 'clientMetadata' to the 'createOpenid4vpAuthorizationResponse' is required."
)
}

Expand All @@ -109,23 +109,59 @@ export async function createOpenid4vpAuthorizationResponse(
})
}

const supportedJarmMetadata = jarmAssertMetadataSupported({
clientMetadata: clientMetadata,
serverMetadata: jarm.serverMetadata,
})
if (
clientMetadata.authorization_encrypted_response_alg ||
clientMetadata.authorization_encrypted_response_env ||
clientMetadata.authorization_signed_response_alg
) {
jarmAssertMetadataSupported({
clientMetadata: clientMetadata,
serverMetadata: jarm.serverMetadata,
})
}

const clientMetaJwks = extractJwksFromClientMetadata({
...clientMetadata,
jwks,
const encJwk = extractJwkFromJwks(jwks, {
supportedAlgValues:
jarm.serverMetadata.authorization_encryption_alg_values_supported ??
(clientMetadata.authorization_encrypted_response_alg
? [clientMetadata.authorization_encrypted_response_alg]
: undefined),
})

if (!clientMetaJwks?.encJwk) {
if (!encJwk) {
throw new Oauth2ServerErrorResponseError({
error: Oauth2ErrorCodes.InvalidRequest,
error_description: 'Could not extract encryption JWK from client metadata. Failed to create JARM response.',
})
}

let enc: string
if (clientMetadata.encrypted_response_enc_values_supported) {
// Take first supported, or otherwise the first value
enc =
jarm.serverMetadata.authorization_encryption_enc_values_supported.find((enc) =>
clientMetadata.encrypted_response_enc_values_supported?.includes(enc)
) ?? clientMetadata.encrypted_response_enc_values_supported[0]
} else {
// Use old value, or otherwise fallback to default
enc = clientMetadata.authorization_encrypted_response_enc ?? 'A128GCM'
}

assertValueSupported({
actual: enc,
supported: jarm.serverMetadata.authorization_encryption_enc_values_supported,
errorMessage: `Invalid 'enc' value ${enc}. Supported values are ${jarm.serverMetadata.authorization_encryption_enc_values_supported.join(', ')}`,
})

const alg = encJwk.alg ?? clientMetadata.authorization_encrypted_response_alg ?? 'ECDH-ES'
assertValueSupported({
actual: alg,
supported: jarm.serverMetadata.authorization_encryption_alg_values_supported,
errorMessage: `Invalid 'alg' value ${alg}. Supported values are ${jarm.serverMetadata.authorization_encryption_alg_values_supported.join(', ')}`,
})

// TODO: we can remove this once support for pre-1.0 versions have been removed
// TODO: we should keep the JARM implementation and move it to oauth2 package
// When the response is NOT only encrypted, the JWT payload needs to include the iss, aud and exp.
let additionalJwtPayload: Record<string, string | number> | undefined
if (jarm?.jwtSigner) {
Expand Down Expand Up @@ -158,17 +194,16 @@ export async function createOpenid4vpAuthorizationResponse(
const result = await createJarmAuthorizationResponse({
jarmAuthorizationResponse: jarmResponsePayload,
jwtSigner: jarm?.jwtSigner,
jweEncryptor:
jarm?.encryption && (supportedJarmMetadata.type === 'encrypt' || supportedJarmMetadata.type === 'sign_encrypt')
? {
method: 'jwk',
publicJwk: clientMetaJwks.encJwk,
apu: jarm.encryption.nonce ? encodeToBase64Url(jarm.encryption.nonce) : undefined,
apv: encodeToBase64Url(authorizationRequestPayload.nonce),
alg: supportedJarmMetadata.client_metadata.authorization_encrypted_response_alg,
enc: supportedJarmMetadata.client_metadata.authorization_encrypted_response_enc,
}
: undefined,
jweEncryptor: jarm?.encryption
? {
method: 'jwk',
publicJwk: encJwk,
apu: jarm.encryption.nonce ? encodeToBase64Url(jarm.encryption.nonce) : undefined,
apv: encodeToBase64Url(authorizationRequestPayload.nonce),
alg,
enc,
}
: undefined,
callbacks: {
signJwt: callbacks.signJwt,
encryptJwe: callbacks.encryptJwe,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type CallbackContext, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2'
import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request'
import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api'
import { getOpenid4vpClientId } from '../client-identifier-scheme/parse-client-identifier-scheme'
import { getOpenid4vpClientId } from '../client-identifier-prefix/parse-client-identifier-prefix'
import type { VerifiedJarmAuthorizationResponse } from '../jarm/jarm-authorization-response/verify-jarm-authorization-response'
import type { JarmHeader } from '../jarm/jarm-authorization-response/z-jarm-authorization-response'
import { isJarmResponseMode } from '../jarm/jarm-response-mode'
Expand Down Expand Up @@ -47,9 +47,7 @@ export async function parseOpenid4vpAuthorizationResponse(
jarmResponseJwt: authorizationResponse.response as string,
callbacks,
authorizationRequestPayload,
// If client_id_scheme was provided we should use the legacy (unprefixed) client id scheme
// TODO: allow both versions, in case of e.g. did:
expectedClientId: expectedClientId.legacyClientId ?? expectedClientId.clientId,
expectedClientId: expectedClientId.effectiveClientId,
})
}

Expand Down
Loading