Skip to content

Commit

Permalink
feat: Allow EBSI attestation client to be the start of a regular VCI …
Browse files Browse the repository at this point in the history
…flow
  • Loading branch information
nklomp committed Jun 21, 2024
1 parent 222c4d4 commit afffd39
Show file tree
Hide file tree
Showing 29 changed files with 433 additions and 502 deletions.
4 changes: 2 additions & 2 deletions packages/contact-manager-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
},
"dependencies": {
"@sphereon/ssi-express-support": "workspace:*",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.5",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.5",
"@sphereon/ssi-sdk.contact-manager": "workspace:*",
"@sphereon/ssi-sdk.core": "workspace:*",
"@sphereon/ssi-sdk.data-store": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createObjects, getConfig } from '@sphereon/ssi-sdk.agent-config'
import { IOID4VCIHolder } from '@sphereon/ssi-sdk.oid4vci-holder'
import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange'
import { IDidAuthSiopOpAuthenticator } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import {IDIDManager, IIdentifier, IKeyManager, IResolver, MinimalImportableKey, TAgent} from '@veramo/core'
import { IDIDManager, IIdentifier, IKeyManager, IResolver, MinimalImportableKey, TAgent } from '@veramo/core'
// @ts-ignore
import cors from 'cors'

Expand Down Expand Up @@ -80,7 +80,9 @@ const tearDown = async (): Promise<boolean> => {
server.closeAllConnections()
}

;(await dbConnection).close()
if (dbConnection) {
;(await dbConnection)?.close()
}
return true
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import { Alg, AuthorizationDetails, CredentialResponse, Jwt } from '@sphereon/oid4vci-common'
import { toJwk, JWK as SphereonJWK } from '@sphereon/ssi-sdk-ext.key-utils'
import { toJwk } from '@sphereon/ssi-sdk-ext.key-utils'
import { IDidAuthSiopOpAuthenticator } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import { IDIDManager, IIdentifier, IKeyManager, MinimalImportableKey, TAgent } from '@veramo/core'
import { fetch } from 'cross-fetch'
//@ts-ignore
import express, { Application, NextFunction, Request, Response } from 'express'
import { importJWK, SignJWT, JWK } from 'jose'
import * as http from 'node:http'
import {Server, createServer} from "http";
import { importJWK, SignJWT } from 'jose'
import { EbsiEnvironment, IEBSIAuthorizationClient, ScopeByDefinition } from '../../src'

type ConfiguredAgent = TAgent<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IEBSIAuthorizationClient>
Expand Down Expand Up @@ -60,11 +60,11 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
provider: 'did:ebsi',
}

const jwk: SphereonJWK = toJwk(secp256r1.privateKeyHex, 'Secp256r1', { isPrivateKey: true })
const jwk = toJwk(secp256r1.privateKeyHex, 'Secp256r1', { isPrivateKey: true })
const kid = `${identifier.did}#keys-1`

