Skip to content

Commit

Permalink
feat: add progress and card attached to auth flow (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoGlastra authored Aug 15, 2024
1 parent fc21784 commit 955dbd1
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 21 deletions.
47 changes: 42 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,19 @@ Note that this class is optimized for a simple auth flow and thus it may not fit
```tsx
import { AusweisAuthFlow } from '@animo-id/expo-ausweis-sdk'
import { useState } from 'react'
import { Button } from 'react-native'
import { StyleSheet, Text, View } from 'react-native'
import { Button, StyleSheet, Text, View } from 'react-native'

export default function App() {
const [message, setMessage] = useState<string>()
const [flow, setFlow] = useState<AusweisAuthFlow>()

const [cardAttachRequested, setCardAttachRequested] = useState(false)
const [isCardAttached, setIsCardAttached] = useState(false)
const [progress, setProgress] = useState(0)

const [requestedAccessRights, setRequestedAccessRights] = useState<string[]>()
const [onAcceptAccessRights, setOnAcceptAccessRights] = useState<(accept: boolean) => void>()

const cancelFlow = () =>
flow
?.cancel()
Expand All @@ -166,23 +172,41 @@ export default function App() {
setMessage(undefined)
setFlow(
new AusweisAuthFlow({
debug: true,
onEnterPin: ({ attemptsRemaining }) => {
// Mock incorrect pin entry
return attemptsRemaining === 1 ? '123456' : '123123'
},
onError: ({ message, reason }) => {
setFlow(undefined)
setCardAttachRequested(false)
setProgress(0)
setMessage(`${reason}: ${message}`)
},
onSuccess: () => {
setFlow(undefined)
setProgress(100)
setCardAttachRequested(false)
setMessage('Successfully ran auth flow')
},
onInsertCard: () => {
// For iOS this will show the NFC scanner modal. on Android we need
onAttachCard: () => {
// iOS will already show the NFC scanner modal, but on Android we need
// use this callback to show the NFC scanner modal.
console.log('please insert card')
setCardAttachRequested(true)
},
onCardAttachedChanged: ({ isCardAttached }) => setIsCardAttached(isCardAttached),
onStatusProgress: ({ progress }) => setProgress(progress),
onRequestAccessRights: ({ effective }) =>
new Promise((resolve) => {
setRequestedAccessRights(effective)
setOnAcceptAccessRights(() => {
return (accept: boolean) => {
resolve({ acceptAccessRights: accept })
setOnAcceptAccessRights(undefined)
setRequestedAccessRights(undefined)
}
})
}),
}).start({
tcTokenUrl: 'https://test.governikus-eid.de/AusweisAuskunft/WebServiceRequesterServlet',
})
Expand All @@ -192,6 +216,19 @@ export default function App() {
return (
<View style={[StyleSheet.absoluteFill, { flex: 1, alignContent: 'center', justifyContent: 'center' }]}>
<Button onPress={flow ? cancelFlow : runAuthFlow} title={flow ? 'Cancel' : 'Start Auth Flow'} />
{flow && <Text>Progress: {progress}%</Text>}
{flow && <Text>Is card attached: {isCardAttached ? 'Yes' : 'No'}</Text>}
{flow && cardAttachRequested && <Text>Please present your card to the NFC scanner</Text>}
{flow && requestedAccessRights && (
<>
<Text>
Requested Access Rights:
{'\n -'}
{requestedAccessRights.join('\n- ')}
</Text>
<Button title="Accept" onPress={() => onAcceptAccessRights?.(true)} />
</>
)}
{message && <Text>{message}</Text>}
</View>
)
Expand Down
47 changes: 42 additions & 5 deletions ausweis-example/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { AusweisAuthFlow } from '@animo-id/expo-ausweis-sdk'
import { useState } from 'react'
import { Button } from 'react-native'
import { StyleSheet, Text, View } from 'react-native'
import { Button, StyleSheet, Text, View } from 'react-native'

export default function App() {
const [message, setMessage] = useState<string>()
const [flow, setFlow] = useState<AusweisAuthFlow>()

const [cardAttachRequested, setCardAttachRequested] = useState(false)
const [isCardAttached, setIsCardAttached] = useState(false)
const [progress, setProgress] = useState(0)

const [requestedAccessRights, setRequestedAccessRights] = useState<string[]>()
const [onAcceptAccessRights, setOnAcceptAccessRights] = useState<(accept: boolean) => void>()

const cancelFlow = () =>
flow
?.cancel()
Expand All @@ -17,23 +23,41 @@ export default function App() {
setMessage(undefined)
setFlow(
new AusweisAuthFlow({
debug: true,
onEnterPin: ({ attemptsRemaining }) => {
// Mock incorrect pin entry
return attemptsRemaining === 1 ? '123456' : '123123'
},
onError: ({ message, reason }) => {
setFlow(undefined)
setCardAttachRequested(false)
setProgress(0)
setMessage(`${reason}: ${message}`)
},
onSuccess: () => {
setFlow(undefined)
setProgress(100)
setCardAttachRequested(false)
setMessage('Successfully ran auth flow')
},
onInsertCard: () => {
// For iOS this will show the NFC scanner modal. on Android we need
onAttachCard: () => {
// iOS will already show the NFC scanner modal, but on Android we need
// use this callback to show the NFC scanner modal.
console.log('please insert card')
setCardAttachRequested(true)
},
onCardAttachedChanged: ({ isCardAttached }) => setIsCardAttached(isCardAttached),
onStatusProgress: ({ progress }) => setProgress(progress),
onRequestAccessRights: ({ effective }) =>
new Promise((resolve) => {
setRequestedAccessRights(effective)
setOnAcceptAccessRights(() => {
return (accept: boolean) => {
resolve({ acceptAccessRights: accept })
setOnAcceptAccessRights(undefined)
setRequestedAccessRights(undefined)
}
})
}),
}).start({
tcTokenUrl: 'https://test.governikus-eid.de/AusweisAuskunft/WebServiceRequesterServlet',
})
Expand All @@ -43,6 +67,19 @@ export default function App() {
return (
<View style={[StyleSheet.absoluteFill, { flex: 1, alignContent: 'center', justifyContent: 'center' }]}>
<Button onPress={flow ? cancelFlow : runAuthFlow} title={flow ? 'Cancel' : 'Start Auth Flow'} />
{flow && <Text>Progress: {progress}%</Text>}
{flow && <Text>Is card attached: {isCardAttached ? 'Yes' : 'No'}</Text>}
{flow && cardAttachRequested && <Text>Please present your card to the NFC scanner</Text>}
{flow && requestedAccessRights && (
<>
<Text>
Requested Access Rights:
{'\n -'}
{requestedAccessRights.join('\n- ')}
</Text>
<Button title="Accept" onPress={() => onAcceptAccessRights?.(true)} />
</>
)}
{message && <Text>{message}</Text>}
</View>
)
Expand Down
135 changes: 124 additions & 11 deletions src/AusweisAuthFlow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type AusweisSdkAccessRightsMessage,
type AusweisSdkAuthMessage,
type AusweisSdkCommand,
type AusweisSdkEnterPinMessage,
Expand Down Expand Up @@ -33,9 +34,60 @@ export interface AusweisAuthFlowOptions {
onEnterPin: (options: OnEnterPinOptions) => Promise<string> | string

/**
* Callback to notify that the card should be inserted/placed on the NFC scanner.
* callback that will be called when a card is attached/detached from the NFC scanner.
*/
onInsertCard?: () => void
onCardAttachedChanged?: (options: { isCardAttached: boolean }) => void

/**
* callback that will be called with status updates on the auth flow progress.
*/
onStatusProgress?: (options: {
/**
* number between 0 and 100 indicating the progress of the auth flow
*/
progress: number
}) => void

/**
* Callback to notify that the card should be attached/placed on the NFC scanner.
*/
onAttachCard?: () => void

/**
* Callback that will be called when the sdk asks for confirmation of access rights.
* If this callback is not provided, the access rights will be automatically accepted.
*
* It will wait for the promise to be resolved, and has no configured timeout.
* If you need to cancel the flow, you should throw/reject the promise or return false for
* for `acceptAccessRights`.
*/
onRequestAccessRights?: (options: {
/**
* Current configured access rights to grant, will be used as the access rights if `true` is returned
* for `acceptAccessRights`.
*/
effective: string[]

/**
* The required access rights. If `acceptAccessRights` is an array and does not contain all access
* rights from the `required` array, the flow will be cancelled as it's not possible to continue.
*/
required: string[]

/**
* Optional access rights. You can include this in the `acceptAccessRights` array, but it is not required.
*/
optional: string[]
}) => Promise<{
/**
* Whether to accept the access rights.
* - `true` - Accept based on passed `effective` access rights
* - `false` - Do not accept (will cancel the auth flow)
* - `string[]` - Accept the access rights from the array. Flow will fail if not all
* `required` access rights are provided.
*/
acceptAccessRights: true | false | string[]
}>

/**
* Callback that will be called when the authentication flow succeeded.
Expand All @@ -51,6 +103,11 @@ export interface AusweisAuthFlowOptions {
* - An action is needed that is not supported by this flow, such as ENTER_CAN or ENTER_PUK
*/
onError: (details: OnErrorDetails) => void

/**
* will enable logging of commands and messages sent/received
*/
debug?: boolean
}

export interface AusweisAuthFlowStartOptions {
Expand Down Expand Up @@ -165,13 +222,29 @@ export class AusweisAuthFlow {

// NOTE: arrow function to have correct binding of this.
private onMessage = (message: AusweisSdkMessage) => {
// TODO: should probably let the user handle access rights?
this.debug('Received message from ausweis sdk', JSON.stringify(message, null, 2))

if (message.msg === 'ACCESS_RIGHTS') {
this.acceptAccessRights()
this.handleAccessRights(message)
}

if (message.msg === 'READER') {
// If card is empty object the card is unknown, we see that as no card attached for this flow
const isCardAttached = message.card !== null && Object.keys(message.card).length > 0

this.options.onCardAttachedChanged?.({
isCardAttached,
})
}

if (message.msg === 'STATUS' && message.workflow === 'AUTH' && typeof message.progress === 'number') {
this.options.onStatusProgress?.({
progress: message.progress,
})
}

if (message.msg === 'INSERT_CARD') {
this.options.onInsertCard?.()
this.options.onAttachCard?.()
}

if (message.msg === 'ENTER_PIN') {
Expand All @@ -190,6 +263,46 @@ export class AusweisAuthFlow {
}
}

private async handleAccessRights(message: AusweisSdkAccessRightsMessage) {
try {
const { acceptAccessRights } = (await this.options.onRequestAccessRights?.({
effective: message.chat.effective,
optional: message.chat.optional,
required: message.chat.required,
})) ?? { acceptAccessRights: true }

if (!acceptAccessRights) {
return this.handleError({
reason: 'user_cancelled',
message: 'Access rights were declined',
})
}

if (Array.isArray(acceptAccessRights)) {
if (!message.chat.required.every((requiredRight) => acceptAccessRights.includes(requiredRight))) {
return this.handleError({
reason: 'user_cancelled',
message: `Not all access rights were accepted. Required are ${message.chat.required.join(', ')}, accepted are ${acceptAccessRights.join(', ')}`,
})
}

this.sendCommand({
cmd: 'SET_ACCESS_RIGHTS',
chat: acceptAccessRights,
})
}
this.sendCommand({
cmd: 'ACCEPT',
})
} catch (error) {
this.handleError({
message: 'Error in onRequestAccessRights callback',
reason: 'unknown',
error,
})
}
}

private async handleEnterPin(message: AusweisSdkEnterPinMessage) {
const retryCounter = message.reader.card?.retryCounter ?? 3

Expand Down Expand Up @@ -234,13 +347,8 @@ export class AusweisAuthFlow {
})
}

private async acceptAccessRights() {
this.sendCommand({
cmd: 'ACCEPT',
})
}

private sendCommand(command: AusweisSdkCommand) {
this.debug('Sending command to ausweis sdk', JSON.stringify(command, null, 2))
sendCommand(command)
this.sentCommands.push(command)
}
Expand All @@ -267,4 +375,9 @@ export class AusweisAuthFlow {
throw new Error('Auth flow not in progress')
}
}

private debug(...args: unknown[]) {
if (!this.options.debug) return
console.log(...args)
}
}

0 comments on commit 955dbd1

Please sign in to comment.