Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OpenID Federation for the verifier <-> holder #27

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -33,6 +33,7 @@
"@sphereon/oid4vci-common": "0.16.1-fix.173",
"@sphereon/oid4vci-issuer": "0.16.1-fix.173",
"@sphereon/ssi-types": "0.30.2-next.135",
"@openid-federation/core": "0.1.1-alpha.6",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
12 changes: 9 additions & 3 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type {
OpenId4VciSendNotificationOptions,
OpenId4VciRequestTokenResponse,
} from './OpenId4VciHolderServiceOptions'
import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions'
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopResolveAuthorizationRequestOptions,
} from './OpenId4vcSiopHolderServiceOptions'

import { injectable, AgentContext } from '@credo-ts/core'

Expand Down Expand Up @@ -40,8 +43,11 @@ export class OpenId4VcHolderApi {
* @param requestJwtOrUri JWT or an SIOPv2 request URI
* @returns the resolved and verified authentication request.
*/
public async resolveSiopAuthorizationRequest(requestJwtOrUri: string) {
return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri)
public async resolveSiopAuthorizationRequest(
requestJwtOrUri: string,
options: OpenId4VcSiopResolveAuthorizationRequestOptions = {}
) {
return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri, options)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,4 +871,6 @@ export class OpenId4VciHolderService {
return jws
}
}

// TODO: Add a function for resolving the entity statement. Which will be used in the holder to verify the entity statement and to show to the user
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopGetOpenIdProviderOptions,
OpenId4VcSiopResolveAuthorizationRequestOptions,
OpenId4VcSiopResolvedAuthorizationRequest,
} from './OpenId4vcSiopHolderServiceOptions'
import type { OpenId4VcJwtIssuer } from '../shared'
Expand Down Expand Up @@ -38,9 +40,12 @@ export class OpenId4VcSiopHolderService {

public async resolveAuthorizationRequest(
agentContext: AgentContext,
requestJwtOrUri: string
requestJwtOrUri: string,
options: OpenId4VcSiopResolveAuthorizationRequestOptions = {}
): Promise<OpenId4VcSiopResolvedAuthorizationRequest> {
const openidProvider = await this.getOpenIdProvider(agentContext)
const openidProvider = await this.getOpenIdProvider(agentContext, {
federation: options.federation,
})

Comment on lines +52 to 55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok for now, but was also discussing with NF and i think we probably need to have some more control over 'trusted' entities in Credo.

So extend the x509 callbacks with global 'verificationContext` and you can either provide it to the call, or we use the global static config, or we use the dynamic callback. And you can either provide trusted federations, trusted x509s, or trusted dids / did methods.

(just rambling here 😄 )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we should think a bit more about this of what a good structure would be.

// parsing happens automatically in verifyAuthorizationRequest
const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri)
Expand Down Expand Up @@ -231,7 +236,7 @@ export class OpenId4VcSiopHolderService {
}
}

