UNPKG

@sphereon/ssi-sdk.ebsi-support

Version:

278 lines (257 loc) • 11.1 kB
import { CreateRequestObjectMode } from '@sphereon/oid4vci-common' import { CredentialMapper, PresentationSubmission } from '@sphereon/ssi-types' import { IAgentPlugin } from '@veramo/core' import fetch from 'cross-fetch' import { CreateEbsiDidOnLedgerResult, CreateEbsiDidParams } from '../did' import { determineWellknownEndpoint, ebsiCreateDidOnLedger as ebsiCreateDidOnLedgerFunction, ebsiGetIssuerMock } from '../did/functions' import { ebsiCreateAttestationAuthRequestURL, ebsiGetAttestation } from '../functions' import { ApiOpts, EBSIAuthAccessTokenGetArgs, EbsiOpenIDMetadata, GetAccessTokenResult, GetPresentationDefinitionSuccessResponse, IRequiredContext, schema, WellknownOpts, } from '../index' import { ExceptionResponse, GetAccessTokenArgs, GetAccessTokenResponse, GetOIDProviderJwksResponse, GetOIDProviderMetadataResponse, GetPresentationDefinitionArgs, GetPresentationDefinitionResponse, IEbsiSupport, } from '../types/IEbsiSupport' import { v4 } from 'uuid' import { defaultHasher } from '@sphereon/ssi-sdk.core' export const ebsiSupportMethods: Array<string> = [ 'ebsiCreateDidOnLedger', 'ebsiWellknownMetadata', 'ebsiAuthorizationServerJwks', 'ebsiPresentationDefinitionGet', 'ebsiAccessTokenGet', 'ebsiCreateAttestationAuthRequestURL', 'ebsiGetAttestation', ] export class EbsiSupport implements IAgentPlugin { readonly schema = schema.IEbsiSupport readonly methods: IEbsiSupport = { ebsiCreateDidOnLedger: this.ebsiCreateDidOnLedger.bind(this), ebsiWellknownMetadata: this.ebsiWellknownMetadata.bind(this), ebsiAuthorizationServerJwks: this.ebsiAuthorizationServerJwks.bind(this), ebsiPresentationDefinitionGet: this.ebsiPresentationDefinitionGet.bind(this), ebsiAccessTokenGet: this.ebsiAccessTokenGet.bind(this), ebsiCreateAttestationAuthRequestURL: ebsiCreateAttestationAuthRequestURL.bind(this), ebsiGetAttestation: ebsiGetAttestation.bind(this), } private async ebsiCreateDidOnLedger(args: CreateEbsiDidParams, context: IRequiredContext): Promise<CreateEbsiDidOnLedgerResult> { return await ebsiCreateDidOnLedgerFunction(args, context) } private async ebsiWellknownMetadata(args: WellknownOpts): Promise<GetOIDProviderMetadataResponse> { const url = determineWellknownEndpoint(args) return await ( await fetch(url, { method: 'GET', headers: { Accept: 'application/json', }, }) ).json() } private async ebsiAuthorizationServerJwks(args: ApiOpts): Promise<GetOIDProviderJwksResponse | ExceptionResponse> { const discoveryMetadata: EbsiOpenIDMetadata = await this.ebsiWellknownMetadata({ ...args, type: 'openid-configuration', }) return await ( await fetch(`${discoveryMetadata.jwks_uri}`, { method: 'GET', headers: { Accept: 'application/jwk-set+json', }, }) ).json() } private async ebsiPresentationDefinitionGet(args: GetPresentationDefinitionArgs): Promise<GetPresentationDefinitionResponse> { const { scope, apiOpts, openIDMetadata } = args const discoveryMetadata: EbsiOpenIDMetadata = openIDMetadata ?? (await this.ebsiWellknownMetadata({ ...apiOpts, type: 'openid-configuration', system: apiOpts?.mock ? 'authorisation' : apiOpts?.system, version: apiOpts?.version ?? 'v4', })) return (await ( await fetch(`${discoveryMetadata.presentation_definition_endpoint}?scope=openid%20${scope}`, { method: 'GET', headers: { Accept: 'application/json', }, }) ).json()) satisfies GetPresentationDefinitionSuccessResponse } private async ebsiAccessTokenGet(args: EBSIAuthAccessTokenGetArgs, context: IRequiredContext): Promise<GetAccessTokenResult> { const { scope, idOpts, jwksUri, clientId, allVerifiableCredentials, redirectUri, environment, skipDidResolution = false } = args const identifier = await context.agent.identifierManagedGetByDid(idOpts) console.log(`Getting access token for ${identifier.did}, scope ${scope} and clientId=${clientId}, skipDidResolution=${skipDidResolution}...`) const openIDMetadata = await this.ebsiWellknownMetadata({ environment, version: 'v4', mock: undefined, system: 'authorisation', type: 'openid-configuration', }) const definitionResponse = await this.ebsiPresentationDefinitionGet({ ...args, openIDMetadata, apiOpts: { environment, version: 'v4', type: 'openid-configuration' }, }) const hasInputDescriptors = definitionResponse.input_descriptors.length > 0 console.log(`PD response`, definitionResponse) if (!hasInputDescriptors) { // Yes EBSI expects VPs without a VC in some situations. This is not according to the PEX spec! // They probably should have used SIOP in these cases. We need to go through hoops as our libs do not expect PDs/VPs without VCs :( console.warn(`No INPUT descriptor returned for scope ${scope}`) } let attestationCredential = args.attestationCredential if (hasInputDescriptors && !attestationCredential) { if (allVerifiableCredentials && allVerifiableCredentials.length > 0) { const pexResult = await context.agent.pexDefinitionFilterCredentials({ presentationDefinition: definitionResponse, credentialFilterOpts: { credentialRole: args.credentialRole, verifiableCredentials: allVerifiableCredentials }, }) if (pexResult.filteredCredentials.length > 0) { const filtered = pexResult.filteredCredentials .map((cred) => CredentialMapper.toUniformCredential(cred, { hasher: defaultHasher })) .filter((cred) => { if (!cred.expirationDate) { return cred } else if (new Date(cred.expirationDate!).getDate() >= Date.now()) { return cred } return undefined }) .filter((cred) => !!cred) if (filtered.length > 0) { attestationCredential = filtered[0] } } } if (!attestationCredential) { console.log(`No attestation credential present. Will get one from within access token method!`) const credentialIssuer = args.credentialIssuer ?? ebsiGetIssuerMock({ environment }) const authReqResult = await context.agent.ebsiCreateAttestationAuthRequestURL({ credentialIssuer, idOpts, formats: ['jwt_vc'], clientId, redirectUri, requestObjectOpts: { iss: clientId, requestObjectMode: CreateRequestObjectMode.REQUEST_OBJECT, jwksUri, }, credentialType: 'VerifiableAuthorisationToOnboard', }) const attestationResult = await context.agent.ebsiGetAttestation({ authReqResult, clientId, opts: { timeout: 30_000 }, }) // @ts-ignore attestationCredential = attestationResult.credentials[0]!.rawVerifiableCredential! as W3CVerifiableCredential } } // FIXME SSISDK-40 should use DCQL // const definition = { // definition: definitionResponse, // location: PresentationDefinitionLocation.TOPLEVEL_PRESENTATION_DEF, // version: SupportedVersion.SIOPv2_D11, // } satisfies PresentationDefinitionWithLocation // const pexResult = hasInputDescriptors // ? await context.agent.pexDefinitionFilterCredentials({ // presentationDefinition: definitionResponse, // credentialFilterOpts: { credentialRole: args.credentialRole, verifiableCredentials: [attestationCredential!] }, // }) // : ({ // // LOL, let's see whether we can trick PEX to create a VP without VCs // filteredCredentials: [], // id: definitionResponse.id, // selectResults: { verifiableCredential: [], areRequiredCredentialsPresent: 'info' }, // } satisfies IPEXFilterResult) // const opSession = await context.agent.siopRegisterOPSession({ // requestJwtOrUri: '', // Siop assumes we use an auth request, which we don't have in this case // op: { checkLinkedDomains: CheckLinkedDomain.NEVER }, // //providedPresentationDefinitions: [definition], // }) //const oid4vp = await opSession.getOID4VP({ allIdentifiers: [identifier.did] }) // const vp = await oid4vp.createVerifiablePresentation( // args.credentialRole, // { dcqlQuery: definition, credentials: pexResult.filteredCredentials }, // { // proofOpts: { domain: openIDMetadata.issuer, nonce: v4(), created: new Date(Date.now() - 120_000).toString() }, // holder: identifier.did, // idOpts: idOpts, // skipDidResolution, // forceNoCredentialsInVP: !hasInputDescriptors, // }, // ) const presentationSubmission = { id: v4(), definition_id: definitionResponse.id, descriptor_map: [] } satisfies PresentationSubmission // FIXME SSISDK-40 //hasInputDescriptors //? vp.presentationSubmission //: ({ id: v4(), definition_id: definitionResponse.id, descriptor_map: [] } satisfies PresentationSubmission) console.log(`Presentation submission`, presentationSubmission) const tokenRequestArgs = { grant_type: 'vp_token', // FIXME SSISDK-40 vp_token: '', //CredentialMapper.toCompactJWT(vp.verifiablePresentations[0]), // FIXME How are we going to send multiple presentations in a vp_token? scope, presentation_submission: presentationSubmission, apiOpts: { environment, version: 'v4' }, openIDMetadata, } satisfies GetAccessTokenArgs console.log(`Access token request:\r\n${JSON.stringify(tokenRequestArgs)}`) const accessTokenResponse = await this.getAccessToken(tokenRequestArgs) console.log(`Access token response:\r\n${JSON.stringify(accessTokenResponse)}`) if (!('access_token' in accessTokenResponse)) { throw Error(`Error response: ${JSON.stringify(accessTokenResponse)}`) } return { accessTokenResponse, // vp, scope, // definition, identifier: identifier, } } private async getAccessToken(args: GetAccessTokenArgs): Promise<GetAccessTokenResponse> { const { grant_type = 'vp_token', scope, vp_token, presentation_submission, apiOpts, openIDMetadata } = args const discoveryMetadata: EbsiOpenIDMetadata = openIDMetadata ?? (await this.ebsiWellknownMetadata({ ...apiOpts, type: 'openid-configuration', })) const request = { grant_type, scope: `openid ${scope}`, vp_token, presentation_submission: JSON.stringify(presentation_submission), } return await ( await fetch(`${discoveryMetadata.token_endpoint}`, { method: 'POST', headers: { ContentType: 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: new URLSearchParams(request), }) ).json() } }