Skip to content

feat: add passkeys support for web - #907

Open
NandanPrabhu wants to merge 4 commits into
mainfrom
feat/web-passkeys
Open

feat: add passkeys support for web#907
NandanPrabhu wants to merge 4 commits into
mainfrom
feat/web-passkeys

Conversation

@NandanPrabhu

@NandanPrabhu NandanPrabhu commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds passkeySignupChallenge, passkeyLoginChallenge, and getTokenByPasskey to Auth0Web, delegating to @auth0/auth0-spa-js's passkey API (passkey.getSignupChallenge, passkey.getLoginChallenge, passkey.getTokenWithPasskey/_requestTokenForPasskey). Requires auth0-spa-js v2.24.0+ loaded on the page.
  • Credential ceremony uses the browser's native WebAuthn API (navigator.credentials.create()/.get()) instead of a native module.
  • getTokenByPasskey's authResponse accepts either the raw PublicKeyCredential returned directly by navigator.credentials (routed through passkey.getTokenWithPasskey, auto-detecting attestation vs assertion) or a JSON string matching the native bridge contract (routed through _requestTokenForPasskey), so app code doesn't need a manual serialization step on web.
  • New platform-interface types: WebPasskeyChallenge, WebPasskeySignupChallengeOptions, WebPasskeyLoginChallengeOptions, WebGetTokenByPasskeyOptions, and a WebException.passkeyError constructor for consistent error mapping.
  • Updates EXAMPLES.md with a new "Passkeys on the Web" section and README with accurate platform-support notes (previously overstated web support before this landed).
  • Adds a PasskeyCard demo to the example app's web build, wired to navigator.credentials.
  • Along the way, fixed two pre-existing bugs in the example app where unguarded Platform.isWindows calls crashed unconditionally on web.

Test plan

  • flutter test (unit) passing in both auth0_flutter and auth0_flutter_platform_interface
  • flutter test --tags browser --platform chrome passing in auth0_flutter, including new passkey coverage in auth0_flutter_web_test.dart (challenge mapping, raw-credential vs JSON-string exchange paths, error propagation)
  • flutter analyze clean across auth0_flutter, auth0_flutter_platform_interface, and the example app
  • Manually verified end-to-end against a real Auth0 tenant with a custom domain and the Passkey grant type enabled — signup, duplicate-signup error handling, and login all completed successfully in Chrome

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added web passkey support for signup and login challenge flows.
    • Added passkey-based token exchange through WebAuthn credentials.
    • Exposed new web passkey challenge and configuration APIs.
  • Bug Fixes

    • Added clearer passkey error reporting, including error codes and messages.
  • Documentation

    • Expanded passkey examples, platform availability details, browser requirements, and WebAuthn usage guidance.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@NandanPrabhu, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4dcc2168-f83b-44b7-960f-00040bf114a6

📥 Commits

Reviewing files that changed from the base of the PR and between da24c83 and ce29575.

⛔ Files ignored due to path filters (8)
  • auth0_flutter/example/lib/example_app.dart is excluded by !**/example/**
  • auth0_flutter/example/lib/passkey_card.dart is excluded by !**/example/**
  • auth0_flutter/example/lib/passkey_web_helper.dart is excluded by !**/example/**
  • auth0_flutter/example/lib/passkey_web_helper_real.dart is excluded by !**/example/**
  • auth0_flutter/example/lib/passkey_web_helper_stub.dart is excluded by !**/example/**
  • auth0_flutter/example/pubspec.yaml is excluded by !**/example/**
  • auth0_flutter/example/web/index.html is excluded by !**/example/**
  • auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart is excluded by !**/*.mocks.dart
📒 Files selected for processing (20)
  • auth0_flutter/EXAMPLES.md
  • auth0_flutter/README.md
  • auth0_flutter/lib/auth0_flutter_web.dart
  • auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart
  • auth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dart
  • auth0_flutter/lib/src/web/auth0_flutter_web_platform_proxy.dart
  • auth0_flutter/lib/src/web/extensions/passkey_extensions.dart
  • auth0_flutter/lib/src/web/extensions/web_exception_extensions.dart
  • auth0_flutter/lib/src/web/js_interop.dart
  • auth0_flutter/lib/src/web/js_interop_utils.dart
  • auth0_flutter/test/web/auth0_extension_type_mocks.dart
  • auth0_flutter/test/web/auth0_flutter_web_test.dart
  • auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart
  • auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart
  • auth0_flutter_platform_interface/lib/src/web/web_exception.dart
  • auth0_flutter_platform_interface/lib/src/web/web_get_token_by_passkey_options.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_challenge.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_login_challenge_options.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_signup_challenge_options.dart
  • auth0_flutter_platform_interface/test/auth0_flutter_web_platform_test.dart

