Skip to content

Commit 6511f32

Browse files
committed
chore: merge openid federation
1 parent 5bf09dc commit 6511f32

18 files changed

+1539
-44
lines changed

packages/openid4vc/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"main": "src/index",
44
"types": "src/index",
55
"version": "0.5.13",
6-
"files": ["build"],
6+
"files": [
7+
"build"
8+
],
79
"license": "Apache-2.0",
810
"publishConfig": {
911
"main": "build/index",
@@ -28,6 +30,7 @@
2830
"class-transformer": "catalog:",
2931
"rxjs": "catalog:",
3032
"zod": "catalog:",
33+
"@openid-federation/core": "0.1.1-alpha.17",
3134
"@openid4vc/openid4vci": "0.3.0-alpha-20251001121503",
3235
"@openid4vc/oauth2": "0.3.0-alpha-20251001121503",
3336
"@openid4vc/openid4vp": "0.3.0-alpha-20251001121503",

packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from './OpenId4VciHolderServiceOptions'
1212
import type {
1313
OpenId4VpAcceptAuthorizationRequestOptions,
14+
OpenId4VpResolveTrustChainsOptions,
1415
ResolveOpenId4VpAuthorizationRequestOptions,
1516
} from './OpenId4vpHolderServiceOptions'
1617

@@ -222,4 +223,8 @@ export class OpenId4VcHolderApi {
222223
public async sendNotification(options: OpenId4VciSendNotificationOptions) {
223224
return this.openId4VciHolderService.sendNotification(this.agentContext, options)
224225
}
226+
227+
public async resolveOpenIdFederationChains(options: OpenId4VpResolveTrustChainsOptions) {
228+
return this.openId4VpHolderService.resolveOpenIdFederationChains(this.agentContext, options)
229+
}
225230
}

packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
} from '@credo-ts/core'
1111
import type {
1212
OpenId4VpAcceptAuthorizationRequestOptions,
13+
OpenId4VpFetchEntityConfigurationOptions,
14+
OpenId4VpResolveTrustChainsOptions,
1315
OpenId4VpResolvedAuthorizationRequest,
1416
ParsedTransactionDataEntry,
1517
ResolveOpenId4VpAuthorizationRequestOptions,
@@ -22,10 +24,15 @@ import {
2224
DifPresentationExchangeService,
2325
DifPresentationExchangeSubmissionLocation,
2426
Hasher,
27+
JwsService,
2528
Kms,
2629
TypedArrayEncoder,
2730
injectable,
2831
} from '@credo-ts/core'
32+
import {
33+
fetchEntityConfiguration as federationFetchEntityConfiguration,
34+
resolveTrustChains as federationResolveTrustChains,
35+
} from '@openid-federation/core'
2936
import {
3037
Openid4vpAuthorizationResponse,
3138
Openid4vpClient,
@@ -51,10 +58,15 @@ export class OpenId4VpHolderService {
5158

5259
private getOpenid4vpClient(
5360
agentContext: AgentContext,
54-
options?: { trustedCertificates?: EncodedX509Certificate[]; isVerifyOpenId4VpAuthorizationRequest?: boolean }
61+
options?: {
62+
trustedCertificates?: EncodedX509Certificate[]
63+
trustedFederationEntityIds?: string[]
64+
isVerifyOpenId4VpAuthorizationRequest?: boolean
65+
}
5566
) {
5667
const callbacks = getOid4vcCallbacks(agentContext, {
5768
trustedCertificates: options?.trustedCertificates,
69+
trustedFederationEntityIds: options?.trustedFederationEntityIds,
5870
isVerifyOpenId4VpAuthorizationRequest: options?.isVerifyOpenId4VpAuthorizationRequest,
5971
})
6072
return new Openid4vpClient({ callbacks })
@@ -123,6 +135,7 @@ export class OpenId4VpHolderService {
123135
): Promise<OpenId4VpResolvedAuthorizationRequest> {
124136
const openid4vpClient = this.getOpenid4vpClient(agentContext, {
125137
trustedCertificates: options?.trustedCertificates,
138+
trustedFederationEntityIds: options?.trustedFederationEntityIds,
126139
isVerifyOpenId4VpAuthorizationRequest: true,
127140
})
128141
const { params } = openid4vpClient.parseOpenid4vpAuthorizationRequest({ authorizationRequest })
@@ -140,11 +153,47 @@ export class OpenId4VpHolderService {
140153
client.prefix !== 'x509_hash' &&
141154
client.prefix !== 'decentralized_identifier' &&
142155
client.prefix !== 'origin' &&
143-
client.prefix !== 'redirect_uri'
156+
client.prefix !== 'redirect_uri' &&
157+
client.prefix !== 'openid_federation'
144158
) {
145159
throw new CredoError(`Client id prefix '${client.prefix}' is not supported`)
146160
}
147161

162+
if (client.prefix === 'openid_federation') {
163+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
164+
165+
const entityConfiguration = await federationFetchEntityConfiguration({
166+
entityId: client.identifier,
167+
verifyJwtCallback: async ({ jwt, jwk }) => {
168+
const res = await jwsService.verifyJws(agentContext, {
169+
jws: jwt,
170+
jwsSigner: {
171+
method: 'jwk',
172+
jwk: Kms.PublicJwk.fromUnknown(jwk),
173+
},
174+
})
175+
176+
return res.isValid
177+
},
178+
})
179+
if (!entityConfiguration)
180+
throw new CredoError(`Unable to fetch entity configuration for entityId '${client.identifier}'`)
181+
182+
const openidRelyingPartyMetadata = entityConfiguration.metadata?.openid_relying_party
183+
if (!openidRelyingPartyMetadata) {
184+
throw new CredoError(`Federation entity '${client.identifier}' does not have 'openid_relying_party' metadata.`)
185+
}
186+
187+
// FIXME: we probably don't want to override this, but otherwise the accept logic doesn't have
188+
// access to the correct metadata. Should we also pass client to accept?
189+
// @ts-ignore
190+
verifiedAuthorizationRequest.authorizationRequestPayload.client_metadata = openidRelyingPartyMetadata
191+
// FIXME: we should not just override the metadata?
192+
// When federation is used we need to use the federation metadata
193+
// @ts-ignore
194+
client.clientMetadata = openidRelyingPartyMetadata
195+
}
196+
148197
const returnValue = {
149198
authorizationRequestPayload: verifiedAuthorizationRequest.authorizationRequestPayload,
150199
origin: options?.origin,
@@ -171,6 +220,7 @@ export class OpenId4VpHolderService {
171220
verifier: {
172221
clientIdPrefix: client.prefix,
173222
effectiveClientId: client.effective,
223+
clientMetadata: client.clientMetadata,
174224
},
175225
transactionData: pexResult?.matchedTransactionData ?? dcqlResult?.matchedTransactionData,
176226
presentationExchange: pexResult?.pex,
@@ -506,6 +556,8 @@ export class OpenId4VpHolderService {
506556

507557
const response = await openid4vpClient.createOpenid4vpAuthorizationResponse({
508558
authorizationRequestPayload,
559+
// We overwrite the client metadata on the authorization request payload when using OpenID Federation
560+
clientMetadata: authorizationRequestPayload.client_metadata,
509561
origin: options.origin,
510562
authorizationResponsePayload: {
511563
vp_token: vpToken,
@@ -583,4 +635,50 @@ export class OpenId4VpHolderService {
583635
presentationDuringIssuanceSession: responseJson?.presentation_during_issuance_session as string | undefined,
584636
} as const
585637
}
638+
639+
public async resolveOpenIdFederationChains(agentContext: AgentContext, options: OpenId4VpResolveTrustChainsOptions) {
640+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
641+
642+
const { entityId, trustAnchorEntityIds } = options
643+
644+
return federationResolveTrustChains({
645+
entityId,
646+
trustAnchorEntityIds,
647+
verifyJwtCallback: async ({ jwt, jwk }) => {
648+
const res = await jwsService.verifyJws(agentContext, {
649+
jws: jwt,
650+
jwsSigner: {
651+
method: 'jwk',
652+
jwk: Kms.PublicJwk.fromUnknown(jwk),
653+
},
654+
})
655+
656+
return res.isValid
657+
},
658+
})
659+
}
660+
661+
public async fetchOpenIdFederationEntityConfiguration(
662+
agentContext: AgentContext,
663+
options: OpenId4VpFetchEntityConfigurationOptions
664+
) {
665+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
666+
667+
const { entityId } = options
668+
669+
return federationFetchEntityConfiguration({
670+
entityId,
671+
verifyJwtCallback: async ({ jwt, jwk }) => {
672+
const res = await jwsService.verifyJws(agentContext, {
673+
jws: jwt,
674+
jwsSigner: {
675+
method: 'jwk',
676+
jwk: Kms.PublicJwk.fromUnknown(jwk),
677+
},
678+
})
679+
680+
return res.isValid
681+
},
682+
})
683+
}
586684
}

packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderServiceOptions.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type {
66
DifPresentationExchangeDefinition,
77
EncodedX509Certificate,
88
} from '@credo-ts/core'
9-
import { ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
9+
import { ClientMetadata, ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
1010
import type { OpenId4VpAuthorizationRequestPayload } from '../shared'
1111

1212
// TODO: export from oid4vp
1313
export type ParsedTransactionDataEntry = NonNullable<ResolvedOpenid4vpAuthorizationRequest['transactionData']>[number]
1414

1515
export interface ResolveOpenId4VpAuthorizationRequestOptions {
1616
trustedCertificates?: EncodedX509Certificate[]
17+
trustedFederationEntityIds?: string[]
1718
origin?: string
1819
}
1920

@@ -83,8 +84,8 @@ export interface OpenId4VpResolvedAuthorizationRequest {
8384
* The client id metadata.
8485
*
8586
* In case of 'openid_federation' client id prefix, this will be the metadata from the federation.
86-
* clientMetadata?: ClientMetadata
8787
*/
88+
clientMetadata?: ClientMetadata
8889
}
8990

9091
/**
@@ -135,3 +136,12 @@ export interface OpenId4VpAcceptAuthorizationRequestOptions {
135136
*/
136137
origin?: string
137138
}
139+
140+
export interface OpenId4VpResolveTrustChainsOptions {
141+
entityId: string
142+
trustAnchorEntityIds: [string, ...string[]]
143+
}
144+
145+
export interface OpenId4VpFetchEntityConfigurationOptions {
146+
entityId: string
147+
}

packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
configureNonceEndpoint,
2222
configureOAuthAuthorizationServerMetadataEndpoint,
2323
} from './router'
24+
import { configureFederationEndpoint } from './router/federationEndpoint'
2425

2526
/**
2627
* @public
@@ -124,6 +125,7 @@ export class OpenId4VcIssuerModule {
124125
configureAuthorizationChallengeEndpoint(endpointRouter, this.config)
125126
configureCredentialEndpoint(endpointRouter, this.config)
126127
configureDeferredCredentialEndpoint(endpointRouter, this.config)
128+
configureFederationEndpoint(endpointRouter)
127129

128130
// First one will be called for all requests (when next is called)
129131
contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { type Buffer, Kms } from '@credo-ts/core'
2+
import type { Response, Router } from 'express'
3+
import type { OpenId4VcIssuanceRequest } from './requestContext'
4+
5+
import { EntityConfigurationClaimsOptions, createEntityConfiguration } from '@openid-federation/core'
6+
7+
import { getRequestContext, sendErrorResponse } from '../../shared/router'
8+
9+
// TODO: It's also possible that the issuer and the verifier can have the same openid-federation endpoint. In that case we need to combine them.
10+
11+
export function configureFederationEndpoint(router: Router) {
12+
// TODO: this whole result needs to be cached and the ttl should be the expires of this node
13+
14+
router.get('/.well-known/openid-federation', async (request: OpenId4VcIssuanceRequest, response: Response, next) => {
15+
const { agentContext, issuer } = getRequestContext(request)
16+
17+
try {
18+
const kms = agentContext.resolve(Kms.KeyManagementApi)
19+
20+
// TODO: Should be only created once per issuer and be used between instances
21+
const federationKey = Kms.PublicJwk.fromPublicJwk(
22+
(
23+
await kms.createKey({
24+
type: {
25+
kty: 'OKP',
26+
crv: 'Ed25519',
27+
},
28+
})
29+
).publicJwk
30+
)
31+
32+
const now = new Date()
33+
const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now
34+
35+
// TODO: We need to generate a key and always use that for the entity configuration
36+
37+
const kid = federationKey.keyId
38+
const alg = federationKey.signatureAlgorithm
39+
40+
const issuerDisplay = issuer.display?.[0]
41+
42+
const entityConfiguration = await createEntityConfiguration({
43+
claims: {
44+
sub: issuer.issuerId,
45+
iss: issuer.issuerId,
46+
iat: now,
47+
exp: expires,
48+
jwks: {
49+
keys: [{ alg, ...federationKey.toJson() } as EntityConfigurationClaimsOptions['jwks']['keys'][number]],
50+
},
51+
metadata: {
52+
federation_entity: issuerDisplay
53+
? {
54+
organization_name: issuerDisplay.name,
55+
logo_uri: issuerDisplay.logo?.uri,
56+
}
57+
: undefined,
58+
openid_provider: {
59+
// TODO: The type isn't correct yet down the line so that needs to be updated before
60+
// credential_issuer: issuerMetadata.issuerUrl,
61+
// token_endpoint: issuerMetadata.tokenEndpoint,
62+
// credential_endpoint: issuerMetadata.credentialEndpoint,
63+
// authorization_server: issuerMetadata.authorizationServer,
64+
// authorization_servers: issuerMetadata.authorizationServer
65+
// ? [issuerMetadata.authorizationServer]
66+
// : undefined,
67+
// credentials_supported: issuerMetadata.credentialsSupported,
68+
// credential_configurations_supported: issuerMetadata.credentialConfigurationsSupported,
69+
// display: issuerMetadata.issuerDisplay,
70+
// dpop_signing_alg_values_supported: issuerMetadata.dpopSigningAlgValuesSupported,
71+
72+
client_registration_types_supported: ['automatic'],
73+
jwks: {
74+
keys: [
75+
// TODO: Not 100% sure if this is the right key that we want to expose here or a different one
76+
issuer.resolvedAccessTokenPublicJwk.toJson() as EntityConfigurationClaimsOptions['jwks']['keys'][number],
77+
],
78+
},
79+
},
80+
},
81+
},
82+
header: {
83+
kid,
84+
alg,
85+
typ: 'entity-statement+jwt',
86+
},
87+
signJwtCallback: async ({ toBeSigned }) => {
88+
const kms = agentContext.resolve(Kms.KeyManagementApi)
89+
const signed = await kms.sign({
90+
data: toBeSigned as Buffer,
91+
algorithm: federationKey.signatureAlgorithm,
92+
keyId: federationKey.keyId,
93+
})
94+
95+
return signed.signature
96+
},
97+
})
98+
99+
response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration)
100+
} catch (error) {
101+
agentContext.config.logger.error('Failed to create entity configuration', {
102+
error,
103+
})
104+
sendErrorResponse(
105+
response,
106+
next,
107+
agentContext.config.logger,
108+
500,
109+
'invalid_request',
110+
'Failed to create entity configuration'
111+
)
112+
return
113+
}
114+
115+
// NOTE: if we don't call next, the agentContext session handler will NOT be called
116+
next()
117+
})
118+
}

packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi'
99
import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig'
1010
import { OpenId4VpVerifierService } from './OpenId4VpVerifierService'
1111
import { OpenId4VcVerifierRepository } from './repository'
12-
import { configureAuthorizationEndpoint } from './router'
12+
import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router'
1313
import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint'
1414

1515
/**
@@ -107,6 +107,10 @@ export class OpenId4VcVerifierModule {
107107
configureAuthorizationEndpoint(endpointRouter, this.config)
108108
configureAuthorizationRequestEndpoint(endpointRouter, this.config)
109109

110+
// TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party
111+
// TODO: But the keys also needs to be available for the request signing. They also needs to get saved because it needs to survive a restart of the agent.
112+
configureFederationEndpoint(endpointRouter, this.config.federation)
113+
110114
// First one will be called for all requests (when next is called)
111115
contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {
112116
const { agentContext } = getRequestContext(req)

0 commit comments

Comments
 (0)