Skip to content

Commit 81c7616

Browse files
authored
fix: OpenID credential metadata extraction and attribute ordering (#1853)
Signed-off-by: Mostafa Gamal <moscd3@gmail.com>
1 parent 9ed3b0f commit 81c7616

10 files changed

Lines changed: 306 additions & 47 deletions

File tree

packages/core/__tests__/modules/openid/OpenIDCredentialRecordProvider.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ describe('OpenIDCredentialRecordProvider', () => {
234234
const bundle = await result.current.resolveBundleForCredential(w3cRecord)
235235

236236
expect(mockGetCredentialForDisplay).toHaveBeenCalledWith(w3cRecord)
237-
expect(mockBuildFieldsFromW3cCredsCredential).toHaveBeenCalledWith(display)
237+
expect(mockBuildFieldsFromW3cCredsCredential).toHaveBeenCalledWith(display, undefined, 'en')
238238
expect(bundleResolver.resolveAllBundles).toHaveBeenCalledWith({
239239
identifiers: {
240240
schemaId: '',
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { ClaimFormat } from '@credo-ts/core'
2+
import { Attribute } from '@bifold/oca/build/legacy'
3+
import { extractOpenId4VcCredentialMetadata } from '../../../src/modules/openid/metadata'
4+
import { buildFieldsFromW3cCredsCredential } from '../../../src/utils/oca'
5+
import { W3cCredentialDisplay } from '../../../src/modules/openid/types'
6+
7+
describe('OpenID metadata', () => {
8+
test('extracts credential display, claim labels, and claim order from credential_metadata', () => {
9+
const result = extractOpenId4VcCredentialMetadata(
10+
{
11+
format: 'jwt_vc_json',
12+
credential_metadata: {
13+
display: [
14+
{
15+
name: 'University Credential',
16+
locale: 'en-US',
17+
logo: {
18+
url: 'https://issuer.example/logo.png',
19+
alt_text: 'University logo',
20+
},
21+
background_color: '#12107c',
22+
text_color: '#FFFFFF',
23+
},
24+
],
25+
claims: [
26+
{
27+
path: ['degree'],
28+
display: [{ name: 'Degree', locale: 'en-US' }],
29+
},
30+
{
31+
path: ['given_name'],
32+
display: [{ name: 'Given Name', locale: 'en-US' }],
33+
},
34+
{
35+
path: ['gpa'],
36+
display: [{ name: 'GPA Score', locale: 'en-US' }],
37+
},
38+
],
39+
},
40+
},
41+
{
42+
id: 'https://issuer.example',
43+
display: [{ name: 'Issuer Example' }],
44+
}
45+
)
46+
47+
expect(result).toEqual({
48+
credential: {
49+
display: [
50+
{
51+
name: 'University Credential',
52+
locale: 'en-US',
53+
logo: {
54+
uri: 'https://issuer.example/logo.png',
55+
altText: 'University logo',
56+
},
57+
backgroundColor: '#12107c',
58+
textColor: '#FFFFFF',
59+
},
60+
],
61+
order: ['degree', 'given_name', 'gpa'],
62+
credential_subject: {
63+
degree: { display: [{ name: 'Degree', locale: 'en-US' }] },
64+
given_name: { display: [{ name: 'Given Name', locale: 'en-US' }] },
65+
gpa: { display: [{ name: 'GPA Score', locale: 'en-US' }] },
66+
},
67+
},
68+
issuer: {
69+
id: 'https://issuer.example',
70+
display: [{ name: 'Issuer Example' }],
71+
},
72+
})
73+
})
74+
75+
test('builds fields using OpenID claim order before remaining payload attributes', () => {
76+
const display: W3cCredentialDisplay = {
77+
id: 'sd-jwt-vc-test',
78+
createdAt: new Date('2026-05-29T00:00:00.000Z'),
79+
display: {
80+
name: 'ID Card',
81+
issuer: { name: 'Issuer Example' },
82+
},
83+
attributes: {
84+
something_nested: { key1: { key2: { key3: 'something nested' } } },
85+
source_document_type: 'id_card',
86+
status: { status_list: { idx: 1 } },
87+
age_is_over_16: true,
88+
family_name: 'Sparrow',
89+
given_name: 'Sally',
90+
},
91+
metadata: {
92+
type: 'ExampleIDCard',
93+
issuer: 'https://issuer.example',
94+
},
95+
claimFormat: ClaimFormat.SdJwtW3cVc,
96+
validUntil: undefined,
97+
validFrom: undefined,
98+
credentialSubject: {
99+
given_name: {
100+
display: [
101+
{ name: 'Given Name', locale: 'en-US' },
102+
{ name: 'Prenom', locale: 'fr-CA' },
103+
],
104+
},
105+
family_name: { display: [{ name: 'Family Name' }] },
106+
age_is_over_16: { display: [{ name: 'Age 16 or Over' }] },
107+
},
108+
attributeOrder: ['given_name', 'family_name', 'age_is_over_16'],
109+
}
110+
111+
const fields = buildFieldsFromW3cCredsCredential(display, undefined, 'fr') as Attribute[]
112+
113+
expect(fields.map((field) => field.name)).toEqual([
114+
'Prenom',
115+
'Family Name',
116+
'Age 16 or Over',
117+
'something_nested',
118+
'source_document_type',
119+
])
120+
})
121+
})

packages/core/src/modules/openid/context/OpenIDCredentialRecordProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export const OpenIDCredentialRecordProvider: React.FC<PropsWithChildren<OpenIDCr
215215
credConnectionId: undefined,
216216
credName: credentialDisplay.display.name,
217217
},
218-
attributes: buildFieldsFromW3cCredsCredential(credentialDisplay),
218+
attributes: buildFieldsFromW3cCredsCredential(credentialDisplay, undefined, i18n.language),
219219
language: i18n.language,
220220
}
221221

packages/core/src/modules/openid/display.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,14 @@ import { detectImageMimeType, formatDate, getHostNameFromUrl, isDateString, sani
1515
import { getOpenId4VcCredentialMetadata } from './metadata'
1616
import { Jwk } from '@credo-ts/core/build/modules/kms/jwk/jwk.mjs'
1717
import { OpenIDCredentialRecord } from './credentialRecord'
18-
19-
function findDisplay<Display extends { locale?: string }>(display?: Display[]): Display | undefined {
20-
if (!display) return undefined
21-
22-
let item = display.find((d) => d.locale?.startsWith('en-'))
23-
if (!item) item = display.find((d) => !d.locale)
24-
if (!item) item = display[0]
25-
26-
return item
27-
}
18+
import { findLocalizedDisplay } from '../../utils/localizedDisplay'
2819

2920
function getOpenId4VcIssuerDisplay(openId4VcMetadata?: OpenId4VcCredentialMetadata | null): CredentialIssuerDisplay {
3021
const issuerDisplay: Partial<CredentialIssuerDisplay> = {}
3122

3223
// Try to extract from openid metadata first
3324
if (openId4VcMetadata) {
34-
const openidIssuerDisplay = findDisplay(openId4VcMetadata.issuer.display)
25+
const openidIssuerDisplay = findLocalizedDisplay(openId4VcMetadata.issuer.display)
3526

3627
if (openidIssuerDisplay) {
3728
issuerDisplay.name = openidIssuerDisplay.name
@@ -45,7 +36,7 @@ function getOpenId4VcIssuerDisplay(openId4VcMetadata?: OpenId4VcCredentialMetada
4536
}
4637

4738
// If the credentialDisplay contains a logo, and the issuerDisplay does not, use the logo from the credentialDisplay
48-
const openidCredentialDisplay = findDisplay(openId4VcMetadata.credential.display)
39+
const openidCredentialDisplay = findLocalizedDisplay(openId4VcMetadata.credential.display)
4940
if (openidCredentialDisplay && !issuerDisplay.logo && openidCredentialDisplay.logo) {
5041
issuerDisplay.logo = {
5142
uri: openidCredentialDisplay.logo?.uri,
@@ -72,7 +63,7 @@ function getOpenId4VcIssuerDisplay(openId4VcMetadata?: OpenId4VcCredentialMetada
7263
function getIssuerDisplay(metadata: OpenId4VcCredentialMetadata | null | undefined): Partial<CredentialIssuerDisplay> {
7364
const issuerDisplay: Partial<CredentialIssuerDisplay> = {}
7465
// Try to extract from openid metadata first
75-
const openidIssuerDisplay = findDisplay(metadata?.issuer.display)
66+
const openidIssuerDisplay = findLocalizedDisplay(metadata?.issuer.display)
7667
issuerDisplay.name = openidIssuerDisplay?.name
7768
issuerDisplay.logo = openidIssuerDisplay?.logo
7869
? {
@@ -82,7 +73,7 @@ function getIssuerDisplay(metadata: OpenId4VcCredentialMetadata | null | undefin
8273
: undefined
8374

8475
// If the credentialDisplay contains a logo, and the issuerDisplay does not, use the logo from the credentialDisplay
85-
const openidCredentialDisplay = findDisplay(metadata?.credential.display)
76+
const openidCredentialDisplay = findLocalizedDisplay(metadata?.credential.display)
8677
if (openidCredentialDisplay && !issuerDisplay.logo && openidCredentialDisplay.logo) {
8778
issuerDisplay.logo = {
8879
uri: openidCredentialDisplay.logo?.uri,
@@ -142,7 +133,7 @@ function getCredentialDisplay(
142133
const credentialDisplay: Partial<CredentialDisplay> = {}
143134

144135
if (openId4VcMetadata) {
145-
const openidCredentialDisplay = findDisplay(openId4VcMetadata.credential.display)
136+
const openidCredentialDisplay = findLocalizedDisplay(openId4VcMetadata.credential.display)
146137
credentialDisplay.name = openidCredentialDisplay?.name
147138
credentialDisplay.description = openidCredentialDisplay?.description
148139
credentialDisplay.textColor = openidCredentialDisplay?.textColor
@@ -217,7 +208,7 @@ function getMdocCredentialDisplay(
217208
const credentialDisplay: Partial<CredentialDisplay> = {}
218209

219210
if (openId4VcMetadata) {
220-
const openidCredentialDisplay = findDisplay(openId4VcMetadata.credential.display)
211+
const openidCredentialDisplay = findLocalizedDisplay(openId4VcMetadata.credential.display)
221212

222213
if (openidCredentialDisplay) {
223214
credentialDisplay.name = openidCredentialDisplay.name
@@ -323,6 +314,10 @@ export function filterAndMapSdJwtKeys(sdJwtVcPayload: Record<string, unknown>) {
323314
}
324315
}
325316

317+
function getCredentialAttributeOrder(openId4VcMetadata?: OpenId4VcCredentialMetadata | null): string[] | undefined {
318+
return openId4VcMetadata?.credential.order
319+
}
320+
326321
export function getCredentialForDisplay(credentialRecord: OpenIDCredentialRecord): W3cCredentialDisplay {
327322
if (credentialRecord instanceof SdJwtVcRecord) {
328323
const { disclosures, jwt } = decodeSdJwtSync(credentialRecord.firstCredential.compact, (data, alg) =>
@@ -351,6 +346,7 @@ export function getCredentialForDisplay(credentialRecord: OpenIDCredentialRecord
351346
validUntil: mapped.raw.validUntil,
352347
validFrom: mapped.raw.validFrom,
353348
credentialSubject: openId4VcMetadata?.credential.credential_subject,
349+
attributeOrder: getCredentialAttributeOrder(openId4VcMetadata),
354350
}
355351
}
356352

@@ -384,6 +380,7 @@ export function getCredentialForDisplay(credentialRecord: OpenIDCredentialRecord
384380
validUntil: mdocInstance.validityInfo.validUntil,
385381
validFrom: mdocInstance.validityInfo.validFrom,
386382
credentialSubject: openId4VcMetadata?.credential.credential_subject,
383+
attributeOrder: getCredentialAttributeOrder(openId4VcMetadata),
387384
}
388385
}
389386

@@ -431,6 +428,7 @@ export function getCredentialForDisplay(credentialRecord: OpenIDCredentialRecord
431428
validUntil: credential.expiryDate ? new Date(credential.expiryDate) : undefined,
432429
validFrom: credential.issuanceDate ? new Date(credential.issuanceDate) : undefined,
433430
credentialSubject: openId4VcMetadata?.credential.credential_subject,
431+
attributeOrder: getCredentialAttributeOrder(openId4VcMetadata),
434432
}
435433
}
436434

packages/core/src/modules/openid/features/OpenIDProofPresentation/components/OpenIDProofRequestBody.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const OpenIDProofRequestBody: React.FC<OpenIDProofRequestBodyProps> = ({
2929
verifierName,
3030
}) => {
3131
const { ColorPalette, Spacing, ListItems } = useTheme()
32-
const { t } = useTranslation()
32+
const { t, i18n } = useTranslation()
3333

3434
const styles = StyleSheet.create({
3535
cardContainer: {
@@ -83,7 +83,7 @@ const OpenIDProofRequestBody: React.FC<OpenIDProofRequestBodyProps> = ({
8383
}
8484

8585
const credentialDisplay = getCredentialForDisplay(credential)
86-
const fields = buildFieldsFromW3cCredsCredential(credentialDisplay, requestedAttributes)
86+
const fields = buildFieldsFromW3cCredsCredential(credentialDisplay, requestedAttributes, i18n.language)
8787

8888
return (
8989
<View key={credentialSimplified.id}>

0 commit comments

Comments
 (0)