Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b723485
feat: working version
Tommylans Nov 5, 2024
bcaed4d
feat: Littlebit of a cleanup for the verifier
Tommylans Nov 6, 2024
1743fb1
fix: typescript error
Tommylans Nov 7, 2024
dcd810d
feat: Processed feedback and used the right keys for the verifier
Tommylans Nov 18, 2024
cb6d70f
feat: Added more logging and added unhappy tests
Tommylans Nov 18, 2024
b06c546
chore: Made some things more logic
Tommylans Nov 18, 2024
2b8bde5
feat: Holder side api for getting more context information
Tommylans Nov 18, 2024
8bb4564
Merge branch 'main' into feature/openid-federation-verfier
Tommylans Nov 19, 2024
f6f766d
fix: Merge conflict and changes
Tommylans Nov 20, 2024
b2b3890
feat: Added fetchEntityConfiguration
Tommylans Nov 20, 2024
4515ad2
Merge branch 'main' into feature/openid-federation-verfier
TimoGlastra Nov 20, 2024
d5ea627
update lock
TimoGlastra Nov 20, 2024
11455b5
fix: OpenID Federation small fixes (#2099)
Tommylans Nov 21, 2024
367dfa2
chore: Update branch with main (#2106)
Tommylans Nov 24, 2024
ff73f53
Merge branch 'main' into feature/openid-federation-verfier
TimoGlastra Nov 24, 2024
274b421
update lockfile
TimoGlastra Nov 24, 2024
8da4250
feat: Support for subordinate entities and authority hints (#2107)
Tommylans Nov 25, 2024
ec09e8a
Merge remote-tracking branch 'upstream/main' into feature/openid-fede…
TimoGlastra Mar 24, 2025
40fd382
updates
TimoGlastra Mar 24, 2025
96dc3bb
cleanup and improvements
TimoGlastra Mar 24, 2025
41409d6
cleanup and improvements
TimoGlastra Mar 24, 2025
f0acafe
federation tests working
TimoGlastra Mar 24, 2025
b702af4
fix: set client metadata
TimoGlastra Mar 24, 2025
b46c116
Merge remote-tracking branch 'upstream/main' into feature/openid-fede…
TimoGlastra Apr 4, 2025
fd9c18e
fix: openid federation tests
TimoGlastra Apr 4, 2025
d28245b
Merge remote-tracking branch 'upstream/main' into feature/openid-fede…
TimoGlastra Jul 25, 2025
44913b1
fix: federation setup
TimoGlastra Jul 26, 2025
490a755
Merge branch 'main' into feature/openid-federation-verfier
hacdias Aug 15, 2025
8e8a518
Merge branch 'main' into feature/openid-federation-verfier
hacdias Aug 19, 2025
bc4dd2e
Merge branch 'main' into feature/openid-federation-verfier
hacdias Aug 20, 2025
03087a8
Merge branch 'main' into feature/openid-federation-verfier
hacdias Aug 21, 2025
4a37b23
Merge branch 'main' into feature/openid-federation-verfier
hacdias Aug 21, 2025
5d669fd
Merge branch 'main' into feature/openid-federation-verfier
hacdias Sep 17, 2025
3c3ecae
fix(openid4vc): agent no longer requires labels
hacdias Sep 17, 2025
c6d9d57
Merge branch 'main' into feature/openid-federation-verfier
hacdias Sep 19, 2025
616afa9
fix: federation tests format
hacdias Sep 19, 2025
83967c9
Merge branch 'main' into feature/openid-federation-verfier
hacdias Sep 25, 2025
dacf37d
fix: federation tests
hacdias Sep 25, 2025
c904e27
Merge branch 'main' into feature/openid-federation-verfier
hacdias Sep 25, 2025
864887f
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 1, 2025
16ef9ce
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 13, 2025
e65e0e5
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 13, 2025
6234637
fix: some issues with federation imports
hacdias Oct 13, 2025
2aa30c2
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 13, 2025
45c1e51
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 15, 2025
40d716c
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 15, 2025
475cb46
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 15, 2025
c1cca87
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 16, 2025
245d083
Merge branch 'main' into feature/openid-federation-verfier
hacdias Oct 16, 2025
0c2ef9f
Merge branch 'main' into feature/openid-federation-verfier
hacdias Nov 4, 2025
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
1 change: 1 addition & 0 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"class-transformer": "catalog:",
"rxjs": "catalog:",
"zod": "catalog:",
"@openid-federation/core": "0.1.1-alpha.17",
"@openid4vc/openid4vci": "0.3.0-alpha-20251031103301",
"@openid4vc/oauth2": "0.3.0-alpha-20251031103301",
"@openid4vc/openid4vp": "0.3.0-alpha-20251031103301",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
import { OpenId4VpHolderService } from './OpenId4vpHolderService'
import type {
OpenId4VpAcceptAuthorizationRequestOptions,
OpenId4VpResolveTrustChainsOptions,
ResolveOpenId4VpAuthorizationRequestOptions,
} from './OpenId4vpHolderServiceOptions'

Expand Down Expand Up @@ -219,4 +220,8 @@ export class OpenId4VcHolderApi {
public async sendNotification(options: OpenId4VciSendNotificationOptions) {
return this.openId4VciHolderService.sendNotification(this.agentContext, options)
}

public async resolveOpenIdFederationChains(options: OpenId4VpResolveTrustChainsOptions) {
return this.openId4VpHolderService.resolveOpenIdFederationChains(this.agentContext, options)
}
}
102 changes: 100 additions & 2 deletions packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ import {
DifPresentationExchangeSubmissionLocation,
Hasher,
injectable,
JwsService,
Kms,
TypedArrayEncoder,
} from '@credo-ts/core'
import {
fetchEntityConfiguration as federationFetchEntityConfiguration,
resolveTrustChains as federationResolveTrustChains,
} from '@openid-federation/core'
import type { Jwk } from '@openid4vc/oauth2'
import {
extractEncryptionJwkFromJwks,
Expand All @@ -35,7 +40,9 @@ import type { OpenId4VpVersion } from '../openid4vc-verifier'
import { getOid4vcCallbacks } from '../shared/callbacks'
import type {
OpenId4VpAcceptAuthorizationRequestOptions,
OpenId4VpFetchEntityConfigurationOptions,
OpenId4VpResolvedAuthorizationRequest,
OpenId4VpResolveTrustChainsOptions,
ParsedTransactionDataEntry,
ResolveOpenId4VpAuthorizationRequestOptions,
} from './OpenId4vpHolderServiceOptions'
Expand All @@ -49,10 +56,15 @@ export class OpenId4VpHolderService {

private getOpenid4vpClient(
agentContext: AgentContext,
options?: { trustedCertificates?: EncodedX509Certificate[]; isVerifyOpenId4VpAuthorizationRequest?: boolean }
options?: {
trustedCertificates?: EncodedX509Certificate[]
trustedFederationEntityIds?: string[]
isVerifyOpenId4VpAuthorizationRequest?: boolean
}
) {
const callbacks = getOid4vcCallbacks(agentContext, {
trustedCertificates: options?.trustedCertificates,
trustedFederationEntityIds: options?.trustedFederationEntityIds,
isVerifyOpenId4VpAuthorizationRequest: options?.isVerifyOpenId4VpAuthorizationRequest,
})
return new Openid4vpClient({ callbacks })
Expand Down Expand Up @@ -121,6 +133,7 @@ export class OpenId4VpHolderService {
): Promise<OpenId4VpResolvedAuthorizationRequest> {
const openid4vpClient = this.getOpenid4vpClient(agentContext, {
trustedCertificates: options?.trustedCertificates,
trustedFederationEntityIds: options?.trustedFederationEntityIds,
isVerifyOpenId4VpAuthorizationRequest: true,
})
const { params } = openid4vpClient.parseOpenid4vpAuthorizationRequest({ authorizationRequest })
Expand All @@ -138,11 +151,47 @@ export class OpenId4VpHolderService {
client.prefix !== 'x509_hash' &&
client.prefix !== 'decentralized_identifier' &&
client.prefix !== 'origin' &&
client.prefix !== 'redirect_uri'
client.prefix !== 'redirect_uri' &&
client.prefix !== 'openid_federation'
) {
throw new CredoError(`Client id prefix '${client.prefix}' is not supported`)
}

if (client.prefix === 'openid_federation') {
const jwsService = agentContext.dependencyManager.resolve(JwsService)

const entityConfiguration = await federationFetchEntityConfiguration({
entityId: client.identifier,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwsSigner: {
method: 'jwk',
jwk: Kms.PublicJwk.fromUnknown(jwk),
},
})

return res.isValid
},
})
if (!entityConfiguration)
throw new CredoError(`Unable to fetch entity configuration for entityId '${client.identifier}'`)

const openidRelyingPartyMetadata = entityConfiguration.metadata?.openid_relying_party
if (!openidRelyingPartyMetadata) {
throw new CredoError(`Federation entity '${client.identifier}' does not have 'openid_relying_party' metadata.`)
}

// FIXME: we probably don't want to override this, but otherwise the accept logic doesn't have
// access to the correct metadata. Should we also pass client to accept?
// @ts-expect-error
verifiedAuthorizationRequest.authorizationRequestPayload.client_metadata = openidRelyingPartyMetadata
// FIXME: we should not just override the metadata?
// When federation is used we need to use the federation metadata
// @ts-expect-error
client.clientMetadata = openidRelyingPartyMetadata
}

const returnValue = {
authorizationRequestPayload: verifiedAuthorizationRequest.authorizationRequestPayload,
origin: options?.origin,
Expand All @@ -169,6 +218,7 @@ export class OpenId4VpHolderService {
verifier: {
clientIdPrefix: client.prefix,
effectiveClientId: client.effective,
clientMetadata: client.clientMetadata,
},
transactionData: pexResult?.matchedTransactionData ?? dcqlResult?.matchedTransactionData,
presentationExchange: pexResult?.pex,
Expand Down Expand Up @@ -504,6 +554,8 @@ export class OpenId4VpHolderService {

const response = await openid4vpClient.createOpenid4vpAuthorizationResponse({
authorizationRequestPayload,
// We overwrite the client metadata on the authorization request payload when using OpenID Federation
clientMetadata: authorizationRequestPayload.client_metadata,
origin: options.origin,
authorizationResponsePayload: {
vp_token: vpToken,
Expand Down Expand Up @@ -581,4 +633,50 @@ export class OpenId4VpHolderService {
presentationDuringIssuanceSession: responseJson?.presentation_during_issuance_session as string | undefined,
} as const
}

public async resolveOpenIdFederationChains(agentContext: AgentContext, options: OpenId4VpResolveTrustChainsOptions) {
const jwsService = agentContext.dependencyManager.resolve(JwsService)

const { entityId, trustAnchorEntityIds } = options

return federationResolveTrustChains({
entityId,
trustAnchorEntityIds,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwsSigner: {
method: 'jwk',
jwk: Kms.PublicJwk.fromUnknown(jwk),
},
})

return res.isValid
},
})
}

public async fetchOpenIdFederationEntityConfiguration(
agentContext: AgentContext,
options: OpenId4VpFetchEntityConfigurationOptions
) {
const jwsService = agentContext.dependencyManager.resolve(JwsService)

const { entityId } = options

return federationFetchEntityConfiguration({
entityId,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwsSigner: {
method: 'jwk',
jwk: Kms.PublicJwk.fromUnknown(jwk),
},
})

return res.isValid
},
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type {
DifPresentationExchangeDefinition,
EncodedX509Certificate,
} from '@credo-ts/core'
import type { ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
import type { ClientMetadata, ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
import type { OpenId4VpAuthorizationRequestPayload } from '../shared'

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

export interface ResolveOpenId4VpAuthorizationRequestOptions {
trustedCertificates?: EncodedX509Certificate[]
trustedFederationEntityIds?: string[]
origin?: string
}

Expand Down Expand Up @@ -83,8 +84,8 @@ export interface OpenId4VpResolvedAuthorizationRequest {
* The client id metadata.
*
* In case of 'openid_federation' client id prefix, this will be the metadata from the federation.
* clientMetadata?: ClientMetadata
*/
clientMetadata?: ClientMetadata
}

/**
Expand Down Expand Up @@ -135,3 +136,12 @@ export interface OpenId4VpAcceptAuthorizationRequestOptions {
*/
origin?: string
}

export interface OpenId4VpResolveTrustChainsOptions {
entityId: string
trustAnchorEntityIds: [string, ...string[]]
}

export interface OpenId4VpFetchEntityConfigurationOptions {
entityId: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
configurePushedAuthorizationRequestEndpoint,
configureRedirectEndpoint,
} from './router'
import { configureFederationEndpoint } from './router/federationEndpoint'

/**
* @public
Expand Down Expand Up @@ -189,6 +190,7 @@ export class OpenId4VcIssuerModule implements Module {
configurePushedAuthorizationRequestEndpoint(issuerEndpointsRouter, this.config)
configureAuthorizationEndpoint(issuerEndpointsRouter, this.config)
configureRedirectEndpoint(issuerEndpointsRouter, this.config)
configureFederationEndpoint(issuerEndpointsRouter)

// First one will be called for all requests (when next is called)
issuerContextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => {
Expand Down
116 changes: 116 additions & 0 deletions packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type Buffer, Kms } from '@credo-ts/core'
import { createEntityConfiguration, type EntityConfigurationClaimsOptions } from '@openid-federation/core'
import type { Response, Router } from 'express'
import { getRequestContext, sendErrorResponse } from '../../shared/router'
import type { OpenId4VcIssuanceRequest } from './requestContext'

// 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.

export function configureFederationEndpoint(router: Router) {
// TODO: this whole result needs to be cached and the ttl should be the expires of this node

router.get('/.well-known/openid-federation', async (request: OpenId4VcIssuanceRequest, response: Response, next) => {
const { agentContext, issuer } = getRequestContext(request)

try {
const kms = agentContext.resolve(Kms.KeyManagementApi)

// TODO: Should be only created once per issuer and be used between instances
const federationKey = Kms.PublicJwk.fromPublicJwk(
(
await kms.createKey({
type: {
kty: 'OKP',
crv: 'Ed25519',
},
})
).publicJwk
)

const now = new Date()
const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now

// TODO: We need to generate a key and always use that for the entity configuration

const kid = federationKey.keyId
const alg = federationKey.signatureAlgorithm

const issuerDisplay = issuer.display?.[0]

const entityConfiguration = await createEntityConfiguration({
claims: {
sub: issuer.issuerId,
iss: issuer.issuerId,
iat: now,
exp: expires,
jwks: {
keys: [{ alg, ...federationKey.toJson() } as EntityConfigurationClaimsOptions['jwks']['keys'][number]],
},
metadata: {
federation_entity: issuerDisplay
? {
organization_name: issuerDisplay.name,
logo_uri: issuerDisplay.logo?.uri,
}
: undefined,
openid_provider: {
// TODO: The type isn't correct yet down the line so that needs to be updated before
// credential_issuer: issuerMetadata.issuerUrl,
// token_endpoint: issuerMetadata.tokenEndpoint,
// credential_endpoint: issuerMetadata.credentialEndpoint,
// authorization_server: issuerMetadata.authorizationServer,
// authorization_servers: issuerMetadata.authorizationServer
// ? [issuerMetadata.authorizationServer]
// : undefined,
// credentials_supported: issuerMetadata.credentialsSupported,
// credential_configurations_supported: issuerMetadata.credentialConfigurationsSupported,
// display: issuerMetadata.issuerDisplay,
// dpop_signing_alg_values_supported: issuerMetadata.dpopSigningAlgValuesSupported,

client_registration_types_supported: ['automatic'],
jwks: {
keys: [
// TODO: Not 100% sure if this is the right key that we want to expose here or a different one
issuer.resolvedAccessTokenPublicJwk.toJson() as EntityConfigurationClaimsOptions['jwks']['keys'][number],
],
},
},
},
},
header: {
kid,
alg,
typ: 'entity-statement+jwt',
},
signJwtCallback: async ({ toBeSigned }) => {
const kms = agentContext.resolve(Kms.KeyManagementApi)
const signed = await kms.sign({
data: toBeSigned as Buffer,
algorithm: federationKey.signatureAlgorithm,
keyId: federationKey.keyId,
})

return signed.signature
},
})

response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration)
} catch (error) {
agentContext.config.logger.error('Failed to create entity configuration', {
error,
})
sendErrorResponse(
response,
next,
agentContext.config.logger,
500,
'invalid_request',
'Failed to create entity configuration'
)
return
}

// NOTE: if we don't call next, the agentContext session handler will NOT be called
next()
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig'
import { OpenId4VpVerifierService } from './OpenId4VpVerifierService'
import { OpenId4VcVerifierRepository } from './repository'
import type { OpenId4VcVerificationRequest } from './router'
import { configureAuthorizationEndpoint } from './router'
import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router'
import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint'

/**
Expand Down Expand Up @@ -75,6 +75,10 @@ export class OpenId4VcVerifierModule implements Module {
configureAuthorizationEndpoint(verifierEndpointRouter, this.config)
configureAuthorizationRequestEndpoint(verifierEndpointRouter, this.config)

// TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party
// 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.
configureFederationEndpoint(verifierEndpointRouter, this.config.federation)

// First one will be called for all requests (when next is called)
verifierContextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {
const { agentContext } = getRequestContext(req)
Expand Down
Loading
Loading