Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: attestation workflow 2 #1890

Merged
merged 3 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading