Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions example/capacitor/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import authgearCapacitor, {
BiometricOptions,
BiometricAccessConstraintIOS,
BiometricLAPolicy,
BiometricAccessConstraintAndroid,
BiometricAuthenticatorAndroid,
WebKitWebViewUIImplementation,
DeviceBrowserUIImplementation,
} from "@authgear/capacitor";
Expand Down Expand Up @@ -76,15 +76,22 @@ const REDIRECT_URI_CAPACITOR = "com.authgear.exampleapp.capacitor://host/path";
const biometricOptions: BiometricOptions = {
ios: {
localizedReason: "Use biometric to authenticate",
localizedCancelTitle: "Customized Cancel",
constraint: BiometricAccessConstraintIOS.BiometryCurrentSet,
policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics,
},
android: {
title: "Biometric Authentication",
subtitle: "Biometric authentication",
description: "Use biometric to authenticate",
negativeButtonText: "Cancel",
constraint: [BiometricAccessConstraintAndroid.BiometricStrong],
negativeButtonText: "Customized Cancel",
allowedAuthenticatorsOnEnable: [
BiometricAuthenticatorAndroid.BiometricStrong,
],
allowedAuthenticatorsOnAuthenticate: [
BiometricAuthenticatorAndroid.BiometricStrong,
BiometricAuthenticatorAndroid.DeviceCredential,
],
invalidatedByBiometricEnrollment: true,
},
};
Expand Down
13 changes: 10 additions & 3 deletions example/reactnative/src/screens/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import authgear, {
BiometricOptions,
BiometricAccessConstraintIOS,
BiometricLAPolicy,
BiometricAccessConstraintAndroid,
BiometricAuthenticatorAndroid,
SessionState,
WebKitWebViewUIImplementation,
} from '@authgear/react-native';
Expand Down Expand Up @@ -132,15 +132,22 @@ const wechatRedirectURI = Platform.select<string>({
const biometricOptions: BiometricOptions = {
ios: {
localizedReason: 'Use biometric to authenticate',
localizedCancelTitle: 'Customized Cancel',
constraint: BiometricAccessConstraintIOS.BiometryCurrentSet,
policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics,
},
android: {
title: 'Biometric Authentication',
subtitle: 'Biometric authentication',
description: 'Use biometric to authenticate',
negativeButtonText: 'Cancel',
constraint: [BiometricAccessConstraintAndroid.BiometricStrong],
negativeButtonText: 'Customized Cancel',
allowedAuthenticatorsOnEnable: [
BiometricAuthenticatorAndroid.BiometricStrong,
],
allowedAuthenticatorsOnAuthenticate: [
BiometricAuthenticatorAndroid.BiometricStrong,
BiometricAuthenticatorAndroid.DeviceCredential,
],
invalidatedByBiometricEnrollment: true,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,19 +237,19 @@ JSONObject getDeviceInfo(Context ctx) throws Exception {
return rootMap;
}

int checkBiometricSupported(Context ctx, int flags) throws Exception {
int checkBiometricSupported(Context ctx, int allowedAuthenticatorsOnEnableFlags, int allowedAuthenticatorsOnAuthenticateFlags) throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
throw this.makeBiometricMinimumAPILevelException();
}

BiometricManager manager = BiometricManager.from(ctx);
int result = manager.canAuthenticate(flags);
int result = manager.canAuthenticate(allowedAuthenticatorsOnEnableFlags);

if (result == BiometricManager.BIOMETRIC_SUCCESS) {
// Further test if the key pair generator can be initialized.
// https://issuetracker.google.com/issues/147374428#comment9
try {
this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", true, flags, true));
this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", true, allowedAuthenticatorsOnAuthenticateFlags, true));
} catch (Exception e) {
// This branch is reachable only when there is a weak face and no strong fingerprints.
// So we treat this situation as BIOMETRIC_ERROR_NONE_ENROLLED.
Expand All @@ -266,11 +266,11 @@ void createBiometricPrivateKey(AppCompatActivity activity, BiometricOptions opti
throw this.makeBiometricMinimumAPILevelException();
}

BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options);
BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options, options.allowedAuthenticatorsOnEnableFlags);
KeyGenParameterSpec spec = this.makeGenerateKeyPairSpec(
options.alias,
true,
this.authenticatorTypesToKeyProperties(options.flags),
this.authenticatorTypesToKeyProperties(options.allowedAuthenticatorsOnAuthenticateFlags),
options.invalidatedByBiometricEnrollment
);
KeyPair keyPair = this.createKeyPair(spec);
Expand All @@ -289,7 +289,7 @@ void signWithBiometricPrivateKey(AppCompatActivity activity, BiometricOptions op
throw this.makeBiometricMinimumAPILevelException();
}

BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options);
BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options, options.allowedAuthenticatorsOnAuthenticateFlags);
KeyPair keyPair = this.getPrivateKey(options.alias);
this.signBiometricJWT(
activity,
Expand Down Expand Up @@ -435,14 +435,15 @@ private KeyPair getPrivateKey(String alias) throws Exception {
}

private BiometricPrompt.PromptInfo buildPromptInfo(
BiometricOptions options
BiometricOptions options,
int flags
) {
BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder();
builder.setTitle(options.title);
builder.setSubtitle(options.subtitle);
builder.setDescription(options.description);
builder.setAllowedAuthenticators(options.flags);
if ((options.flags & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) {
builder.setAllowedAuthenticators(flags);
if ((flags & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) {
builder.setNegativeButtonText(options.negativeButtonText);
}
return builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,14 @@ private void handleOpenAuthorizeURLWithWebView(PluginCall call, ActivityResult a
@PluginMethod
public void checkBiometricSupported(PluginCall call) {
JSObject android = call.getObject("android");
JSONArray constraint = this.jsObjectGetArray(android, "constraint");
int flags = this.constraintToFlag(constraint);
JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable");
JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate");
int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable);
int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate);

Context ctx = this.getContext();
try {
int result = this.implementation.checkBiometricSupported(ctx, flags);
int result = this.implementation.checkBiometricSupported(ctx, allowedAuthenticatorsOnEnableFlags, allowedAuthenticatorsOnAuthenticateFlags);
if (result == BiometricManager.BIOMETRIC_SUCCESS) {
call.resolve();
} else {
Expand All @@ -214,19 +216,23 @@ public void createBiometricPrivateKey(PluginCall call) {
String kid = call.getString("kid");
String alias = "com.authgear.keys.biometric." + kid;
JSObject android = call.getObject("android");
JSONArray constraint = this.jsObjectGetArray(android, "constraint");
boolean invalidatedByBiometricEnrollment = android.getBool("invalidatedByBiometricEnrollment");
int flags = this.constraintToFlag(constraint);
String title = android.getString("title");
String subtitle = android.getString("subtitle");
String description = android.getString("description");
String negativeButtonText = android.getString("negativeButtonText");

JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable");
JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate");
int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable);
int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate);

BiometricOptions options = new BiometricOptions();
options.payload = payload;
options.kid = kid;
options.alias = alias;
options.flags = flags;
options.allowedAuthenticatorsOnEnableFlags = allowedAuthenticatorsOnEnableFlags;
options.allowedAuthenticatorsOnAuthenticateFlags = allowedAuthenticatorsOnAuthenticateFlags;
options.invalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment;
options.title = title;
options.subtitle = subtitle;
Expand Down Expand Up @@ -269,19 +275,23 @@ public void signWithBiometricPrivateKey(PluginCall call) {
String kid = call.getString("kid");
String alias = "com.authgear.keys.biometric." + kid;
JSObject android = call.getObject("android");
JSONArray constraint = this.jsObjectGetArray(android, "constraint");
boolean invalidatedByBiometricEnrollment = android.getBool("invalidatedByBiometricEnrollment");
int flags = this.constraintToFlag(constraint);
String title = android.getString("title");
String subtitle = android.getString("subtitle");
String description = android.getString("description");
String negativeButtonText = android.getString("negativeButtonText");

JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable");
JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate");
int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable);
int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate);

BiometricOptions options = new BiometricOptions();
options.payload = payload;
options.kid = kid;
options.alias = alias;
options.flags = flags;
options.allowedAuthenticatorsOnEnableFlags = allowedAuthenticatorsOnEnableFlags;
options.allowedAuthenticatorsOnAuthenticateFlags = allowedAuthenticatorsOnAuthenticateFlags;
options.invalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment;
options.title = title;
options.subtitle = subtitle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ class BiometricOptions {
JSObject payload;
String kid;
String alias;
int flags;
int allowedAuthenticatorsOnEnableFlags;
int allowedAuthenticatorsOnAuthenticateFlags;
boolean invalidatedByBiometricEnrollment;
String title;
String subtitle;
Expand Down
7 changes: 6 additions & 1 deletion packages/authgear-capacitor/ios/Plugin/AuthgearPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ public class AuthgearPlugin: CAPPlugin {
@objc func checkBiometricSupported(_ call: CAPPluginCall) {
let ios = call.getObject("ios")!
let policyString = ios["policy"] as! String
let localizedCancelTitle = ios["localizedCancelTitle"] as? String
DispatchQueue.main.async {
do {
try self.impl.checkBiometricSupported(policyString: policyString)
try self.impl.checkBiometricSupported(policyString: policyString, localizedCancelTitle: localizedCancelTitle)
call.resolve()
} catch {
error.reject(call)
Expand All @@ -153,12 +154,14 @@ public class AuthgearPlugin: CAPPlugin {
let constraint = ios["constraint"] as! String
let policyString = ios["policy"] as! String
let localizedReason = ios["localizedReason"] as! String
let localizedCancelTitle = ios["localizedCancelTitle"] as? String
let tag = "com.authgear.keys.biometric.\(kid)"

DispatchQueue.main.async {
self.impl.createBiometricPrivateKey(
policyString: policyString,
localizedReason: localizedReason,
localizedCancelTitle: localizedCancelTitle,
constraint: constraint,
kid: kid,
tag: tag,
Expand All @@ -182,12 +185,14 @@ public class AuthgearPlugin: CAPPlugin {
let ios = call.getObject("ios")!
let policyString = ios["policy"] as! String
let localizedReason = ios["localizedReason"] as! String
let localizedCancelTitle = ios["localizedCancelTitle"] as? String
let tag = "com.authgear.keys.biometric.\(kid)"

DispatchQueue.main.async {
self.impl.signWithBiometricPrivateKey(
policyString: policyString,
localizedReason: localizedReason,
localizedCancelTitle: localizedCancelTitle,
kid: kid,
tag: tag,
payload: payload
Expand Down
13 changes: 8 additions & 5 deletions packages/authgear-capacitor/ios/Plugin/AuthgearPluginImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,10 @@ import Capacitor
controller?.start()
}

@objc func checkBiometricSupported(policyString: String) throws {
@objc func checkBiometricSupported(policyString: String, localizedCancelTitle: String?) throws {
if #available(iOS 11.3, *) {
let policy = LAPolicy.from(string: policyString)!
let laContext = self.makeLAContext(policy: policy)
let laContext = self.makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle)
var error: NSError?
laContext.canEvaluatePolicy(policy, error: &error)
if let error = error {
Expand All @@ -267,14 +267,15 @@ import Capacitor
@objc func createBiometricPrivateKey(
policyString: String,
localizedReason: String,
localizedCancelTitle: String?,
constraint: String,
kid: String,
tag: String,
payload: [String: Any],
completion: @escaping (String?, Error?) -> Void
) {
let policy = LAPolicy.from(string: policyString)!
let ctx = makeLAContext(policy: policy)
let ctx = makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle)
ctx.evaluatePolicy(policy, localizedReason: localizedReason) { ok, error in
if let error = error {
completion(nil, error)
Expand All @@ -296,13 +297,14 @@ import Capacitor
@objc func signWithBiometricPrivateKey(
policyString: String,
localizedReason: String,
localizedCancelTitle: String?,
kid: String,
tag: String,
payload: [String: Any],
completion: @escaping (String?, Error?) -> Void
) {
let policy = LAPolicy.from(string: policyString)!
let ctx = makeLAContext(policy: policy)
let ctx = makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle)
ctx.evaluatePolicy(policy, localizedReason: localizedReason) { ok, error in
if let error = error {
completion(nil, error)
Expand Down Expand Up @@ -393,11 +395,12 @@ import Capacitor
return window
}

private func makeLAContext(policy: LAPolicy) -> LAContext {
private func makeLAContext(policy: LAPolicy, localizedCancelTitle: String?) -> LAContext {
let ctx = LAContext()
if policy == LAPolicy.deviceOwnerAuthenticationWithBiometrics {
ctx.localizedFallbackTitle = "";
}
ctx.localizedCancelTitle = localizedCancelTitle
return ctx
}

Expand Down
33 changes: 30 additions & 3 deletions packages/authgear-capacitor/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,17 @@ export enum BiometricAccessConstraintIOS {
*/
export interface BiometricOptionsIOS {
/**
* See https://developer.apple.com/documentation/localauthentication/lacontext/1514176-evaluatepolicy#parameters
* See https://developer.apple.com/documentation/localauthentication/lacontext/localizedreason
*
* @public
*/
localizedReason: string;
/**
* See https://developer.apple.com/documentation/localauthentication/lacontext/localizedcanceltitle
*
* @public
*/
localizedCancelTitle?: string;
constraint: BiometricAccessConstraintIOS;
policy: BiometricLAPolicy;
}
Expand All @@ -291,7 +297,7 @@ export interface BiometricOptionsIOS {
*
* @public
*/
export enum BiometricAccessConstraintAndroid {
export enum BiometricAuthenticatorAndroid {
/**
* The user can use Class 3 biometric to authenticate.
*
Expand Down Expand Up @@ -340,7 +346,28 @@ export interface BiometricOptionsAndroid {
* @public
*/
negativeButtonText: string;
constraint: BiometricAccessConstraintAndroid[];
/**
* Set the allowed authenticators when the user enables biometric.
* This must be a subset of allowedAuthenticatorsOnAuthenticate because
* you normally want to ensure the user performed at least once biometric authentication during setup,
* but allow the user to fallback to passcode for subsequent biometric authentication.
*
* See {@link BiometricAuthenticatorAndroid}
*
* @public
*/
allowedAuthenticatorsOnEnable: BiometricAuthenticatorAndroid[];
/**
* Set the allowed authenticators when the user performs biometric authentication.
* This must be a superset of allowedAuthenticatorsOnEnable because
* you normally want to ensure the user performed at least once biometric authentication during setup,
* but allow the user to fallback to passcode for subsequent biometric authentication.
*
* See {@link BiometricAuthenticatorAndroid}
*
* @public
*/
allowedAuthenticatorsOnAuthenticate: BiometricAuthenticatorAndroid[];
/**
* The user needs to set up biometric again when a new biometric is enrolled or all enrolled biometrics are removed.
*
Expand Down
Loading