feat: add passkeys support for web - #907
Conversation
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (8)
📒 Files selected for processing (20)
WalkthroughWeb passkey support is added through new Auth0Web APIs, platform contracts, JavaScript interop bindings, challenge and credential exchange handling, error conversion, tests, and WebAuthn documentation. ChangesWeb passkey flows
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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (5)
auth0_flutter/example/lib/example_app.dartis excluded by!**/example/**auth0_flutter/example/lib/passkey_card.dartis excluded by!**/example/**auth0_flutter/example/pubspec.yamlis excluded by!**/example/**auth0_flutter/example/web/index.htmlis excluded by!**/example/**auth0_flutter/test/web/auth0_flutter_web_test.mocks.dartis excluded by!**/*.mocks.dart
📒 Files selected for processing (20)
auth0_flutter/EXAMPLES.mdauth0_flutter/README.mdauth0_flutter/lib/auth0_flutter_web.dartauth0_flutter/lib/src/web/auth0_flutter_plugin_real.dartauth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dartauth0_flutter/lib/src/web/auth0_flutter_web_platform_proxy.dartauth0_flutter/lib/src/web/extensions/passkey_extensions.dartauth0_flutter/lib/src/web/extensions/web_exception_extensions.dartauth0_flutter/lib/src/web/js_interop.dartauth0_flutter/lib/src/web/js_interop_utils.dartauth0_flutter/test/web/auth0_extension_type_mocks.dartauth0_flutter/test/web/auth0_flutter_web_test.dartauth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dartauth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dartauth0_flutter_platform_interface/lib/src/web/web_exception.dartauth0_flutter_platform_interface/lib/src/web/web_get_token_by_passkey_options.dartauth0_flutter_platform_interface/lib/src/web/web_passkey_challenge.dartauth0_flutter_platform_interface/lib/src/web/web_passkey_login_challenge_options.dartauth0_flutter_platform_interface/lib/src/web/web_passkey_signup_challenge_options.dartauth0_flutter_platform_interface/test/auth0_flutter_web_platform_test.dart
| WebException.passkeyError(final String error, final String message, | ||
| [final Map<String, dynamic>? details]) | ||
| : this(WebException._passkeyError, message, | ||
| {'code': error, ...details ?? {}}); |
There was a problem hiding this comment.
🎯 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.
| 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.
| @JSExport() | ||
| class PasskeyChallengeResponseImpl { | ||
| String authSession = throw Exception(''); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| @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.
| final passkeyChallengeCredentials = interop.PasskeyChallengeResponse( | ||
| authSession: 'auth-session-123', | ||
| publicKey: {'challenge': 'challenge-value'}.jsify() as JSObject, | ||
| ); |
There was a problem hiding this comment.
🎯 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
da24c83 to
9d31d91
Compare
9d31d91 to
f746e0c
Compare
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.
f746e0c to
ce29575
Compare
Summary
passkeySignupChallenge,passkeyLoginChallenge, andgetTokenByPasskeytoAuth0Web, delegating to@auth0/auth0-spa-js's passkey API (passkey.getSignupChallenge,passkey.getLoginChallenge,passkey.getTokenWithPasskey/_requestTokenForPasskey). Requiresauth0-spa-jsv2.24.0+ loaded on the page.navigator.credentials.create()/.get()) instead of a native module.getTokenByPasskey'sauthResponseaccepts either the rawPublicKeyCredentialreturned directly bynavigator.credentials(routed throughpasskey.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.WebPasskeyChallenge,WebPasskeySignupChallengeOptions,WebPasskeyLoginChallengeOptions,WebGetTokenByPasskeyOptions, and aWebException.passkeyErrorconstructor for consistent error mapping.EXAMPLES.mdwith a new "Passkeys on the Web" section and README with accurate platform-support notes (previously overstated web support before this landed).PasskeyCarddemo to the example app's web build, wired tonavigator.credentials.Platform.isWindowscalls crashed unconditionally on web.Test plan
flutter test(unit) passing in bothauth0_flutterandauth0_flutter_platform_interfaceflutter test --tags browser --platform chromepassing inauth0_flutter, including new passkey coverage inauth0_flutter_web_test.dart(challenge mapping, raw-credential vs JSON-string exchange paths, error propagation)flutter analyzeclean acrossauth0_flutter,auth0_flutter_platform_interface, and the example app🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation