Skip to content

Commit

Permalink
chore: cleanup and small bugfix
Browse files Browse the repository at this point in the history
Signed-off-by: Tom Lanser <[email protected]>
  • Loading branch information
Tommylans committed Dec 5, 2024
1 parent 647f274 commit 2d13142
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 44 deletions.
84 changes: 44 additions & 40 deletions packages/oid4vci/src/__tests__/Oid4vciClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { callbacks, getSignJwtCallback, parseXwwwFormUrlEncoded } from '../../..
import { AuthorizationFlow, Oid4vciClient } from '../Oid4vciClient'
import { extractScopesForCredentialConfigurationIds } from '../metadata/credential-issuer/credential-configurations'
import { bdrDraft13 } from './__fixtures__/bdr'
import { paradymDraft11, paradymDraft13 } from './__fixtures__/paradym'
import { paradymDraft11, paradymDraft13, paradymDraft13Federation } from './__fixtures__/paradym'
import { presentationDuringIssuance } from './__fixtures__/presentationDuringIssuance'

const server = setupServer()
Expand Down Expand Up @@ -685,8 +685,13 @@ describe('Oid4vciClient', () => {
})

test('receive a credential from Paradym using federation', async () => {
const { d, ...publicKeyJwk } = paradymDraft13.holderPrivateKeyJwk
const jwtSigner = getSignJwtCallback([paradymDraft13.holderPrivateKeyJwk])
const { d, ...publicKeyJwk } = paradymDraft13Federation.holderPrivateKeyJwk
const jwtSigner = getSignJwtCallback([paradymDraft13Federation.holderPrivateKeyJwk])
const jwkKid = await calculateJwkThumbprint({
hashAlgorithm: HashAlgorithm.Sha256,
hashCallback: callbacks.hash,
jwk: paradymDraft13Federation.holderPrivateKeyJwk,
})

const entityConfigurationJwt = await createEntityConfiguration({
signJwtCallback: async ({ jwk, toBeSigned }) => {
Expand All @@ -709,71 +714,69 @@ describe('Oid4vciClient', () => {
return decodeBase64(signature)
},
header: {
kid: await calculateJwkThumbprint({
hashAlgorithm: HashAlgorithm.Sha256,
hashCallback: callbacks.hash,
jwk: paradymDraft13.holderPrivateKeyJwk,
}),
kid: jwkKid,
alg: 'ES256',
typ: 'entity-statement+jwt',
},
claims: {
exp: new Date(new Date().getTime() + 1000 * 60 * 60 * 24).getTime(),
iat: new Date().getTime(),
sub: paradymDraft13.credentialIssuerMetadata.credential_issuer,
iss: paradymDraft13.credentialIssuerMetadata.credential_issuer,
sub: paradymDraft13Federation.credentialIssuerMetadata.credential_issuer,
iss: paradymDraft13Federation.credentialIssuerMetadata.credential_issuer,
jwks: {
keys: [
{
...publicKeyJwk,
alg: 'ES256',
kid: await calculateJwkThumbprint({
hashAlgorithm: HashAlgorithm.Sha256,
hashCallback: callbacks.hash,
jwk: paradymDraft13.holderPrivateKeyJwk,
}),
kid: jwkKid,
},
],
},
metadata: {
federation_entity: paradymDraft13Federation.federationEntity,
openid_provider: {
...paradymDraft13.credentialIssuerMetadata,
...paradymDraft13Federation.credentialIssuerMetadata,
client_registration_types_supported: ['automatic'],
},
},
},
})

server.resetHandlers(
http.get(paradymDraft13.credentialOfferUri.replace('?raw=true', ''), () =>
HttpResponse.json(paradymDraft13.credentialOfferObject)
http.get(paradymDraft13Federation.credentialOfferUri.replace('?raw=true', ''), () =>
HttpResponse.json(paradymDraft13Federation.credentialOfferObject)
),
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-federation`, () =>
HttpResponse.text(entityConfigurationJwt, {
headers: {
'Content-Type': 'application/entity-statement+jwt',
},
})
http.get(
`${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-federation`,
() =>
HttpResponse.text(entityConfigurationJwt, {
headers: {
'Content-Type': 'application/entity-statement+jwt',
},
})
),
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, () =>
HttpResponse.json(paradymDraft13.credentialIssuerMetadata)
http.get(
`${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`,
() => HttpResponse.json(paradymDraft13Federation.credentialIssuerMetadata)
),
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`, () =>
HttpResponse.text(undefined, { status: 404 })
http.get(
`${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`,
() => HttpResponse.text(undefined, { status: 404 })
),
http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/oauth-authorization-server`, () =>
HttpResponse.text(undefined, { status: 404 })
http.get(
`${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/oauth-authorization-server`,
() => HttpResponse.text(undefined, { status: 404 })
),
http.post(paradymDraft13.credentialIssuerMetadata.token_endpoint, async ({ request }) => {
http.post(paradymDraft13Federation.credentialIssuerMetadata.token_endpoint, async ({ request }) => {
expect(parseXwwwFormUrlEncoded(await request.text())).toEqual({
'pre-authorized_code': '1130293840889780123292078',
grant_type: preAuthorizedCodeGrantIdentifier,
resource: credentialOffer.credential_issuer,
})

return HttpResponse.json(paradymDraft13.accessTokenResponse)
return HttpResponse.json(paradymDraft13Federation.accessTokenResponse)
}),
http.post(paradymDraft13.credentialIssuerMetadata.credential_endpoint, async ({ request }) => {
http.post(paradymDraft13Federation.credentialIssuerMetadata.credential_endpoint, async ({ request }) => {
expect(await request.json()).toEqual({
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge',
Expand All @@ -782,7 +785,7 @@ describe('Oid4vciClient', () => {
jwt: expect.any(String),
},
})
return HttpResponse.json(paradymDraft13.credentialResponse)
return HttpResponse.json(paradymDraft13Federation.credentialResponse)
})
)

Expand All @@ -794,25 +797,26 @@ describe('Oid4vciClient', () => {
},
})

const credentialOffer = await client.resolveCredentialOffer(paradymDraft13.credentialOffer)
expect(credentialOffer).toStrictEqual(paradymDraft13.credentialOfferObject)
const credentialOffer = await client.resolveCredentialOffer(paradymDraft13Federation.credentialOffer)
expect(credentialOffer).toStrictEqual(paradymDraft13Federation.credentialOfferObject)

const issuerMetadata = await client.resolveIssuerMetadata(credentialOffer.credential_issuer)
expect(issuerMetadata.credentialIssuer).toStrictEqual({
...paradymDraft13.credentialIssuerMetadata,
...paradymDraft13Federation.credentialIssuerMetadata,
client_registration_types_supported: ['automatic'],
})

const { accessTokenResponse, authorizationServer } = await client.retrievePreAuthorizedCodeAccessTokenFromOffer({
credentialOffer,
issuerMetadata,
})
expect(accessTokenResponse).toStrictEqual(paradymDraft13.accessTokenResponse)
expect(authorizationServer).toStrictEqual(paradymDraft13.credentialIssuerMetadata.credential_issuer)
expect(accessTokenResponse).toStrictEqual(paradymDraft13Federation.accessTokenResponse)
expect(authorizationServer).toStrictEqual(paradymDraft13Federation.credentialIssuerMetadata.credential_issuer)

const encodedJwk = Buffer.from(JSON.stringify(publicKeyJwk)).toString('base64url')
const didUrl = `did:jwk:${encodedJwk}#0`

// This is the holder side that will create a proof of possession of the private key
const { jwt: proofJwt } = await client.createCredentialRequestJwtProof({
issuerMetadata,
signer: {
Expand Down Expand Up @@ -850,6 +854,6 @@ describe('Oid4vciClient', () => {
jwt: proofJwt,
},
})
expect(credentialResponse.credentialResponse).toStrictEqual(paradymDraft13.credentialResponse)
expect(credentialResponse.credentialResponse).toStrictEqual(paradymDraft13Federation.credentialResponse)
})
})
150 changes: 150 additions & 0 deletions packages/oid4vci/src/__tests__/__fixtures__/paradym.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,156 @@ export const paradymDraft13 = {
},
}

export const paradymDraft13Federation = {
credentialOffer:
'https://paradym.id/invitation?credential_offer_uri=https%3A%2F%2Fparadym.id%2Finvitation%2Fdraft-13-issuer%2Foffers%2Fb99db8f1-4fa2-4b27-8dc7-ecf81478eb9b%3Fraw%3Dtrue',
credentialOfferUri:
'https://paradym.id/invitation/draft-13-issuer/offers/b99db8f1-4fa2-4b27-8dc7-ecf81478eb9b?raw=true',
credentialOfferObject: {
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': '1130293840889780123292078',
user_pin_required: false,
},
},
credential_configuration_ids: ['clvi9a5od00127pap4obzoeuf'],
credential_issuer: 'https://agent.paradym.id/oid4vci/draft-13-issuer',
},
authorizationServerMetadata: null,
credentialIssuerMetadata: {
credential_issuer: 'https://agent.paradym.id/oid4vci/draft-13-issuer',
token_endpoint: 'https://agent.paradym.id/oid4vci/draft-13-issuer/token',
credential_endpoint: 'https://agent.paradym.id/oid4vci/draft-13-issuer/credential',
credentials_supported: [
{
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/iuoQGyxlww-ParadymContributor',
id: 'clv2gbawu000tfkrk5l067h1h',
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
cryptographic_suites_supported: ['EdDSA', 'ES256'],
display: [
{
name: 'Paradym Contributor',
description: 'Contributed to the Paradym Release',
background_color: '#5535ed',
background_image: {},
text_color: '#ffffff',
},
],
},
{
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge',
id: 'clvi9a5od00127pap4obzoeuf',
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
cryptographic_suites_supported: ['EdDSA', 'ES256'],
display: [
{
name: 'Employee Badge',
description: 'Credential for employee badge',
background_color: '#000000',
background_image: { url: 'https://github.com/animo.png' },
text_color: '#ffffff',
},
],
},
{
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/ULaVABcapZ-Heyo',
id: 'clx4z0auo00a6f0sibkutdqor',
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
cryptographic_suites_supported: ['EdDSA', 'ES256'],
display: [
{
name: 'Direct issuance revocation',
background_color: '#000000',
background_image: {},
text_color: '#ffffff',
},
],
},
],
credential_configurations_supported: {
clv2gbawu000tfkrk5l067h1h: {
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
credential_signing_alg_values_supported: ['EdDSA', 'ES256'],
proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } },
display: [
{
name: 'Paradym Contributor',
description: 'Contributed to the Paradym Release',
background_color: '#5535ed',
background_image: {},
text_color: '#ffffff',
},
],
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/iuoQGyxlww-ParadymContributor',
},
clvi9a5od00127pap4obzoeuf: {
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
credential_signing_alg_values_supported: ['EdDSA', 'ES256'],
proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } },
display: [
{
name: 'Employee Badge',
description: 'Credential for employee badge',
background_color: '#000000',
background_image: { url: 'https://github.com/animo.png' },
text_color: '#ffffff',
},
],
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge',
},
clx4z0auo00a6f0sibkutdqor: {
cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'],
credential_signing_alg_values_supported: ['EdDSA', 'ES256'],
proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } },
display: [
{
name: 'Direct issuance revocation',
background_color: '#000000',
background_image: {},
text_color: '#ffffff',
},
],
format: 'vc+sd-jwt',
vct: 'https://metadata.paradym.id/types/ULaVABcapZ-Heyo',
},
},
display: [{ name: 'Animo', logo: { url: 'https://github.com/animo.png', alt_text: 'Logo of Animo Solutions' } }],
},
federationEntity: {
organization_name: 'Animo',
logo_uri: 'https://github.com/animo.png',
homepage_uri: 'https://animo.id',
contacts: ['[email protected]'],
},
accessTokenResponse: {
access_token:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjhXWGU4UFBEVTRrTnpIbjJuZ0FTUmFsUmxiZHF6NEJjYkU3X0RTUTN0aUkifX0.eyJwcmVBdXRob3JpemVkQ29kZSI6IjExMzAyOTM4NDA4ODk3ODAxMjMyOTIwNzgiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5wYXJhZHltLmlkL29pZDR2Y2kvOWI2ZGY1YmMtNTk2NS00YWVjLWEzOWEtMDNjYjNiMjc4NmI1IiwiZXhwIjoxNzI5NTY3ODk2LCJpYXQiOjE3Mjk1Njc3MTZ9.iBeLzXv7Z6kwGpgrT-5XoCyWOkl4FMDixVMqPbdSkLYq8eqU-iJWSWPsoqGnNhZ8B2H6zaEYKpxbZhdvSauSBg',
token_type: 'bearer',
expires_in: 180,
c_nonce: '463253917094869172078310',
c_nonce_expires_in: 300,
authorization_pending: false,
},
credentialResponse: {
credential:
'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdWFiek4xdzRxcnlINENMY1pRSmllaEpRd3VpM3V3RVloSjEzd01qWG5RTnQifQ.eyJkZXBhcnRtZW50IjoiU3RyaW5nIHZhbHVlIiwidmN0IjoiaHR0cHM6Ly9tZXRhZGF0YS5wYXJhZHltLmlkL3R5cGVzLzZmVEVnRlVMdjItRW1wbG95ZWVCYWRnZSIsImNuZiI6eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbmdpT2lKQlJWaHdTSGt4TUVkb2RGZG9iRlpRVG0xeVJuTmllWFJmZDBSelVWODNjVE5rYWs1dWNtaDZhbDgwSWl3aWVTSTZJa1JIVkVGRFQwRkJibEZVWlhCaFJEUXdaM2xIT1Zwc0xXOUVhRTlzZGpOVlFteFVkSGhKWlhJMVpXOGlMQ0pqY25ZaU9pSlFMVEkxTmlKOSMwIn0sImlzcyI6ImRpZDp3ZWI6bWV0YWRhdGEucGFyYWR5bS5pZDowYzIwNzI3Ny02NjU2LTQ2MzItOWQyOC03MGYzNGRkZTllYTIiLCJpYXQiOjE3Mjk1Njc3MTcsIl9zZCI6WyJHY2NVSjNGdW1pb3I0bDdqZnpXSmFvUjBTSzNaQU5penQ4ZW1HOTBVLWQ0IiwiVWhCUEp5VWdmTFFnX1JUUnJLM2xNN25ldWhZT3JlYVNBNTlVNTJQUnNZUSIsInYtTHhvdDhWczBzWi1VMGRZZ0dZMFppem9BYVc4eWt3NC1kaVhDODhIeEkiLCJ2Y1pwZW5nUkhsV2hEUUhhczVvOW1TNUM3VDRFUU92ekVWcC05MHF2OHVVIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.sceont7otHdLLwAMNQkkppakbZiX0YVWgxTejugsZUkaOFtIYVM8pNLZTs-Oi_AIRvwIZ6uuuMK8TOmp6QSfAg~WyI2NDU1NTI0ODUwOTEwNzMzMDk3MzI1NDUiLCJpc19hZG1pbiIsdHJ1ZV0~WyIxMDc0NzQzNzg3OTY1NzI0ODkxNDkwMDciLCJsYXN0X25hbWUiLCJTdHJpbmcgdmFsdWUiXQ~WyI2OTg2MDU4NTY2NDc3MjQwNTE0MTI3MTMiLCJmaXJzdF9uYW1lIiwiU3RyaW5nIHZhbHVlIl0~WyIyODIyODI0Nzg0ODQ4MTYxNjA4NDM0NTkiLCJlbXBsb3llZV9pZCIsIlN0cmluZyB2YWx1ZSJd~',
c_nonce: '1f476b83-00fc-44e6-8cfa-c52c5df12d08',
c_nonce_expires_in: 300,
},
holderPrivateKeyJwk: {
kty: 'EC',
x: 'AEXpHy10GhtWhlVPNmrFsbyt_wDsQ_7q3djNnrhzj_4',
y: 'DGTACOAAnQTepaD40gyG9Zl-oDhOlv3UBlTtxIer5eo',
crv: 'P-256',
d: 'C75pQj72AAl6SCsBW8AKTKxqLGk2Fw7NutIpWZ-xjvE',
},
}

