f-23 / react-native-passkey

Passkeys for React Native
MIT License
135 stars 31 forks source link

Feat/allow credential ids #19

Closed peterferguson closed 9 months ago

peterferguson commented 1 year ago

Add credentialIDs to iOS api

This pr adds the ability to pass a set of allowed credentialIDs to the iOS authenticate function. This enables the developer to force the user to choose a specific passkey when signing in. This is important in a crypto context where passkeys are used as an alternative to a seed phrase. In this situation you want force the user to use a specific key so they don't sign a transaction with the wrong key. But, of course, there are more non-crypto use-cases.

We have been using this patch in production for the past 7 months so I thought it was about time to create a pr! ```diff diff --git a/ios/Passkey.m b/ios/Passkey.m index 4c50f0d51709d1cb490af4b5c8092fe42cd3c2d9..aa81705149322fb8da2e957ae243c5f89c2bcecc 100644 --- a/ios/Passkey.m +++ b/ios/Passkey.m @@ -13,6 +13,7 @@ RCT_EXTERN_METHOD(register:(NSString)identifier RCT_EXTERN_METHOD(auth:(NSString)identifier withChallenge:(NSString)challenge + withCredentialIDs:(NSArray) credentialIDs withSecurityKey:(BOOL) securityKey withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject); diff --git a/ios/Passkey.swift b/ios/Passkey.swift index ead171651f8ab8ca8f0c9554d28871e4f9c358c6..b754e06e5e07a46e777ec5faf3063b08b4625674 100644 --- a/ios/Passkey.swift +++ b/ios/Passkey.swift @@ -69,8 +69,8 @@ class Passkey: NSObject { } } - @objc(auth:withChallenge:withSecurityKey:withResolver:withRejecter:) - func auth(_ identifier: String, challenge: String, securityKey: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(auth:withChallenge:withCredentialIDs:withSecurityKey:withResolver:withRejecter:) + func auth(_ identifier: String, challenge: String, credentialIDs: Array, securityKey: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { // Convert challenge to correct type guard let challengeData: Data = Data(base64Encoded: challenge) else { @@ -92,6 +92,18 @@ class Passkey: NSObject { // Create a new assertion request without security key let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: identifier); let authRequest = platformProvider.createCredentialAssertionRequest(challenge: challengeData); + if (credentialIDs.isEmpty == false) { + var ids: [ASAuthorizationPlatformPublicKeyCredentialDescriptor] = []; + + credentialIDs.forEach { base64Id in + guard let id: Data = Data(base64Encoded: base64Id) + else { return; } + + ids.append(ASAuthorizationPlatformPublicKeyCredentialDescriptor.init(credentialID: id)); + } + + authRequest.allowedCredentials = ids; + } authController = ASAuthorizationController(authorizationRequests: [authRequest]); } @@ -152,6 +164,7 @@ enum PassKeyError: String, Error { case notSupported = "NotSupported" case requestFailed = "RequestFailed" case cancelled = "UserCancelled" + case invalidCredentialId = "InvalidCredentialId" case invalidChallenge = "InvalidChallenge" case notConfigured = "NotConfigured" case unknown = "UnknownError" diff --git a/lib/commonjs/Passkey.js b/lib/commonjs/Passkey.js index 59528c08ab4f225e4dbe7df0313a2ddd1f73e621..08630d3367401722fed5411a3b3626584906e14e 100644 --- a/lib/commonjs/Passkey.js +++ b/lib/commonjs/Passkey.js @@ -76,7 +76,7 @@ class Passkey { throw _PasskeyError.InvalidChallengeError; } try { - return await _NativePasskey.NativePasskey.auth(this.identifier, challenge, (options === null || options === void 0 ? void 0 : options.withSecurityKey) ?? false); + return await NativePasskey.auth(this.identifier, challenge, (options === null || options === void 0 ? void 0 : options.credentialIDs) ?? [], (options === null || options === void 0 ? void 0 : options.withSecurityKey) ?? false); } catch (error) { throw (0, _PasskeyError.handleNativeError)(error); } diff --git a/lib/module/Passkey.js b/lib/module/Passkey.js index 1f3067d956591b1f3569aa802129fed30d7e06cc..78d830025a165249b59db669888d155279fba021 100644 --- a/lib/module/Passkey.js +++ b/lib/module/Passkey.js @@ -70,7 +70,7 @@ export class Passkey { throw InvalidChallengeError; } try { - return await NativePasskey.auth(this.identifier, challenge, (options === null || options === void 0 ? void 0 : options.withSecurityKey) ?? false); + return await NativePasskey.auth(this.identifier, challenge, (options === null || options === void 0 ? void 0 : options.credentialIDs) ?? [], (options === null || options === void 0 ? void 0 : options.withSecurityKey) ?? false); } catch (error) { throw handleNativeError(error); } diff --git a/lib/typescript/Passkey.d.ts b/lib/typescript/Passkey.d.ts index 4c492f5b9dbe5d85d55ea70139a76669c4e8b72a..04305549e889f0c40f4395414927e69469ffe4fe 100644 --- a/lib/typescript/Passkey.d.ts +++ b/lib/typescript/Passkey.d.ts @@ -41,7 +41,8 @@ export declare class Passkey { static isSupported(): Promise; } export interface PasskeyOptions { - withSecurityKey: boolean; + credentialIDs?: Array; + withSecurityKey?: boolean; } /** * The result of a successful registration request ```

Things to note

Supporting both credentialIDs & withSecurityKey

I want to open a discussion around the desired api for combining these two options. How best to design the api is unclear.

To my mind there are two obvious approaches:

  1. Allow each credential to have a specific transport type that you wish to enforce

     export type PasskeyOptions = {
         credentialIDs: Array<string>;
         withSecurityKey?: false;
      } | {
         credentialIDs?: Array<{credentialID: string, transports: Array<AllowedTransports> }>;
         withSecurityKey: true;
      }
  2. Allow the user to pass acceptable transports

      export type PasskeyOptions = {
          credentialIDs: Array<string>;
          withSecurityKey?: false;
       } | {
          credentialIDs?: Array<string>;
          transports?: Array<AllowedTransports>
          withSecurityKey: true;
       }

Transport enum choice

For the transports there is also a choice to be made, whether to go with the Apple naming convention

  export type AllowedTransports = 'nfc' | 'bluetooth' | 'usb' | 'all'

Or the w3c definitions?

 export type AllowedTransports = 'nfc' | 'ble' | 'internal' | 'usb'
peterferguson commented 1 year ago

Hey just realised that in the version 2 the credentials are actually passed with the request but it doesn't look like they are respected in the iOS version?

This actually answers my questions about the api etc too so I will update the pr to account for this in a bit 👍

f-23 commented 1 year ago

Thank you for the PR!

You are right, for cross-platform compatibility reasons I had to change the way requests are made in version 2.

Since Android just takes the json request as is and uses it to register/authenticate the user we can assume that the allowCredentials option gives developers enough control over targeting specific credentials (and transports).

iOS works a bit different, so we would need to extract the credential descriptors from the json request and pass it in separately, similar to the way you proposed. This would include the transports data which could be translated to the iOS-specific enum and replace the withSecurityKey option.

Let me know what you think.

github-actions[bot] commented 9 months ago

This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.

github-actions[bot] commented 9 months ago

This PR was closed because it has been stalled for 10 days with no activity.