Improve code, logic and validation of Biometrics auth

This commit is contained in:
Nichole Martinez
2022-03-21 20:46:17 +08:00
parent 159dbfe8d4
commit 999386aae4
6 changed files with 159 additions and 60 deletions

View File

@@ -13,7 +13,8 @@ const model = createModel(
{
events: {
SETUP_PASSCODE: (passcode: string) => ({ passcode }),
SETUP_BIOMETRICS: () => ({}),
SETUP_BIOMETRICS: (biometrics: string) => ({ biometrics }),
RESET_BIOMETRICS: () => ({}),
LOGOUT: () => ({}),
LOGIN: () => ({}),
STORE_RESPONSE: (response?: unknown) => ({ response }),
@@ -23,6 +24,9 @@ const model = createModel(
export const AuthEvents = model.events;
type SetupBiometricsEvent = EventFrom<typeof model, 'SETUP_BIOMETRICS'>;
export const authMachine = model.createMachine(
{
tsTypes: {} as import('./auth.typegen').Typegen0,
@@ -74,6 +78,10 @@ export const authMachine = model.createMachine(
unauthorized: {
on: {
LOGIN: 'authorized',
RESET_BIOMETRICS: {
target: 'settingUp',
actions: ['setBiometrics', 'storeContext'],
}
},
},
authorized: {
@@ -109,15 +117,19 @@ export const authMachine = model.createMachine(
}),
setBiometrics: model.assign({
biometrics: () => 'true',
biometrics: (_, event: SetupBiometricsEvent) => event.biometrics,
}),
},
guards: {
hasData: (_, event: StoreResponseEvent) => event.response != null,
hasPasscodeSet: (context) => context.passcode !== '',
hasBiometricSet: (context) => context.biometrics !== ''
hasPasscodeSet: (context) => {
return context.passcode !== ''
},
hasBiometricSet: (context) => {
return context.biometrics !== ''
}
},
}
);

View File

@@ -1,6 +1,6 @@
import { createModel } from "xstate/lib/model";
import * as LocalAuthentication from 'expo-local-authentication';
import { assign, StateFrom } from "xstate";
import { assign, send, StateFrom } from "xstate";
// --- CREATE MODEL -----------------------------------------------------------
@@ -9,11 +9,13 @@ const model = createModel(
isAvailable : false,
authTypes : [],
isEnrolled : false,
status : null
status : null,
retry : false,
},
{
events: {
AUTHENTICATE: () => ({})
AUTHENTICATE: () => ({}),
RETRY_AUTHENTICATE: () => ({})
}
}
);
@@ -90,11 +92,11 @@ export const biometricsMachine = model.createMachine(
authenticating: {
invoke: {
src: () => async () => {
console.log('[BIOMETRIC_MACHINE] authenticating invoked');
// console.log('[BIOMETRIC_MACHINE] authenticating invoked');
let res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Biometric Authentication',
})
console.log("[BIOMETRIC_MACHINE] authenticating result", res)
// console.log("[BIOMETRIC_MACHINE] authenticating result", res)
return res.success;
},
onError: 'failure',
@@ -105,6 +107,18 @@ export const biometricsMachine = model.createMachine(
},
},
reauthenticating: {
always: [
{
target: 'authenticating',
cond: 'checkIfAvailable'
},
{
target: 'failure.unenrolled'
}
]
},
// checks authentication status
authentication: {
always: [
@@ -130,12 +144,21 @@ export const biometricsMachine = model.createMachine(
meta: 'Device does not support Biometrics'
},
unenrolled: {
meta: 'To use Biometrics, please enroll your fingerprint in your device settings'
meta: 'To use Biometrics, please enroll your fingerprint in your device settings',
on: {
RETRY_AUTHENTICATE: {
actions: assign({retry: (context, event) => true}),
target: [
'#biometrics.initEnrolled',
'#biometrics.reauthenticating'
]
}
}
},
failed: {
after: {
// after 3 seconds, transition to available
3000: { target: '..available' }
// after 1 seconds, transition to available
1000: { target: '#biometrics.available' }
},
meta: 'Failed to authenticate with Biometrics'
},
@@ -149,7 +172,8 @@ export const biometricsMachine = model.createMachine(
},
{
actions: {},
actions: {
},
guards: {
checkIfAvailable: ctx => ctx.isAvailable && ctx.isEnrolled,
checkIfUnavailable: ctx => !ctx.isAvailable,
@@ -167,6 +191,28 @@ export const BiometricsEvents = model.events;
type State = StateFrom<typeof biometricsMachine>;
export function selectFailMessage(state: State) {
return Object.values(state.meta).join(', ');
}
export function selectIsEnabled(state: State) {
return state.matches('available');
return state.matches('available') ||
state.matches({ failure: 'unenrolled' });
}
export function selectIsUnvailable(state: State) {
return state.matches({ failure: 'unavailable' });
}
export function selectIsSuccess(state: State) {
return state.matches('success');
}
export function selectError(state: State) {
return state.matches({failure: 'error'}) ? selectFailMessage(state) : '';
}
export function selectUnenrolledNotice(state: State) {
// console.log("the unenrolled context is?", state.context)
return state.matches({ failure: 'unenrolled' }) && state.context.retry ? selectFailMessage(state) : '';
}

View File

@@ -8,14 +8,13 @@ import { useAuthScreen } from './AuthScreenController';
export const AuthScreen: React.FC<RootRouteProps> = (props) => {
const controller = useAuthScreen(props);
// console.log('-------->CONTROLLER', controller)
return (
<Column fill padding="32" backgroundColor={Colors.White}>
<MessageOverlay
isVisible={controller.hasAlertMsg != null}
isVisible={controller.alertMsg != ''}
onBackdropPress={controller.hideAlert}
title={controller.hasAlertMsg}
title={controller.alertMsg}
/>
<Column>
<Text align="center">
@@ -29,7 +28,7 @@ export const AuthScreen: React.FC<RootRouteProps> = (props) => {
<Button
title="Use biometrics"
margin="0 0 8 0"
disabled={!controller.enableBiometric}
disabled={!controller.isEnabledBio}
onPress={controller.useBiometrics}
/>
<Button

View File

@@ -1,22 +1,37 @@
import { useInterpret, useMachine, useSelector } from '@xstate/react';
import { useMachine, useSelector } from '@xstate/react';
import { useContext, useEffect, useState } from 'react';
import { AuthEvents, selectSettingUp, selectAuthorized } from '../machines/auth';
import { RootRouteProps } from '../routes';
import { GlobalContext } from '../shared/GlobalContext';
import { biometricsMachine, selectIsEnabled } from '../machines/biometrics';
import {
biometricsMachine,
selectError,
selectIsEnabled,
selectIsSuccess,
selectIsUnvailable,
selectUnenrolledNotice
} from '../machines/biometrics';
export function useAuthScreen(props: RootRouteProps) {
const { appService } = useContext(GlobalContext);
const authService = appService.children.get('auth');
const [hasAlertMsg, setHasAlertMsg] = useState(null);
const authService = appService.children.get('auth');
const isSettingUp = useSelector(authService, selectSettingUp);
const isAuthorized = useSelector(authService, selectAuthorized);
const isSettingUp = useSelector(authService, selectSettingUp);
const isAuthorized = useSelector(authService, selectAuthorized);
const biometricService = useInterpret(biometricsMachine);
const [biometricState, biometricSend] = useMachine(biometricsMachine);
const [alertMsg, setHasAlertMsg] = useState('');
const [
biometricState,
biometricSend,
bioService
] = useMachine(biometricsMachine);
const isEnabledBio:boolean = useSelector(bioService, selectIsEnabled);
const isUnavailableBio:boolean = useSelector(bioService, selectIsUnvailable);
const isSuccessBio:boolean = useSelector(bioService, selectIsSuccess);
const errorMsgBio:string = useSelector(bioService, selectError);
const unEnrolledNoticeBio:string = useSelector(bioService, selectUnenrolledNotice);
const enableBiometric:boolean = useSelector(biometricService, selectIsEnabled);
useEffect(() => {
@@ -28,43 +43,52 @@ export function useAuthScreen(props: RootRouteProps) {
return
}
console.log("[BIOMETRICS] value", biometricState.value);
console.log("[BIOMETRICS] context", biometricState.context);
// if biometic state is success then lets send auth service BIOMETRICS
if (biometricState.matches('success')) {
authService.send(AuthEvents.SETUP_BIOMETRICS());
if (isSuccessBio) {
authService.send(AuthEvents.SETUP_BIOMETRICS('true'));
// handle biometric failure unknown error
} else if (errorMsgBio) {
// show alert message whenever biometric state gets failure
setHasAlertMsg(errorMsgBio);
// handle any unenrolled notice
} else if (unEnrolledNoticeBio) {
setHasAlertMsg(unEnrolledNoticeBio);
// we dont need to see this page to user once biometric is unavailable on its device
} else if (isUnavailableBio) {
props.navigation.navigate('Passcode', { setup: isSettingUp });
}
}, [
isAuthorized,
isSuccessBio,
isUnavailableBio,
errorMsgBio,
unEnrolledNoticeBio,
]);
const useBiometrics = () => {
if (biometricState.matches({failure: 'unenrolled'})) {
biometricSend({ type: 'RETRY_AUTHENTICATE' });
return;
}
// handle biometric failure
if (biometricState.matches('failure')) {
// we dont need to see this page to user once biometric is unavailable on its device
if (biometricState.matches({ failure: 'unavailable' })) {
props.navigation.navigate('Passcode', { setup: isSettingUp });
return;
}
// show alert message whenever biometric state gets failure
setHasAlertMsg(Object.values(biometricState.meta).join(', '));
}
}, [isAuthorized, biometricState]);
const useBiometrics = () => {
biometricSend({ type: 'AUTHENTICATE' });
};
const hideAlert = () => {
setHasAlertMsg(null);
setHasAlertMsg('');
}
return {
isSettingUp,
hasAlertMsg,
enableBiometric,
alertMsg,
isEnabledBio,
hideAlert,
useBiometrics,
usePasscode: () => {

View File

@@ -1,5 +1,5 @@
import { useMachine, useSelector } from '@xstate/react';
import { useContext, useEffect } from 'react';
import { useContext, useEffect, useState } from 'react';
import { AuthEvents, selectAuthorized } from '../machines/auth';
import { RootRouteProps } from '../routes';
import { GlobalContext } from '../shared/GlobalContext';
@@ -9,7 +9,8 @@ export function useBiometricScreen(props: RootRouteProps) {
const { appService } = useContext(GlobalContext);
const authService = appService.children.get('auth');
const [biometricState, biometricSend] = useMachine(biometricsMachine);
const [initAuthBio, updateInitAuthBio] = useState(true);
const [bioState, bioSend] = useMachine(biometricsMachine);
const isAuthorized = useSelector(authService, selectAuthorized);
@@ -22,19 +23,36 @@ export function useBiometricScreen(props: RootRouteProps) {
return;
}
console.log("[BIOMETRICS] value", biometricState.value);
console.log("[BIOMETRICS] context", biometricState.context);
if (initAuthBio && bioState.matches('available')) {
bioSend({ type: 'AUTHENTICATE' });
// so we only init authentication of biometrics just once
updateInitAuthBio(false);
}
// if biometic state is success then lets send auth service BIOMETRICS
if (biometricState.matches('success')) {
if (bioState.matches('success')) {
authService.send(AuthEvents.LOGIN());
return;
}
}, [isAuthorized, biometricState]);
if (
bioState.matches({ failure: 'unavailable' }) ||
bioState.matches({ failure: 'unenrolled' })
) {
authService.send(AuthEvents.RESET_BIOMETRICS());
props.navigation.reset({
index: 0,
routes: [{ name: 'Auth' }],
});
return;
}
}, [isAuthorized, bioState]);
const useBiometrics = () => {
biometricSend({ type: 'AUTHENTICATE' });
bioSend({ type: 'AUTHENTICATE' });
};
return {

View File

@@ -18,7 +18,7 @@ export function useWelcomeScreen(props: RootRouteProps) {
unlockPage: () => {
if (!isSettingUp && passcode !== '') {
props.navigation.navigate('Passcode', { setup: isSettingUp });
} else if (!isSettingUp && biometrics) {
} else if (!isSettingUp && biometrics !== '') {
props.navigation.navigate('Biometric', { setup: isSettingUp });
} else {
props.navigation.navigate('Auth');