Skip to content

Conversation

@sparten11740
Copy link
Contributor

@sparten11740 sparten11740 commented Sep 29, 2025

Add support for the WebAuthn PRF extension to derive values such as symmetric encryption keys from the passkey.

This PR deals with the more common case of requesting derived values independent of the credential used (eval). The spec also allows for supplying inputs by credential id through the evalByCredential property. If you accept contributions and this PR lands, I am happy to add support for evalByCredential in a follow up PR.

Towards #31

Test Plan

- [ ] Hit the "Derive Key" button in the example app and authenticate with a native passkey (1Password f.i. does not support PRF)
- [ ] verify that the derived value is printed bewlo
Simulator Screenshot - iPhone 16 Pro Max - 2025-09-29 at 20 49 29
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-09-29.at.20.49.55.mp4

Summary by CodeRabbit

  • New Features

    • Added WebAuthn PRF extension support for create/get flows, exposing PRF results in clientExtensionResults on supported platforms (iOS 18+ and web).
    • Requests can include PRF inputs; PRF results are returned and displayed (example app: new "Derive Key (PRF)" button).
    • Passkey creation now requests largeBlob support on non-Android platforms.
  • Refactor

    • Renamed CreationReponse → CreationResponse.
    • getPublicKey now returns ArrayBuffer instead of Uint8Array.

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Adds WebAuthn PRF extension support across web, iOS, and the example app: normalizes PRF inputs, wires PRF into create/get requests, marshals PRF outputs into clientExtensionResults.prf, updates TypeScript types and public signatures, and implements iOS 18+ conditional PRF handling plus a small example UI change.

Changes