export const paradymDraft11 = {
credentialOffer:
'https://paradym.id/invitation?credential_offer_uri=https%3A%2F%2Fparadym.id%2Finvitation%2Fdraft-11-issuer%2Foffers%2Fb99db8f1-4fa2-4b27-8dc7-ecf81478eb9b%3Fraw%3Dtrue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export const vCredentialRequestJwtProofTypeHeader = v.pipe(
({ kid, jwk }) => jwk === undefined || kid === undefined,
`Both 'jwk' and 'kid' are defined. Only one is allowed`
),
v.check(({ trust_chain, kid }) => !trust_chain || !kid, `When 'trust_chain' is provided, 'kid' is required`)
v.check(
({ trust_chain, kid }) => !trust_chain || kid !== undefined,
`When 'trust_chain' is provided, 'kid' is required`
)
)
export type CredentialRequestJwtProofTypeHeader = v.InferOutput<typeof vCredentialRequestJwtProofTypeHeader>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export async function fetchCredentialIssuerMetadata(
credentialIssuer: string,
options: FetchCredentialIssuerMetadataOptions
): Promise<{ credentialIssuerMetadata: CredentialIssuerMetadata; originalDraftVersion: Oid4vciDraftVersion } | null> {
// TODO: What should we do when it has the property trust_chain?

let result: v.InferOutput<typeof vCredentialIssuerMetadataWithDraftVersion> | null = null

const entityConfiguration = await fetchEntityConfiguration({
Expand All @@ -42,7 +40,7 @@ export async function fetchCredentialIssuerMetadata(
{
alg: jwk.alg,
method: 'jwk',
publicJwk: jwk as Jwk, // TODO: Check why this type is not correct
publicJwk: jwk as Jwk,
},
{
header: header as JwtHeader,
Expand Down

0 comments on commit 2d13142

Please sign in to comment.