feat: add Touch ID authentication support for macOS (#16707)
This PR adds Touch ID authentication support for macOS with two new `SystemPreferences` methods. 1. `systemPreferences.promptForTouchID()` returns a Promise that resolves with `true` if successful and rejects with an error message if authentication could not be completed. 2. `systemPreferences.isTouchIDAvailable()` returns a Boolean that's `true` if this device is a Mac running a supported OS that has the necessary hardware for Touch ID and `false` otherwise.
This commit is contained in:
parent
228805353f
commit
46a24c82ff
6 changed files with 121 additions and 0 deletions
1
BUILD.gn
1
BUILD.gn
|
@ -690,6 +690,7 @@ if (is_mac) {
|
||||||
libs = [
|
libs = [
|
||||||
"AVFoundation.framework",
|
"AVFoundation.framework",
|
||||||
"Carbon.framework",
|
"Carbon.framework",
|
||||||
|
"LocalAuthentication.framework",
|
||||||
"QuartzCore.framework",
|
"QuartzCore.framework",
|
||||||
"Quartz.framework",
|
"Quartz.framework",
|
||||||
"Security.framework",
|
"Security.framework",
|
||||||
|
|
|
@ -93,6 +93,8 @@ void SystemPreferences::BuildPrototype(
|
||||||
.SetMethod("setAppLevelAppearance",
|
.SetMethod("setAppLevelAppearance",
|
||||||
&SystemPreferences::SetAppLevelAppearance)
|
&SystemPreferences::SetAppLevelAppearance)
|
||||||
.SetMethod("getSystemColor", &SystemPreferences::GetSystemColor)
|
.SetMethod("getSystemColor", &SystemPreferences::GetSystemColor)
|
||||||
|
.SetMethod("canPromptTouchID", &SystemPreferences::CanPromptTouchID)
|
||||||
|
.SetMethod("promptTouchID", &SystemPreferences::PromptTouchID)
|
||||||
.SetMethod("isTrustedAccessibilityClient",
|
.SetMethod("isTrustedAccessibilityClient",
|
||||||
&SystemPreferences::IsTrustedAccessibilityClient)
|
&SystemPreferences::IsTrustedAccessibilityClient)
|
||||||
.SetMethod("getMediaAccessStatus",
|
.SetMethod("getMediaAccessStatus",
|
||||||
|
|
|
@ -95,6 +95,10 @@ class SystemPreferences : public mate::EventEmitter<SystemPreferences>
|
||||||
|
|
||||||
std::string GetSystemColor(const std::string& color, mate::Arguments* args);
|
std::string GetSystemColor(const std::string& color, mate::Arguments* args);
|
||||||
|
|
||||||
|
bool CanPromptTouchID();
|
||||||
|
v8::Local<v8::Promise> PromptTouchID(v8::Isolate* isolate,
|
||||||
|
const std::string& reason);
|
||||||
|
|
||||||
static bool IsTrustedAccessibilityClient(bool prompt);
|
static bool IsTrustedAccessibilityClient(bool prompt);
|
||||||
|
|
||||||
// TODO(codebytere): Write tests for these methods once we
|
// TODO(codebytere): Write tests for these methods once we
|
||||||
|
|
|
@ -8,14 +8,19 @@
|
||||||
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <LocalAuthentication/LocalAuthentication.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
|
||||||
#include "atom/browser/mac/atom_application.h"
|
#include "atom/browser/mac/atom_application.h"
|
||||||
#include "atom/browser/mac/dict_util.h"
|
#include "atom/browser/mac/dict_util.h"
|
||||||
#include "atom/common/native_mate_converters/gurl_converter.h"
|
#include "atom/common/native_mate_converters/gurl_converter.h"
|
||||||
#include "atom/common/native_mate_converters/value_converter.h"
|
#include "atom/common/native_mate_converters/value_converter.h"
|
||||||
|
#include "base/mac/scoped_cftyperef.h"
|
||||||
#include "base/mac/sdk_forward_declarations.h"
|
#include "base/mac/sdk_forward_declarations.h"
|
||||||
|
#include "base/sequenced_task_runner.h"
|
||||||
#include "base/strings/stringprintf.h"
|
#include "base/strings/stringprintf.h"
|
||||||
#include "base/strings/sys_string_conversions.h"
|
#include "base/strings/sys_string_conversions.h"
|
||||||
|
#include "base/threading/sequenced_task_runner_handle.h"
|
||||||
#include "base/values.h"
|
#include "base/values.h"
|
||||||
#include "native_mate/object_template_builder.h"
|
#include "native_mate/object_template_builder.h"
|
||||||
#include "net/base/mac/url_conversions.h"
|
#include "net/base/mac/url_conversions.h"
|
||||||
|
@ -438,6 +443,72 @@ std::string SystemPreferences::GetSystemColor(const std::string& color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SystemPreferences::CanPromptTouchID() {
|
||||||
|
if (@available(macOS 10.12.2, *)) {
|
||||||
|
base::scoped_nsobject<LAContext> context([[LAContext alloc] init]);
|
||||||
|
if (![context
|
||||||
|
canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
|
||||||
|
error:nil])
|
||||||
|
return false;
|
||||||
|
if (@available(macOS 10.13.2, *))
|
||||||
|
return [context biometryType] == LABiometryTypeTouchID;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTouchIDCompleted(scoped_refptr<util::Promise> promise) {
|
||||||
|
promise->Resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTouchIDFailed(scoped_refptr<util::Promise> promise,
|
||||||
|
const std::string& reason) {
|
||||||
|
promise->RejectWithErrorMessage(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Promise> SystemPreferences::PromptTouchID(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
const std::string& reason) {
|
||||||
|
scoped_refptr<util::Promise> promise = new util::Promise(isolate);
|
||||||
|
if (@available(macOS 10.12.2, *)) {
|
||||||
|
base::scoped_nsobject<LAContext> context([[LAContext alloc] init]);
|
||||||
|
base::ScopedCFTypeRef<SecAccessControlRef> access_control =
|
||||||
|
base::ScopedCFTypeRef<SecAccessControlRef>(
|
||||||
|
SecAccessControlCreateWithFlags(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
kSecAccessControlPrivateKeyUsage |
|
||||||
|
kSecAccessControlUserPresence,
|
||||||
|
nullptr));
|
||||||
|
|
||||||
|
scoped_refptr<base::SequencedTaskRunner> runner =
|
||||||
|
base::SequencedTaskRunnerHandle::Get();
|
||||||
|
|
||||||
|
[context
|
||||||
|
evaluateAccessControl:access_control
|
||||||
|
operation:LAAccessControlOperationUseKeySign
|
||||||
|
localizedReason:[NSString stringWithUTF8String:reason.c_str()]
|
||||||
|
reply:^(BOOL success, NSError* error) {
|
||||||
|
if (!success) {
|
||||||
|
runner->PostTask(
|
||||||
|
FROM_HERE,
|
||||||
|
base::BindOnce(
|
||||||
|
&OnTouchIDFailed, promise,
|
||||||
|
std::string([error.localizedDescription
|
||||||
|
UTF8String])));
|
||||||
|
} else {
|
||||||
|
runner->PostTask(
|
||||||
|
FROM_HERE,
|
||||||
|
base::BindOnce(&OnTouchIDCompleted, promise));
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
promise->RejectWithErrorMessage(
|
||||||
|
"This API is not available on macOS versions older than 10.12.2");
|
||||||
|
}
|
||||||
|
return promise->GetHandle();
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) {
|
bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) {
|
||||||
NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)};
|
NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)};
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "base/mac/scoped_sending_event.h"
|
#include "base/mac/scoped_sending_event.h"
|
||||||
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
#import <LocalAuthentication/LocalAuthentication.h>
|
||||||
|
|
||||||
// Forward Declare Appearance APIs
|
// Forward Declare Appearance APIs
|
||||||
@interface NSApplication (HighSierraSDK)
|
@interface NSApplication (HighSierraSDK)
|
||||||
|
@ -16,6 +17,22 @@
|
||||||
- (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14));
|
- (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14));
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
#if !defined(MAC_OS_X_VERSION_10_13_2)
|
||||||
|
|
||||||
|
// forward declare Touch ID APIs
|
||||||
|
typedef NS_ENUM(NSInteger, LABiometryType) {
|
||||||
|
LABiometryTypeNone = 0,
|
||||||
|
LABiometryTypeFaceID = 1,
|
||||||
|
LABiometryTypeTouchID = 2,
|
||||||
|
} API_AVAILABLE(macosx(10.13.2));
|
||||||
|
|
||||||
|
@interface LAContext (HighSierraPointTwoSDK)
|
||||||
|
@property(nonatomic, readonly)
|
||||||
|
LABiometryType biometryType API_AVAILABLE(macosx(10.13.2));
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
// forward declare Access APIs
|
// forward declare Access APIs
|
||||||
typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM;
|
typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM;
|
||||||
|
|
||||||
|
|
|
@ -380,6 +380,32 @@ You can use the `setAppLevelAppearance` API to set this value.
|
||||||
Sets the appearance setting for your application, this should override the
|
Sets the appearance setting for your application, this should override the
|
||||||
system default and override the value of `getEffectiveAppearance`.
|
system default and override the value of `getEffectiveAppearance`.
|
||||||
|
|
||||||
|
### `systemPreferences.canPromptTouchID()` _macOS_
|
||||||
|
|
||||||
|
Returns `Boolean` - whether or not this device has the ability to use Touch ID.
|
||||||
|
|
||||||
|
**NOTE:** This API will return `false` on macOS systems older than Sierra 10.12.2.
|
||||||
|
|
||||||
|
### `systemPreferences.promptTouchID(reason)` _macOS_
|
||||||
|
|
||||||
|
* `reason` String - The reason you are asking for Touch ID authentication
|
||||||
|
|
||||||
|
Returns `Promise<void>` - resolves if the user has successfully authenticated with Touch ID.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { systemPreferences } = require('electron')
|
||||||
|
|
||||||
|
systemPreferences.promptTouchID('To get consent for a Security-Gated Thing').then(success => {
|
||||||
|
console.log('You have successfully authenticated with Touch ID!')
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This API itself will not protect your user data; rather, it is a mechanism to allow you to do so. Native apps will need to set [Access Control Constants](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags?language=objc) like [`kSecAccessControlUserPresence`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroluserpresence?language=objc) on the their keychain entry so that reading it would auto-prompt for Touch ID biometric consent. This could be done with [`node-keytar`](https://github.com/atom/node-keytar), such that one would store an encryption key with `node-keytar` and only fetch it if `promptTouchID()` resolves.
|
||||||
|
|
||||||
|
**NOTE:** This API will return a rejected Promise on macOS systems older than Sierra 10.12.2.
|
||||||
|
|
||||||
### `systemPreferences.isTrustedAccessibilityClient(prompt)` _macOS_
|
### `systemPreferences.isTrustedAccessibilityClient(prompt)` _macOS_
|
||||||
|
|
||||||
* `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted.
|
* `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted.
|
||||||
|
|
Loading…
Add table
Reference in a new issue