@sphereon/ssi-sdk.ebsi-support
Version:
259 lines (241 loc) • 9.54 kB
text/typescript
import { decodeUriAsJson } from '@sphereon/did-auth-siop'
import { getIssuerName } from '@sphereon/oid4vci-common'
import {
ConnectionType,
CorrelationIdentifierType,
IdentityOrigin,
NonPersistedParty,
Party,
PartyOrigin,
PartyTypeType,
} from '@sphereon/ssi-sdk.data-store-types'
import { OID4VCIMachine, OID4VCIMachineEvents, OID4VCIMachineInterpreter, OID4VCIMachineState } from '@sphereon/ssi-sdk.oid4vci-holder'
import { Siopv2MachineInterpreter, Siopv2MachineState, Siopv2OID4VPLinkHandler } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import { CredentialRole } from '@sphereon/ssi-types'
import fetch from 'cross-fetch'
import { logger } from '../index'
import { IRequiredContext } from '../types/IEbsiSupport'
import { AttestationAuthRequestUrlResult } from './Attestation'
export const addContactCallback = (context: IRequiredContext) => {
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
const { serverMetadata, hasContactConsent, contactAlias } = state.context
if (!serverMetadata) {
return Promise.reject(Error('Missing serverMetadata in context'))
}
const issuerUrl: URL = new URL(serverMetadata.issuer)
const correlationId: string = `${issuerUrl.protocol}//${issuerUrl.hostname}`
let issuerName: string = getIssuerName(correlationId, serverMetadata.credentialIssuerMetadata)
const party: NonPersistedParty = {
contact: {
displayName: issuerName,
legalName: issuerName,
},
// FIXME maybe its nicer if we can also just use the id only
// TODO using the predefined party type from the contact migrations here
// TODO this is not used as the screen itself adds one, look at the params of the screen, this is not being passed in
partyType: {
id: '3875c12e-fdaa-4ef6-a340-c936e054b627',
origin: PartyOrigin.EXTERNAL,
type: PartyTypeType.ORGANIZATION,
name: 'Sphereon_default_type',
tenantId: '95e09cfc-c974-4174-86aa-7bf1d5251fb4',
},
uri: correlationId,
identities: [
{
alias: correlationId,
roles: [CredentialRole.ISSUER],
origin: IdentityOrigin.EXTERNAL,
identifier: {
type: CorrelationIdentifierType.URL,
correlationId: issuerUrl.hostname,
},
// TODO WAL-476 add support for correct connection
connection: {
type: ConnectionType.OPENID_CONNECT,
config: {
clientId: '138d7bf8-c930-4c6e-b928-97d3a4928b01',
clientSecret: '03b3955f-d020-4f2a-8a27-4e452d4e27a0',
scopes: ['auth'],
issuer: 'https://example.com/app-test',
redirectUrl: 'app:/callback',
dangerouslyAllowInsecureHttpRequests: true,
clientAuthMethod: 'post' as const,
},
},
},
],
}
const onCreate = async ({
party,
issuerUrl,
issuerName,
correlationId,
}: {
party: NonPersistedParty
issuerUrl: string
issuerName: string
correlationId: string
}): Promise<void> => {
const displayName = party.contact.displayName ?? issuerName
const contacts: Array<Party> = await context.agent.cmGetContacts({
filter: [
{
contact: {
// Searching on legalName as displayName is not unique, and we only support organizations for now
legalName: displayName,
},
},
],
})
if (contacts.length === 0 || !contacts[0]?.contact) {
const contact = await context.agent.cmAddContact({
...party,
displayName,
legalName: displayName,
contactType: {
type: PartyTypeType.ORGANIZATION,
name: displayName,
origin: PartyOrigin.EXTERNAL,
tenantId: party.tenantId ?? '1',
},
})
oid4vciMachine.send({
type: OID4VCIMachineEvents.CREATE_CONTACT,
data: contact,
})
}
}
const onConsentChange = async (hasConsent: boolean): Promise<void> => {
oid4vciMachine.send({
type: OID4VCIMachineEvents.SET_CONTACT_CONSENT,
data: hasConsent,
})
}
const onAliasChange = async (alias: string): Promise<void> => {
oid4vciMachine.send({
type: OID4VCIMachineEvents.SET_CONTACT_ALIAS,
data: alias,
})
}
if (!issuerName) {
issuerName = `EBSI unknown (${issuerUrl})`
} else if (issuerName.startsWith('http')) {
issuerName = `EBSI ${issuerName.replace(/https?:\/\//, '')}`
}
if (!contactAlias) {
return await onAliasChange(issuerName)
}
issuerName = contactAlias
if (!hasContactConsent) {
return await onConsentChange(true)
}
await onCreate({ party, issuerName, issuerUrl: issuerUrl.toString(), correlationId })
}
}
export const handleErrorCallback = (context: IRequiredContext) => {
return async (oid4vciMachine: OID4VCIMachineInterpreter | Siopv2MachineInterpreter, state: OID4VCIMachineState | Siopv2MachineState) => {
console.error(`error callback event: ${state.event}`, state.context.error)
logger.trace(state.event)
}
}
export const selectCredentialsCallback = (context: IRequiredContext) => {
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
const { contact, credentialToSelectFrom, selectedCredentials } = state.context
if (selectedCredentials && selectedCredentials.length > 0) {
logger.info(`selected: ${selectedCredentials.join(', ')}`)
oid4vciMachine.send({
type: OID4VCIMachineEvents.NEXT,
})
return
} else if (!contact) {
return Promise.reject(Error('Missing contact in context'))
}
const onSelectType = async (selectedCredentials: Array<string>): Promise<void> => {
console.log(`Selected credentials: ${selectedCredentials.join(', ')}`)
oid4vciMachine.send({
type: OID4VCIMachineEvents.SET_SELECTED_CREDENTIALS,
data: selectedCredentials,
})
}
await onSelectType(credentialToSelectFrom.map((sel) => sel.credentialId))
}
}
export const authorizationCodeUrlCallback = (
{
authReqResult,
vpLinkHandler,
}: {
authReqResult: AttestationAuthRequestUrlResult
vpLinkHandler: Siopv2OID4VPLinkHandler
},
context: IRequiredContext,
) => {
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
const url = state.context.authorizationCodeURL
console.log('navigateAuthorizationCodeURL: ', url)
if (!url) {
return Promise.reject(Error('Missing authorization URL in context'))
}
const onOpenAuthorizationUrl = async (url: string): Promise<void> => {
console.log('onOpenAuthorizationUrl being invoked: ', url)
oid4vciMachine.send({
type: OID4VCIMachineEvents.INVOKED_AUTHORIZATION_CODE_REQUEST,
data: url,
})
const response = await fetch(url, { redirect: 'manual' })
if (response.status < 301 || response.status > 302) {
throw Error(`When doing a headless auth, we expect to be redirected on getting the authz URL`)
}
const openidUri = response.headers.get('location')
if (!openidUri || !openidUri.startsWith('openid://')) {
let error: string | undefined = undefined
if (openidUri) {
if (openidUri.includes('error')) {
error = 'Authorization server error: '
const decoded = decodeUriAsJson(openidUri)
if ('error' in decoded && decoded.error) {
error += decoded.error + ', '
}
if ('error_description' in decoded && decoded.error_description) {
error += decoded.error_description
}
}
}
throw Error(
error ??
`Expected a openid:// URI to be returned from EBSI in headless mode. Returned: ${openidUri}, ${JSON.stringify(await response.text())}`,
)
}
console.log(`onOpenAuthorizationUrl after openUrl: ${url}`)
const kid = authReqResult.authKey.meta?.jwkThumbprint
? `${authReqResult.identifier.did}#${authReqResult.authKey.meta.jwkThumbprint}`
: authReqResult.identifier.kid
await vpLinkHandler.handle(openidUri, { idOpts: { ...authReqResult.identifier, kmsKeyRef: kid } })
}
await onOpenAuthorizationUrl(url)
}
}
export const reviewCredentialsCallback = (context: IRequiredContext) => {
return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
console.log(`# REVIEW CREDENTIALS:`)
console.log(JSON.stringify(state.context.credentialsToAccept, null, 2))
oid4vciMachine.send({
type: OID4VCIMachineEvents.NEXT,
})
}
}
export const siopDoneCallback = ({ oid4vciMachine }: { oid4vciMachine: OID4VCIMachine }, context: IRequiredContext) => {
return async (oid4vpMachine: Siopv2MachineInterpreter, state: Siopv2MachineState) => {
// console.log('SIOP result:')
// console.log(JSON.stringify(state.context, null , 2))
if (!state.context.authorizationResponseData?.queryParams?.code) {
throw Error(`No code was returned from the authorization step`)
}
oid4vciMachine.interpreter.send({
type: OID4VCIMachineEvents.PROVIDE_AUTHORIZATION_CODE_RESPONSE,
data: state.context.authorizationResponseData.url!,
})
console.log(`SIOP DONE!`)
}
}