Skip to content

Commit

Permalink
fix: attestation workflow 2 (#1890)
Browse files Browse the repository at this point in the history
Signed-off-by: Jason C. Leach <[email protected]>
  • Loading branch information
jleach authored Mar 28, 2024
1 parent dfa6903 commit 8a71739
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 92 deletions.
10 changes: 3 additions & 7 deletions app/src/helpers/Attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> => {
const format = (await agent.proofs.getFormatData(proof.id)) as unknown as AttestationProofRequestFormat
const formatToUse = format.request?.anoncreds ? 'anoncreds' : 'indy'
Expand All @@ -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<CredentialExchangeRecord[]> => {
export const getAvailableAttestationCredentials = async (agent: BifoldAgent): Promise<CredentialExchangeRecord[]> => {
const credentials = await agent.credentials.getAll()

return credentials.filter((record) => {
Expand Down Expand Up @@ -176,7 +172,7 @@ export const credentialsMatchForProof = async (
*/
export const attestationCredentialRequired = async (agent: BifoldAgent, proofId: string): Promise<boolean> => {
const proof = await agent?.proofs.getById(proofId)
const isAttestation = await isProofRequestingAttestation(proof, agent, attestationCredDefIds)
const isAttestation = await isProofRequestingAttestation(proof, agent)

if (!isAttestation) {
return false
Expand Down
35 changes: 18 additions & 17 deletions app/src/helpers/BCIDHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const removeExistingInvitationIfRequired = async (
}
}

export const receiveBCIDInvite = async (
export const connectToIASAgent = async (
agent: Agent,
store: BCState,
t: TFunction<'translation', undefined>
Expand Down Expand Up @@ -138,18 +138,15 @@ export const cleanupAfterServiceCardAuthentication = (status: AuthenticationResu
}

export const authenticateWithServiceCard = async (
store: BCState,
setWorkflowInProgress: React.Dispatch<React.SetStateAction<boolean>>,
agentDetails: WellKnownAgentDetails,
t: TFunction<'translation', undefined>,
callback?: (connectionId?: string) => void
legacyConnectionDid: string,
iasPortalUrl: string,
callback?: (status: boolean) => void
): Promise<void> => {
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>', did), {
const result = await InAppBrowser.openAuth(url, redirectUrlTemplate.replace('<did>', legacyConnectionDid), {
// iOS
dismissButtonStyle: 'cancel',
// Android
Expand All @@ -166,36 +163,39 @@ export const authenticateWithServiceCard = async (
result.type === AuthenticationResultType.Cancel &&
typeof (result as unknown as RedirectResult).url === 'undefined'
) {
setWorkflowInProgress(false)
// setWorkflowInProgress(false)
callback && callback(false)

return
}

if (
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
// word "cancel" the credential offer workflow has been canceled by
// 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 {
Expand All @@ -208,19 +208,20 @@ export const authenticateWithServiceCard = async (
cleanupAfterServiceCardAuthentication(
code === ErrorCodes.CanceledByUser ? AuthenticationResultType.Cancel : AuthenticationResultType.Fail
)

DeviceEventEmitter.emit(BifoldEventTypes.ERROR_ADDED, error)
}
}

export const startFlow = async (
export const startBCServicesCardAuthenticationWorkflow = async (
agent: Agent,
store: BCState,
setWorkflowInProgress: React.Dispatch<React.SetStateAction<boolean>>,
t: TFunction<'translation', undefined>,
connectionEstablishedCallback?: (connectionId?: string) => void
) => {
try {
const remoteAgentDetails = await receiveBCIDInvite(agent, store, t)
const remoteAgentDetails = await connectToIASAgent(agent, store, t)

if (connectionEstablishedCallback) {
connectionEstablishedCallback(remoteAgentDetails.connectionId)
Expand Down
25 changes: 0 additions & 25 deletions app/src/hooks/credential-offer-trigger.ts

This file was deleted.

166 changes: 123 additions & 43 deletions app/src/screens/PersonCredential.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { ProofState } 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 { ProofState, ProofExchangeRecord, CredentialState } from '@aries-framework/core'
import { useAgent, useProofByState, useCredentialByState } from '@aries-framework/react-hooks'
import {
useConfiguration,
useStore,
useTheme,
Button,
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'
Expand All @@ -10,8 +21,8 @@ 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 { useCredentialOfferTrigger } from '../hooks/credential-offer-trigger'
import { getAvailableAttestationCredentials } from '../helpers/Attestation'
import { connectToIASAgent, authenticateWithServiceCard, WellKnownAgentDetails } from '../helpers/BCIDHelper'
import { BCState } from '../store'

export default function PersonCredential() {
Expand All @@ -21,14 +32,13 @@ export default function PersonCredential() {
const [workflowInProgress, setWorkflowInProgress] = useState<boolean>(false)
const { ColorPallet, TextTheme } = useTheme()
const { t } = useTranslation()
const [remoteAgentConnectionId, setRemoteAgentConnectionId] = useState<string | undefined>()
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<WellKnownAgentDetails | undefined>()
const { loading: attestationLoading } = useAttestation ? useAttestation() : { loading: false }
const [didCompleteAttestationProofRequest, sedDidCompleteAttestationProofRequest] = useState<boolean>(false)

const styles = StyleSheet.create({
pageContainer: {
Expand Down Expand Up @@ -84,62 +94,132 @@ 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) => {
// 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,
})

await agent.proofs.acceptRequest({
proofRecordId: proofRequest.id,
proofFormats: credentials.proofFormats,
})

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,
params: { credentialId },
})
}

useEffect(() => {
isBCServicesCardInstalled().then((result) => {
setAppInstalled(result)
})
}, [])

useEffect(() => {
if (!agent) {
const acceptPersonCredentialOffer = useCallback(() => {
if (!agent || !store || !t) {
return
}

if (!attestationLoading && !didStartAttestationWorkflow) {
setDidStartAttestationWorkflow(true)
// Start the Spinner and any text that indicates the workflow is in progress
// and the user needs to wait.
setWorkflowInProgress(true)

connectToIASAgent(agent, store, t)
.then((remoteAgentDetails: WellKnownAgentDetails) => {
setRemoteAgentDetails(remoteAgentDetails)

agent.config.logger.error(`Connected to IAS agent, connectionId: ${remoteAgentDetails?.connectionId}`)
})
.catch((error) => {
agent.config.logger.error(`Connected to connect to IAS agent, error: ${error.message}`)
})
}, [])

useEffect(() => {
// 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.
if (attestationLoading || !remoteAgentDetails || !agent) {
return
}

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,
})
}
}
// 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 === remoteAgentDetails.connectionId)
if (!proofRequest) {
// No proof from our IAS Agent to respond to, do nothing.
return
}

acceptAttestationProofRequest()
.then(() => {
acceptAttestationProofRequest(agent, proofRequest)
.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])
}, [attestationLoading, receivedProofRequests, remoteAgentDetails, agent])

const acceptPersonCredentialOffer = useCallback(() => {
if (!agent) {
useEffect(() => {
if (!remoteAgentDetails || !remoteAgentDetails.legacyConnectionDid || !didCompleteAttestationProofRequest) {
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)
}, [])
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)
Expand Down

0 comments on commit 8a71739

Please sign in to comment.