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/true-dolls-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openid4vc/openid4vci": minor
---

feat(openid4vci): supported for creating, resolving and verifiying signed credential issuer metadata
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ describe('Oauth2AuthorizationServer', () => {
sub: 'something',
},
signature: expect.any(String),
compact: expect.any(String),
})
})

Expand Down Expand Up @@ -306,6 +307,7 @@ describe('Oauth2AuthorizationServer', () => {
sub: 'a9ad80ef-18b6-4087-9b88-55f5b14a33da',
},
signature: expect.any(String),
compact: expect.any(String),
})
})
})
2 changes: 2 additions & 0 deletions packages/oauth2/src/common/jwt/decode-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type DecodeJwtResult<
header: InferSchemaOrDefaultOutput<HeaderSchema, typeof zJwtHeader>
payload: InferSchemaOrDefaultOutput<PayloadSchema, typeof zJwtPayload>
signature: string
compact: string
}

export function decodeJwt<
Expand Down Expand Up @@ -67,6 +68,7 @@ export function decodeJwt<
header: header as InferSchemaOrDefaultOutput<HeaderSchema, typeof zJwtHeader>,
payload: payload as InferSchemaOrDefaultOutput<PayloadSchema, typeof zJwtPayload>,
signature: jwtParts[2],
compact: options.jwt,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export async function fetchAuthorizationServerMetadata(
let authorizationServerResult = await fetchWellKnownMetadata(
authorizationServerWellKnownMetadataUrl,
zAuthorizationServerMetadata,
fetch
{
fetch,
}
)

if (
Expand All @@ -42,15 +44,19 @@ export async function fetchAuthorizationServerMetadata(
authorizationServerResult = await fetchWellKnownMetadata(
nonCompliantAuthorizationServerWellKnownMetadataUrl,
zAuthorizationServerMetadata,
fetch
{
fetch,
}
)
}

if (!authorizationServerResult) {
authorizationServerResult = await fetchWellKnownMetadata(
openIdConfigurationWellKnownMetadataUrl,
zAuthorizationServerMetadata,
fetch
{
fetch,
}
)
}

Expand Down
22 changes: 19 additions & 3 deletions packages/oauth2/src/metadata/fetch-well-known-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import { type BaseSchema, ContentType, createZodFetcher, type Fetch, InvalidFetc
import type z from 'zod'
import { ValidationError } from '../../../utils/src/error/ValidationError'

export interface FetchWellKnownMetadataOptions {
/**
* Custom fetch implementation to use for fetching the metadata
*/
fetch?: Fetch

/**
* The accepted content types. If not provided a default of `ContentType.Json`
* will be used. This will be used for the `Accept` header, as well as verified
* against the `Content-Type` response header.
*/
acceptedContentType?: [ContentType, ...ContentType[]]
}

/**
* Fetch well known metadata and validate the response.
*
Expand All @@ -16,11 +30,13 @@ import { ValidationError } from '../../../utils/src/error/ValidationError'
export async function fetchWellKnownMetadata<Schema extends BaseSchema>(
wellKnownMetadataUrl: string,
schema: Schema,
fetch?: Fetch
options?: FetchWellKnownMetadataOptions
): Promise<z.infer<Schema> | null> {
const fetcher = createZodFetcher(fetch)
const fetcher = createZodFetcher(options?.fetch)

const acceptedContentType = options?.acceptedContentType ?? [ContentType.Json]

const { result, response } = await fetcher(schema, ContentType.Json, wellKnownMetadataUrl)
const { result, response } = await fetcher(schema, acceptedContentType, wellKnownMetadataUrl)
if (response.status === 404) {
return null
}
Expand Down
2 changes: 1 addition & 1 deletion packages/openid4vci/src/Openid4vciClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class Openid4vciClient {

public async resolveIssuerMetadata(credentialIssuer: string): Promise<IssuerMetadataResult> {
return resolveIssuerMetadata(credentialIssuer, {
fetch: this.options.callbacks.fetch,
callbacks: this.options.callbacks,
})
}

Expand Down
16 changes: 16 additions & 0 deletions packages/openid4vci/src/Openid4vciIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
verifyCredentialRequestJwtProof,
} from './formats/proof-type/jwt/jwt-proof-type'
import { extractKnownCredentialConfigurationSupportedFormats } from './metadata/credential-issuer/credential-issuer-metadata'
import {
type CreateSignedCredentialIssuerMetadataJwtOptions,
createSignedCredentialIssuerMetadataJwt,
} from './metadata/credential-issuer/signed-credential-issuer-metadata'
import {
type CredentialIssuerMetadata,
zCredentialIssuerMetadata,
Expand Down Expand Up @@ -73,6 +77,18 @@ export class Openid4vciIssuer {
)
}

/**
* Validates credential issuer metadata structure is correct and creates signed credential issuer metadata JWT
*/
public createSignedCredentialIssuerMetadataJwt(
options: Omit<CreateSignedCredentialIssuerMetadataJwtOptions, 'callbacks'>
): Promise<string> {
return createSignedCredentialIssuerMetadataJwt({
callbacks: this.options.callbacks,
...options,
})
}

public async createCredentialOffer(
options: Pick<
CreateCredentialOfferOptions,
Expand Down
19 changes: 19 additions & 0 deletions packages/openid4vci/src/__tests__/Openid4vciClient.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ describe('Openid4vciClient', () => {
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, () =>
HttpResponse.json(paradymDraft13.credentialIssuerMetadata)
),
http.get('https://agent.paradym.id/.well-known/openid-credential-issuer/oid4vci/draft-13-issuer', () =>
HttpResponse.text(undefined, { status: 404 })
),
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`, () =>
HttpResponse.text(undefined, { status: 404 })
),
Expand Down Expand Up @@ -124,6 +127,7 @@ describe('Openid4vciClient', () => {
nonce: '463253917094869172078310',
},
signature: expect.any(String),
compact: expect.any(String),
})

const credentialResponse = await client.retrieveCredentials({
Expand All @@ -146,6 +150,9 @@ describe('Openid4vciClient', () => {
http.get(`${paradymDraft11.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, () =>
HttpResponse.json(paradymDraft11.credentialIssuerMetadata)
),
http.get('https://agent.paradym.id/.well-known/openid-credential-issuer/oid4vci/draft-11-issuer', () =>
HttpResponse.text(undefined, { status: 404 })
),
http.get(`${paradymDraft11.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`, () =>
HttpResponse.text(undefined, { status: 404 })
),
Expand Down Expand Up @@ -293,6 +300,7 @@ describe('Openid4vciClient', () => {
nonce: '463253917094869172078310',
},
signature: expect.any(String),
compact: expect.any(String),
})

const credentialResponse = await client.retrieveCredentials({
Expand All @@ -309,6 +317,9 @@ describe('Openid4vciClient', () => {

test('receive a credential from bdr using draft 13', async () => {
server.resetHandlers(
http.get(`https://demo.pid-issuer.bundesdruckerei.de/.well-known/openid-credential-issuer/c`, () =>
HttpResponse.json(undefined, { status: 404 })
),
http.get(`${bdrDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, () =>
HttpResponse.json(bdrDraft13.credentialIssuerMetadata)
),
Expand Down Expand Up @@ -358,6 +369,7 @@ describe('Openid4vciClient', () => {
htm: 'POST',
},
signature: expect.any(String),
compact: expect.any(String),
})
expect(parseXwwwFormUrlEncoded(await request.text())).toEqual({
client_id: 'some-random-client-id',
Expand Down Expand Up @@ -400,6 +412,7 @@ describe('Openid4vciClient', () => {
ath: 'i5Jbpn1_j8TgO3O4K6Y9D_f9k1lkOPMqa0uCo8nIRd4',
},
signature: expect.any(String),
compact: expect.any(String),
})
expect(await request.json()).toEqual({
format: 'vc+sd-jwt',
Expand Down Expand Up @@ -503,6 +516,7 @@ describe('Openid4vciClient', () => {
nonce: 'sjNMiqyfmBeD1qioCVyqvS',
},
signature: expect.any(String),
compact: expect.any(String),
})

const credentialResponse = await client.retrieveCredentials({
Expand Down Expand Up @@ -678,6 +692,7 @@ describe('Openid4vciClient', () => {
nonce: 'sjNMiqyfmBeD1qioCVyqvS',
},
signature: expect.any(String),
compact: expect.any(String),
})

const credentialResponse = await client.retrieveCredentials({
Expand Down Expand Up @@ -730,6 +745,7 @@ describe('Openid4vciClient', () => {
expect(resolvedIssuerMetadata).toStrictEqual({
originalDraftVersion: Openid4vciDraftVersion.Draft14,
credentialIssuer: issuerMetadata,
signedCredentialIssuer: undefined,
authorizationServers: [authorizationMetadata],
})

Expand Down Expand Up @@ -762,6 +778,7 @@ describe('Openid4vciClient', () => {
originalDraftVersion: Openid4vciDraftVersion.Draft14,
credentialIssuer: issuerMetadata,
authorizationServers: [authorizationMetadata],
signedCredentialIssuer: undefined,
})

server.resetHandlers(
Expand Down Expand Up @@ -793,6 +810,7 @@ describe('Openid4vciClient', () => {
expect(resolvedIssuerMetadata3).toStrictEqual({
originalDraftVersion: Openid4vciDraftVersion.Draft14,
credentialIssuer: issuerMetadata,
signedCredentialIssuer: undefined,
authorizationServers: [authorizationMetadata],
})

Expand Down Expand Up @@ -826,6 +844,7 @@ describe('Openid4vciClient', () => {
expect(resolvedIssuerMetadata4).toStrictEqual({
originalDraftVersion: Openid4vciDraftVersion.Draft14,
credentialIssuer: { ...issuerMetadata, token_endpoint: 'https://example.com/issuer-id/token' },
signedCredentialIssuer: undefined,
authorizationServers: [
{
issuer: 'https://example.com/issuer-id',
Expand Down
Loading