-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Allow EBSI attestation client to be the start of a regular VCI …
…flow
- Loading branch information
Showing
29 changed files
with
433 additions
and
502 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
238 changes: 132 additions & 106 deletions
238
packages/ebsi-authorization-client/src/functions/Attestation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.