EddyVerbruggen / nativescript-fingerprint-auth

:nail_care: 👱‍♂️ Forget passwords, use a fingerprint scanner or facial recognition!
MIT License
134 stars 33 forks source link

How do you store the user's password in the iOS / Android keychain when enrolling in biometrics? #86

Open justintoth opened 3 years ago

justintoth commented 3 years ago

I'm attempting to implement this nativescript-fingerprint-auth package in my NativeScript / Angular app, however I'm having trouble understanding something... The code examples show how to retrieve the user's password after they have already enrolled in biometrics (Face Id or Touch Id). Basically you call fingerprintAuth.verifyFingerprint and then you get back a "userEnteredPassword" that you can populate on the signin form and then auto-sign the user in.

However, nowhere in the docs can I find where when you first enroll the user in biometrics do you pass in the password that you want to store in the iOS / Android password store / keychain. I tried the demo code, and indeed when I call the verifyFingerprint method after having already enrolled them, the userEnteredPassword returned is undefined. So, am I missing something or does the demo code not show how to store the password?

Here is my code, in case it helps:

export class LoginComponent extends BaseComponent {
    public emailAddress: string;
    public password: string;
    private fingerprintAuth: FingerprintAuth;
    public biometricTouchAvailable: boolean;
    public biometricFaceAvailable: boolean;
    public enrollInBiometrics = true;
    public loading = false;
    public validators = new Validators();

    @AutoGC()
    public subscription: Subscription;

    constructor(
        private storageService: StorageService
    ) {
        super();
        this.emailAddress = this.user ? this.user.emailAddress : this.authService.emailAddress;
        this.fingerprintAuth = new FingerprintAuth();
        // Check availability of biometrics.
            this.fingerprintAuth.available().then((result: BiometricIDAvailableResult) => {
                this.biometricFaceAvailable = result.face;
                this.biometricTouchAvailable = result.touch;
            });
    }

    public submit(skipBiometricsCheck = false) {
        if (!skipBiometricsCheck && this.shouldValidateBiometrics) {
            this.enrollOrValidateBiometrics();
            return;
        }
        if (this.validators.focusInvalid())
            return;
        this.loading = true;
        this.subscription = this.authService.authenticate({ emailAddress: this.emailAddress, password: this.password }).subscribe(
            user => {
                this.loading = false;
                if (this.shouldEnrollBiometrics)
                    this.enrollOrValidateBiometrics();
                else
                    this.authService.redirect(user);
            },
            exc => {
                showAlert(exc.error?.description || exc.toString());
                this.loading = false;
            },
        );
    }

    private get shouldValidateBiometrics() {
        if (!this.biometricFaceAvailable && !this.biometricTouchAvailable)
            return false;
        if (!this.enrolledInBiometrics)
            return false;
        if (!this.emailAddress)
            return false;
        if (this.password)
            return false;
        return true;
    }

    private get shouldEnrollBiometrics() {
        if (!this.biometricFaceAvailable && !this.biometricTouchAvailable)
            return false;
        if (this.enrolledInBiometrics)
            return false;
        if (!this.enrollInBiometrics)
            return false;
        return true;
    }

    private enrollOrValidateBiometrics() {
        const enroll = !this.enrolledInBiometrics;
        // TODO:[JT] On biometrics enroll, how do we store the app password in the keychain?
        console.log(`${enroll ? 'Enroll' : 'Validate'} biometrics`);
        this.fingerprintAuth.verifyFingerprint(
            {
                title: `Enable ${this.biometricFaceAvailable ? 'Face Id' : 'Touch Id'}`,
                message: `Sign into Housters using ${this.biometricFaceAvailable ? 'Face Id' : 'Touch Id'}`, 
                useCustomAndroidUI: true
            })
            .then((enteredPassword?: string) => {
                let succeeded = false;
                if (enroll) {
                    console.log('Successfully enrolled in biometric identification');
                    this.enrolledInBiometrics = true;
                    succeeded = true;
                } else {
                    // compare enteredPassword to the one the user previously configured for your app (which is not the users system password!)
                    if (enteredPassword) {
                        this.password = enteredPassword;
                        console.log('Biometric identification is already set up, attempting signin with enteredPassword: ' + enteredPassword);
                        succeeded = true;
                    } else
                        console.error('Biometric identification is already set up, but failed with empty enteredPassword: ' + enteredPassword);
                }
                if (succeeded) {
                    enroll ?
                        this.authService.redirect(this.user) :
                        this.submit(true);
                }
            })
            .catch(err => {
                console.error(`Biometric identification failed: ${JSON.stringify(err)}`);
                this.submit(true);
            });
    }

    private get enrolledInBiometrics() {
        return (Boolean)(this.storageService.get(StorageCacheKeys.ENROLLED_IN_BIOMETRICS)) || false;
    }

    private set enrolledInBiometrics(value) {
        this.storageService.set(StorageCacheKeys.ENROLLED_IN_BIOMETRICS, value);
    }
}