Walkthrough

Web passkey support is added through new Auth0Web APIs, platform contracts, JavaScript interop bindings, challenge and credential exchange handling, error conversion, tests, and WebAuthn documentation.

Changes

Web passkey flows

Layer / File(s) Summary
Public passkey contracts
auth0_flutter_platform_interface/lib/src/web/*, auth0_flutter/lib/auth0_flutter_web.dart
Adds challenge, signup, login, token-exchange, and passkey error types, platform methods, exports, and Auth0Web APIs.
WebAuthn interop and conversions
auth0_flutter/lib/src/web/js_interop.dart, auth0_flutter/lib/src/web/auth0_flutter_web_platform_proxy.dart, auth0_flutter/lib/src/web/extensions/*
Connects Dart APIs to auth0-spa-js passkey methods, converts options and responses, parses JSON credentials, and maps JavaScript errors.
Platform implementation
auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart, auth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dart
Implements signup/login challenge retrieval and raw or JSON credential token exchange on Web; non-Web calls throw UnsupportedError.
Validation and documentation
auth0_flutter/test/web/*, auth0_flutter_platform_interface/test/*, auth0_flutter/README.md, auth0_flutter/EXAMPLES.md
Tests successful mappings, credential routing, and error handling, and documents the WebAuthn flow and required auth0-spa-js version.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Auth0Web
  participant Auth0FlutterPlugin
  participant auth0-spa-js
  App->>Auth0Web: request passkey challenge
  Auth0Web->>Auth0FlutterPlugin: call challenge API
  Auth0FlutterPlugin->>auth0-spa-js: fetch signup or login challenge
  auth0-spa-js-->>Auth0FlutterPlugin: return authSession and public-key options
  Auth0FlutterPlugin-->>Auth0Web: return WebPasskeyChallenge
  App->>Auth0Web: exchange WebAuthn credential
  Auth0Web->>Auth0FlutterPlugin: call getTokenByPasskey
  Auth0FlutterPlugin->>auth0-spa-js: exchange credential and authSession
  auth0-spa-js-->>Auth0FlutterPlugin: return credentials
  Auth0FlutterPlugin-->>Auth0Web: return Credentials
Loading

Suggested reviewers: pmathew92, sanchitmehtagit, utkrishtsahu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding web passkeys support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-passkeys

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@auth0_flutter_platform_interface/lib/src/web/web_exception.dart`:
- Around line 25-28: Update the WebException.passkeyError constructor so the
optional details map is merged before assigning the code field, ensuring the
provided error value remains authoritative even when details contains a code
key.

In
`@auth0_flutter_platform_interface/lib/src/web/web_get_token_by_passkey_options.dart`:
- Around line 7-13: Constrain authResponse in WebGetTokenByPasskeyOptions and
the other public passkey boundary to Object, validating that values are
supported String or JSAny before casting and rejecting null-equivalent or
invalid credentials; add coverage in
auth0_flutter/test/web/auth0_flutter_web_test.dart for both cases. In
auth0_flutter/lib/auth0_flutter_web.dart and both passkey examples in
auth0_flutter/EXAMPLES.md, guard nullable WebAuthn results before passing them
onward.

In `@auth0_flutter/lib/auth0_flutter_web.dart`:
- Around line 500-628: Make the passkey contract explicit: in
auth0_flutter/lib/auth0_flutter_web.dart lines 500-628, document that passkey
support requires auth0-spa-js 2.24.0 or newer and describe only the public
PasskeyApiClient methods as supported; do not present _requestTokenForPasskey as
a supported path unless the SDK pins or vendors that internal contract. Update
auth0_flutter/test/web/auth0_flutter_web_test.dart lines 678-816 to assert the
public-method contract, and update auth0_flutter/EXAMPLES.md lines 1224-1280 to
state the version requirement and avoid relying on the internal method.

In `@auth0_flutter/lib/src/web/extensions/web_exception_extensions.dart`:
- Around line 56-61: The fromPasskeyError mapping currently reads only
PasskeyError fields, so token-exchange GenericError codes and descriptions are
lost. Update fromPasskeyError to recognize GenericError’s error and
error_description properties and preserve them when creating the WebException;
retain the existing PasskeyError fallback behavior, and add coverage for an
OAuth token-exchange error.

In `@auth0_flutter/lib/src/web/js_interop_utils.dart`:
- Around line 9-10: Validate the JSON-parsed passkey values before assigning
them to interop parameters: in auth0_flutter/lib/src/web/js_interop_utils.dart
lines 9-10, stop treating parseJson’s result as automatically object-shaped; in
auth0_flutter/lib/src/web/extensions/passkey_extensions.dart line 19, ensure
credential is a JSObject; and in
auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart line 367, validate
credential and userMetadata against their required JSAny/JSObject? types before
constructing the interop params, rejecting or handling invalid user-supplied
values.

In `@auth0_flutter/test/web/auth0_extension_type_mocks.dart`:
- Around line 27-30: Update PasskeyChallengeResponseImpl so constructing it does
not throw during authSession initialization; provide a non-throwing default
value while preserving the exported mock’s ability to be populated by the test
bridge.

In `@auth0_flutter/test/web/auth0_flutter_web_test.dart`:
- Around line 679-682: Update the passkeyChallengeCredentials fixture to
construct the publicKey JSObject directly rather than casting the result of
jsify(). Remove the unchecked as JSObject cast while preserving the existing
challenge value and PasskeyChallengeResponse setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 726bb35a-405c-47cd-b5bc-885b12529173

📥 Commits

Reviewing files that changed from the base of the PR and between 51d4a78 and da24c83.

⛔ Files ignored due to path filters (5)
  • auth0_flutter/example/lib/example_app.dart is excluded by !**/example/**
  • auth0_flutter/example/lib/passkey_card.dart is excluded by !**/example/**
  • auth0_flutter/example/pubspec.yaml is excluded by !**/example/**
  • auth0_flutter/example/web/index.html is excluded by !**/example/**
  • auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart is excluded by !**/*.mocks.dart