private async getOpenIdProvider(agentContext: AgentContext) {
private async getOpenIdProvider(agentContext: AgentContext, options: OpenId4VcSiopGetOpenIdProviderOptions = {}) {
const builder = OP.builder()
.withExpiresIn(6000)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
Expand All @@ -242,7 +247,11 @@ export class OpenId4VcSiopHolderService {
SupportedVersion.SIOPv2_D12_OID4VP_D20,
])
.withCreateJwtCallback(getCreateJwtCallback(agentContext))
.withVerifyJwtCallback(getVerifyJwtCallback(agentContext))
.withVerifyJwtCallback(
getVerifyJwtCallback(agentContext, {
federation: options.federation,
})
)
.withHasher(Hasher.hash)

const openidProvider = builder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export interface OpenId4VcSiopAcceptAuthorizationRequestOptions {
* The verified authorization request.
*/
authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest

// TODO: Not sure if this also needs the federation because the validation of the authorization is already done with the ResolveAuthorizationRequest
}

export interface OpenId4VcSiopResolveAuthorizationRequestOptions {
federation?: {
/**
* The entity IDs of the trusted issuers.
*/
trustedEntityIds?: string[]
}
}

// FIXME: rethink properties
Expand All @@ -56,3 +67,12 @@ export interface OpenId4VcSiopAuthorizationResponseSubmission {
status: number
submittedResponse: OpenId4VcSiopAuthorizationResponsePayload
}

export interface OpenId4VcSiopGetOpenIdProviderOptions {
federation?: {
/**
* The entity IDs of the trusted issuers.
*/
trustedEntityIds?: string[]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
configureCredentialEndpoint,
configureIssuerMetadataEndpoint,
} from './router'
import { configureFederationEndpoint } from './router/federationEndpoint'

/**
* @public
Expand Down Expand Up @@ -120,6 +121,11 @@ export class OpenId4VcIssuerModule implements Module {
configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint)
configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint)

// The federation endpoint is optional
if (this.config.federationEndpoint) {
configureFederationEndpoint(endpointRouter, this.config.federationEndpoint)
}

// First one will be called for all requests (when next is called)
contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => {
const { agentContext } = getRequestContext(req)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
OpenId4VciCredentialEndpointConfig,
OpenId4VciCredentialOfferEndpointConfig,
} from './router'
import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint'
import type { Optional } from '@credo-ts/core'
import type { Router } from 'express'

Expand Down Expand Up @@ -35,6 +36,7 @@ export interface OpenId4VcIssuerModuleConfigOptions {
OpenId4VciAccessTokenEndpointConfig,
'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds'
>
federation?: Optional<OpenId4VcSiopFederationEndpointConfig, 'endpointPath'>
}
}

Expand Down Expand Up @@ -94,4 +96,14 @@ export class OpenId4VcIssuerModuleConfig {
endpointPath: userOptions.endpointPath ?? '/offers',
}
}

public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined {
const userOptions = this.options.endpoints.federation
if (!userOptions) return undefined

return {
...userOptions,
endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation',
Tommylans marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
102 changes: 102 additions & 0 deletions packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { OpenId4VcIssuanceRequest } from './requestContext'
import type { FederationKeyCallback } from '../../shared/federation'
import type { Buffer } from '@credo-ts/core'
import type { Router, Response } from 'express'

import { getJwkFromKey } from '@credo-ts/core'
import { createEntityConfiguration } from '@openid-federation/core'

import { getRequestContext, sendErrorResponse } from '../../shared/router'
import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService'

export interface OpenId4VcSiopFederationEndpointConfig {
/**
* The path at which the credential endpoint should be made available. Note that it will be
* hosted at a subpath to take into account multiple tenants and issuers.
*
* @default /.well-known/openid-federation
*/
endpointPath: string

// TODO: Not sure about the property name yet.
//TODO: More information is needed than only the key also the client id etc
keyCallback: FederationKeyCallback<{
issuerId: string
}>
}

// 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, config: OpenId4VcSiopFederationEndpointConfig) {
router.get(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => {
const { agentContext, issuer } = getRequestContext(request)
const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService)

try {
const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer)
// TODO: Use a type here from sphreon
const transformedMetadata = {
Tommylans marked this conversation as resolved.
Show resolved Hide resolved
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,
} as const

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

const { key } = await config.keyCallback(agentContext, {
issuerId: issuer.issuerId,
})

const jwk = getJwkFromKey(key)
const kid = 'key-1'
const alg = jwk.supportedSignatureAlgorithms[0]

const issuerDisplay = issuerMetadata.issuerDisplay?.[0]

const entityConfiguration = await createEntityConfiguration({
claims: {
sub: issuerMetadata.issuerUrl,
iss: issuerMetadata.issuerUrl,
iat: now,
exp: expires,
jwks: {
keys: [{ kid, alg, ...jwk.toJson() }],
},
metadata: {
federation_entity: issuerDisplay
? {
organization_name: issuerDisplay.organization_name,
logo_uri: issuerDisplay.logo_uri,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

organization_name and logo_uri are different in te issuer display. I think name and logo.uri. I think we can just reuse them instead of requiring duplicate parameters?

}
: undefined,
openid_credential_issuer: transformedMetadata,
},
},
header: {
kid,
alg,
typ: 'entity-statement+jwt',
},
signJwtCallback: ({ toBeSigned }) =>
agentContext.wallet.sign({
data: toBeSigned as Buffer,
key,
}),
})

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

// 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 @@ -142,6 +142,15 @@ export class OpenId4VcSiopVerifierService {
} else if (jwtIssuer.method === 'did') {
clientId = jwtIssuer.didUrl.split('#')[0]
clientIdScheme = 'did'
} else if (jwtIssuer.method === 'custom') {
// TODO: Currently used as openid federation, but the jwtIssuer should also be openid-federation
if (!jwtIssuer.options) throw new CredoError(`Custom jwtIssuer must have options defined.`)
if (!jwtIssuer.options.clientId) throw new CredoError(`Custom jwtIssuer must have clientId defined.`)
if (typeof jwtIssuer.options.clientId !== 'string')
throw new CredoError(`Custom jwtIssuer's clientId must be a string.`)

clientIdScheme = 'entity_id'
clientId = jwtIssuer.options.clientId
} else {
throw new CredoError(
`Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did' and 'x5c' are supported.`
Expand Down Expand Up @@ -231,6 +240,8 @@ export class OpenId4VcSiopVerifierService {
)

const requestClientId = await authorizationRequest.getMergedProperty<string>('client_id')
// TODO: Is this needed for the verification of the federation?
const requestClientIdScheme = await authorizationRequest.getMergedProperty<ClientIdScheme>('client_id_scheme')
const requestNonce = await authorizationRequest.getMergedProperty<string>('nonce')
const requestState = await authorizationRequest.getMergedProperty<string>('state')
const responseUri = await authorizationRequest.getMergedProperty<string>('response_uri')
Expand All @@ -251,6 +262,7 @@ export class OpenId4VcSiopVerifierService {
presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition,
authorizationResponseUrl,
clientId: requestClientId,
clientIdScheme: requestClientIdScheme,
})

// This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library
Expand Down Expand Up @@ -463,7 +475,7 @@ export class OpenId4VcSiopVerifierService {
return this.openId4VcVerificationSessionRepository.getById(agentContext, verificationSessionId)
}

private async getRelyingParty(
public async getRelyingParty(
agentContext: AgentContext,
verifierId: string,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi'
import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig'
import { OpenId4VcVerifierRepository } from './repository'
import { OpenId4VcRelyingPartyEventHandler } from './repository/OpenId4VcRelyingPartyEventEmitter'
import { configureAuthorizationEndpoint } from './router'
import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router'
import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint'

/**
Expand Down Expand Up @@ -115,6 +115,9 @@ export class OpenId4VcVerifierModule implements Module {
// Configure endpoints
configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint)
configureAuthorizationRequestEndpoint(endpointRouter, this.config.authorizationRequestEndpoint)
if (this.config.federationEndpoint) {
configureFederationEndpoint(endpointRouter, this.config.federationEndpoint)
}

// First one will be called for all requests (when next is called)
contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint'
import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint'
import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint'
import type { Optional } from '@credo-ts/core'
import type { Router } from 'express'

Expand All @@ -24,6 +25,7 @@ export interface OpenId4VcVerifierModuleConfigOptions {
endpoints?: {
authorization?: Optional<OpenId4VcSiopAuthorizationEndpointConfig, 'endpointPath'>
authorizationRequest?: Optional<OpenId4VcSiopAuthorizationRequestEndpointConfig, 'endpointPath'>
federation?: Optional<OpenId4VcSiopFederationEndpointConfig, 'endpointPath'>
}
}

Expand Down Expand Up @@ -60,4 +62,15 @@ export class OpenId4VcVerifierModuleConfig {
endpointPath: userOptions?.endpointPath ?? '/authorize',
}
}

public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined {
// Use user supplied options, or return defaults.
const userOptions = this.options.endpoints?.federation
if (!userOptions) return undefined

return {
...userOptions,
endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always well-known, not configurable

}
}
}
Loading
Loading