Skip to content

Commit ca9a3c8

Browse files
committed
chore: merge openid federation
Signed-off-by: Henrique Dias <mail@hacdias.com>
1 parent 09f40a7 commit ca9a3c8

18 files changed

+1526
-26
lines changed

packages/openid4vc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"class-transformer": "catalog:",
4343
"rxjs": "catalog:",
4444
"zod": "catalog:",
45+
"@openid-federation/core": "0.1.1-alpha.17",
4546
"@openid4vc/openid4vci": "0.3.0-alpha-20251001121503",
4647
"@openid4vc/oauth2": "0.3.0-alpha-20251001121503",
4748
"@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
@@ -22,6 +22,7 @@ import type {
2222
import { OpenId4VpHolderService } from './OpenId4vpHolderService'
2323
import type {
2424
OpenId4VpAcceptAuthorizationRequestOptions,
25+
OpenId4VpResolveTrustChainsOptions,
2526
ResolveOpenId4VpAuthorizationRequestOptions,
2627
} from './OpenId4vpHolderServiceOptions'
2728

@@ -219,4 +220,8 @@ export class OpenId4VcHolderApi {
219220
public async sendNotification(options: OpenId4VciSendNotificationOptions) {
220221
return this.openId4VciHolderService.sendNotification(this.agentContext, options)
221222
}
223+
224+
public async resolveOpenIdFederationChains(options: OpenId4VpResolveTrustChainsOptions) {
225+
return this.openId4VpHolderService.resolveOpenIdFederationChains(this.agentContext, options)
226+
}
222227
}

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

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import {
1616
DifPresentationExchangeSubmissionLocation,
1717
Hasher,
1818
injectable,
19+
JwsService,
1920
Kms,
2021
TypedArrayEncoder,
2122
} from '@credo-ts/core'
23+
import {
24+
fetchEntityConfiguration as federationFetchEntityConfiguration,
25+
resolveTrustChains as federationResolveTrustChains,
26+
} from '@openid-federation/core'
2227
import type { Jwk } from '@openid4vc/oauth2'
2328
import {
2429
extractEncryptionJwkFromJwks,
@@ -35,7 +40,9 @@ import type { OpenId4VpVersion } from '../openid4vc-verifier'
3540
import { getOid4vcCallbacks } from '../shared/callbacks'
3641
import type {
3742
OpenId4VpAcceptAuthorizationRequestOptions,
43+
OpenId4VpFetchEntityConfigurationOptions,
3844
OpenId4VpResolvedAuthorizationRequest,
45+
OpenId4VpResolveTrustChainsOptions,
3946
ParsedTransactionDataEntry,
4047
ResolveOpenId4VpAuthorizationRequestOptions,
4148
} from './OpenId4vpHolderServiceOptions'
@@ -49,10 +56,15 @@ export class OpenId4VpHolderService {
4956

5057
private getOpenid4vpClient(
5158
agentContext: AgentContext,
52-
options?: { trustedCertificates?: EncodedX509Certificate[]; isVerifyOpenId4VpAuthorizationRequest?: boolean }
59+
options?: {
60+
trustedCertificates?: EncodedX509Certificate[]
61+
trustedFederationEntityIds?: string[]
62+
isVerifyOpenId4VpAuthorizationRequest?: boolean
63+
}
5364
) {
5465
const callbacks = getOid4vcCallbacks(agentContext, {
5566
trustedCertificates: options?.trustedCertificates,
67+
trustedFederationEntityIds: options?.trustedFederationEntityIds,
5668
isVerifyOpenId4VpAuthorizationRequest: options?.isVerifyOpenId4VpAuthorizationRequest,
5769
})
5870
return new Openid4vpClient({ callbacks })
@@ -121,6 +133,7 @@ export class OpenId4VpHolderService {
121133
): Promise<OpenId4VpResolvedAuthorizationRequest> {
122134
const openid4vpClient = this.getOpenid4vpClient(agentContext, {
123135
trustedCertificates: options?.trustedCertificates,
136+
trustedFederationEntityIds: options?.trustedFederationEntityIds,
124137
isVerifyOpenId4VpAuthorizationRequest: true,
125138
})
126139
const { params } = openid4vpClient.parseOpenid4vpAuthorizationRequest({ authorizationRequest })
@@ -138,11 +151,47 @@ export class OpenId4VpHolderService {
138151
client.prefix !== 'x509_hash' &&
139152
client.prefix !== 'decentralized_identifier' &&
140153
client.prefix !== 'origin' &&
141-
client.prefix !== 'redirect_uri'
154+
client.prefix !== 'redirect_uri' &&
155+
client.prefix !== 'openid_federation'
142156
) {
143157
throw new CredoError(`Client id prefix '${client.prefix}' is not supported`)
144158
}
145159

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

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

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 type { ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
9+
import type { 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
@@ -19,6 +19,7 @@ import {
1919
configureNonceEndpoint,
2020
configureOAuthAuthorizationServerMetadataEndpoint,
2121
} from './router'
22+
import { configureFederationEndpoint } from './router/federationEndpoint'
2223

2324
/**
2425
* @public
@@ -122,6 +123,7 @@ export class OpenId4VcIssuerModule {
122123
configureAuthorizationChallengeEndpoint(endpointRouter, this.config)
123124
configureCredentialEndpoint(endpointRouter, this.config)
124125
configureDeferredCredentialEndpoint(endpointRouter, this.config)
126+
configureFederationEndpoint(endpointRouter)
125127

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

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig'
77
import { OpenId4VpVerifierService } from './OpenId4VpVerifierService'
88
import { OpenId4VcVerifierRepository } from './repository'
99
import type { OpenId4VcVerificationRequest } from './router'
10-
import { configureAuthorizationEndpoint } from './router'
10+
import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router'
1111
import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint'
1212

1313
/**
@@ -105,6 +105,10 @@ export class OpenId4VcVerifierModule {
105105
configureAuthorizationEndpoint(endpointRouter, this.config)
106106
configureAuthorizationRequestEndpoint(endpointRouter, this.config)
107107

108+
// TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party
109+
// 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.
110+
configureFederationEndpoint(endpointRouter, this.config.federation)
111+
108112
// First one will be called for all requests (when next is called)
109113
contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {
110114
const { agentContext } = getRequestContext(req)

0 commit comments

Comments
 (0)