Skip to content

Commit

Permalink
feat: working version
Browse files Browse the repository at this point in the history
Signed-off-by: Tom Lanser <[email protected]>
  • Loading branch information
Tommylans committed Nov 5, 2024
1 parent 5695055 commit 592ab26
Show file tree
Hide file tree
Showing 17 changed files with 908 additions and 41 deletions.
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-next.168",
"@sphereon/oid4vci-issuer": "0.16.1-next.168",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"@openid-federation/core": "0.1.1-alpha.5",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
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
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',
}
}
}
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 = {
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,
}
: 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 @@ -139,6 +139,14 @@ export class OpenId4VcSiopVerifierService {
} else if (jwtIssuer.method === 'did') {
clientId = jwtIssuer.didUrl.split('#')[0]
clientIdScheme = 'did'
} else if (jwtIssuer.method === 'custom') {
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 @@ -227,6 +235,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 presentationDefinitionsWithLocation = await authorizationRequest.getPresentationDefinitions()
Expand All @@ -246,6 +256,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 @@ -452,7 +463,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',
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,44 @@ describe('OpenId4VcVerifier', () => {
expect(jwt.payload.iss).toEqual(verifier.did)
expect(jwt.payload.sub).toEqual(verifier.did)
})

it('check openid proof request format (entity id)', async () => {
const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier()
const { authorizationRequest, verificationSession } =
await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({
requestSigner: {
method: 'openid-federation',
clientId: 'http://localhost:3001/verifier',
},
verifierId: openIdVerifier.verifierId,
})

expect(
authorizationRequest.startsWith(
`openid://?client_id=${encodeURIComponent(verifier.did)}&request_uri=http%3A%2F%2Fredirect-uri%2F${
openIdVerifier.verifierId
}%2Fauthorization-requests%2F`
)
).toBe(true)

const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt)

expect(jwt.header.kid)

expect(jwt.header.kid).toEqual(verifier.kid)
expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA)
expect(jwt.header.typ).toEqual('JWT')
expect(jwt.payload.additionalClaims.scope).toEqual('openid')
expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did)
expect(jwt.payload.additionalClaims.response_uri).toEqual(
`http://redirect-uri/${openIdVerifier.verifierId}/authorize`
)
expect(jwt.payload.additionalClaims.response_mode).toEqual('direct_post')
expect(jwt.payload.additionalClaims.nonce).toBeDefined()
expect(jwt.payload.additionalClaims.state).toBeDefined()
expect(jwt.payload.additionalClaims.response_type).toEqual('id_token')
expect(jwt.payload.iss).toEqual(verifier.did)
expect(jwt.payload.sub).toEqual(verifier.did)
})
})
})
Loading

0 comments on commit 592ab26

Please sign in to comment.