async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
const importedJwk = await importJWK(jwk as JWK)
const importedJwk = await importJWK(jwk)
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ ...args.header, kid: kid! })
.setIssuer(identifier.did)
Expand All @@ -78,7 +78,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
describe('EBSI Authorization Client Agent Plugin', (): void => {
let agent: ConfiguredAgent
let credentialResponse: CredentialResponse
let server: http.Server<any, any>
let server: Server<any, any>

beforeAll(async (): Promise<void> => {
await testContext.setup()
Expand Down Expand Up @@ -173,7 +173,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
}
res.json({ message: 'Mock called!' })
})
server = http.createServer(app)
server = createServer(app)
server.listen(port)

const REDIRECT_MOCK_URL = `http://localhost:${port}/mock`
Expand Down
12 changes: 6 additions & 6 deletions packages/ebsi-authorization-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
"@sphereon/ssi-sdk.presentation-exchange": "workspace:*",
"@sphereon/ssi-sdk.oid4vci-holder": "workspace:*",
"@sphereon/ssi-sdk.siopv2-oid4vp-op-auth": "workspace:*",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.1-next.5",
"@sphereon/ssi-sdk.core": "workspace:*",
"@sphereon/ssi-types": "workspace:*",
"@sphereon/ssi-sdk-ext.did-provider-ebsi": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.did-provider-ebsi": "0.21.1-next.5",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.5",
"@veramo/core": "4.2.0",
"@veramo/utils": "4.2.0",
"cross-fetch": "^3.1.8",
Expand All @@ -36,10 +36,10 @@
"devDependencies": {
"@sphereon/oid4vci-client": "0.12.0",
"@sphereon/oid4vci-common": "0.12.0",
"tunnelmole": "^2.2.14",
"@sphereon/ssi-sdk.agent-config": "workspace:*",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.5",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.1-next.5",
"@sphereon/ssi-sdk.data-store": "workspace:*",
"@transmute/json-web-signature": "0.7.0-unstable.81",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.19.1",
Expand Down
238 changes: 132 additions & 106 deletions packages/ebsi-authorization-client/src/functions/Attestation.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1,155 @@
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import {OpenID4VCIClient} from '@sphereon/oid4vci-client'
import {
AuthorizationDetails,
CredentialConfigurationSupported,
EndpointMetadataResult,
getJson,
getTypesFromCredentialSupported,
OID4VCICredentialFormat,
ProofOfPossessionCallbacks,
RequestObjectOpts,
AuthorizationDetails,
AuthzFlowType,
CredentialConfigurationSupported,
getJson,
getTypesFromCredentialSupported,
OID4VCICredentialFormat,
ProofOfPossessionCallbacks,
RequestObjectOpts,
} from '@sphereon/oid4vci-common'
import { getIdentifier, IIdentifierOpts } from '@sphereon/ssi-sdk-ext.did-utils'
import { getAuthenticationKey, signCallback } from '@sphereon/ssi-sdk.oid4vci-holder'
import { IIdentifier } from '@veramo/core'
import { _ExtendedIKey } from '@veramo/utils'
import { calculateJwkThumbprintForKey } from '@sphereon/ssi-sdk-ext.key-utils'
import { IRequiredContext } from '../types/IEBSIAuthorizationClient'
import {getIdentifier, IIdentifierOpts} from '@sphereon/ssi-sdk-ext.did-utils'
import {calculateJwkThumbprintForKey} from '@sphereon/ssi-sdk-ext.key-utils'
import {getAuthenticationKey, signCallback, StartOid4vciFlowData} from '@sphereon/ssi-sdk.oid4vci-holder'
import {IIdentifier} from '@veramo/core'
import {_ExtendedIKey} from '@veramo/utils'
import {IRequiredContext} from '../types/IEBSIAuthorizationClient'

export interface AttestationAuthRequestUrlResult {
authUrl: string
supportedConfigurations: Array<CredentialConfigurationSupported>
metadata: EndpointMetadataResult
identifier: IIdentifier
authKey: _ExtendedIKey
oid4vpState: string
export interface AttestationAuthRequestUrlResult extends StartOid4vciFlowData {
authorizationCodeURL: string
identifier: IIdentifier
authKey: _ExtendedIKey
}

/**
* Method to generate an authz url for getting attestation credentials from a (R)TAO on EBSI using a cloud/service wallet
*
* This method can be used standalone. But it can also be used instead of the `oid4vciHolderPrepareStart` agent method,
* to start a OID4VCI holder flow.
* In fact this function calls the prepareStart agent method to ensure the result is compatible
*
* @param opts
* @param context
*/
export const ebsiCreateAttestationRequestAuthURL = async (
opts: {
credentialIssuer: string
credentialType: string
idOpts: IIdentifierOpts
requestObjectOpts: RequestObjectOpts
clientId?: string
redirectUri?: string
formats?: Array<Extract<OID4VCICredentialFormat, 'jwt_vc' | 'jwt_vc_json'>>
},
context: IRequiredContext,
opts: {
credentialIssuer: string
credentialType: string
idOpts: IIdentifierOpts
requestObjectOpts: RequestObjectOpts
clientId?: string
redirectUri?: string
formats?: Array<Extract<OID4VCICredentialFormat, 'jwt_vc' | 'jwt_vc_json'>>
},
context: IRequiredContext,
): Promise<AttestationAuthRequestUrlResult> => {
const { credentialIssuer, credentialType, idOpts, redirectUri, requestObjectOpts, formats = ['jwt_vc', 'jwt_vc_json'] } = opts
const identifier = await getIdentifier(idOpts, context)
if (identifier.provider !== 'did:ebsi' && identifier.provider !== 'did:key') {
throw Error(
`EBSI only supports did:key for natural persons and did:ebsi for legal persons. Provider: ${identifier.provider}, did: ${identifier.did}`,
)
}
// This only works of the DID is actually registered, otherwise use our internal KMS; that is why the offline argument is passed in when type is Verifiable Auth to Onboard, as no DID is present at that point yet
const authKey = await getAuthenticationKey({
identifier,
offlineWhenNoDIDRegistered: credentialType === 'VerifiableAuthorisationToOnboard',
noVerificationMethodFallback: true,
context,
})
const kid = authKey.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({ key: authKey })
const clientId = opts.clientId ?? identifier.did
const {
credentialIssuer,
credentialType,
idOpts,
redirectUri,
requestObjectOpts,
formats = ['jwt_vc', 'jwt_vc_json']
} = opts
const identifier = await getIdentifier(idOpts, context)
if (identifier.provider !== 'did:ebsi' && identifier.provider !== 'did:key') {
throw Error(
`EBSI only supports did:key for natural persons and did:ebsi for legal persons. Provider: ${identifier.provider}, did: ${identifier.did}`,
)
}
// This only works if the DID is actually registered, otherwise use our internal KMS;
// that is why the offline argument is passed in when type is Verifiable Auth to Onboard, as no DID is present at that point yet
const authKey = await getAuthenticationKey({
identifier,
offlineWhenNoDIDRegistered: credentialType === 'VerifiableAuthorisationToOnboard',
noVerificationMethodFallback: true,
context,
})
const kid = authKey.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({key: authKey})
const clientId = opts.clientId ?? identifier.did

const vciClient = await OpenID4VCIClient.fromCredentialIssuer({
credentialIssuer: credentialIssuer,
kid,
clientId,
createAuthorizationRequestURL: false, // We will do that down below
retrieveServerMetadata: true,
})
const vciClient = await OpenID4VCIClient.fromCredentialIssuer({
credentialIssuer: credentialIssuer,
kid,
clientId,
createAuthorizationRequestURL: false, // We will do that down below
retrieveServerMetadata: true,
})

const allMatches = vciClient.getCredentialsSupported(false)
let arrayMatches: Array<CredentialConfigurationSupported>
if (Array.isArray(allMatches)) {
arrayMatches = allMatches
} else {
arrayMatches = Object.entries(allMatches).map(([id, supported]) => {
supported.id = id
return supported
const allMatches = vciClient.getCredentialsSupported(false)
let arrayMatches: Array<CredentialConfigurationSupported>
if (Array.isArray(allMatches)) {
arrayMatches = allMatches
} else {
arrayMatches = Object.entries(allMatches).map(([id, supported]) => {
supported.id = id
return supported
})
}
const supportedConfigurations = arrayMatches
.filter((supported) => getTypesFromCredentialSupported(supported, {filterVerifiableCredential: false}).includes(credentialType))
.filter((supported) => (supported.format === 'jwt_vc' || supported.format === 'jwt_vc_json') && formats.includes(supported.format))
if (supportedConfigurations.length === 0) {
throw Error(`Could not find '${credentialType}' with format(s) '${formats.join(',')}' in list of supported types for issuer: ${credentialIssuer}`)
}
const authorizationDetails = supportedConfigurations.map((supported) => {
return {
type: 'openid_credential',
format: supported.format,
types: getTypesFromCredentialSupported(supported),
} as AuthorizationDetails
})
}
const supportedConfigurations = arrayMatches
.filter((supported) => getTypesFromCredentialSupported(supported, { filterVerifiableCredential: false }).includes(credentialType))
.filter((supported) => (supported.format === 'jwt_vc' || supported.format === 'jwt_vc_json') && formats.includes(supported.format))
if (supportedConfigurations.length === 0) {
throw Error(`Could not find '${credentialType}' with format(s) '${formats.join(',')}' in list of supported types for issuer: ${credentialIssuer}`)
}
const authorizationDetails = supportedConfigurations.map((supported) => {
return {
type: 'openid_credential',
format: supported.format,
types: getTypesFromCredentialSupported(supported),
} as AuthorizationDetails
})

const signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
signCallback: signCallback(vciClient, idOpts, context),
}
const authUrl = await vciClient.createAuthorizationRequestUrl({
authorizationRequest: {
redirectUri,
clientId,
authorizationDetails,
requestObjectOpts: {
...requestObjectOpts,
signCallbacks,
kid: requestObjectOpts.kid ?? kid,
},
},
})
const signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
signCallback: signCallback(vciClient, idOpts, context),
}
const authorizationRequest = {
redirectUri,
clientId,
authorizationDetails,
requestObjectOpts: {
...requestObjectOpts,
signCallbacks,
kid: requestObjectOpts.kid ?? kid,
},
}
const authorizationCodeURL = await vciClient.createAuthorizationRequestUrl({
authorizationRequest
})

return {
authUrl,
identifier,
authKey,
supportedConfigurations,
metadata: vciClient.endpointMetadata,
oid4vpState: await vciClient.exportState(),
}
// Makes sure our response become compatible. The start method doesn't do an awful lot when a state is passed in
// like we do. This method does not start the actual workflow. It ensures al data is present to start the flow.
const startData = await context.agent.oid4vciHolderPrepareStart({
requestData: {
createAuthorizationRequestURL: false,
flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW,
uri: credentialIssuer,
existingClientState: JSON.parse(await vciClient.exportState())
},
authorizationRequestOpts: authorizationRequest
})

return {
...startData,
authorizationCodeURL,
identifier,
authKey
}
}

/**
* Normally you would use the browser to let the user make this call in the front channel,
* however EBSI mainly uses mocks at present, and we want to be able to test as well
*/
export const ebsiAuthRequestExecution = async (authRequestResult: AttestationAuthRequestUrlResult, opts?: {}) => {
const { oid4vpState, authUrl } = authRequestResult
const vciClient = await OpenID4VCIClient.fromState({ state: oid4vpState })
const {oid4vciClientState, authorizationCodeURL} = authRequestResult
const vciClient = await OpenID4VCIClient.fromState({state: oid4vciClientState})

console.log(`URL: ${authUrl}, according to client: ${vciClient.authorizationURL}`)
console.log(`URL: ${authorizationCodeURL}, according to client: ${vciClient.authorizationURL}`)

const authResponse = await getJson<any>(authUrl)
const location: string | null = authResponse.origResponse.headers.get('location')
const authResponse = await getJson<any>(authorizationCodeURL)
const location: string | null = authResponse.origResponse.headers.get('location')

console.log(`LOCATION: ${location}`)
console.log(`LOCATION: ${location}`)
}
3 changes: 3 additions & 0 deletions packages/ebsi-authorization-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
{
"path": "../oid4vci-holder"
},
{
"path": "../data-store"
},
{
"path": "../presentation-exchange"
},
Expand Down
Loading

0 comments on commit afffd39

Please sign in to comment.