From e82c6fb2a26d7f071cfe003a0b014e2769ae1758 Mon Sep 17 00:00:00 2001 From: "Jason C. Leach" Date: Thu, 28 Mar 2024 07:50:55 -0700 Subject: [PATCH 1/3] feat: attestation workflow 2 Signed-off-by: Jason C. Leach --- app/src/helpers/Attestation.ts | 10 +-- app/src/helpers/BCIDHelper.ts | 6 +- app/src/screens/PersonCredential.tsx | 116 ++++++++++++++++++--------- 3 files changed, 85 insertions(+), 47 deletions(-) diff --git a/app/src/helpers/Attestation.ts b/app/src/helpers/Attestation.ts index 79a0803fc..d0451b9cc 100644 --- a/app/src/helpers/Attestation.ts +++ b/app/src/helpers/Attestation.ts @@ -102,8 +102,7 @@ const formatForProofWithId = async (agent: BifoldAgent, proofId: string, filterB */ export const isProofRequestingAttestation = async ( proof: ProofExchangeRecord, - agent: BifoldAgent, - attestationCredDefIds: string[] + agent: BifoldAgent ): Promise => { const format = (await agent.proofs.getFormatData(proof.id)) as unknown as AttestationProofRequestFormat const formatToUse = format.request?.anoncreds ? 'anoncreds' : 'indy' @@ -120,10 +119,7 @@ export const isProofRequestingAttestation = async ( * @param attestationCredDefIds Cred def IDs for used attestation * @returns All available attestation credentials */ -export const getAvailableAttestationCredentials = async ( - agent: BifoldAgent, - attestationCredDefIds: string[] -): Promise => { +export const getAvailableAttestationCredentials = async (agent: BifoldAgent): Promise => { const credentials = await agent.credentials.getAll() return credentials.filter((record) => { @@ -176,7 +172,7 @@ export const credentialsMatchForProof = async ( */ export const attestationCredentialRequired = async (agent: BifoldAgent, proofId: string): Promise => { const proof = await agent?.proofs.getById(proofId) - const isAttestation = await isProofRequestingAttestation(proof, agent, attestationCredDefIds) + const isAttestation = await isProofRequestingAttestation(proof, agent) if (!isAttestation) { return false diff --git a/app/src/helpers/BCIDHelper.ts b/app/src/helpers/BCIDHelper.ts index 7756d4c25..5becd05b0 100644 --- a/app/src/helpers/BCIDHelper.ts +++ b/app/src/helpers/BCIDHelper.ts @@ -57,7 +57,7 @@ export const removeExistingInvitationIfRequired = async ( } } -export const receiveBCIDInvite = async ( +export const connectToIASAgent = async ( agent: Agent, store: BCState, t: TFunction<'translation', undefined> @@ -212,7 +212,7 @@ export const authenticateWithServiceCard = async ( } } -export const startFlow = async ( +export const startBCServicesCardAuthenticationWorkflow = async ( agent: Agent, store: BCState, setWorkflowInProgress: React.Dispatch>, @@ -220,7 +220,7 @@ export const startFlow = async ( connectionEstablishedCallback?: (connectionId?: string) => void ) => { try { - const remoteAgentDetails = await receiveBCIDInvite(agent, store, t) + const remoteAgentDetails = await connectToIASAgent(agent, store, t) if (connectionEstablishedCallback) { connectionEstablishedCallback(remoteAgentDetails.connectionId) diff --git a/app/src/screens/PersonCredential.tsx b/app/src/screens/PersonCredential.tsx index 3fca1424b..b1e363c9c 100644 --- a/app/src/screens/PersonCredential.tsx +++ b/app/src/screens/PersonCredential.tsx @@ -1,6 +1,14 @@ -import { ProofState } from '@aries-framework/core' +import { ProofState, ProofExchangeRecord } from '@aries-framework/core' import { useAgent, useProofByState } from '@aries-framework/react-hooks' -import { useConfiguration, useStore, useTheme, Button, ButtonType, testIdWithKey } from '@hyperledger/aries-bifold-core' +import { + useConfiguration, + useStore, + useTheme, + Button, + ButtonType, + testIdWithKey, + BifoldAgent, +} from '@hyperledger/aries-bifold-core' import React, { useState, useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, Text, View, TouchableOpacity, Linking, Platform, ScrollView } from 'react-native' @@ -10,9 +18,10 @@ import Icon from 'react-native-vector-icons/MaterialIcons' import PersonIssuance1 from '../assets/img/PersonIssuance1.svg' import PersonIssuance2 from '../assets/img/PersonIssuance2.svg' import LoadingIcon from '../components/LoadingIcon' -import { startFlow } from '../helpers/BCIDHelper' +import { connectToIASAgent, startBCServicesCardAuthenticationWorkflow } from '../helpers/BCIDHelper' import { useCredentialOfferTrigger } from '../hooks/credential-offer-trigger' import { BCState } from '../store' +import { isProofRequestingAttestation, getAvailableAttestationCredentials } from '../helpers/Attestation' export default function PersonCredential() { const { agent } = useAgent() @@ -84,62 +93,95 @@ export default function PersonCredential() { return await Linking.canOpenURL('ca.bc.gov.id.servicescard://') } + // Use this function to accept the attestation proof request. + const acceptAttestationProofRequest = async (agent: BifoldAgent, proofRequest: ProofExchangeRecord) => { + // This will throw if we don't have the necessary credentials + const credentials = await agent.proofs.selectCredentialsForRequest({ + proofRecordId: proofRequest.id, + }) + + await agent.proofs.acceptRequest({ + proofRecordId: proofRequest.id, + proofFormats: credentials.proofFormats, + }) + } + + // Use this function to connect to the IAS agent and start the + const connectToAgent = async () => { + const remoteAgentDetails = await connectToIASAgent(agent, store, t) + setRemoteAgentConnectionId(remoteAgentDetails.connectionId) + } + + // useEffect(() => { isBCServicesCardInstalled().then((result) => { setAppInstalled(result) }) }, []) - useEffect(() => { + const acceptPersonCredentialOffer = useCallback(() => { + console.log('useEffect connect to IAS agent') + if (!agent) { return } + console.log('A') + + // Start the Spinner and any text that indicates the workflow is in progress + // and the user needs to wait. + setWorkflowInProgress(true) + console.log('B') + + connectToAgent() + .then(() => { + console.log('C') - if (!attestationLoading && !didStartAttestationWorkflow) { - setDidStartAttestationWorkflow(true) + agent.config.logger.error(`Connected to IAS agent, connectionId: ${remoteAgentConnectionId}`) + }) + .catch((error) => { + console.log('D') + + agent.config.logger.error(`Connected to connect to IAS agent, error: ${error.message}`) + }) + }, []) + + useEffect(() => { + console.log('useEffect accept attestation proof request') + // If we are fetching an attestation credential, do no yet have + // a remote connection ID to the IAS agent, or the agent is not + // initialized, do nothing. + console.log('1') + + if (attestationLoading || !remoteAgentConnectionId || !agent) { + console.log('2') return } + console.log('3') + + // TODO:(jl) We need to set a 10 second timeout. - const acceptAttestationProofRequest = async () => { - if (!attestationLoading && didStartAttestationWorkflow && remoteAgentConnectionId) { - // TODO:(jl) These proofs are hidden. If we find any stale ones we should remove - // them by declining them or deleting them. - const proofRequest = receivedProofRequests.find((proof) => proof.connectionId === remoteAgentConnectionId) - if (proofRequest) { - // This will throw if we don't have the necessary credentials - const credentials = await agent.proofs.selectCredentialsForRequest({ - proofRecordId: proofRequest.id, - }) - - await agent.proofs.acceptRequest({ - proofRecordId: proofRequest.id, - proofFormats: credentials.proofFormats, - }) - } - } + // We have an attestation credential and can respond to an + // attestation proof request. + const proofRequest = receivedProofRequests.find((proof) => proof.connectionId === remoteAgentConnectionId) + if (!proofRequest) { + console.log('4') + + // No proof from our IAS Agent to respond to, do nothing. + return } + console.log('5') - acceptAttestationProofRequest() + acceptAttestationProofRequest(agent, proofRequest) .then(() => { + // TODO:(jl) We can unblock the workflow and proceed with + // authentication. agent.config.logger.info(`Accepted IAS attestation proof request.`) }) .catch((error) => { agent.config.logger.error(`Unable to accept IAS attestation proof request, error: ${error.message}`) }) - }, [attestationLoading, receivedProofRequests]) - - const acceptPersonCredentialOffer = useCallback(() => { - if (!agent) { - return - } - - setWorkflowInProgress(true) - // TODO(jl): This should be renamed to something more specific like - // `startBCServicesCardAuthenticationWorkflow` so its obvious what "flow" - // is starting. - startFlow(agent, store, setWorkflowInProgress, t, setRemoteAgentConnectionId) - }, []) + }, [attestationLoading, receivedProofRequests, remoteAgentConnectionId, agent]) const getBCServicesCardApp = useCallback(() => { setAppInstalled(true) From 6b576f3ea83c5dd1d87ea2ea2dec1c61844fa97c Mon Sep 17 00:00:00 2001 From: "Jason C. Leach" Date: Thu, 28 Mar 2024 10:01:59 -0700 Subject: [PATCH 2/3] feat: continue work on attestation Signed-off-by: Jason C. Leach --- app/src/helpers/BCIDHelper.ts | 29 +++--- app/src/hooks/credential-offer-trigger.ts | 25 ----- app/src/screens/PersonCredential.tsx | 112 +++++++++++++++------- 3 files changed, 91 insertions(+), 75 deletions(-) delete mode 100644 app/src/hooks/credential-offer-trigger.ts diff --git a/app/src/helpers/BCIDHelper.ts b/app/src/helpers/BCIDHelper.ts index 5becd05b0..9abbbd731 100644 --- a/app/src/helpers/BCIDHelper.ts +++ b/app/src/helpers/BCIDHelper.ts @@ -138,18 +138,15 @@ export const cleanupAfterServiceCardAuthentication = (status: AuthenticationResu } export const authenticateWithServiceCard = async ( - store: BCState, - setWorkflowInProgress: React.Dispatch>, - agentDetails: WellKnownAgentDetails, - t: TFunction<'translation', undefined>, - callback?: (connectionId?: string) => void + legacyConnectionDid: string, + iasPortalUrl: string, + callback?: (status: boolean) => void ): Promise => { try { - const did = agentDetails.legacyConnectionDid as string - const url = `${store.developer.environment.iasPortalUrl}/${did}` + const url = `${iasPortalUrl}/${legacyConnectionDid}` if (await InAppBrowser.isAvailable()) { - const result = await InAppBrowser.openAuth(url, redirectUrlTemplate.replace('', did), { + const result = await InAppBrowser.openAuth(url, redirectUrlTemplate.replace('', legacyConnectionDid), { // iOS dismissButtonStyle: 'cancel', // Android @@ -166,7 +163,9 @@ export const authenticateWithServiceCard = async ( result.type === AuthenticationResultType.Cancel && typeof (result as unknown as RedirectResult).url === 'undefined' ) { - setWorkflowInProgress(false) + // setWorkflowInProgress(false) + callback && callback(false) + return } @@ -174,17 +173,17 @@ export const authenticateWithServiceCard = async ( result.type === AuthenticationResultType.Dismiss && typeof (result as unknown as RedirectResult).url === 'undefined' ) { - callback && callback(agentDetails.connectionId) + callback && callback(true) } // When `result.type` is "Success" and `result.url` contains the // word "success" the credential offer workflow has been completed. if ( result.type === AuthenticationResultType.Success && - (result as unknown as RedirectResult).url.includes(did) && + (result as unknown as RedirectResult).url.includes(legacyConnectionDid) && (result as unknown as RedirectResult).url.includes('success') ) { - callback && callback(agentDetails.connectionId) + callback && callback(true) } // When `result.type` is "Success" and `result.url` contains the @@ -192,10 +191,11 @@ export const authenticateWithServiceCard = async ( // the user. if ( result.type === AuthenticationResultType.Success && - (result as unknown as RedirectResult).url.includes(did) && + (result as unknown as RedirectResult).url.includes(legacyConnectionDid) && (result as unknown as RedirectResult).url.includes('cancel') ) { - setWorkflowInProgress(false) + callback && callback(false) + // setWorkflowInProgress(false) return } } else { @@ -208,6 +208,7 @@ export const authenticateWithServiceCard = async ( cleanupAfterServiceCardAuthentication( code === ErrorCodes.CanceledByUser ? AuthenticationResultType.Cancel : AuthenticationResultType.Fail ) + DeviceEventEmitter.emit(BifoldEventTypes.ERROR_ADDED, error) } } diff --git a/app/src/hooks/credential-offer-trigger.ts b/app/src/hooks/credential-offer-trigger.ts deleted file mode 100644 index 17d20be81..000000000 --- a/app/src/hooks/credential-offer-trigger.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CredentialState } from '@aries-framework/core' -import { useCredentialByState } from '@aries-framework/react-hooks' -import { Screens, Stacks } from '@hyperledger/aries-bifold-core' -import { useNavigation } from '@react-navigation/native' -import { useEffect } from 'react' - -export const useCredentialOfferTrigger = (workflowConnectionId?: string): void => { - const navigation = useNavigation() - const offers = useCredentialByState(CredentialState.OfferReceived) - - const goToCredentialOffer = (credentialId?: string) => { - navigation.getParent()?.navigate(Stacks.NotificationStack, { - screen: Screens.CredentialOffer, - params: { credentialId }, - }) - } - - useEffect(() => { - for (const credential of offers) { - if (credential.state == CredentialState.OfferReceived && credential.connectionId === workflowConnectionId) { - goToCredentialOffer(credential.id) - } - } - }, [offers, workflowConnectionId]) -} diff --git a/app/src/screens/PersonCredential.tsx b/app/src/screens/PersonCredential.tsx index b1e363c9c..1a5263ed6 100644 --- a/app/src/screens/PersonCredential.tsx +++ b/app/src/screens/PersonCredential.tsx @@ -1,5 +1,5 @@ -import { ProofState, ProofExchangeRecord } from '@aries-framework/core' -import { useAgent, useProofByState } from '@aries-framework/react-hooks' +import { ProofState, ProofExchangeRecord, CredentialState } from '@aries-framework/core' +import { useAgent, useProofByState, useCredentialByState } from '@aries-framework/react-hooks' import { useConfiguration, useStore, @@ -8,7 +8,10 @@ import { ButtonType, testIdWithKey, BifoldAgent, + Screens, + Stacks, } from '@hyperledger/aries-bifold-core' +import { useNavigation } from '@react-navigation/native' import React, { useState, useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, Text, View, TouchableOpacity, Linking, Platform, ScrollView } from 'react-native' @@ -18,10 +21,9 @@ import Icon from 'react-native-vector-icons/MaterialIcons' import PersonIssuance1 from '../assets/img/PersonIssuance1.svg' import PersonIssuance2 from '../assets/img/PersonIssuance2.svg' import LoadingIcon from '../components/LoadingIcon' -import { connectToIASAgent, startBCServicesCardAuthenticationWorkflow } from '../helpers/BCIDHelper' -import { useCredentialOfferTrigger } from '../hooks/credential-offer-trigger' +import { getAvailableAttestationCredentials } from '../helpers/Attestation' +import { connectToIASAgent, authenticateWithServiceCard, WellKnownAgentDetails } from '../helpers/BCIDHelper' import { BCState } from '../store' -import { isProofRequestingAttestation, getAvailableAttestationCredentials } from '../helpers/Attestation' export default function PersonCredential() { const { agent } = useAgent() @@ -30,14 +32,13 @@ export default function PersonCredential() { const [workflowInProgress, setWorkflowInProgress] = useState(false) const { ColorPallet, TextTheme } = useTheme() const { t } = useTranslation() - const [remoteAgentConnectionId, setRemoteAgentConnectionId] = useState() - const [didStartAttestationWorkflow, setDidStartAttestationWorkflow] = useState(false) const { useAttestation } = useConfiguration() - const { loading: attestationLoading } = useAttestation ? useAttestation() : { loading: false } + const receivedCredentialOffers = useCredentialByState(CredentialState.OfferReceived) const receivedProofRequests = useProofByState(ProofState.RequestReceived) - // This fn contains the logic to automatically process the Person Credential - // offer and navigate to the offer accept screen. - useCredentialOfferTrigger(remoteAgentConnectionId) + const navigation = useNavigation() + const [remoteAgentDetails, setRemoteAgentDetails] = useState() + const { loading: attestationLoading } = useAttestation ? useAttestation() : { loading: false } + const [didCompleteAttestationProofRequest, sedDidCompleteAttestationProofRequest] = useState(false) const styles = StyleSheet.create({ pageContainer: { @@ -95,6 +96,12 @@ export default function PersonCredential() { // Use this function to accept the attestation proof request. const acceptAttestationProofRequest = async (agent: BifoldAgent, proofRequest: ProofExchangeRecord) => { + // Sanity check to make sure we have the necessary credentials + const credential = await getAvailableAttestationCredentials(agent) + if (credential.length === 0) { + return false + } + // This will throw if we don't have the necessary credentials const credentials = await agent.proofs.selectCredentialsForRequest({ proofRecordId: proofRequest.id, @@ -104,15 +111,23 @@ export default function PersonCredential() { proofRecordId: proofRequest.id, proofFormats: credentials.proofFormats, }) + + return true + } + + const goToCredentialOffer = (credentialId?: string) => { + navigation.getParent()?.navigate(Stacks.NotificationStack, { + screen: Screens.CredentialOffer, + params: { credentialId }, + }) } // Use this function to connect to the IAS agent and start the const connectToAgent = async () => { const remoteAgentDetails = await connectToIASAgent(agent, store, t) - setRemoteAgentConnectionId(remoteAgentDetails.connectionId) + setRemoteAgentDetails(remoteAgentDetails) } - // useEffect(() => { isBCServicesCardInstalled().then((result) => { setAppInstalled(result) @@ -120,68 +135,93 @@ export default function PersonCredential() { }, []) const acceptPersonCredentialOffer = useCallback(() => { - console.log('useEffect connect to IAS agent') - if (!agent) { return } - console.log('A') // Start the Spinner and any text that indicates the workflow is in progress // and the user needs to wait. setWorkflowInProgress(true) - console.log('B') connectToAgent() .then(() => { - console.log('C') - - agent.config.logger.error(`Connected to IAS agent, connectionId: ${remoteAgentConnectionId}`) + agent.config.logger.error(`Connected to IAS agent, connectionId: ${remoteAgentDetails?.connectionId}`) }) .catch((error) => { - console.log('D') - agent.config.logger.error(`Connected to connect to IAS agent, error: ${error.message}`) }) }, []) useEffect(() => { - console.log('useEffect accept attestation proof request') // If we are fetching an attestation credential, do no yet have // a remote connection ID to the IAS agent, or the agent is not // initialized, do nothing. - console.log('1') - - if (attestationLoading || !remoteAgentConnectionId || !agent) { - console.log('2') - + if (attestationLoading || !remoteAgentDetails || !agent) { return } - console.log('3') // TODO:(jl) We need to set a 10 second timeout. // We have an attestation credential and can respond to an // attestation proof request. - const proofRequest = receivedProofRequests.find((proof) => proof.connectionId === remoteAgentConnectionId) + const proofRequest = receivedProofRequests.find((proof) => proof.connectionId === remoteAgentDetails.connectionId) if (!proofRequest) { - console.log('4') - // No proof from our IAS Agent to respond to, do nothing. return } - console.log('5') acceptAttestationProofRequest(agent, proofRequest) - .then(() => { - // TODO:(jl) We can unblock the workflow and proceed with + .then((status: boolean) => { + // We can unblock the workflow and proceed with // authentication. + sedDidCompleteAttestationProofRequest(status) + agent.config.logger.info(`Accepted IAS attestation proof request.`) }) .catch((error) => { + sedDidCompleteAttestationProofRequest(false) + agent.config.logger.error(`Unable to accept IAS attestation proof request, error: ${error.message}`) }) - }, [attestationLoading, receivedProofRequests, remoteAgentConnectionId, agent]) + }, [attestationLoading, receivedProofRequests, remoteAgentDetails, agent]) + + useEffect(() => { + if (!remoteAgentDetails || !remoteAgentDetails.legacyConnectionDid || !didCompleteAttestationProofRequest) { + return + } + + const cb = (status: boolean) => { + agent!.config.logger.error(`Service card authentication reported ${status}`) + + setWorkflowInProgress(false) + } + + const { iasPortalUrl } = store.developer.environment + const { legacyConnectionDid } = remoteAgentDetails + + authenticateWithServiceCard(legacyConnectionDid, iasPortalUrl, cb) + .then(() => { + agent!.config.logger.error('Completed service card authentication successfully') + }) + .catch((error) => { + agent!.config.logger.error('Completed service card authentication with errir, error: ', error.message) + }) + }, [remoteAgentDetails, didCompleteAttestationProofRequest]) + + useEffect(() => { + if (!remoteAgentDetails || !remoteAgentDetails.connectionId) { + return + } + + for (const credential of receivedCredentialOffers) { + if ( + credential.state == CredentialState.OfferReceived && + credential.connectionId === remoteAgentDetails.connectionId + ) { + goToCredentialOffer(credential.id) + } + } + }, [receivedCredentialOffers, remoteAgentDetails]) const getBCServicesCardApp = useCallback(() => { setAppInstalled(true) From 82a163a2a631e919afcdd8b914e2d9cc575e64ac Mon Sep 17 00:00:00 2001 From: "Jason C. Leach" Date: Thu, 28 Mar 2024 10:10:14 -0700 Subject: [PATCH 3/3] feat: continue work on attestation Signed-off-by: Jason C. Leach --- app/src/screens/PersonCredential.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/screens/PersonCredential.tsx b/app/src/screens/PersonCredential.tsx index 1a5263ed6..c23a23030 100644 --- a/app/src/screens/PersonCredential.tsx +++ b/app/src/screens/PersonCredential.tsx @@ -115,6 +115,8 @@ export default function PersonCredential() { return true } + // when a person credential offer is received, show the + // offer screen to the user. const goToCredentialOffer = (credentialId?: string) => { navigation.getParent()?.navigate(Stacks.NotificationStack, { screen: Screens.CredentialOffer, @@ -122,12 +124,6 @@ export default function PersonCredential() { }) } - // Use this function to connect to the IAS agent and start the - const connectToAgent = async () => { - const remoteAgentDetails = await connectToIASAgent(agent, store, t) - setRemoteAgentDetails(remoteAgentDetails) - } - useEffect(() => { isBCServicesCardInstalled().then((result) => { setAppInstalled(result) @@ -135,7 +131,7 @@ export default function PersonCredential() { }, []) const acceptPersonCredentialOffer = useCallback(() => { - if (!agent) { + if (!agent || !store || !t) { return } @@ -143,8 +139,10 @@ export default function PersonCredential() { // and the user needs to wait. setWorkflowInProgress(true) - connectToAgent() - .then(() => { + connectToIASAgent(agent, store, t) + .then((remoteAgentDetails: WellKnownAgentDetails) => { + setRemoteAgentDetails(remoteAgentDetails) + agent.config.logger.error(`Connected to IAS agent, connectionId: ${remoteAgentDetails?.connectionId}`) }) .catch((error) => {