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

Implement Biometrics Authentication #125

Merged
merged 24 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
179d3ce
initial
danicaMat Feb 28, 2022
db6d76d
initial
danicaMat Feb 28, 2022
5d4a566
Fix conflict
nicholemnl Mar 18, 2022
4be2aa1
Add fix for LocalAuthentication errors
rjmangubat23 Mar 17, 2022
9913b18
Fix conflict from rebase develop
nicholemnl Mar 22, 2022
b221fe5
initial
danicaMat Feb 28, 2022
b753de3
resolve conflict from rebase develop
nicholemnl Mar 22, 2022
e7cffa5
Init biometrics machine
nicholemnl Mar 21, 2022
159dbfe
Fix auth with Biometrics logic and validations
nicholemnl Mar 21, 2022
999386a
Improve code, logic and validation of Biometrics auth
nicholemnl Mar 21, 2022
e43755d
Improve code for biometric screen
nicholemnl Mar 22, 2022
bda3b3f
Apply logic to always have passcode
nicholemnl Mar 22, 2022
4848e75
Fix navigation from biometrics to passcode
nicholemnl Mar 22, 2022
1aaf45e
Clean code
nicholemnl Mar 22, 2022
51c73b5
Resolve conflict from develop rebase
nicholemnl Mar 24, 2022
e82fe47
Clean code and prepare code to disable many attempts
nicholemnl Mar 23, 2022
62666b0
Fix improvements of biometrics machine
nicholemnl Mar 25, 2022
d96e907
Generate packagelock from conflict
nicholemnl Apr 7, 2022
049f589
Implement toggle Biometrics enable/disable
nicholemnl Apr 7, 2022
26a130c
Apply color on the toggle biometric switch
nicholemnl Apr 7, 2022
6093618
handled bio,etrick unlock under profile
danicaMat Apr 8, 2022
60a793b
chore: make husky hook executable
pmigueld Apr 8, 2022
63ea1c0
Fix biometrics toggle in profile
nicholemnl Apr 11, 2022
f641f7f
added biometrics checker to disable biometric unlock if there is none
danicaMat Apr 13, 2022
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
4 changes: 3 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
Expand All @@ -28,7 +30,7 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@nlpaolo/mosip-resident-app"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustPan" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustPan" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
Expand Down
23 changes: 18 additions & 5 deletions machines/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ContextFrom, EventFrom, send, StateFrom } from 'xstate';
import { log } from 'xstate/lib/actions';
import { createModel } from 'xstate/lib/model';
import { AppServices } from '../shared/GlobalContext';
import { StoreEvents, StoreResponseEvent } from './store';
Expand All @@ -13,7 +14,7 @@ const model = createModel(
{
events: {
SETUP_PASSCODE: (passcode: string) => ({ passcode }),
SETUP_BIOMETRICS: () => ({}),
SETUP_BIOMETRICS: (biometrics: string) => ({ biometrics }),
LOGOUT: () => ({}),
LOGIN: () => ({}),
STORE_RESPONSE: (response?: unknown) => ({ response }),
Expand All @@ -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,
Expand Down Expand Up @@ -55,6 +59,7 @@ export const authMachine = model.createMachine(
checkingAuth: {
always: [
{ cond: 'hasPasscodeSet', target: 'unauthorized' },
{ cond: 'hasBiometricSet', target: 'unauthorized' },
{ target: 'settingUp' },
],
},
Expand All @@ -64,9 +69,9 @@ export const authMachine = model.createMachine(
target: 'authorized',
actions: ['setPasscode', 'storeContext'],
},
// TODO: biometrics login
SETUP_BIOMETRICS: {
target: 'authorized',
// Note! dont authorized yet we need to setup passcode too as discuss
// target: 'authorized',
actions: ['setBiometrics', 'storeContext'],
},
},
Expand All @@ -79,6 +84,9 @@ export const authMachine = model.createMachine(
authorized: {
on: {
LOGOUT: 'unauthorized',
SETUP_BIOMETRICS: {
actions: ['setBiometrics', 'storeContext'],
},
},
},
},
Expand Down Expand Up @@ -109,14 +117,19 @@ export const authMachine = model.createMachine(
}),

setBiometrics: model.assign({
biometrics: '', // TODO
biometrics: (_, event: SetupBiometricsEvent) => event.biometrics,
}),
},

guards: {
hasData: (_, event: StoreResponseEvent) => event.response != null,

hasPasscodeSet: (context) => context.passcode !== '',
hasPasscodeSet: (context) => {
return context.passcode !== ''
},
hasBiometricSet: (context) => {
return context.biometrics !== ''
}
},
}
);
Expand Down
1 change: 1 addition & 0 deletions machines/auth.typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Typegen0 {
'eventsCausingGuards': {
hasData: 'STORE_RESPONSE';
hasPasscodeSet: '';
hasBiometricSet: '';
};
'eventsCausingDelays': {};
'matchesStates':
Expand Down
278 changes: 278 additions & 0 deletions machines/biometrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { createModel } from "xstate/lib/model";
import * as LocalAuthentication from 'expo-local-authentication';
import { assign, EventFrom, MetaObject, StateFrom } from "xstate";


// ----- CREATE MODEL ---------------------------------------------------------
const model = createModel(
{
isAvailable : false,
authTypes : [],
isEnrolled : false,
status : null,
retry : false,
},
{
events: {
SET_IS_AVAILABLE: (data: boolean) => ({ data }),
SET_AUTH: (data: any[]) => ({ data }),
SET_IS_ENROLLED: (data: boolean) => ({ data }),
SET_STATUS: (data: boolean) => ({ data }),

AUTHENTICATE: () => ({}),
RETRY_AUTHENTICATE: () => ({})
}
}
);
// ----------------------------------------------------------------------------


// ----- CREATE MACHINE -------------------------------------------------------
export const biometricsMachine = model.createMachine(
{
id: 'biometrics',
context: model.initialContext,
initial: 'init',
states: {

// Initializing biometrics states
init: {
invoke: {
src: LocalAuthentication.hasHardwareAsync,
onError: 'failure',
onDone: {
target: 'initAuthTypes',
actions: ['setIsAvailable'],
}
}
},

initAuthTypes: {
invoke: {
src: LocalAuthentication.supportedAuthenticationTypesAsync,
onError: 'failure',
onDone: {
target: 'initEnrolled',
actions: ['setAuthTypes'],
}
}
},

initEnrolled: {
invoke: {
src: LocalAuthentication.isEnrolledAsync,
onError: 'failure',
onDone: {
target: 'checking',
actions: ['setIsEnrolled']
}
}
},

// Checks whether we need to proceed if its available otherwise it gets to failure
checking: {
always: [
{
target: 'available',
cond: 'checkIfAvailable'
},
{
target: 'failure.unavailable',
cond: 'checkIfUnavailable'
},
{
target: 'failure.unenrolled',
cond: 'checkIfUnenrolled'
}
]
},


// if available then wait for any event
available: {
on: {
AUTHENTICATE: 'authenticating'
}
},

// authenticating biometrics
authenticating: {
invoke: {
src: () => async () => {
let res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Biometric Authentication',

// below can only works for IOS not android
// disableDeviceFallback: true,
// fallbackLabel: 'Invalid fingerprint attempts, Please try again.'
})
return res.success;
},
onError: 'failure',
onDone: {
target: 'authentication',
actions: ['setStatus']
}
},
},

reauthenticating: {
always: [
{
target: 'authenticating',
cond: 'checkIfAvailable'
},
{
target: 'failure.unenrolled'
}
]
},

// checks authentication status
authentication: {
always: [
{
target: 'success',
cond: 'isStatusSuccess'
},
{
target: 'failure.failed',
cond: 'isStatusFail'
}
]
},

success: {
type: 'final'
},

failure: {
initial: 'error',
states: {
unavailable: {
meta: {
message: 'Device does not support Biometrics'
}
},
unenrolled: {
meta: {
message: 'To use Biometrics, please enroll your fingerprint in your device settings',
},
on: {
RETRY_AUTHENTICATE: {
//actions: assign({retry: (context, event) => true}),
actions: ['setRetry'],
target: [
'#biometrics.initEnrolled',
'#biometrics.reauthenticating'
]
}
}
},
failed: {
after: {
// after 1 seconds, transition to available
1000: '#biometrics.available'
},
meta: {
message: 'Failed to authenticate with Biometrics'
}
},
error: {
meta: {
message: 'There seems to be an error in Biometrics authentication'
}
},
}
}

}
},

