diff --git a/README.md b/README.md index e1cf654..a4365b3 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,21 @@ try { } ``` -### Security Keys (iOS-specific) +### Force Platform or Security Key (iOS-specific) -You can allow or disallow users to register using a Security Key (like [Yubikey](https://www.yubico.com/)). +You can force users to register and authenticate using either a platform key, a security key (like [Yubikey](https://www.yubico.com/)) or allow both using the following methods. This only works on iOS, Android will ignore these instructions. -For this just set the `authenticatorAttachment` field in your Passkey request to `platform` or `cross-platform`, depending on your preference. +#### Create Passkey + +- `Passkey.create()` - Allow the user to choose between platform and security passkey +- `Passkey.createPlatformKey()` - Force the user to create a platform passkey +- `Passkey.createSecurityKey()` - Force the user to create a security passkey + +#### Get Passkey + +- `Passkey.get()` - Allow the user to choose between platform and security passkey +- `Passkey.getPlatformKey()` - Force the user to authenticate using a platform passkey +- `Passkey.getSecurityKey()` - Force the user to authenticate using a security passkey ### Extensions diff --git a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt index 85a78fd..ea99c83 100644 --- a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt +++ b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt @@ -25,7 +25,7 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav } @ReactMethod - fun create(requestJson: String, promise: Promise) { + fun create(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, promise: Promise) { val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext) val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(requestJson) @@ -70,7 +70,7 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav } @ReactMethod - fun get(requestJson: String, promise: Promise) { + fun get(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, promise: Promise) { val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext) val getCredentialRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(requestJson))) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b3e430b..b27576e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -329,7 +329,7 @@ PODS: - React-jsinspector (0.71.18) - React-logger (0.71.18): - glog - - react-native-passkey (2.1.1): + - react-native-passkey (3.0.0-beta2): - React-Core - React-perflogger (0.71.18) - React-RCTActionSheet (0.71.18): @@ -573,7 +573,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: a7c83b31436843459a1961bfd74b96033dc77234 + boost: 7dcd2de282d72e344012f7d6564d024930a6a440 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 5f81939bc6d6bcd3e71bba3744753c2a88991395 @@ -592,7 +592,7 @@ SPEC CHECKSUMS: hermes-engine: 251dcc3511fc68678e2ee96dd7d175d9489b781e libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: 467cd79889cbcfd008d0ff7c697f73765d4578c3 RCTTypeSafety: 71c8b219dc633684896452fee38a1e442d167dd6 React: b807b09e4402c12dcbc44176e50533dca10d01f0 @@ -606,7 +606,7 @@ SPEC CHECKSUMS: React-jsiexecutor: a60e1117c011cbb6a95418f4bf67d6dfbbf81772 React-jsinspector: 7218c92923292e20326a1eed3104fe90393ca449 React-logger: 166cf6649b5ea9e26ab816073dd0e68020561d65 - react-native-passkey: b2125378c7fe3638349acc7dc37e90721d22fb8a + react-native-passkey: 88d3bb48dc38bdc69f0baf019b0faf355c27e6ec React-perflogger: c8849042b03392681ebd5d99a0775dbadfad2c74 React-RCTActionSheet: 6bd3b502da266d69942e9ce4bc13666f90544620 React-RCTAnimation: 63fbec04ae279b387ac786898bd5a918b8875f48 diff --git a/ios/Passkey.m b/ios/Passkey.m index fff09f1..321a4dd 100644 --- a/ios/Passkey.m +++ b/ios/Passkey.m @@ -4,10 +4,14 @@ @interface RCT_EXTERN_MODULE(Passkey, NSObject) RCT_EXTERN_METHOD(create:(NSString)request + withForcePlatformKey:(BOOL)forcePlatformKey + withForceSecurityKey:(BOOL)forceSecurityKey withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject); RCT_EXTERN_METHOD(get:(NSString)request + withForcePlatformKey:(BOOL)forcePlatformKey + withForceSecurityKey:(BOOL)forceSecurityKey withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject); diff --git a/ios/Passkey.swift b/ios/Passkey.swift index 597f4d6..809d655 100644 --- a/ios/Passkey.swift +++ b/ios/Passkey.swift @@ -20,8 +20,8 @@ class Passkey: NSObject, RNPasskeyResultHandler { /** Main create entrypoint */ - @objc(create:withResolver:withRejecter:) - func create(_ request: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(create:withForcePlatformKey:withForceSecurityKey:withResolver:withRejecter:) + func create(_ request: String, forcePlatformKey: Bool, forceSecurityKey: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { do { passkeyHandler = RNPasskeyHandler(resolve, reject); @@ -46,7 +46,7 @@ class Passkey: NSObject, RNPasskeyResultHandler { let securityKeyRequest: ASAuthorizationRequest = self.configureCreateSecurityKeyRequest(challenge: challenge, userId: userId, request: requestJSON); // Get authorization controller - let authController: ASAuthorizationController = self.configureAuthController(authenticatorAttachement: requestJSON.authenticatorSelection?.authenticatorAttachment, platformKeyRequest: platformKeyRequest, securityKeyRequest: securityKeyRequest); + let authController: ASAuthorizationController = self.configureAuthController(forcePlatformKey: forcePlatformKey, forceSecurityKey: forceSecurityKey, platformKeyRequest: platformKeyRequest, securityKeyRequest: securityKeyRequest); let passkeyDelegate = PasskeyDelegate(completionHandler: self); @@ -65,8 +65,8 @@ class Passkey: NSObject, RNPasskeyResultHandler { /** Main get entrypoint */ - @objc(get:withResolver:withRejecter:) - func get(_ request: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(get:withForcePlatformKey:withForceSecurityKey:withResolver:withRejecter:) + func get(_ request: String, forcePlatformKey: Bool, forceSecurityKey: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { do { passkeyHandler = RNPasskeyHandler(resolve, reject); @@ -84,7 +84,7 @@ class Passkey: NSObject, RNPasskeyResultHandler { let securityKeyRequest: ASAuthorizationRequest = self.configureGetSecurityKeyRequest(challenge: challenge, request: requestJSON); // Get authorization controller - let authController: ASAuthorizationController = self.configureAuthController(platformKeyRequest: platformKeyRequest, securityKeyRequest: securityKeyRequest); + let authController: ASAuthorizationController = self.configureAuthController(forcePlatformKey: forcePlatformKey, forceSecurityKey: forceSecurityKey, platformKeyRequest: platformKeyRequest, securityKeyRequest: securityKeyRequest); let passkeyDelegate = PasskeyDelegate(completionHandler: self); @@ -240,16 +240,16 @@ class Passkey: NSObject, RNPasskeyResultHandler { /** Creates and returns authorization controller depending on selected request types */ - private func configureAuthController(authenticatorAttachement: AuthenticatorAttachment? = .crossPlatform, platformKeyRequest: ASAuthorizationRequest, securityKeyRequest: ASAuthorizationRequest) -> ASAuthorizationController { - // Determine if we show platformKeyRequest, securityKeyRequest, or both - var authorizationRequests: [ASAuthorizationRequest] = [platformKeyRequest] - - if (authenticatorAttachement == .crossPlatform) { - authorizationRequests.append(securityKeyRequest); + private func configureAuthController(forcePlatformKey: Bool, forceSecurityKey: Bool, platformKeyRequest: ASAuthorizationRequest, securityKeyRequest: ASAuthorizationRequest) -> ASAuthorizationController { + if (forcePlatformKey) { + return ASAuthorizationController(authorizationRequests: [platformKeyRequest]); } - // Create auth controller - return ASAuthorizationController(authorizationRequests: authorizationRequests); + if (forceSecurityKey) { + return ASAuthorizationController(authorizationRequests: [securityKeyRequest]); + } + + return ASAuthorizationController(authorizationRequests: [platformKeyRequest, securityKeyRequest]); } /** diff --git a/src/Passkey.ts b/src/Passkey.ts index 9000ac9..acd361c 100644 --- a/src/Passkey.ts +++ b/src/Passkey.ts @@ -26,7 +26,67 @@ export class Passkey { try { const response: PasskeyCreateResult = (await NativePasskey.create( - JSON.stringify(request) + JSON.stringify(request), + false, // forcePlatformKey + false // forceSecurityKey + )) as PasskeyCreateResult; + + return response; + } catch (error) { + throw handleNativeError(error); + } + } + + /** + * Creates a new Passkey + * Forces the usage of a platform authenticator on iOS + * + * @param request The FIDO2 Attestation Request in JSON format + * @param options An object containing options for the registration process + * @returns The FIDO2 Attestation Result in JSON format + * @throws + */ + public static async createPlatformKey( + request: PasskeyCreateRequest + ): Promise { + if (!Passkey.isSupported()) { + throw NotSupportedError; + } + + try { + const response: PasskeyCreateResult = (await NativePasskey.create( + JSON.stringify(request), + true, // forcePlatformKey + false // forceSecurityKey + )) as PasskeyCreateResult; + + return response; + } catch (error) { + throw handleNativeError(error); + } + } + + /** + * Creates a new Passkey + * Forces the usage of a security authenticator on iOS + * + * @param request The FIDO2 Attestation Request in JSON format + * @param options An object containing options for the registration process + * @returns The FIDO2 Attestation Result in JSON format + * @throws + */ + public static async createSecurityKey( + request: PasskeyCreateRequest + ): Promise { + if (!Passkey.isSupported()) { + throw NotSupportedError; + } + + try { + const response: PasskeyCreateResult = (await NativePasskey.create( + JSON.stringify(request), + false, // forcePlatformKey + true // forceSecurityKey )) as PasskeyCreateResult; return response; @@ -52,7 +112,67 @@ export class Passkey { try { const response: PasskeyGetResult = (await NativePasskey.get( - JSON.stringify(request) + JSON.stringify(request), + false, // forcePlatformKey + false // forceSecurityKey + )) as PasskeyGetResult; + + return response; + } catch (error) { + throw handleNativeError(error); + } + } + + /** + * Authenticates using an existing Passkey + * Forces the usage of a platform authenticator on iOS + * + * @param request The FIDO2 Assertion Request in JSON format + * @param options An object containing options for the authentication process + * @returns The FIDO2 Assertion Result in JSON format + * @throws + */ + public static async getPlatformKey( + request: PasskeyGetRequest + ): Promise { + if (!Passkey.isSupported()) { + throw NotSupportedError; + } + + try { + const response: PasskeyGetResult = (await NativePasskey.get( + JSON.stringify(request), + true, // forcePlatformKey + false // forceSecurityKey + )) as PasskeyGetResult; + + return response; + } catch (error) { + throw handleNativeError(error); + } + } + + /** + * Authenticates using an existing Passkey + * Forces the usage of a security authenticator on iOS + * + * @param request The FIDO2 Assertion Request in JSON format + * @param options An object containing options for the authentication process + * @returns The FIDO2 Assertion Result in JSON format + * @throws + */ + public static async getSecurityKey( + request: PasskeyGetRequest + ): Promise { + if (!Passkey.isSupported()) { + throw NotSupportedError; + } + + try { + const response: PasskeyGetResult = (await NativePasskey.get( + JSON.stringify(request), + false, // forcePlatformKey + true // forceSecurityKey )) as PasskeyGetResult; return response;