@sphereon/ssi-sdk.ebsi-authorization-client
Version:
<!--suppress HtmlDeprecatedAttribute --> <h1 align="center"> <br> <a href="https://www.sphereon.com"><img src="https://sphereon.com/content/themes/sphereon/assets/img/logo.svg" alt="Sphereon" width="400"></a> <br>EBSI Authorization Client (Typescrip
145 lines (134 loc) • 5.89 kB
text/typescript
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import {
AuthorizationDetails,
AuthzFlowType,
CredentialConfigurationSupported,
getJson,
getTypesFromCredentialSupported,
OID4VCICredentialFormat,
ProofOfPossessionCallbacks,
RequestObjectOpts,
} from '@sphereon/oid4vci-common'
import { getIdentifier, IIdentifierOpts } from '@sphereon/ssi-sdk-ext.did-utils'
import { calculateJwkThumbprintForKey } from '@sphereon/ssi-sdk-ext.key-utils'
import { getAuthenticationKey, IssuanceOpts, PrepareStartArgs, signCallback, SupportedDidMethodEnum } from '@sphereon/ssi-sdk.oid4vci-holder'
import { IIdentifier } from '@veramo/core'
import { _ExtendedIKey } from '@veramo/utils'
import { IRequiredContext } from '../types/IEBSIAuthorizationClient'
export interface AttestationAuthRequestUrlResult extends Omit<Required<PrepareStartArgs>, 'issuanceOpt'> {
issuanceOpt?: IssuanceOpts
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 as input for the `oid4vciHolderStart` agent method,
* to start a OID4VCI holder flow.
*
* @param opts
* @param context
*/
export const ebsiCreateAttestationAuthRequestURL = 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,
): 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 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 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 signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
signCallback: await signCallback(vciClient, idOpts, context),
}
const authorizationRequestOpts = {
redirectUri,
clientId,
authorizationDetails,
requestObjectOpts: {
...requestObjectOpts,
signCallbacks,
kid: requestObjectOpts.kid ?? kid,
},
}
// 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
const authorizationCodeURL = await vciClient.createAuthorizationRequestUrl({
authorizationRequest: authorizationRequestOpts,
})
return {
requestData: {
createAuthorizationRequestURL: false,
flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW,
uri: credentialIssuer,
existingClientState: JSON.parse(await vciClient.exportState()),
},
authorizationRequestOpts,
authorizationCodeURL,
identifier,
authKey,
didMethodPreferences: [SupportedDidMethodEnum.DID_EBSI, SupportedDidMethodEnum.DID_KEY],
}
}
/**
* 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 { requestData, authorizationCodeURL } = authRequestResult
const vciClient = await OpenID4VCIClient.fromState({ state: requestData?.existingClientState! })
console.log(`URL: ${authorizationCodeURL}, according to client: ${vciClient.authorizationURL}`)
const authResponse = await getJson<any>(authorizationCodeURL)
const location: string | null = authResponse.origResponse.headers.get('location')
console.log(`LOCATION: ${location}`)
}