📒 Files selected for processing (20)
  • auth0_flutter/EXAMPLES.md
  • auth0_flutter/README.md
  • auth0_flutter/lib/auth0_flutter_web.dart
  • auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart
  • auth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dart
  • auth0_flutter/lib/src/web/auth0_flutter_web_platform_proxy.dart
  • auth0_flutter/lib/src/web/extensions/passkey_extensions.dart
  • auth0_flutter/lib/src/web/extensions/web_exception_extensions.dart
  • auth0_flutter/lib/src/web/js_interop.dart
  • auth0_flutter/lib/src/web/js_interop_utils.dart
  • auth0_flutter/test/web/auth0_extension_type_mocks.dart
  • auth0_flutter/test/web/auth0_flutter_web_test.dart
  • auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart
  • auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart
  • auth0_flutter_platform_interface/lib/src/web/web_exception.dart
  • auth0_flutter_platform_interface/lib/src/web/web_get_token_by_passkey_options.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_challenge.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_login_challenge_options.dart
  • auth0_flutter_platform_interface/lib/src/web/web_passkey_signup_challenge_options.dart
  • auth0_flutter_platform_interface/test/auth0_flutter_web_platform_test.dart

Comment on lines +25 to +28
WebException.passkeyError(final String error, final String message,
[final Map<String, dynamic>? details])
: this(WebException._passkeyError, message,
{'code': error, ...details ?? {}});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the underlying passkey error code authoritative.

details['code'] currently overwrites error. Merge details first, then assign code.