{
actions: {
nicholemnl marked this conversation as resolved.
Show resolved Hide resolved

setIsAvailable: model.assign({
isAvailable: (_, event: SetIsAvailableEvent) => event.data
}),

setAuthTypes: model.assign({
authTypes: (_, event: SetAuthTypesEvent) => event.data
}),

setIsEnrolled: model.assign({
isEnrolled: (_, event: SetIsEnrolledEvent) => event.data
}),

setStatus: model.assign({
status: (_, event: SetStatusEvent) => event.data
}),

setRetry: model.assign({
retry: () => true
}),
},
guards: {
isStatusSuccess: ctx => ctx.status,
isStatusFail: ctx => !ctx.status,
checkIfAvailable: ctx => ctx.isAvailable && ctx.isEnrolled,
checkIfUnavailable: ctx => !ctx.isAvailable,
checkIfUnenrolled: ctx => !ctx.isEnrolled
}
}

);

// ----------------------------------------------------------------------------



// ----- TYPES ----------------------------------------------------------------

type SetStatusEvent = EventFrom<typeof model, 'SET_STATUS'>;
type SetIsAvailableEvent = EventFrom<typeof model, 'SET_IS_AVAILABLE'>;
type SetAuthTypesEvent = EventFrom<typeof model, 'SET_AUTH'>;
type SetIsEnrolledEvent = EventFrom<typeof model, 'SET_IS_ENROLLED'>;
type State = StateFrom<typeof biometricsMachine>;




// ----- OTHER EXPORTS --------------------------------------------------------
export const BiometricsEvents = model.events;


export function selectFailMessage(state: State) {
return Object.values(state.meta)
.map((m: MetaObject) => m.message)
.join(', ');
}

export function selectIsEnabled(state: State) {
return state.matches('available') ||
state.matches({ failure: 'unenrolled' });
}

export function selectIsAvailable(state: State) {
return state.matches('available');
}

export function selectIsUnvailable(state: State) {
return state.matches({ failure: 'unavailable' });
}

export function selectIsUnenrolled(state: State) {
return state.matches({ failure: 'unenrolled' });
}

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) {
return state.matches({ failure: 'unenrolled' }) && state.context.retry ? selectFailMessage(state) : '';
}
Loading