@sphereon/ssi-sdk.ebsi-support 0.26.1-unstable.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +13 -0
- package/dist/agent/EbsiSupport.d.ts +12 -0
- package/dist/agent/EbsiSupport.d.ts.map +1 -0
- package/dist/agent/EbsiSupport.js +202 -0
- package/dist/agent/EbsiSupport.js.map +1 -0
- package/dist/did/EbsiDidProvider.d.ts +47 -0
- package/dist/did/EbsiDidProvider.d.ts.map +1 -0
- package/dist/did/EbsiDidProvider.js +172 -0
- package/dist/did/EbsiDidProvider.js.map +1 -0
- package/dist/did/EbsiDidResolver.d.ts +5 -0
- package/dist/did/EbsiDidResolver.d.ts.map +1 -0
- package/dist/did/EbsiDidResolver.js +10 -0
- package/dist/did/EbsiDidResolver.js.map +1 -0
- package/dist/did/functions.d.ts +66 -0
- package/dist/did/functions.d.ts.map +1 -0
- package/dist/did/functions.js +416 -0
- package/dist/did/functions.js.map +1 -0
- package/dist/did/index.d.ts +6 -0
- package/dist/did/index.d.ts.map +1 -0
- package/dist/did/index.js +6 -0
- package/dist/did/index.js.map +1 -0
- package/dist/did/services/EbsiRPCService.d.ts +13 -0
- package/dist/did/services/EbsiRPCService.d.ts.map +1 -0
- package/dist/did/services/EbsiRPCService.js +64 -0
- package/dist/did/services/EbsiRPCService.js.map +1 -0
- package/dist/did/services/EbsiRestService.d.ts +37 -0
- package/dist/did/services/EbsiRestService.d.ts.map +1 -0
- package/dist/did/services/EbsiRestService.js +90 -0
- package/dist/did/services/EbsiRestService.js.map +1 -0
- package/dist/did/types.d.ts +386 -0
- package/dist/did/types.d.ts.map +1 -0
- package/dist/did/types.js +47 -0
- package/dist/did/types.js.map +1 -0
- package/dist/functions/Attestation.d.ts +32 -0
- package/dist/functions/Attestation.d.ts.map +1 -0
- package/dist/functions/Attestation.js +182 -0
- package/dist/functions/Attestation.js.map +1 -0
- package/dist/functions/AttestationHeadlessCallbacks.d.ts +17 -0
- package/dist/functions/AttestationHeadlessCallbacks.d.ts.map +1 -0
- package/dist/functions/AttestationHeadlessCallbacks.js +194 -0
- package/dist/functions/AttestationHeadlessCallbacks.js.map +1 -0
- package/dist/functions/index.d.ts +7 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +8 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types/IEbsiSupport.d.ts +211 -0
- package/dist/types/IEbsiSupport.d.ts.map +1 -0
- package/dist/types/IEbsiSupport.js +5 -0
- package/dist/types/IEbsiSupport.js.map +1 -0
- package/package.json +86 -0
- package/src/agent/EbsiSupport.ts +250 -0
- package/src/did/EbsiDidProvider.ts +269 -0
- package/src/did/EbsiDidResolver.ts +16 -0
- package/src/did/functions.ts +528 -0
- package/src/did/index.ts +5 -0
- package/src/did/services/EbsiRPCService.ts +68 -0
- package/src/did/services/EbsiRestService.ts +117 -0
- package/src/did/types.ts +449 -0
- package/src/functions/Attestation.ts +262 -0
- package/src/functions/AttestationHeadlessCallbacks.ts +242 -0
- package/src/functions/index.ts +15 -0
- package/src/index.ts +8 -0
- package/src/types/IEbsiSupport.ts +241 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
|
|
2
|
+
import {
|
|
3
|
+
Alg,
|
|
4
|
+
AuthorizationDetails,
|
|
5
|
+
AuthorizationRequestOpts,
|
|
6
|
+
AuthzFlowType,
|
|
7
|
+
CredentialConfigurationSupported,
|
|
8
|
+
getJson,
|
|
9
|
+
getTypesFromCredentialSupported,
|
|
10
|
+
ProofOfPossessionCallbacks,
|
|
11
|
+
} from '@sphereon/oid4vci-common'
|
|
12
|
+
import { getAuthenticationKey, getIdentifier, SupportedDidMethodEnum } from '@sphereon/ssi-sdk-ext.did-utils'
|
|
13
|
+
import { calculateJwkThumbprintForKey } from '@sphereon/ssi-sdk-ext.key-utils'
|
|
14
|
+
import {
|
|
15
|
+
IssuanceOpts,
|
|
16
|
+
OID4VCICallbackStateListener,
|
|
17
|
+
OID4VCIMachineInterpreter,
|
|
18
|
+
OID4VCIMachineState,
|
|
19
|
+
OID4VCIMachineStates,
|
|
20
|
+
PrepareStartArgs,
|
|
21
|
+
signatureAlgorithmFromKey,
|
|
22
|
+
signCallback,
|
|
23
|
+
} from '@sphereon/ssi-sdk.oid4vci-holder'
|
|
24
|
+
import {
|
|
25
|
+
OID4VPCallbackStateListener,
|
|
26
|
+
Siopv2MachineInterpreter,
|
|
27
|
+
Siopv2MachineState,
|
|
28
|
+
Siopv2MachineStates,
|
|
29
|
+
} from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
|
|
30
|
+
import { Siopv2OID4VPLinkHandler } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth/dist/link-handler'
|
|
31
|
+
import { IIdentifier } from '@veramo/core'
|
|
32
|
+
import { _ExtendedIKey } from '@veramo/utils'
|
|
33
|
+
import { waitFor } from 'xstate/lib/waitFor'
|
|
34
|
+
import { logger } from '../index'
|
|
35
|
+
import { AttestationResult, CreateAttestationAuthRequestURLArgs, EbsiEnvironment, GetAttestationArgs, IRequiredContext } from '../types/IEbsiSupport'
|
|
36
|
+
import {
|
|
37
|
+
addContactCallback,
|
|
38
|
+
authorizationCodeUrlCallback,
|
|
39
|
+
handleErrorCallback,
|
|
40
|
+
reviewCredentialsCallback,
|
|
41
|
+
selectCredentialsCallback,
|
|
42
|
+
siopDoneCallback,
|
|
43
|
+
} from './AttestationHeadlessCallbacks'
|
|
44
|
+
import { getEbsiApiBaseUrl } from './index'
|
|
45
|
+
|
|
46
|
+
export interface AttestationAuthRequestUrlResult extends Omit<Required<PrepareStartArgs>, 'issuanceOpt'> {
|
|
47
|
+
issuanceOpt?: IssuanceOpts
|
|
48
|
+
authorizationCodeURL: string
|
|
49
|
+
identifier: IIdentifier
|
|
50
|
+
authKey: _ExtendedIKey
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Method to generate an authz url for getting attestation credentials from a (R)TAO on EBSI using a cloud/service wallet
|
|
55
|
+
*
|
|
56
|
+
* This method can be used standalone. But it can also be used as input for the `oid4vciHolderStart` agent method,
|
|
57
|
+
* to start a OID4VCI holder flow.
|
|
58
|
+
*
|
|
59
|
+
* @param opts
|
|
60
|
+
* @param context
|
|
61
|
+
*/
|
|
62
|
+
export const ebsiCreateAttestationAuthRequestURL = async (
|
|
63
|
+
{
|
|
64
|
+
clientId: clientIdArg,
|
|
65
|
+
credentialIssuer,
|
|
66
|
+
credentialType,
|
|
67
|
+
idOpts,
|
|
68
|
+
redirectUri,
|
|
69
|
+
requestObjectOpts,
|
|
70
|
+
formats = ['jwt_vc', 'jwt_vc_json'],
|
|
71
|
+
}: CreateAttestationAuthRequestURLArgs,
|
|
72
|
+
context: IRequiredContext,
|
|
73
|
+
): Promise<AttestationAuthRequestUrlResult> => {
|
|
74
|
+
const identifier = await getIdentifier(idOpts, context)
|
|
75
|
+
if (identifier.provider !== 'did:ebsi' && identifier.provider !== 'did:key') {
|
|
76
|
+
throw Error(
|
|
77
|
+
`EBSI only supports did:key for natural persons and did:ebsi for legal persons. Provider: ${identifier.provider}, did: ${identifier.did}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
// This only works if the DID is actually registered, otherwise use our internal KMS;
|
|
81
|
+
// 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
|
|
82
|
+
const authKey = await getAuthenticationKey(identifier, context, credentialType === 'VerifiableAuthorisationToOnboard', true)
|
|
83
|
+
const kid = authKey.meta?.jwkThumbprint ?? calculateJwkThumbprintForKey({ key: authKey })
|
|
84
|
+
const clientId = clientIdArg ?? identifier.did
|
|
85
|
+
|
|
86
|
+
const vciClient = await OpenID4VCIClient.fromCredentialIssuer({
|
|
87
|
+
credentialIssuer,
|
|
88
|
+
kid,
|
|
89
|
+
clientId,
|
|
90
|
+
createAuthorizationRequestURL: false, // We will do that down below
|
|
91
|
+
retrieveServerMetadata: true,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const allMatches = vciClient.getCredentialsSupported(false)
|
|
95
|
+
let arrayMatches: Array<CredentialConfigurationSupported>
|
|
96
|
+
if (Array.isArray(allMatches)) {
|
|
97
|
+
arrayMatches = allMatches
|
|
98
|
+
} else {
|
|
99
|
+
arrayMatches = Object.entries(allMatches).map(([id, supported]) => {
|
|
100
|
+
supported.id = id
|
|
101
|
+
return supported
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
const supportedConfigurations = arrayMatches
|
|
105
|
+
.filter((supported) => getTypesFromCredentialSupported(supported, { filterVerifiableCredential: false }).includes(credentialType))
|
|
106
|
+
.filter((supported) => (supported.format === 'jwt_vc' || supported.format === 'jwt_vc_json') && formats.includes(supported.format))
|
|
107
|
+
if (supportedConfigurations.length === 0) {
|
|
108
|
+
throw Error(`Could not find '${credentialType}' with format(s) '${formats.join(',')}' in list of supported types for issuer: ${credentialIssuer}`)
|
|
109
|
+
}
|
|
110
|
+
const authorizationDetails = supportedConfigurations.map((supported) => {
|
|
111
|
+
return {
|
|
112
|
+
type: 'openid_credential',
|
|
113
|
+
format: supported.format,
|
|
114
|
+
types: getTypesFromCredentialSupported(supported),
|
|
115
|
+
} as AuthorizationDetails
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
|
|
119
|
+
signCallback: signCallback(vciClient, idOpts, context),
|
|
120
|
+
}
|
|
121
|
+
const authorizationRequestOpts = {
|
|
122
|
+
redirectUri,
|
|
123
|
+
clientId,
|
|
124
|
+
authorizationDetails,
|
|
125
|
+
requestObjectOpts: {
|
|
126
|
+
...requestObjectOpts,
|
|
127
|
+
signCallbacks,
|
|
128
|
+
kid: requestObjectOpts.kid ?? kid,
|
|
129
|
+
},
|
|
130
|
+
} satisfies AuthorizationRequestOpts
|
|
131
|
+
// todo: Do we really need to do this, or can we just set the create option to true at this point? We are passing in the authzReq opts
|
|
132
|
+
const authorizationCodeURL = await vciClient.createAuthorizationRequestUrl({
|
|
133
|
+
authorizationRequest: authorizationRequestOpts,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
requestData: {
|
|
138
|
+
createAuthorizationRequestURL: false,
|
|
139
|
+
flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW,
|
|
140
|
+
uri: credentialIssuer,
|
|
141
|
+
existingClientState: JSON.parse(await vciClient.exportState()),
|
|
142
|
+
},
|
|
143
|
+
accessTokenOpts: {
|
|
144
|
+
clientOpts: {
|
|
145
|
+
alg: Alg[await signatureAlgorithmFromKey({ key: authKey })],
|
|
146
|
+
clientId,
|
|
147
|
+
kid,
|
|
148
|
+
signCallbacks,
|
|
149
|
+
clientAssertionType: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
authorizationRequestOpts,
|
|
153
|
+
authorizationCodeURL,
|
|
154
|
+
identifier,
|
|
155
|
+
// @ts-ignore
|
|
156
|
+
authKey,
|
|
157
|
+
didMethodPreferences: [SupportedDidMethodEnum.DID_EBSI, SupportedDidMethodEnum.DID_KEY],
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const ebsiGetAttestationInterpreter = async (
|
|
162
|
+
{ clientId, authReqResult }: Omit<GetAttestationArgs, 'opts'>,
|
|
163
|
+
context: IRequiredContext,
|
|
164
|
+
): Promise<OID4VCIMachineInterpreter> => {
|
|
165
|
+
const identifier = authReqResult.identifier
|
|
166
|
+
const vciStateCallbacks = new Map<OID4VCIMachineStates, (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => Promise<void>>()
|
|
167
|
+
const vpStateCallbacks = new Map<Siopv2MachineStates, (oid4vpMachine: Siopv2MachineInterpreter, state: Siopv2MachineState) => Promise<void>>()
|
|
168
|
+
|
|
169
|
+
const oid4vciMachine = await context.agent.oid4vciHolderGetMachineInterpreter({
|
|
170
|
+
...authReqResult,
|
|
171
|
+
issuanceOpt: {
|
|
172
|
+
identifier,
|
|
173
|
+
didMethod: SupportedDidMethodEnum.DID_EBSI,
|
|
174
|
+
kid: authReqResult.authKey.meta?.jwkThumbprint ?? authReqResult.authKey.kid,
|
|
175
|
+
},
|
|
176
|
+
clientOpts: {
|
|
177
|
+
clientAssertionType: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
178
|
+
kid: authReqResult.authKey.meta?.jwkThumbprint ?? authReqResult.authKey.kid,
|
|
179
|
+
clientId,
|
|
180
|
+
},
|
|
181
|
+
didMethodPreferences: [SupportedDidMethodEnum.DID_EBSI, SupportedDidMethodEnum.DID_KEY],
|
|
182
|
+
stateNavigationListener: OID4VCICallbackStateListener(vciStateCallbacks),
|
|
183
|
+
})
|
|
184
|
+
const vpLinkHandler = new Siopv2OID4VPLinkHandler({
|
|
185
|
+
protocols: ['openid:'],
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
context,
|
|
188
|
+
noStateMachinePersistence: true,
|
|
189
|
+
stateNavigationListener: OID4VPCallbackStateListener(vpStateCallbacks),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
vpStateCallbacks
|
|
193
|
+
.set(Siopv2MachineStates.done, siopDoneCallback({ oid4vciMachine }, context))
|
|
194
|
+
.set(Siopv2MachineStates.handleError, handleErrorCallback(context))
|
|
195
|
+
|
|
196
|
+
vciStateCallbacks
|
|
197
|
+
.set(OID4VCIMachineStates.handleError, handleErrorCallback(context))
|
|
198
|
+
.set(OID4VCIMachineStates.addContact, addContactCallback(context))
|
|
199
|
+
.set(OID4VCIMachineStates.selectCredentials, selectCredentialsCallback(context))
|
|
200
|
+
.set(
|
|
201
|
+
OID4VCIMachineStates.initiateAuthorizationRequest,
|
|
202
|
+
authorizationCodeUrlCallback(
|
|
203
|
+
{
|
|
204
|
+
authReqResult,
|
|
205
|
+
vpLinkHandler,
|
|
206
|
+
},
|
|
207
|
+
context,
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
.set(OID4VCIMachineStates.reviewCredentials, reviewCredentialsCallback(context))
|
|
211
|
+
|
|
212
|
+
return oid4vciMachine.interpreter
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const ebsiGetAttestation = async (
|
|
216
|
+
{ clientId, authReqResult, opts = { timeout: 30_000 } }: GetAttestationArgs,
|
|
217
|
+
context: IRequiredContext,
|
|
218
|
+
): Promise<AttestationResult> => {
|
|
219
|
+
const interpreter = await ebsiGetAttestationInterpreter({ clientId, authReqResult }, context)
|
|
220
|
+
const state = await waitFor(interpreter.start(), (state) => state.matches('done') || state.matches('handleError'), {
|
|
221
|
+
timeout: opts.timeout ?? 30_000,
|
|
222
|
+
})
|
|
223
|
+
if (state.matches('handleError')) {
|
|
224
|
+
console.error(JSON.stringify(state.context.error))
|
|
225
|
+
throw Error(JSON.stringify(state.context.error))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
contactAlias: state.context.contactAlias,
|
|
230
|
+
contact: state.context.contact!,
|
|
231
|
+
credentialBranding: state.context.credentialBranding,
|
|
232
|
+
identifier: state.context.issuanceOpt?.identifier ?? authReqResult.identifier,
|
|
233
|
+
error: state.context.error,
|
|
234
|
+
credentials: state.context.credentialsToAccept,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Normally you would use the browser to let the user make this call in the front channel,
|
|
240
|
+
* however EBSI mainly uses mocks at present, and we want to be able to test as well
|
|
241
|
+
*/
|
|
242
|
+
export const ebsiAuthRequestExecution = async (authRequestResult: AttestationAuthRequestUrlResult, opts?: {}) => {
|
|
243
|
+
const { requestData, authorizationCodeURL } = authRequestResult
|
|
244
|
+
const vciClient = await OpenID4VCIClient.fromState({ state: requestData?.existingClientState! })
|
|
245
|
+
|
|
246
|
+
logger.debug(`URL: ${authorizationCodeURL}, according to client: ${vciClient.authorizationURL}`)
|
|
247
|
+
|
|
248
|
+
const authResponse = await getJson<any>(authorizationCodeURL)
|
|
249
|
+
const location: string | null = authResponse.origResponse.headers.get('location')
|
|
250
|
+
logger.debug(`LOCATION: ${location}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const ebsiGetIssuer = ({ credentialIssuer, environment = 'pilot' }: { credentialIssuer?: string; environment?: EbsiEnvironment }): string => {
|
|
254
|
+
if (credentialIssuer) {
|
|
255
|
+
return credentialIssuer
|
|
256
|
+
}
|
|
257
|
+
if (environment !== 'pilot') {
|
|
258
|
+
return `${getEbsiApiBaseUrl({ environment, version: 'v3' })}/issuer-mock`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw Error(`EBSI environment ${environment} needs explicit credential issuer`)
|
|
262
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { getIssuerName } from '@sphereon/oid4vci-common'
|
|
2
|
+
import {
|
|
3
|
+
ConnectionType,
|
|
4
|
+
CorrelationIdentifierType,
|
|
5
|
+
CredentialRole,
|
|
6
|
+
IdentityOrigin,
|
|
7
|
+
NonPersistedParty,
|
|
8
|
+
Party,
|
|
9
|
+
PartyOrigin,
|
|
10
|
+
PartyTypeType,
|
|
11
|
+
} from '@sphereon/ssi-sdk.data-store'
|
|
12
|
+
import { OID4VCIMachine, OID4VCIMachineEvents, OID4VCIMachineInterpreter, OID4VCIMachineState } from '@sphereon/ssi-sdk.oid4vci-holder'
|
|
13
|
+
import { Siopv2MachineInterpreter, Siopv2MachineState } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
|
|
14
|
+
import { Siopv2OID4VPLinkHandler } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth/dist/link-handler'
|
|
15
|
+
import fetch from 'cross-fetch'
|
|
16
|
+
import { logger } from '../index'
|
|
17
|
+
import { IRequiredContext } from '../types/IEbsiSupport'
|
|
18
|
+
import { AttestationAuthRequestUrlResult } from './Attestation'
|
|
19
|
+
|
|
20
|
+
export const addContactCallback = (context: IRequiredContext) => {
|
|
21
|
+
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
|
|
22
|
+
const { serverMetadata, hasContactConsent, contactAlias } = state.context
|
|
23
|
+
|
|
24
|
+
if (!serverMetadata) {
|
|
25
|
+
return Promise.reject(Error('Missing serverMetadata in context'))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const issuerUrl: URL = new URL(serverMetadata.issuer)
|
|
29
|
+
const correlationId: string = `${issuerUrl.protocol}//${issuerUrl.hostname}`
|
|
30
|
+
let issuerName: string = getIssuerName(correlationId, serverMetadata.credentialIssuerMetadata)
|
|
31
|
+
|
|
32
|
+
const party: NonPersistedParty = {
|
|
33
|
+
contact: {
|
|
34
|
+
displayName: issuerName,
|
|
35
|
+
legalName: issuerName,
|
|
36
|
+
},
|
|
37
|
+
// FIXME maybe its nicer if we can also just use the id only
|
|
38
|
+
// TODO using the predefined party type from the contact migrations here
|
|
39
|
+
// TODO this is not used as the screen itself adds one, look at the params of the screen, this is not being passed in
|
|
40
|
+
partyType: {
|
|
41
|
+
id: '3875c12e-fdaa-4ef6-a340-c936e054b627',
|
|
42
|
+
origin: PartyOrigin.EXTERNAL,
|
|
43
|
+
type: PartyTypeType.ORGANIZATION,
|
|
44
|
+
name: 'Sphereon_default_type',
|
|
45
|
+
tenantId: '95e09cfc-c974-4174-86aa-7bf1d5251fb4',
|
|
46
|
+
},
|
|
47
|
+
uri: correlationId,
|
|
48
|
+
identities: [
|
|
49
|
+
{
|
|
50
|
+
alias: correlationId,
|
|
51
|
+
roles: [CredentialRole.ISSUER],
|
|
52
|
+
origin: IdentityOrigin.EXTERNAL,
|
|
53
|
+
identifier: {
|
|
54
|
+
type: CorrelationIdentifierType.URL,
|
|
55
|
+
correlationId: issuerUrl.hostname,
|
|
56
|
+
},
|
|
57
|
+
// TODO WAL-476 add support for correct connection
|
|
58
|
+
connection: {
|
|
59
|
+
type: ConnectionType.OPENID_CONNECT,
|
|
60
|
+
config: {
|
|
61
|
+
clientId: '138d7bf8-c930-4c6e-b928-97d3a4928b01',
|
|
62
|
+
clientSecret: '03b3955f-d020-4f2a-8a27-4e452d4e27a0',
|
|
63
|
+
scopes: ['auth'],
|
|
64
|
+
issuer: 'https://example.com/app-test',
|
|
65
|
+
redirectUrl: 'app:/callback',
|
|
66
|
+
dangerouslyAllowInsecureHttpRequests: true,
|
|
67
|
+
clientAuthMethod: 'post' as const,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const onCreate = async ({
|
|
75
|
+
party,
|
|
76
|
+
issuerUrl,
|
|
77
|
+
issuerName,
|
|
78
|
+
correlationId,
|
|
79
|
+
}: {
|
|
80
|
+
party: NonPersistedParty
|
|
81
|
+
issuerUrl: string
|
|
82
|
+
issuerName: string
|
|
83
|
+
correlationId: string
|
|
84
|
+
}): Promise<void> => {
|
|
85
|
+
const displayName = party.contact.displayName ?? issuerName
|
|
86
|
+
const contacts: Array<Party> = await context.agent.cmGetContacts({
|
|
87
|
+
filter: [
|
|
88
|
+
{
|
|
89
|
+
contact: {
|
|
90
|
+
// Searching on legalName as displayName is not unique, and we only support organizations for now
|
|
91
|
+
legalName: displayName,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
if (contacts.length === 0 || !contacts[0]?.contact) {
|
|
97
|
+
const contact = await context.agent.cmAddContact({
|
|
98
|
+
...party,
|
|
99
|
+
displayName,
|
|
100
|
+
legalName: displayName,
|
|
101
|
+
contactType: {
|
|
102
|
+
type: PartyTypeType.ORGANIZATION,
|
|
103
|
+
name: displayName,
|
|
104
|
+
origin: PartyOrigin.EXTERNAL,
|
|
105
|
+
tenantId: party.tenantId ?? '1',
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
oid4vciMachine.send({
|
|
109
|
+
type: OID4VCIMachineEvents.CREATE_CONTACT,
|
|
110
|
+
data: contact,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const onConsentChange = async (hasConsent: boolean): Promise<void> => {
|
|
116
|
+
oid4vciMachine.send({
|
|
117
|
+
type: OID4VCIMachineEvents.SET_CONTACT_CONSENT,
|
|
118
|
+
data: hasConsent,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const onAliasChange = async (alias: string): Promise<void> => {
|
|
123
|
+
oid4vciMachine.send({
|
|
124
|
+
type: OID4VCIMachineEvents.SET_CONTACT_ALIAS,
|
|
125
|
+
data: alias,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!issuerName) {
|
|
130
|
+
issuerName = `EBSI unknown (${issuerUrl})`
|
|
131
|
+
} else if (issuerName.startsWith('http')) {
|
|
132
|
+
issuerName = `EBSI ${issuerName.replace(/https?:\/\//, '')}`
|
|
133
|
+
}
|
|
134
|
+
if (!contactAlias) {
|
|
135
|
+
return await onAliasChange(issuerName)
|
|
136
|
+
}
|
|
137
|
+
issuerName = contactAlias
|
|
138
|
+
if (!hasContactConsent) {
|
|
139
|
+
return await onConsentChange(true)
|
|
140
|
+
}
|
|
141
|
+
await onCreate({ party, issuerName, issuerUrl: issuerUrl.toString(), correlationId })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const handleErrorCallback = (context: IRequiredContext) => {
|
|
146
|
+
return async (oid4vciMachine: OID4VCIMachineInterpreter | Siopv2MachineInterpreter, state: OID4VCIMachineState | Siopv2MachineState) => {
|
|
147
|
+
console.error(`error callback event: ${state.event}`, state.context.error)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const selectCredentialsCallback = (context: IRequiredContext) => {
|
|
152
|
+
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
|
|
153
|
+
const { contact, credentialToSelectFrom, selectedCredentials } = state.context
|
|
154
|
+
|
|
155
|
+
if (selectedCredentials && selectedCredentials.length > 0) {
|
|
156
|
+
logger.info(`selected: ${selectedCredentials.join(', ')}`)
|
|
157
|
+
oid4vciMachine.send({
|
|
158
|
+
type: OID4VCIMachineEvents.NEXT,
|
|
159
|
+
})
|
|
160
|
+
return
|
|
161
|
+
} else if (!contact) {
|
|
162
|
+
return Promise.reject(Error('Missing contact in context'))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const onSelectType = async (selectedCredentials: Array<string>): Promise<void> => {
|
|
166
|
+
console.log(`Selected credentials: ${selectedCredentials.join(', ')}`)
|
|
167
|
+
oid4vciMachine.send({
|
|
168
|
+
type: OID4VCIMachineEvents.SET_SELECTED_CREDENTIALS,
|
|
169
|
+
data: selectedCredentials,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await onSelectType(credentialToSelectFrom.map((sel) => sel.credentialId))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const authorizationCodeUrlCallback = (
|
|
178
|
+
{
|
|
179
|
+
authReqResult,
|
|
180
|
+
vpLinkHandler,
|
|
181
|
+
}: {
|
|
182
|
+
authReqResult: AttestationAuthRequestUrlResult
|
|
183
|
+
vpLinkHandler: Siopv2OID4VPLinkHandler
|
|
184
|
+
},
|
|
185
|
+
context: IRequiredContext,
|
|
186
|
+
) => {
|
|
187
|
+
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
|
|
188
|
+
const url = state.context.authorizationCodeURL
|
|
189
|
+
console.log('navigateAuthorizationCodeURL: ', url)
|
|
190
|
+
if (!url) {
|
|
191
|
+
return Promise.reject(Error('Missing authorization URL in context'))
|
|
192
|
+
}
|
|
193
|
+
const onOpenAuthorizationUrl = async (url: string): Promise<void> => {
|
|
194
|
+
console.log('onOpenAuthorizationUrl being invoked: ', url)
|
|
195
|
+
oid4vciMachine.send({
|
|
196
|
+
type: OID4VCIMachineEvents.INVOKED_AUTHORIZATION_CODE_REQUEST,
|
|
197
|
+
data: url,
|
|
198
|
+
})
|
|
199
|
+
const response = await fetch(url, { redirect: 'manual' })
|
|
200
|
+
if (response.status < 301 || response.status > 302) {
|
|
201
|
+
throw Error(`When doing a headless auth, we expect to be redirected on getting the authz URL`)
|
|
202
|
+
}
|
|
203
|
+
const openidUri = response.headers.get('location')
|
|
204
|
+
if (!openidUri || !openidUri.startsWith('openid://')) {
|
|
205
|
+
throw Error(
|
|
206
|
+
`Expected a openid:// URI to be returned from EBSI in headless mode. Returned: ${openidUri}, ${JSON.stringify(await response.text())}`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(`onOpenAuthorizationUrl after openUrl: ${url}`)
|
|
211
|
+
const kid = authReqResult.authKey.meta?.jwkThumbprint
|
|
212
|
+
? `${authReqResult.identifier.did}#${authReqResult.authKey.meta.jwkThumbprint}`
|
|
213
|
+
: authReqResult.authKey.kid
|
|
214
|
+
await vpLinkHandler.handle(openidUri, { idOpts: { identifier: authReqResult.identifier, kid } })
|
|
215
|
+
}
|
|
216
|
+
await onOpenAuthorizationUrl(url)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const reviewCredentialsCallback = (context: IRequiredContext) => {
|
|
221
|
+
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
|
|
222
|
+
console.log(`# REVIEW CREDENTIALS:`)
|
|
223
|
+
console.log(JSON.stringify(state.context.credentialsToAccept, null, 2))
|
|
224
|
+
oid4vciMachine.send({
|
|
225
|
+
type: OID4VCIMachineEvents.NEXT,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const siopDoneCallback = ({ oid4vciMachine }: { oid4vciMachine: OID4VCIMachine }, context: IRequiredContext) => {
|
|
231
|
+
return async (oid4vpMachine: Siopv2MachineInterpreter, state: Siopv2MachineState) => {
|
|
232
|
+
// console.log('SIOP result:')
|
|
233
|
+
// console.log(JSON.stringify(state.context, null , 2))
|
|
234
|
+
if (!state.context.authorizationResponseData?.queryParams?.code) {
|
|
235
|
+
throw Error(`No code was returned from the authorization step`)
|
|
236
|
+
}
|
|
237
|
+
oid4vciMachine.interpreter.send({
|
|
238
|
+
type: OID4VCIMachineEvents.PROVIDE_AUTHORIZATION_CODE_RESPONSE,
|
|
239
|
+
data: state.context.authorizationResponseData.url!,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiOpts, EbsiEnvironment } from '../types/IEbsiSupport'
|
|
2
|
+
|
|
3
|
+
export const getEbsiApiBaseUrl = ({
|
|
4
|
+
environment = 'pilot',
|
|
5
|
+
version,
|
|
6
|
+
system = 'pilot',
|
|
7
|
+
}: ApiOpts & { system?: 'authorisation' | 'conformance' | 'did-registry' | EbsiEnvironment }) => {
|
|
8
|
+
return `https://api-${environment}.ebsi.eu/${system}/${version}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const wait = async (timeoutInMS: number) => {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, timeoutInMS))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export * from './Attestation'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Loggers } from '@sphereon/ssi-types'
|
|
2
|
+
|
|
3
|
+
export const logger = Loggers.DEFAULT.get('sphereon:ebsi-support')
|
|
4
|
+
const schema = require('../plugin.schema.json')
|
|
5
|
+
export { schema }
|
|
6
|
+
export { EbsiSupport } from './agent/EbsiSupport'
|
|
7
|
+
export * from './types/IEbsiSupport'
|
|
8
|
+
export { EbsiDidProvider } from './did'
|