Proposed fix
-            {'code': error, ...details ?? {}});
+            <String, dynamic>{
+              if (details != null) ...details,
+              'code': error,
+            });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WebException.passkeyError(final String error, final String message,
[final Map<String, dynamic>? details])
: this(WebException._passkeyError, message,
{'code': error, ...details ?? {}});
WebException.passkeyError(final String error, final String message,
[final Map<String, dynamic>? details])
: this(WebException._passkeyError, message,
<String, dynamic>{
if (details != null) ...details,
'code': error,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter_platform_interface/lib/src/web/web_exception.dart` around lines
25 - 28, Update the WebException.passkeyError constructor so the optional
details map is merged before assigning the code field, ensuring the provided
error value remains authoritative even when details contains a code key.

Comment thread auth0_flutter/lib/auth0_flutter_web.dart
Comment thread auth0_flutter/lib/src/web/extensions/web_exception_extensions.dart
Comment thread auth0_flutter/lib/src/web/js_interop_utils.dart
Comment on lines +27 to +30
@JSExport()
class PasskeyChallengeResponseImpl {
String authSession = throw Exception('');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the exported mock constructible.

PasskeyChallengeResponseImpl() immediately throws while initializing authSession, so it cannot be exported and populated by the test bridge.

Proposed fix
 class PasskeyChallengeResponseImpl {
-  String authSession = throw Exception('');
+  late String authSession;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@JSExport()
class PasskeyChallengeResponseImpl {
String authSession = throw Exception('');
}
`@JSExport`()
class PasskeyChallengeResponseImpl {
late String authSession;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter/test/web/auth0_extension_type_mocks.dart` around lines 27 - 30,
Update PasskeyChallengeResponseImpl so constructing it does not throw during
authSession initialization; provide a non-throwing default value while
preserving the exported mock’s ability to be populated by the test bridge.

Comment on lines +679 to +682
final passkeyChallengeCredentials = interop.PasskeyChallengeResponse(
authSession: 'auth-session-123',
publicKey: {'challenge': 'challenge-value'}.jsify() as JSObject,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid the unchecked JSObject cast in the fixture.

Construct the JSObject directly instead of casting the result of jsify().

As per path instructions, “use as only after guards.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter/test/web/auth0_flutter_web_test.dart` around lines 679 - 682,
Update the passkeyChallengeCredentials fixture to construct the publicKey
JSObject directly rather than casting the result of jsify(). Remove the
unchecked as JSObject cast while preserving the existing challenge value and
PasskeyChallengeResponse setup.

Source: Path instructions

Implements passkeySignupChallenge, passkeyLoginChallenge, and
getTokenByPasskey on Auth0Web by delegating to auth0-spa-js's
passkey API (requires v2.24.0+), mirroring the react-native-auth0
web passkeys implementation.
example_app.dart is compiled for every platform (Android, iOS, macOS,
Windows, web), but it directly imported dart:js_interop and
package:web to drive navigator.credentials for the passkey demo.
Those libraries don't exist outside a web compile target, so every
native CI job failed with "Dart library 'dart:js_interop' is not
available on this platform."

Moves the WebAuthn calls into passkey_web_helper.dart, conditionally
exporting a real (web) vs stub (non-web) implementation via
dart.library.js_interop, mirroring the pattern the plugin itself
already uses in lib/src/web.dart.
- Apply JsInteropUtils.stripNulls uniformly across
  passkeySignupChallenge, passkeyLoginChallenge, and
  getTokenByPasskey, matching the convention used everywhere else
  in auth0_flutter_plugin_real.dart.
- Validate getTokenByPasskey's authResponse type before dispatching,
  raising a clear ArgumentError for unsupported shapes instead of
  letting an unrelated cast fail deep inside the JS interop call and
  get mis-mapped as a PasskeyError.
- Note in EXAMPLES.md and the dartdoc that Auth0Web's `realm`
  parameter is the web-side name for what native calls `connection`,
  since they refer to the same database connection.
getTokenByPasskey's raw-credential path calls auth0-spa-js's
passkey.getTokenWithPasskey(), which internally routes through
Auth0Client._requestTokenForPasskey() -> _requestToken() ->
the /oauth/token endpoint. Failures there (invalid_grant,
access_denied, mfa_required, etc.) are NOT wrapped in a PasskeyError
-- they propagate as GenericError/MfaRequiredError/AuthenticationError,
which carry error/error_description (and, for mfa_required,
mfa_token/mfa_requirements), not code/message.

WebExceptionExtension.fromPasskeyError previously assumed every
passkey failure had the {code, message} shape used by
passkeySignupChallenge/passkeyLoginChallenge (PasskeyRegisterError/
PasskeyChallengeError) and the passkey_invalid_credential check.
Any token-exchange failure fell through to the code?.toDart ??
'passkey_error' fallback, discarding the real error code, message,
and the mfa_token needed to continue an MFA flow.

fromPasskeyError now detects the {error, error_description} shape
and delegates to the existing fromJsObject mapping, so a passkey
token exchange that requires MFA surfaces identically to
credentials()/customTokenExchange() (code == 'MFA_REQUIRED',
details['mfaToken']), and other token-endpoint errors surface as
AUTHENTICATION_ERROR with the real OAuth error code intact.

Documents both this and the separate DOMException surface from
navigator.credentials.create()/.get() (uncaught by Auth0Web, since
that call happens in app code) in EXAMPLES.md.
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.

1 participant