Cohort / File(s) Summary
Example app (PRF demo UI and flow)
example/src/app/index.tsx
Adds Derive Key (PRF) button and handler; wires PRF flow and displays clientExtensionResults.prf; adjusts result typing.
Type definitions & public API surface
src/ReactNativePasskeys.types.ts, src/index.ts
Adds PRF input/output types and prf on extensions; updates create/get signatures to accept prf; adds/exports new PRF types and adjusts getPublicKey return shape.
Web implementation & utils
src/ReactNativePasskeysModule.web.ts, src/utils/prf.ts
Normalizes PRF inputs, attaches extensions.prf to create/get, extracts and serializes PRF results into clientExtensionResults.prf; fixes CreationReponseCreationResponse rename.
iOS: delegate and response models
ios/PasskeyDelegate.swift, ios/PasskeyResponses.swift
For iOS 18+, reads credential.prf in registration/assertion paths and maps into clientExtensionResults.prf; adds PRF output/value structs and prf field to client outputs.
iOS: request preparation, shared types & errors
ios/ReactNativePasskeysModule.swift, ios/Shared.swift, ios/PasskeyExceptions.swift
Adds PRF input struct mappings and wiring into platform registration/assertion requests (guarded #available(iOS 18, *)); updates functions to throws where decoding may fail; adds SymmetricKey serialization helpers and new InvalidPRFInputException.
Repository metadata
package.json
Trailing newline adjustment only.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App
  participant RN as RN API
  participant Platform as Web/iOS
  participant Auth as Platform AuthN

  rect #e6f2ff
    note over App,RN: Registration (create) with PRF
    User->>App: Tap Create / Derive Key
    App->>RN: create({ extensions:{ prf.eval:{first,second?}, largeBlob? } })
    RN->>Platform: normalizePRFInputs -> invoke create()
    Platform->>Auth: Create credential (PRF ext)
    Auth-->>Platform: Credential + extensions { prf, largeBlob? }
    Platform-->>RN: Response incl. clientExtensionResults.prf
    RN-->>App: CreationResponse (prf outputs)
  end

  rect #eaffea
    note over App,RN: Assertion (get) with PRF
    User->>App: Tap Get / Derive Key
    App->>RN: get({ extensions:{ prf.eval:{first,second?}, largeBlob? } })
    RN->>Platform: normalizePRFInputs -> invoke get()
    Platform->>Auth: Get assertion (PRF ext)
    Auth-->>Platform: Assertion + extensions { prf, largeBlob? }
    Platform-->>RN: Response incl. clientExtensionResults.prf
    RN-->>App: GetResponse (prf outputs)
  end

  note over Platform: iOS: PRF handling guarded by #available(iOS 18+)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I twitched my nose at PRF’s new tune,
Two salts dance beneath the moon.
Web and iOS hum in rhyme,
Keys derived, one hop at a time.
Off I bound with bytes anew—🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: add support for PRF (eval)" clearly and accurately summarizes the main change in this pull request. The changeset introduces comprehensive PRF (Pseudo-Random Function) extension support across multiple files including iOS native code (PasskeyDelegate.swift, PasskeyResponses.swift, ReactNativePasskeysModule.swift), web implementation, TypeScript type definitions, and the example app. The title specifically mentions "eval" which corresponds to the eval input mechanism for PRF that is implemented throughout the codebase, as confirmed by the PR objectives stating this implements "the common case of requesting derived values independent of the credential via the eval input." The title is concise, technically accurate, and would be immediately clear to developers scanning the project history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5773c and 8d5e222.

📒 Files selected for processing (6)
  • example/src/app/index.tsx (4 hunks)
  • ios/PasskeyExceptions.swift (1 hunks)
  • ios/ReactNativePasskeysModule.swift (6 hunks)
  • ios/Shared.swift (4 hunks)
  • src/ReactNativePasskeys.types.ts (4 hunks)
  • src/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ReactNativePasskeys.types.ts
  • src/index.ts
  • example/src/app/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
ios/ReactNativePasskeysModule.swift (1)
ios/PasskeyDelegate.swift (4)
  • iOS (21-26)
  • iOS (28-31)
  • iOS (33-39)
  • iOS (41-190)
🔇 Additional comments (6)
ios/PasskeyExceptions.swift (1)

65-69: LGTM!

The new InvalidPRFInputException follows the established pattern and provides a clear error message for invalid PRF inputs.

ios/ReactNativePasskeysModule.swift (3)

45-45: LGTM: Function signatures updated to propagate PRF decoding errors.

Both preparePlatformAssertionRequest and preparePlatformRegistrationRequest now throw, allowing PRF input validation errors to propagate correctly.

Also applies to: 81-81


216-234: LGTM: Safe PRF input decoding in registration path.

The PRF handling correctly uses guard let to safely decode base64URL inputs and throws InvalidPRFInputException on failure, addressing the force-unwrap crash risk flagged in previous reviews.


297-312: LGTM: Safe PRF input decoding in assertion path.

The PRF handling mirrors the registration path with safe decoding and proper error handling, addressing the force-unwrap crash risk flagged in previous reviews.

ios/Shared.swift (2)

3-3: LGTM: PRF input structures correctly defined.

The PRF input structures (AuthenticationExtensionsPrfEvalInputs and AuthenticationExtensionsPrfInputs) correctly model the WebAuthn PRF extension inputs and integrate properly with AuthenticationExtensionsClientInputs.

Also applies to: 252-262, 272-273


345-357: LGTM: SymmetricKey serialization uses correct base64url encoding.

The serialization methods correctly use toBase64URLEncodedString() to produce base64url-encoded output as required by the WebAuthn specification, addressing the concern raised in previous reviews.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (3)
ios/Shared.swift (1)

252-262: Consider consistent naming convention for PRF structures.

The input structures use Prf (e.g., AuthenticationExtensionsPrfInputs) while the output structures in PasskeyResponses.swift use PRF (e.g., AuthenticationExtensionsPRFOutputsJSON). For consistency across the codebase, consider using the same casing convention.

Apply this diff for consistency:

-internal struct AuthenticationExtensionsPrfEvalInputs: Record {
+internal struct AuthenticationExtensionsPRFEvalInputs: Record {
   @Field
   var first: Base64URLString
   @Field
   var second: Base64URLString?
 }
 
-internal struct AuthenticationExtensionsPrfInputs: Record {
+internal struct AuthenticationExtensionsPRFInputs: Record {
   @Field
-  var eval: AuthenticationExtensionsPrfEvalInputs?
+  var eval: AuthenticationExtensionsPRFEvalInputs?
 }
src/utils/prf.ts (1)

7-24: Minor: Remove redundant optional chaining and consider adding documentation.

The function correctly normalizes PRF inputs, but has a small redundancy:

  1. Line 21: The optional chaining prf.eval?.second is redundant since prf.eval is already verified to exist at line 14.
  2. Consider adding JSDoc documentation to describe the normalization behavior and return type variations.

Apply this diff:

 export function normalizePRFInputs(request: PublicKeyCredentialCreationOptionsJSON | PublicKeyCredentialRequestOptionsJSON) {
 	const { prf } = request.extensions ?? {}
 
 	if (!prf) {
 		return
 	}
 
 	if (!prf.eval) {
 		return {}
 	}
 
 	return {
 		eval: {
 			first: base64URLStringToBuffer(prf.eval.first),
-			second: prf.eval.second ? base64URLStringToBuffer(prf.eval?.second) : undefined
+			second: prf.eval.second ? base64URLStringToBuffer(prf.eval.second) : undefined
 		}
 	}
 }

Optionally, add JSDoc:

/**
 * Normalizes PRF extension inputs by converting base64url strings to ArrayBuffers.
 * @param request - The credential creation or request options
 * @returns Normalized PRF inputs with ArrayBuffers, empty object if prf exists but eval is missing, or undefined if no prf
 */
export function normalizePRFInputs(request: PublicKeyCredentialCreationOptionsJSON | PublicKeyCredentialRequestOptionsJSON) {
  // ...
}
example/src/app/index.tsx (1)

76-76: Consider more specific typing for result state.

The any type reduces type safety. For an example app demonstrating the API, consider using a union type of the possible response shapes to provide better IntelliSense and catch potential issues.

Example:

const [result, setResult] = React.useState<
  | NonNullable<Awaited<ReturnType<typeof passkey.create>>>
  | NonNullable<Awaited<ReturnType<typeof passkey.get>>>
  | { clientExtensionResults: { prf?: any } }
>();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bff6158 and 8e5773c.

📒 Files selected for processing (10)
  • example/src/app/index.tsx (4 hunks)
  • ios/PasskeyDelegate.swift (2 hunks)
  • ios/PasskeyResponses.swift (2 hunks)
  • ios/ReactNativePasskeysModule.swift (2 hunks)
  • ios/Shared.swift (4 hunks)
  • package.json (1 hunks)
  • src/ReactNativePasskeys.types.ts (4 hunks)
  • src/ReactNativePasskeysModule.web.ts (7 hunks)
  • src/index.ts (3 hunks)
  • src/utils/prf.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/utils/prf.ts (2)
src/ReactNativePasskeys.types.ts (2)
  • PublicKeyCredentialCreationOptionsJSON (51-61)
  • PublicKeyCredentialRequestOptionsJSON (66-73)
src/utils/base64.ts (1)
  • base64URLStringToBuffer (9-34)
src/index.ts (1)
src/ReactNativePasskeys.types.ts (2)
  • AuthenticationExtensionsLargeBlobInputs (143-153)
  • AuthenticationExtensionsPrfInputs (121-123)
ios/ReactNativePasskeysModule.swift (1)
ios/PasskeyDelegate.swift (4)
  • iOS (21-26)
  • iOS (28-31)
  • iOS (33-39)
  • iOS (41-190)
src/ReactNativePasskeysModule.web.ts (4)
src/ReactNativePasskeys.types.ts (2)
  • CreationResponse (212-220)
  • AuthenticationExtensionsClientOutputs (157-167)
src/utils/prf.ts (1)
  • normalizePRFInputs (7-24)
src/utils/warn-user-of-missing-webauthn-extensions.ts (1)
  • warnUserOfMissingWebauthnExtensions (9-23)
src/utils/base64.ts (1)
  • bufferToBase64URLString (42-53)
ios/PasskeyDelegate.swift (2)
src/ReactNativePasskeys.types.ts (3)
  • AuthenticationExtensionsPRFOutputsJSON (201-206)
  • AuthenticationExtensionsPRFValuesJSON (193-196)
  • AuthenticationExtensionsClientOutputsJSON (170-173)
ios/Shared.swift (2)
  • serialize (346-350)
  • serialize (354-356)
example/src/app/index.tsx (2)
android/src/main/java/expo/modules/passkeys/PasskeyOptions.kt (1)
  • rp (10-36)
src/utils/base64.ts (1)
  • bufferToBase64URLString (42-53)
🔇 Additional comments (24)
package.json (1)

53-53: LGTM! Trailing newline added.

This is a standard formatting change that aligns with common conventions.

ios/PasskeyResponses.swift (2)

113-116: LGTM!

The PRF field addition follows the existing pattern for client extension outputs and correctly uses an optional type.


136-153: LGTM!

The PRF structures correctly implement the WebAuthn specification. The field types and optionality align with the spec requirements, and the Base64URLString usage is appropriate for serialized PRF values.

ios/Shared.swift (2)

3-3: LGTM!

CryptoKit import is necessary for the SymmetricKey serialization extensions added later in the file.


272-273: LGTM!

The PRF field addition to client inputs follows the established pattern and correctly uses an optional type.

example/src/app/index.tsx (2)

91-94: LGTM!

The PRF extension is correctly added to the registration request. Using an empty object signals PRF support without providing eval inputs, which aligns with the WebAuthn PRF extension behavior during registration.


214-216: LGTM!

The new button is correctly wired to the deriveKey handler and has a clear, descriptive label.

src/index.ts (2)

5-5: LGTM!

The import of AuthenticationExtensionsPrfInputs correctly expands the public type surface to support PRF extension inputs.


30-30: LGTM!

The prf extension is correctly added as an optional field alongside largeBlob in the create request extensions.

ios/PasskeyDelegate.swift (4)

73-76: LGTM!

The clientExtensionResults correctly includes both largeBlob and prf fields for the registration response.


145-148: LGTM!

The clientExtensionResults for assertion correctly includes both largeBlob and prf fields. Note that prf.enabled is correctly omitted in the assertion flow, as it only applies to registration per the WebAuthn spec.


133-143: Optional handling is correct—SymmetricKey? extension exists.

Verification confirms that Shared.swift defines an extension on SymmetricKey? (lines 353-357) providing a serialize() method that returns String?. The code at line 138 correctly calls $0.second.serialize() where second is SymmetricKey?, and the extension handles the optional case by mapping over it. Both registration (line 66) and assertion (line 138) flows use the same pattern consistently.

No changes are needed.

Likely an incorrect or invalid review comment.


60-71: Review comment addresses non-existent issue—code correctly handles optional second value.

The code at line 66 (it.second.serialize()) and line 138 ($0.second.serialize()) is correct. The codebase includes an extension on Optional<SymmetricKey> (lines 353-357 in Shared.swift) that provides func serialize() -> String?, allowing serialize() to be called directly on optional values without requiring optional chaining. The extension handles the nil case internally by returning Optional<String>.

This is a valid Swift pattern where methods are extended on the Optional type itself, making the original concern about runtime crashes or compilation failures unfounded.

Likely an incorrect or invalid review comment.

src/ReactNativePasskeysModule.web.ts (8)

14-14: LGTM! Typo fix.

The rename from CreationReponse to CreationResponse corrects a typo in the type name.


16-16: LGTM!

The import of normalizePRFInputs correctly brings in the utility function for PRF input handling.


52-55: LGTM!

The PRF extension is correctly wired into the create request via normalizePRFInputs, which converts base64URL strings to ArrayBuffers as required by the native WebAuthn API.


63-63: LGTM!

Destructuring prf from extensions allows separate handling of PRF results for serialization.


87-93: LGTM!

The PRF results are correctly serialized in the registration response:

  • The enabled field is included (registration-only per spec)
  • Both first and second values are properly converted from ArrayBuffer to base64URL strings
  • Optional chaining ensures safe handling when PRF results are absent

114-114: LGTM!

The PRF extension is correctly wired into the get request via normalizePRFInputs.


143-143: LGTM!

Destructuring pattern correctly separates PRF results from other extension outputs.


167-172: LGTM!

The PRF results are correctly serialized in the assertion response:

  • The enabled field is correctly omitted (only present during registration per spec)
  • Both first and second values are properly converted from ArrayBuffer to base64URL strings
  • Optional chaining ensures safe handling when PRF results are absent
src/ReactNativePasskeys.types.ts (3)

135-135: LGTM!

The addition of the optional prf field to AuthenticationExtensionsClientInputs correctly integrates PRF extension support into the client inputs interface.


156-167: LGTM!

The PRF extension output structure correctly mirrors the largeBlob pattern, transforming Base64URLString values to ArrayBuffer for native representation. The use of Omit and type intersection maintains consistency with the existing codebase style.


212-219: LGTM! Typo fix and spec alignment.

The interface rename from CreationReponse to CreationResponse corrects a typo. The getPublicKey() return type change from Uint8Array | null to ArrayBuffer | null aligns with the WebAuthn specification and maintains consistency with other native type representations in this file (e.g., largeBlob.blob, prf.results.first).

Note: These are breaking changes for consumers, but they represent necessary corrections.

@peterferguson
Copy link
Owner

awesome @sparten11740 thanks for the pr will take a look at this tomorrow morning 👍

@peterferguson
Copy link
Owner

peterferguson commented Oct 6, 2025

Nice looks good locally!

I will do some work on the android side & publish both together if that is cool 👍

@sparten11740
Copy link
Contributor Author

Sounds good 👍

@peterferguson peterferguson merged commit 792e0be into peterferguson:main Oct 7, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants