diff --git a/.changeset/afraid-points-brush.md b/.changeset/afraid-points-brush.md
new file mode 100644
index 0000000..0f286b3
--- /dev/null
+++ b/.changeset/afraid-points-brush.md
@@ -0,0 +1,5 @@
+---
+"react-native-passkeys": minor
+---
+
+Adds an `ios.requireBiometrics` flag to specify the LAPolicy required
diff --git a/example/src/app/index.tsx b/example/src/app/index.tsx
index e803c9d..9a33fb7 100644
--- a/example/src/app/index.tsx
+++ b/example/src/app/index.tsx
@@ -79,7 +79,7 @@ export default function App() {
>(null);
const [credentialId, setCredentialId] = React.useState("");
- const createPasskey = async () => {
+ const createPasskey = async (config?: passkey.PasskeysConfig) => {
try {
const json = await passkey.create({
challenge,
@@ -90,7 +90,7 @@ export default function App() {
...(Platform.OS !== "android" && {
extensions: { largeBlob: { support: "required" } },
}),
- });
+ }, config);
console.log("creation json -", json);
@@ -103,14 +103,14 @@ export default function App() {
}
};
- const authenticatePasskey = async () => {
+ const authenticatePasskey = async (config?: passkey.PasskeysConfig) => {
const json = await passkey.get({
rpId: rp.id,
challenge,
...(credentialId && {
allowCredentials: [{ id: credentialId, type: "public-key" }],
}),
- });
+ }, config);
console.log("authentication json -", json);
@@ -176,12 +176,18 @@ export default function App() {
Passkeys are {passkey.isSupported() ? "Supported" : "Not Supported"}
{credentialId && User Credential ID: {credentialId}}
-
+ createPasskey()}>
Create
-
+ createPasskey({ios:{requireBiometrics: false}})}>
+ Create (biometrics not required)
+
+ authenticatePasskey()}>
Authenticate
+ authenticatePasskey({ios:{requireBiometrics: false}})}>
+ Authenticate (biometrics not required)
+
Add Blob
diff --git a/ios/ReactNativePasskeysModule.swift b/ios/ReactNativePasskeysModule.swift
index c42a593..c379c68 100644
--- a/ios/ReactNativePasskeysModule.swift
+++ b/ios/ReactNativePasskeysModule.swift
@@ -25,11 +25,11 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler {
return false
}
- AsyncFunction("get") { (request: PublicKeyCredentialRequestOptions, promise: Promise) throws in
+ AsyncFunction("get") { (request: PublicKeyCredentialRequestOptions, requireBiometrics: Bool, promise: Promise) throws in
do {
// - all the throws are already in the helper `isAvailable` so we don't need to do anything
// ? this seems like a code smell ... what is the best way to do this
- let _ = try isAvailable()
+ let _ = try isAvailable(requireBiometrics: requireBiometrics)
}
catch let error {
throw error
@@ -49,11 +49,11 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler {
passkeyDelegate.performAuthForController(controller: authController);
}.runOnQueue(.main)
- AsyncFunction("create") { (request: PublicKeyCredentialCreationOptions, promise: Promise) throws in
+ AsyncFunction("create") { (request: PublicKeyCredentialCreationOptions, requireBiometrics: Bool, promise: Promise) throws in
do {
// - all the throws are already in the helper `isAvailable` so we don't need to do anything
// ? this seems like a code smell ... what is the best way to do this
- let _ = try isAvailable()
+ let _ = try isAvailable(requireBiometrics: requireBiometrics)
}
catch let error {
throw error
@@ -98,7 +98,7 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler {
}
- private func isAvailable() throws -> Bool {
+ private func isAvailable(requireBiometrics: Bool = true) throws -> Bool {
if #unavailable(iOS 15.0) {
throw NotSupportedException()
}
@@ -107,7 +107,15 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler {
throw PendingPasskeyRequestException()
}
- if LAContext().biometricType == .none {
+ let context = LAContext()
+
+ // Check the local authentication policy can be evaluated
+ let policy: LAPolicy = requireBiometrics ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication
+ guard context.canEvaluatePolicy(policy, error: nil) else {
+ throw BiometricException()
+ }
+
+ if requireBiometrics && context.biometricType == .none {
throw BiometricException()
}
@@ -316,11 +324,6 @@ extension LAContext {
var biometricType: BiometricType {
var error: NSError?
- guard self.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
- // Capture these recoverable error thru Crashlytics
- return .none
- }
-
if #available(iOS 11.0, *) {
switch self.biometryType {
case .none:
diff --git a/src/ReactNativePasskeysModule.ts b/src/ReactNativePasskeysModule.ts
index 5d496e1..76003ce 100644
--- a/src/ReactNativePasskeysModule.ts
+++ b/src/ReactNativePasskeysModule.ts
@@ -1,7 +1,9 @@
-import { requireNativeModule } from "expo-modules-core";
+import { Platform, requireNativeModule } from "expo-modules-core";
import type {
PublicKeyCredentialCreationOptionsJSON,
RegistrationResponseJSON,
+ PublicKeyCredentialRequestOptionsJSON,
+ AuthenticationResponseJSON,
} from "./ReactNativePasskeys.types";
import { NotSupportedError } from "./errors";
@@ -12,12 +14,26 @@ const passkeys = requireNativeModule("ReactNativePasskeys");
export default {
...passkeys,
+ async get(
+ request: PublicKeyCredentialRequestOptionsJSON,
+ requireBiometrics: boolean
+ ): Promise {
+ return Platform.OS === "ios"
+ ? await passkeys.get(request, requireBiometrics)
+ : await passkeys.get(request);
+ },
+
async create(
request: PublicKeyCredentialCreationOptionsJSON,
+ requireBiometrics: boolean
): Promise {
if (!this.isSupported) throw new NotSupportedError();
- const credential = await passkeys.create(request);
+ const credential =
+ Platform.OS === "ios"
+ ? await passkeys.create(request, requireBiometrics)
+ : await passkeys.create(request);
+
return {
...credential,
response: {
diff --git a/src/index.ts b/src/index.ts
index fd86eba..71e3d57 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,22 +22,52 @@ export function isAutoFillAvalilable(): boolean {
return ReactNativePasskeysModule.isAutoFillAvalilable()
}
+export interface PasskeysConfig {
+ /**
+ * Options and configuration specific to the iOS platform.
+ */
+ ios?: {
+ /**
+ * Defines the [local authentication policy](https://developer.apple.com/documentation/localauthentication/lapolicy) to use:
+ * - `true`: Use the `deviceOwnerAuthenticationWithBiometrics` policy.
+ * - `false`: Use the `deviceOwnerAuthentication` policy.
+ * Defaults to `true`.
+ *
+ * @see {@linkcode https://developer.apple.com/documentation/localauthentication/lapolicy/deviceownerauthenticationwithbiometrics|LAPolicy.deviceOwnerAuthenticationWithBiometrics}
+ * @see {@linkcode https://developer.apple.com/documentation/localauthentication/lapolicy/deviceownerauthentication|LAPolicy.deviceOwnerAuthentication}
+ */
+ requireBiometrics?: boolean
+ }
+}
+
+export interface PasskeysCreateOptions extends PasskeysConfig {}
+
export async function create(
request: Omit & {
// - only largeBlob is supported currently on iOS
// - no extensions are currently supported on Android
extensions?: { largeBlob?: AuthenticationExtensionsLargeBlobInputs }
} & Pick,
+ options?: PasskeysCreateOptions
): Promise {
- return await ReactNativePasskeysModule.create(request)
+ return ReactNativePasskeysModule.create(
+ request,
+ options?.ios?.requireBiometrics ?? true
+ )
}
+export interface PasskeysGetOptions extends PasskeysConfig {}
+
export async function get(
request: Omit & {
// - only largeBlob is supported currently on iOS
// - no extensions are currently supported on Android
extensions?: { largeBlob?: AuthenticationExtensionsLargeBlobInputs }
},
+ options?: PasskeysGetOptions
): Promise {
- return ReactNativePasskeysModule.get(request)
+ return ReactNativePasskeysModule.get(
+ request,
+ options?.ios?.requireBiometrics ?? true
+ )
}