Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fresh-coins-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vci": minor
---

feat: update draft 16 to v1.0. This only introduces one breaking change requiring a transaction_id in the deferred credential response. For this reason we replaced the Draft16 support with V1. All other drafts are still supported.
2 changes: 1 addition & 1 deletion packages/openid4vci/src/Openid4vciClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export class Openid4vciClient {

if (
issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.Draft15 ||
issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.Draft16
issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.V1
) {
credentialResponse = await retrieveCredentialsWithCredentialConfigurationId({
accessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,25 @@ export function createCredentialResponse(options: CreateCredentialResponseOption
} satisfies CredentialResponse)
}

export type CreateDeferredCredentialResponseOptions = {
credentials?: DeferredCredentialResponse['credentials']
export type CreateDeferredCredentialResponseOptions = (
| {
credentials: DeferredCredentialResponse['credentials']
notificationId?: string

interval?: number

notificationId?: string
transactionId?: never
interval?: never
}
| {
/**
* The `transaction_id` used to identify the deferred issuance transaction.
*/
transactionId: string
interval: number

credentials?: never
notificationId?: never
}
) & {
/**
* Additional payload to include in the deferred credential response
*/
Expand All @@ -62,7 +74,10 @@ export function createDeferredCredentialResponse(options: CreateDeferredCredenti
return parseWithErrorHandling(zDeferredCredentialResponse, {
credentials: options.credentials,
notification_id: options.notificationId,

transaction_id: options.transactionId,
interval: options.interval,

...options.additionalPayload,
} satisfies DeferredCredentialResponse)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
zMsoMdocCredentialIssuerMetadataDraft14,
zSdJwtDcCredentialIssuerMetadata,
} from '../formats/credential'
import { zLegacySdJwtVcCredentialIssuerMetadataDraft16 } from '../formats/credential/sd-jwt-vc/z-sd-jwt-vc'
import { zLegacySdJwtVcCredentialIssuerMetadataV1 } from '../formats/credential/sd-jwt-vc/z-sd-jwt-vc'
import { zSdJwtW3VcCredentialIssuerMetadata } from '../formats/credential/w3c-vc/z-w3c-sd-jwt-vc'
import { getCredentialConfigurationSupportedById } from '../metadata/credential-issuer/credential-issuer-metadata'
import type { IssuerMetadataResult } from '../metadata/fetch-issuer-metadata'
Expand All @@ -40,7 +40,7 @@ export function getCredentialRequestFormatPayloadForCredentialConfigurationId(
)

if (
zIs(zLegacySdJwtVcCredentialIssuerMetadataDraft16, credentialConfiguration) ||
zIs(zLegacySdJwtVcCredentialIssuerMetadataV1, credentialConfiguration) ||
zIs(zLegacySdJwtVcCredentialIssuerMetadataDraft14, credentialConfiguration)
) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function retrieveCredentialsWithCredentialConfigurationId(
) {
if (
options.issuerMetadata.originalDraftVersion !== Openid4vciDraftVersion.Draft15 &&
options.issuerMetadata.originalDraftVersion !== Openid4vciDraftVersion.Draft16
options.issuerMetadata.originalDraftVersion !== Openid4vciDraftVersion.V1
) {
throw new Openid4vciError(
'Requesting credentials based on credential configuration ID is not supported in OpenID4VCI below draft 15. Make sure to provide the format and format specific claims in the request.'
Expand Down Expand Up @@ -119,10 +119,10 @@ export interface RetrieveCredentialsWithFormatOptions extends RetrieveCredential
export async function retrieveCredentialsWithFormat(options: RetrieveCredentialsWithFormatOptions) {
if (
options.issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.Draft15 ||
options.issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.Draft16
options.issuerMetadata.originalDraftVersion === Openid4vciDraftVersion.V1
) {
throw new Openid4vciError(
'Requesting credentials based on format is not supported in OpenID4VCI draft 15. Provide the credential configuration id directly in the request.'
'Requesting credentials based on format is not supported on OpenID4VCI above draft 15. Provide the credential configuration id directly in the request.'
)
}

Expand Down Expand Up @@ -331,7 +331,11 @@ export async function retrieveDeferredCredentials(

// Try to parse the credential response
const deferredCredentialResponseResult = isResponseContentType(ContentType.Json, resourceResponse.response)
? zDeferredCredentialResponse.safeParse(await resourceResponse.response.clone().json())
? zDeferredCredentialResponse
.refine((response) => response.credentials || response.transaction_id === options.transactionId, {
error: `Transaction id in deferred credential response does not match transaction id in deferred credential request '${options.transactionId}'`,
})
.safeParse(await resourceResponse.response.clone().json())
: undefined
if (!deferredCredentialResponseResult?.success) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ const zBaseCredentialResponse = z
z.array(zCredentialEncoding),
])
.optional(),
interval: z.number().int().positive().optional(),
notification_id: z.string().optional(),

transaction_id: z.string().optional(),
interval: z.number().int().positive().optional(),
})
.loose()

export const zCredentialResponse = zBaseCredentialResponse
.extend({
credential: z.optional(zCredentialEncoding),
transaction_id: z.string().optional(),

c_nonce: z.string().optional(),
c_nonce_expires_in: z.number().int().optional(),
Expand Down Expand Up @@ -65,14 +66,29 @@ export const zCredentialErrorResponse = z

export type CredentialErrorResponse = z.infer<typeof zCredentialErrorResponse>

export const zDeferredCredentialResponse = zBaseCredentialResponse.refine(
(value) => {
const { credentials, interval } = value
return [credentials, interval].filter((i) => i !== undefined).length === 1
},
{
message: `Exactly one of 'credentials' or 'interval' MUST be defined.`,
export const zDeferredCredentialResponse = zBaseCredentialResponse.superRefine((value, ctx) => {
const { credentials, transaction_id, interval, notification_id } = value

if ([credentials, transaction_id].filter((i) => i !== undefined).length !== 1) {
ctx.addIssue({
code: 'custom',
message: `Exactly one of 'credentials', or 'transaction_id' MUST be defined.`,
})
}

if (transaction_id && !interval) {
ctx.addIssue({
code: 'custom',
message: `'interval' MUST be defined when 'transaction_id' is defined.`,
})
}

if (notification_id && credentials) {
ctx.addIssue({
code: 'custom',
message: `'notification_id' MUST NOT be defined when 'credentials' is not defined.`,
})
}
)
})

export type DeferredCredentialResponse = z.infer<typeof zDeferredCredentialResponse>
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type LegacySdJwtVcFormatIdentifier = z.infer<typeof zLegacySdJwtVcFormatI
* of the OpenID for Verifiable Presentations specification. Please update your
* implementations accordingly.
*/
export const zLegacySdJwtVcCredentialIssuerMetadataDraft16 = zCredentialConfigurationSupportedCommon.extend({
export const zLegacySdJwtVcCredentialIssuerMetadataV1 = zCredentialConfigurationSupportedCommon.extend({
vct: z.string(),
format: zLegacySdJwtVcFormatIdentifier,
order: z.optional(z.array(z.string())),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { zCompactJwt } from '@openid4vc/oauth2'
import { type InferOutputUnion, type Simplify, zHttpsUrl } from '@openid4vc/utils'
import z from 'zod'
import {
Expand Down Expand Up @@ -29,7 +28,7 @@ import {
zSdJwtDcCredentialIssuerMetadataDraft15,
zSdJwtDcFormatIdentifier,
} from '../../formats/credential'
import { zLegacySdJwtVcCredentialIssuerMetadataDraft16 } from '../../formats/credential/sd-jwt-vc/z-sd-jwt-vc'
import { zLegacySdJwtVcCredentialIssuerMetadataV1 } from '../../formats/credential/sd-jwt-vc/z-sd-jwt-vc'
import {
zSdJwtW3VcCredentialIssuerMetadata,
zSdJwtW3VcCredentialIssuerMetadataDraft15,
Expand All @@ -48,7 +47,7 @@ const allCredentialIssuerMetadataFormats = [
zJwtVcJsonCredentialIssuerMetadata,
zSdJwtW3VcCredentialIssuerMetadata,
zSdJwtW3VcCredentialIssuerMetadataDraft15,
zLegacySdJwtVcCredentialIssuerMetadataDraft16,
zLegacySdJwtVcCredentialIssuerMetadataV1,
zSdJwtDcCredentialIssuerMetadataDraft15,
zMsoMdocCredentialIssuerMetadataDraft15,
zJwtVcJsonLdCredentialIssuerMetadataDraft15,
Expand Down Expand Up @@ -132,8 +131,8 @@ const zCredentialIssuerMetadataDisplayEntry = z
.loose()
export type CredentialIssuerMetadataDisplayEntry = z.infer<typeof zCredentialIssuerMetadataDisplayEntry>

export type CredentialIssuerMetadata = z.infer<typeof zCredentialIssuerMetadataDraft14Draft15Draft16>
const zCredentialIssuerMetadataDraft14Draft15Draft16 = z
export type CredentialIssuerMetadata = z.infer<typeof zCredentialIssuerMetadataDraft14Draft15V1>
const zCredentialIssuerMetadataDraft14Draft15V1 = z
.object({
credential_issuer: zHttpsUrl,
authorization_servers: z.array(zHttpsUrl).optional(),
Expand All @@ -157,7 +156,6 @@ const zCredentialIssuerMetadataDraft14Draft15Draft16 = z
})
.loose()
.optional(),
signed_metadata: zCompactJwt.optional(),
display: z.array(zCredentialIssuerMetadataDisplayEntry).optional(),
credential_configurations_supported: z.record(z.string(), zCredentialConfigurationSupportedWithFormats),
})
Expand Down Expand Up @@ -258,7 +256,7 @@ export const zCredentialConfigurationSupportedDraft11To16 = z
.pipe(zCredentialConfigurationSupportedWithFormats)

// Transforms credential configuration supported from draft 16 to draft 15
const zCredentialConfigurationSupportedDraft16To15 = zCredentialConfigurationSupportedWithFormats.transform(
const zCredentialConfigurationSupportedV1ToDraft15 = zCredentialConfigurationSupportedWithFormats.transform(
({ credential_metadata, ...rest }) => ({
...credential_metadata,
...rest,
Expand All @@ -267,7 +265,7 @@ const zCredentialConfigurationSupportedDraft16To15 = zCredentialConfigurationSup

// Transforms credential configuration supported to credentials_supported format
// Ignores unknown formats
const zCredentialConfigurationSupportedDraft16To11 = zCredentialConfigurationSupportedDraft16To15
const zCredentialConfigurationSupportedV1ToDraft11 = zCredentialConfigurationSupportedV1ToDraft15
.and(
z
.object({
Expand Down Expand Up @@ -358,19 +356,19 @@ export const zCredentialIssuerMetadataDraft11To16 = z
})
.loose()
)
.pipe(zCredentialIssuerMetadataDraft14Draft15Draft16)
.pipe(zCredentialIssuerMetadataDraft14Draft15V1)

/**
* Typing is a bit off on this one
*/
export type CredentialIssuerMetadataDraft11 = Simplify<
CredentialIssuerMetadata & {
authorization_server?: string
credentials_supported: z.infer<typeof zCredentialConfigurationSupportedDraft16To11>[]
credentials_supported: z.infer<typeof zCredentialConfigurationSupportedV1ToDraft11>[]
}
>

export const zCredentialIssuerMetadataWithDraft11 = zCredentialIssuerMetadataDraft14Draft15Draft16
export const zCredentialIssuerMetadataWithDraft11 = zCredentialIssuerMetadataDraft14Draft15V1
.transform((issuerMetadata) => ({
...issuerMetadata,
...(issuerMetadata.authorization_servers ? { authorization_server: issuerMetadata.authorization_servers[0] } : {}),
Expand All @@ -380,20 +378,20 @@ export const zCredentialIssuerMetadataWithDraft11 = zCredentialIssuerMetadataDra
})),
}))
.pipe(
zCredentialIssuerMetadataDraft14Draft15Draft16.extend({
credentials_supported: z.array(zCredentialConfigurationSupportedDraft16To11),
zCredentialIssuerMetadataDraft14Draft15V1.extend({
credentials_supported: z.array(zCredentialConfigurationSupportedV1ToDraft11),
})
)

export const zCredentialIssuerMetadata = z.union([
// First prioritize draft 16/15/14 (and 13)
zCredentialIssuerMetadataDraft14Draft15Draft16,
zCredentialIssuerMetadataDraft14Draft15V1,
// Then try parsing draft 11 and transform into draft 16
zCredentialIssuerMetadataDraft11To16,
])

export const zCredentialIssuerMetadataWithDraftVersion = z.union([
zCredentialIssuerMetadataDraft14Draft15Draft16.transform((credentialIssuerMetadata) => {
zCredentialIssuerMetadataDraft14Draft15V1.transform((credentialIssuerMetadata) => {
const credentialConfigurations = Object.values(credentialIssuerMetadata.credential_configurations_supported)

const isDraft15 = credentialConfigurations.some((configuration) => {
Expand All @@ -413,21 +411,20 @@ export const zCredentialIssuerMetadataWithDraftVersion = z.union([
return false
})

const isDraft16 = credentialConfigurations.some((configuration) => {
// Added in draft 16
return configuration.credential_metadata
})
// Added in draft 16, but since there's no other breaking changes
// we assume V1 is used when we detect V1
const isV1 = credentialConfigurations.some((configuration) => configuration.credential_metadata)

return {
credentialIssuerMetadata,
originalDraftVersion: isDraft16
? Openid4vciDraftVersion.Draft16
originalDraftVersion: isV1
? Openid4vciDraftVersion.V1
: isDraft15
? Openid4vciDraftVersion.Draft15
: Openid4vciDraftVersion.Draft14,
}
}),
// Then try parsing draft 11 and transform into draft 14
// Then try parsing draft 11 and transform into draft 16
zCredentialIssuerMetadataDraft11To16.transform((credentialIssuerMetadata) => ({
credentialIssuerMetadata,
originalDraftVersion: Openid4vciDraftVersion.Draft11,
Expand Down
2 changes: 1 addition & 1 deletion packages/openid4vci/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export enum Openid4vciDraftVersion {
Draft16 = 'Draft16',
V1 = 'V1',
Draft15 = 'Draft15',
Draft14 = 'Draft14',
Draft11 = 'Draft11',
Expand Down