diff --git a/packages/corbado_auth_firebase/example/windows/flutter/generated_plugins.cmake b/packages/corbado_auth_firebase/example/windows/flutter/generated_plugins.cmake index b8ca9121..1fab54d6 100644 --- a/packages/corbado_auth_firebase/example/windows/flutter/generated_plugins.cmake +++ b/packages/corbado_auth_firebase/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_auth firebase_core flutter_secure_storage_windows + passkeys_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/passkeys/passkeys/example/lib/pages/base_page.dart b/packages/passkeys/passkeys/example/lib/pages/base_page.dart index 6686553d..b5a3880f 100644 --- a/packages/passkeys/passkeys/example/lib/pages/base_page.dart +++ b/packages/passkeys/passkeys/example/lib/pages/base_page.dart @@ -9,7 +9,6 @@ class BasePage extends StatelessWidget { @override Widget build(BuildContext context) { - SchedulerBinding.instance.addPostFrameCallback((_) { if (!context.mounted) return; DebugOverlay.show(context); diff --git a/packages/passkeys/passkeys/example/lib/pages/error_page.dart b/packages/passkeys/passkeys/example/lib/pages/error_page.dart index 59e27ca3..3053681f 100644 --- a/packages/passkeys/passkeys/example/lib/pages/error_page.dart +++ b/packages/passkeys/passkeys/example/lib/pages/error_page.dart @@ -36,7 +36,11 @@ class ErrorPage extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - Text(hint!, textAlign: TextAlign.center, style: TextStyle(fontSize: 12),), + Text( + hint!, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12), + ), ], ), ], diff --git a/packages/passkeys/passkeys/example/lib/providers.dart b/packages/passkeys/passkeys/example/lib/providers.dart index d35c5cdb..780c169b 100644 --- a/packages/passkeys/passkeys/example/lib/providers.dart +++ b/packages/passkeys/passkeys/example/lib/providers.dart @@ -12,7 +12,7 @@ final authServiceProvider = Provider((ref) { return AuthService(rps: rps, authenticator: authenticator); }); -final doctorProvider = StreamProvider((ref) async*{ +final doctorProvider = StreamProvider((ref) async* { final authService = ref.watch(authServiceProvider); await for (final value in authService.authenticator.resultStream) { diff --git a/packages/passkeys/passkeys/example/lib/widgets/debug_overlay.dart b/packages/passkeys/passkeys/example/lib/widgets/debug_overlay.dart index 37048490..763cd6b4 100644 --- a/packages/passkeys/passkeys/example/lib/widgets/debug_overlay.dart +++ b/packages/passkeys/passkeys/example/lib/widgets/debug_overlay.dart @@ -96,8 +96,10 @@ class _DebugOverlayWidget extends HookConsumerWidget { ), ), const SizedBox(height: 2), - Text(info.description, - style: const TextStyle(fontSize: 12),), + Text( + info.description, + style: const TextStyle(fontSize: 12), + ), if (info.platforms.isNotEmpty) ...[ const SizedBox(height: 4), Text( diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java index 0d7d5270..325990b5 100644 --- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java +++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java @@ -89,6 +89,7 @@ public void register( @Nullable Long timeout, @Nullable String attestation, @NonNull List excludeCredentials, + @Nullable String extensions, @NonNull Messages.Result result) { if (android.os.Build.VERSION.SDK_INT < 28) { result.error(new Messages.FlutterError("android-passkey-unsupported", @@ -120,7 +121,8 @@ public void register( timeout, authSelectionType, attestation, - excludeCredentialsType); + excludeCredentialsType, + extensions); try { String options = createCredentialOptions.toJSON().toString(); @@ -164,6 +166,10 @@ public void onResult(CreateCredentialResponse res) { .setClientDataJSON(response.getString("clientDataJSON")) .setAttestationObject(response.getString("attestationObject")) .setTransports(typedTransports) + .setClientExtensionResults( + json.has("clientExtensionResults") + ? json.getJSONObject("clientExtensionResults").toString() + : null) .build()); } catch (JSONException e) { Log.e(TAG, "Error parsing response: " + resp, e); @@ -224,6 +230,7 @@ public void onError(CreateCredentialException e) { public void authenticate(@NonNull String relyingPartyId, @NonNull String challenge, @Nullable Long timeout, @Nullable String userVerification, @Nullable List allowCredentials, @Nullable Boolean preferImmediatelyAvailableCredentials, + @Nullable String extensions, @NonNull Messages.Result result) { if (android.os.Build.VERSION.SDK_INT < 28) { result.error(new Messages.FlutterError("android-passkey-unsupported", @@ -238,7 +245,7 @@ public void authenticate(@NonNull String relyingPartyId, @NonNull String challen .collect(Collectors.toList()); } GetCredentialOptions getCredentialOptions = new GetCredentialOptions(challenge, timeout, relyingPartyId, - allowCredentialsType, userVerification); + allowCredentialsType, userVerification, extensions); try { String options = getCredentialOptions.toJSON().toString(); @@ -283,7 +290,12 @@ public void onResult(GetCredentialResponse res) { final Messages.AuthenticateResponse msg = new Messages.AuthenticateResponse.Builder() .setId(id).setRawId(rawId).setClientDataJSON(clientDataJSON) .setAuthenticatorData(authenticatorData).setSignature(signature) - .setUserHandle(userHandle).build(); + .setUserHandle(userHandle) + .setClientExtensionResults( + json.has("clientExtensionResults") + ? json.getJSONObject("clientExtensionResults").toString() + : null) + .build(); result.success(msg); } catch (JSONException e) { diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java index 2b5e2bd1..62e590c8 100644 --- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java +++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java @@ -722,6 +722,17 @@ public void setTransports(@NonNull List setterArg) { this.transports = setterArg; } + /** JSON-encoded client extension results */ + private @Nullable String clientExtensionResults; + + public @Nullable String getClientExtensionResults() { + return clientExtensionResults; + } + + public void setClientExtensionResults(@Nullable String setterArg) { + this.clientExtensionResults = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ RegisterResponse() {} @@ -762,6 +773,13 @@ public static final class Builder { return this; } + private @Nullable String clientExtensionResults; + + public @NonNull Builder setClientExtensionResults(@Nullable String setterArg) { + this.clientExtensionResults = setterArg; + return this; + } + public @NonNull RegisterResponse build() { RegisterResponse pigeonReturn = new RegisterResponse(); pigeonReturn.setId(id); @@ -769,18 +787,20 @@ public static final class Builder { pigeonReturn.setClientDataJSON(clientDataJSON); pigeonReturn.setAttestationObject(attestationObject); pigeonReturn.setTransports(transports); + pigeonReturn.setClientExtensionResults(clientExtensionResults); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(5); + ArrayList toListResult = new ArrayList(6); toListResult.add(id); toListResult.add(rawId); toListResult.add(clientDataJSON); toListResult.add(attestationObject); toListResult.add(transports); + toListResult.add(clientExtensionResults); return toListResult; } @@ -796,6 +816,8 @@ ArrayList toList() { pigeonResult.setAttestationObject((String) attestationObject); Object transports = list.get(4); pigeonResult.setTransports((List) transports); + Object clientExtensionResults = list.get(5); + pigeonResult.setClientExtensionResults((String) clientExtensionResults); return pigeonResult; } } @@ -889,6 +911,17 @@ public void setUserHandle(@NonNull String setterArg) { this.userHandle = setterArg; } + /** JSON-encoded client extension results */ + private @Nullable String clientExtensionResults; + + public @Nullable String getClientExtensionResults() { + return clientExtensionResults; + } + + public void setClientExtensionResults(@Nullable String setterArg) { + this.clientExtensionResults = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ AuthenticateResponse() {} @@ -936,6 +969,13 @@ public static final class Builder { return this; } + private @Nullable String clientExtensionResults; + + public @NonNull Builder setClientExtensionResults(@Nullable String setterArg) { + this.clientExtensionResults = setterArg; + return this; + } + public @NonNull AuthenticateResponse build() { AuthenticateResponse pigeonReturn = new AuthenticateResponse(); pigeonReturn.setId(id); @@ -944,19 +984,21 @@ public static final class Builder { pigeonReturn.setAuthenticatorData(authenticatorData); pigeonReturn.setSignature(signature); pigeonReturn.setUserHandle(userHandle); + pigeonReturn.setClientExtensionResults(clientExtensionResults); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(6); + ArrayList toListResult = new ArrayList(7); toListResult.add(id); toListResult.add(rawId); toListResult.add(clientDataJSON); toListResult.add(authenticatorData); toListResult.add(signature); toListResult.add(userHandle); + toListResult.add(clientExtensionResults); return toListResult; } @@ -974,6 +1016,8 @@ ArrayList toList() { pigeonResult.setSignature((String) signature); Object userHandle = list.get(5); pigeonResult.setUserHandle((String) userHandle); + Object clientExtensionResults = list.get(6); + pigeonResult.setClientExtensionResults((String) clientExtensionResults); return pigeonResult; } } @@ -1053,9 +1097,9 @@ public interface PasskeysApi { void hasPasskeySupport(@NonNull Result result); - void register(@NonNull String challenge, @NonNull RelyingParty relyingParty, @NonNull User user, @Nullable AuthenticatorSelection authenticatorSelection, @Nullable List pubKeyCredParams, @Nullable Long timeout, @Nullable String attestation, @NonNull List excludeCredentials, @NonNull Result result); + void register(@NonNull String challenge, @NonNull RelyingParty relyingParty, @NonNull User user, @Nullable AuthenticatorSelection authenticatorSelection, @Nullable List pubKeyCredParams, @Nullable Long timeout, @Nullable String attestation, @NonNull List excludeCredentials, @Nullable String extensions, @NonNull Result result); - void authenticate(@NonNull String relyingPartyId, @NonNull String challenge, @Nullable Long timeout, @Nullable String userVerification, @Nullable List allowCredentials, @Nullable Boolean preferImmediatelyAvailableCredentials, @NonNull Result result); + void authenticate(@NonNull String relyingPartyId, @NonNull String challenge, @Nullable Long timeout, @Nullable String userVerification, @Nullable List allowCredentials, @Nullable Boolean preferImmediatelyAvailableCredentials, @Nullable String extensions, @NonNull Result result); void cancelCurrentAuthenticatorOperation(@NonNull Result result); @@ -1136,6 +1180,7 @@ public void error(Throwable error) { Number timeoutArg = (Number) args.get(5); String attestationArg = (String) args.get(6); List excludeCredentialsArg = (List) args.get(7); + String extensionsArg = (String) args.get(8); Result resultCallback = new Result() { public void success(RegisterResponse result) { @@ -1149,7 +1194,7 @@ public void error(Throwable error) { } }; - api.register(challengeArg, relyingPartyArg, userArg, authenticatorSelectionArg, pubKeyCredParamsArg, (timeoutArg == null) ? null : timeoutArg.longValue(), attestationArg, excludeCredentialsArg, resultCallback); + api.register(challengeArg, relyingPartyArg, userArg, authenticatorSelectionArg, pubKeyCredParamsArg, (timeoutArg == null) ? null : timeoutArg.longValue(), attestationArg, excludeCredentialsArg, extensionsArg, resultCallback); }); } else { channel.setMessageHandler(null); @@ -1170,6 +1215,7 @@ public void error(Throwable error) { String userVerificationArg = (String) args.get(3); List allowCredentialsArg = (List) args.get(4); Boolean preferImmediatelyAvailableCredentialsArg = (Boolean) args.get(5); + String extensionsArg = (String) args.get(6); Result resultCallback = new Result() { public void success(AuthenticateResponse result) { @@ -1183,7 +1229,7 @@ public void error(Throwable error) { } }; - api.authenticate(relyingPartyIdArg, challengeArg, (timeoutArg == null) ? null : timeoutArg.longValue(), userVerificationArg, allowCredentialsArg, preferImmediatelyAvailableCredentialsArg, resultCallback); + api.authenticate(relyingPartyIdArg, challengeArg, (timeoutArg == null) ? null : timeoutArg.longValue(), userVerificationArg, allowCredentialsArg, preferImmediatelyAvailableCredentialsArg, extensionsArg, resultCallback); }); } else { channel.setMessageHandler(null); diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/login/GetCredentialOptions.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/login/GetCredentialOptions.java index 60f7869d..d6b59f37 100644 --- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/login/GetCredentialOptions.java +++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/login/GetCredentialOptions.java @@ -17,13 +17,15 @@ public class GetCredentialOptions { private String rpId; private List allowCredentials; private String userVerification; + private String extensions; - public GetCredentialOptions(String challenge, Long timeout, String rpId, List allowCredentials, String userVerification) { + public GetCredentialOptions(String challenge, Long timeout, String rpId, List allowCredentials, String userVerification, String extensions) { this.challenge = challenge; this.timeout = timeout; this.rpId = rpId; this.allowCredentials = allowCredentials; this.userVerification = userVerification; + this.extensions = extensions; } public JSONObject toJSON() throws JSONException { @@ -42,6 +44,9 @@ public JSONObject toJSON() throws JSONException { } }).toArray())); } + if (extensions != null) { + json.put("extensions", new JSONObject(extensions)); + } return json; } diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/signup/CreateCredentialOptions.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/signup/CreateCredentialOptions.java index 0951d41e..20fcb856 100644 --- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/signup/CreateCredentialOptions.java +++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/models/signup/CreateCredentialOptions.java @@ -21,6 +21,8 @@ public class CreateCredentialOptions { private List excludeCredentials; + private String extensions; + public CreateCredentialOptions( String challenge, RelyingPartyType rp, @@ -29,7 +31,8 @@ public CreateCredentialOptions( Long timeout, AuthenticatorSelectionType authenticatorSelection, String attestation, - List excludeCredentials + List excludeCredentials, + String extensions ) { this.challenge = challenge; this.rp = rp; @@ -39,6 +42,7 @@ public CreateCredentialOptions( this.authenticatorSelection = authenticatorSelection; this.attestation = attestation; this.excludeCredentials = excludeCredentials; + this.extensions = extensions; } public JSONObject toJSON() throws JSONException { @@ -54,6 +58,8 @@ public JSONObject toJSON() throws JSONException { json.put("authenticatorSelection", authenticatorSelection.toJSON()); if (excludeCredentials != null) json.put("excludeCredentials", new JSONArray(excludeCredentials.stream().map(ExcludeCredentialType::toJSON).toArray())); + if (extensions != null) + json.put("extensions", new JSONObject(extensions)); return json; } diff --git a/packages/passkeys/passkeys_android/lib/messages.g.dart b/packages/passkeys/passkeys_android/lib/messages.g.dart index 7636bf51..7528f6a8 100644 --- a/packages/passkeys/passkeys_android/lib/messages.g.dart +++ b/packages/passkeys/passkeys_android/lib/messages.g.dart @@ -219,6 +219,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -236,6 +237,9 @@ class RegisterResponse { /// The supported transports for the authenticator List transports; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -243,6 +247,7 @@ class RegisterResponse { clientDataJSON, attestationObject, transports, + clientExtensionResults, ]; } @@ -254,6 +259,7 @@ class RegisterResponse { clientDataJSON: result[2]! as String, attestationObject: result[3]! as String, transports: (result[4] as List?)!.cast(), + clientExtensionResults: result[5] as String?, ); } } @@ -267,6 +273,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, required this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -286,6 +293,9 @@ class AuthenticateResponse { String userHandle; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -294,6 +304,7 @@ class AuthenticateResponse { authenticatorData, signature, userHandle, + clientExtensionResults, ]; } @@ -306,6 +317,7 @@ class AuthenticateResponse { authenticatorData: result[3]! as String, signature: result[4]! as String, userHandle: result[5]! as String, + clientExtensionResults: result[6] as String?, ); } } @@ -346,21 +358,21 @@ class _PasskeysApiCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return AllowCredential.decode(readValue(buffer)!); - case 129: + case 129: return AuthenticateResponse.decode(readValue(buffer)!); - case 130: + case 130: return AuthenticatorSelection.decode(readValue(buffer)!); - case 131: + case 131: return ExcludeCredential.decode(readValue(buffer)!); - case 132: + case 132: return PubKeyCredParam.decode(readValue(buffer)!); - case 133: + case 133: return RegisterResponse.decode(readValue(buffer)!); - case 134: + case 134: return RelyingParty.decode(readValue(buffer)!); - case 135: + case 135: return User.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -380,10 +392,10 @@ class PasskeysApi { Future canAuthenticate() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.passkeys_android.PasskeysApi.canAuthenticate', codec, + 'dev.flutter.pigeon.passkeys_android.PasskeysApi.canAuthenticate', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -407,10 +419,10 @@ class PasskeysApi { Future hasPasskeySupport() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.passkeys_android.PasskeysApi.hasPasskeySupport', codec, + 'dev.flutter.pigeon.passkeys_android.PasskeysApi.hasPasskeySupport', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -432,12 +444,30 @@ class PasskeysApi { } } - Future register(String arg_challenge, RelyingParty arg_relyingParty, User arg_user, AuthenticatorSelection? arg_authenticatorSelection, List? arg_pubKeyCredParams, int? arg_timeout, String? arg_attestation, List arg_excludeCredentials) async { + Future register( + String arg_challenge, + RelyingParty arg_relyingParty, + User arg_user, + AuthenticatorSelection? arg_authenticatorSelection, + List? arg_pubKeyCredParams, + int? arg_timeout, + String? arg_attestation, + List arg_excludeCredentials, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_android.PasskeysApi.register', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_challenge, arg_relyingParty, arg_user, arg_authenticatorSelection, arg_pubKeyCredParams, arg_timeout, arg_attestation, arg_excludeCredentials]) as List?; + final List? replyList = await channel.send([ + arg_challenge, + arg_relyingParty, + arg_user, + arg_authenticatorSelection, + arg_pubKeyCredParams, + arg_timeout, + arg_attestation, + arg_excludeCredentials, + arg_extensions + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -459,12 +489,26 @@ class PasskeysApi { } } - Future authenticate(String arg_relyingPartyId, String arg_challenge, int? arg_timeout, String? arg_userVerification, List? arg_allowCredentials, bool? arg_preferImmediatelyAvailableCredentials) async { + Future authenticate( + String arg_relyingPartyId, + String arg_challenge, + int? arg_timeout, + String? arg_userVerification, + List? arg_allowCredentials, + bool? arg_preferImmediatelyAvailableCredentials, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_android.PasskeysApi.authenticate', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_relyingPartyId, arg_challenge, arg_timeout, arg_userVerification, arg_allowCredentials, arg_preferImmediatelyAvailableCredentials]) as List?; + final List? replyList = await channel.send([ + arg_relyingPartyId, + arg_challenge, + arg_timeout, + arg_userVerification, + arg_allowCredentials, + arg_preferImmediatelyAvailableCredentials, + arg_extensions + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -488,10 +532,10 @@ class PasskeysApi { Future cancelCurrentAuthenticatorOperation() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.passkeys_android.PasskeysApi.cancelCurrentAuthenticatorOperation', codec, + 'dev.flutter.pigeon.passkeys_android.PasskeysApi.cancelCurrentAuthenticatorOperation', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/passkeys/passkeys_android/lib/passkeys_android.dart b/packages/passkeys/passkeys_android/lib/passkeys_android.dart index 0002cd94..52bc9bf3 100644 --- a/packages/passkeys/passkeys_android/lib/passkeys_android.dart +++ b/packages/passkeys/passkeys_android/lib/passkeys_android.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:passkeys_android/messages.g.dart'; import 'package:passkeys_platform_interface/passkeys_platform_interface.dart'; @@ -32,6 +34,7 @@ class PasskeysAndroid extends PasskeysPlatform { ); }).toList(), request.preferImmediatelyAvailableCredentials, + request.extensions != null ? jsonEncode(request.extensions) : null, ); return AuthenticateResponseType( @@ -40,7 +43,10 @@ class PasskeysAndroid extends PasskeysPlatform { clientDataJSON: r.clientDataJSON, authenticatorData: r.authenticatorData, signature: r.signature, - userHandle: r.userHandle); + userHandle: r.userHandle, + clientExtensionResults: r.clientExtensionResults != null + ? jsonDecode(r.clientExtensionResults!) as Map? + : null); } @override @@ -92,7 +98,8 @@ class PasskeysAndroid extends PasskeysPlatform { request.attestation, request.excludeCredentials .map((e) => ExcludeCredential(id: e.id, type: e.type)) - .toList()); + .toList(), + request.extensions != null ? jsonEncode(request.extensions) : null); return RegisterResponseType( id: r.id, @@ -100,6 +107,9 @@ class PasskeysAndroid extends PasskeysPlatform { clientDataJSON: r.clientDataJSON, attestationObject: r.attestationObject, transports: r.transports.whereType().toList(), + clientExtensionResults: r.clientExtensionResults != null + ? jsonDecode(r.clientExtensionResults!) as Map? + : null, ); } diff --git a/packages/passkeys/passkeys_android/pigeons/messages.dart b/packages/passkeys/passkeys_android/pigeons/messages.dart index a2f251c5..8a619ec9 100644 --- a/packages/passkeys/passkeys_android/pigeons/messages.dart +++ b/packages/passkeys/passkeys_android/pigeons/messages.dart @@ -105,6 +105,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -121,6 +122,9 @@ class RegisterResponse { /// The supported transports for the authenticator final List transports; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } /// Represents an authenticate response @@ -133,6 +137,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, required this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -151,6 +156,9 @@ class AuthenticateResponse { final String signature; final String userHandle; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } @HostApi() @@ -171,6 +179,7 @@ abstract class PasskeysApi { int? timeout, String? attestation, List excludeCredentials, + String? extensions, ); @async @@ -180,7 +189,8 @@ abstract class PasskeysApi { int? timeout, String? userVerification, List? allowCredentials, - bool? preferImmediatelyAvailableCredentials); + bool? preferImmediatelyAvailableCredentials, + String? extensions); @async void cancelCurrentAuthenticatorOperation(); diff --git a/packages/passkeys/passkeys_darwin/darwin/Classes/AuthenticateController.swift b/packages/passkeys/passkeys_darwin/darwin/Classes/AuthenticateController.swift index 118e8462..e8809ad0 100644 --- a/packages/passkeys/passkeys_darwin/darwin/Classes/AuthenticateController.swift +++ b/packages/passkeys/passkeys_darwin/darwin/Classes/AuthenticateController.swift @@ -14,9 +14,11 @@ import FlutterMacOS class AuthenticateController: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding, Cancellable { public var completion: ((Result) -> Void)? private var innerCancel: (() -> Void)?; + private var extensions: [String: Any]? - init(completion: @escaping ((Result) -> Void)) { + init(completion: @escaping ((Result) -> Void), extensions: [String: Any]? = nil) { self.completion = completion; + self.extensions = extensions; } func run(requests: [ASAuthorizationRequest], conditionalUI: Bool, preferImmediatelyAvailableCredentials: Bool) { @@ -63,13 +65,38 @@ class AuthenticateController: NSObject, ASAuthorizationControllerDelegate, ASAut completion?(.success(response)) break case let r as ASAuthorizationPlatformPublicKeyCredentialAssertion: + var extensionResults: [String: Any] = [:] + + // Extract PRF assertion result if available + if #available(iOS 18.0, macOS 15.0, *) { + if extensions?["prf"] != nil { + if let prfResult = r.prf { + var results: [String: Any] = ["first": prfResult.first.toBase64URL()] + if let second = prfResult.second { + results["second"] = second.toBase64URL() + } + extensionResults["prf"] = ["results": results] + } + } + } + + let clientExtensionResultsJson: String? + if !extensionResults.isEmpty, + let jsonData = try? JSONSerialization.data(withJSONObject: extensionResults), + let jsonString = String(data: jsonData, encoding: .utf8) { + clientExtensionResultsJson = jsonString + } else { + clientExtensionResultsJson = nil + } + let response = AuthenticateResponse( id: r.credentialID.toBase64URL(), rawId: r.credentialID.toBase64URL(), clientDataJSON: r.rawClientDataJSON.toBase64URL(), authenticatorData: r.rawAuthenticatorData.toBase64URL(), signature: r.signature.toBase64URL(), - userHandle: r.userID?.toBase64URL() + userHandle: r.userID?.toBase64URL(), + clientExtensionResults: clientExtensionResultsJson ) completion?(.success(response)) diff --git a/packages/passkeys/passkeys_darwin/darwin/Classes/PasskeysPlugin.swift b/packages/passkeys/passkeys_darwin/darwin/Classes/PasskeysPlugin.swift index 629907e4..a2b5fed1 100644 --- a/packages/passkeys/passkeys_darwin/darwin/Classes/PasskeysPlugin.swift +++ b/packages/passkeys/passkeys_darwin/darwin/Classes/PasskeysPlugin.swift @@ -53,6 +53,7 @@ public class PasskeysPlugin: NSObject, FlutterPlugin, PasskeysApi { canBeSecurityKey: Bool = true, residentKeyPreference: String?, attestationPreference: String?, + extensions: String?, completion: @escaping (Result) -> Void ) { guard (try? canAuthenticate()) == true else { @@ -138,12 +139,39 @@ public class PasskeysPlugin: NSObject, FlutterPlugin, PasskeysApi { requests.append(externalRequest) } + // Parse extensions JSON + var extensionsDict: [String: Any]? + if let extensionsJson = extensions, + let data = extensionsJson.data(using: .utf8), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + extensionsDict = parsed + } + + // Apply PRF extension to platform registration requests if available + if #available(iOS 18.0, macOS 15.0, *) { + if let prfExt = extensionsDict?["prf"] as? [String: Any] { + for request in requests { + if let platformRequest = request as? ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest { + if let eval = prfExt["eval"] as? [String: Any], + let firstB64 = eval["first"] as? String, + let firstData = Data.fromBase64Url(firstB64) { + let secondData = (eval["second"] as? String).flatMap { Data.fromBase64Url($0) } + let saltValues = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues.saltInput1(firstData, saltInput2: secondData) + platformRequest.prf = .inputValues(saltValues) + } else { + platformRequest.prf = .checkForSupport + } + } + } + } + } + func wrappedCompletion(result: Result) { lock.unlock() completion(result) } - let con = RegisterController(completion: wrappedCompletion) + let con = RegisterController(completion: wrappedCompletion, extensions: extensionsDict) con.run(requests: requests) inFlightController = con } @@ -154,6 +182,7 @@ public class PasskeysPlugin: NSObject, FlutterPlugin, PasskeysApi { conditionalUI: Bool, allowedCredentials: [CredentialType], preferImmediatelyAvailableCredentials: Bool, + extensions: String?, completion: @escaping (Result) -> Void ) { guard (try? canAuthenticate()) == true else { @@ -182,7 +211,49 @@ public class PasskeysPlugin: NSObject, FlutterPlugin, PasskeysApi { requests.append(externalRequest) } - let con = AuthenticateController(completion: completion) + // Parse extensions JSON + var extensionsDict: [String: Any]? + if let extensionsJson = extensions, + let data = extensionsJson.data(using: .utf8), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + extensionsDict = parsed + } + + // Apply PRF extension to platform assertion requests if available + if #available(iOS 18.0, macOS 15.0, *) { + if let prfExt = extensionsDict?["prf"] as? [String: Any], + let eval = prfExt["eval"] as? [String: Any], + let firstB64 = eval["first"] as? String, + let firstData = Data.fromBase64Url(firstB64) { + let secondData = (eval["second"] as? String).flatMap { Data.fromBase64Url($0) } + let inputValues = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues.saltInput1(firstData, saltInput2: secondData) + + var perCredentialValues: [Data: ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues]? + if let evalByCredential = prfExt["evalByCredential"] as? [String: [String: Any]] { + perCredentialValues = [:] + for (credId, salts) in evalByCredential { + if let credIdData = Data.fromBase64Url(credId), + let saltFirstB64 = salts["first"] as? String, + let saltFirstData = Data.fromBase64Url(saltFirstB64) { + let saltSecondData = (salts["second"] as? String).flatMap { Data.fromBase64Url($0) } + perCredentialValues?[credIdData] = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues.saltInput1(saltFirstData, saltInput2: saltSecondData) + } + } + } + + let prfInput = ASAuthorizationPublicKeyCredentialPRFAssertionInput.inputValues( + inputValues, perCredentialInputValues: perCredentialValues + ) + + for request in requests { + if let platformRequest = request as? ASAuthorizationPlatformPublicKeyCredentialAssertionRequest { + platformRequest.prf = prfInput + } + } + } + } + + let con = AuthenticateController(completion: completion, extensions: extensionsDict) con.run(requests: requests, conditionalUI: conditionalUI, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentials) inFlightController = con } @@ -308,3 +379,11 @@ extension Data { return result } } + +import CryptoKit + +extension SymmetricKey { + func toBase64URL() -> String { + return withUnsafeBytes { Data(Array($0)).toBase64URL() } + } +} diff --git a/packages/passkeys/passkeys_darwin/darwin/Classes/RegisterController.swift b/packages/passkeys/passkeys_darwin/darwin/Classes/RegisterController.swift index 5d4920b8..a764f699 100644 --- a/packages/passkeys/passkeys_darwin/darwin/Classes/RegisterController.swift +++ b/packages/passkeys/passkeys_darwin/darwin/Classes/RegisterController.swift @@ -14,9 +14,11 @@ import FlutterMacOS class RegisterController: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding, Cancellable { private var completion: ((Result) -> Void)? private var cancelAuthorization: (() -> Void)?; + private var extensions: [String: Any]? - init(completion: @escaping ((Result) -> Void)) { + init(completion: @escaping ((Result) -> Void), extensions: [String: Any]? = nil) { self.completion = completion; + self.extensions = extensions; } func run(requests: [ASAuthorizationRequest]) { @@ -37,12 +39,41 @@ class RegisterController: NSObject, ASAuthorizationControllerDelegate, ASAuthori func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential { case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: + var extensionResults: [String: Any] = [:] + + // Extract PRF registration result if available + if #available(iOS 18.0, macOS 15.0, *) { + if extensions?["prf"] != nil { + if let prfResult = credentialRegistration.prf { + var prfDict: [String: Any] = ["enabled": prfResult.isSupported] + if let first = prfResult.first { + var results: [String: Any] = ["first": first.toBase64URL()] + if let second = prfResult.second { + results["second"] = second.toBase64URL() + } + prfDict["results"] = results + } + extensionResults["prf"] = prfDict + } + } + } + + let clientExtensionResultsJson: String? + if !extensionResults.isEmpty, + let jsonData = try? JSONSerialization.data(withJSONObject: extensionResults), + let jsonString = String(data: jsonData, encoding: .utf8) { + clientExtensionResultsJson = jsonString + } else { + clientExtensionResultsJson = nil + } + let response = RegisterResponse( id: credentialRegistration.credentialID.toBase64URL(), rawId: credentialRegistration.credentialID.toBase64URL(), clientDataJSON: credentialRegistration.rawClientDataJSON.toBase64URL(), attestationObject: credentialRegistration.rawAttestationObject!.toBase64URL(), - transports: [] + transports: [], + clientExtensionResults: clientExtensionResultsJson ) completion?(.success(response)) break diff --git a/packages/passkeys/passkeys_darwin/darwin/Classes/messages.swift b/packages/passkeys/passkeys_darwin/darwin/Classes/messages.swift index 9b52cbd9..12f767b0 100644 --- a/packages/passkeys/passkeys_darwin/darwin/Classes/messages.swift +++ b/packages/passkeys/passkeys_darwin/darwin/Classes/messages.swift @@ -135,6 +135,8 @@ struct RegisterResponse { var attestationObject: String /// The supported transports for the authenticator var transports: [String?] + /// JSON-encoded client extension results + var clientExtensionResults: String? = nil static func fromList(_ list: [Any?]) -> RegisterResponse? { let id = list[0] as! String @@ -142,13 +144,15 @@ struct RegisterResponse { let clientDataJSON = list[2] as! String let attestationObject = list[3] as! String let transports = list[4] as! [String?] + let clientExtensionResults: String? = nilOrValue(list[5]) return RegisterResponse( id: id, rawId: rawId, clientDataJSON: clientDataJSON, attestationObject: attestationObject, - transports: transports + transports: transports, + clientExtensionResults: clientExtensionResults ) } func toList() -> [Any?] { @@ -158,6 +162,7 @@ struct RegisterResponse { clientDataJSON, attestationObject, transports, + clientExtensionResults, ] } } @@ -177,6 +182,8 @@ struct AuthenticateResponse { /// Signed challenge var signature: String var userHandle: String? = nil + /// JSON-encoded client extension results + var clientExtensionResults: String? = nil static func fromList(_ list: [Any?]) -> AuthenticateResponse? { let id = list[0] as! String @@ -185,6 +192,7 @@ struct AuthenticateResponse { let authenticatorData = list[3] as! String let signature = list[4] as! String let userHandle: String? = nilOrValue(list[5]) + let clientExtensionResults: String? = nilOrValue(list[6]) return AuthenticateResponse( id: id, @@ -192,7 +200,8 @@ struct AuthenticateResponse { clientDataJSON: clientDataJSON, authenticatorData: authenticatorData, signature: signature, - userHandle: userHandle + userHandle: userHandle, + clientExtensionResults: clientExtensionResults ) } func toList() -> [Any?] { @@ -203,6 +212,7 @@ struct AuthenticateResponse { authenticatorData, signature, userHandle, + clientExtensionResults, ] } } @@ -267,8 +277,8 @@ class PasskeysApiCodec: FlutterStandardMessageCodec { protocol PasskeysApi { func canAuthenticate() throws -> Bool func hasBiometrics() throws -> Bool - func register(challenge: String, relyingParty: RelyingParty, user: User, excludeCredentials: [CredentialType], pubKeyCredValues: [Int64], canBePlatformAuthenticator: Bool, canBeSecurityKey: Bool, residentKeyPreference: String?, attestationPreference: String?, completion: @escaping (Result) -> Void) - func authenticate(relyingPartyId: String, challenge: String, conditionalUI: Bool, allowedCredentials: [CredentialType], preferImmediatelyAvailableCredentials: Bool, completion: @escaping (Result) -> Void) + func register(challenge: String, relyingParty: RelyingParty, user: User, excludeCredentials: [CredentialType], pubKeyCredValues: [Int64], canBePlatformAuthenticator: Bool, canBeSecurityKey: Bool, residentKeyPreference: String?, attestationPreference: String?, extensions: String?, completion: @escaping (Result) -> Void) + func authenticate(relyingPartyId: String, challenge: String, conditionalUI: Bool, allowedCredentials: [CredentialType], preferImmediatelyAvailableCredentials: Bool, extensions: String?, completion: @escaping (Result) -> Void) func cancelCurrentAuthenticatorOperation(completion: @escaping (Result) -> Void) } @@ -317,7 +327,8 @@ class PasskeysApiSetup { let canBeSecurityKeyArg = args[6] as! Bool let residentKeyPreferenceArg: String? = nilOrValue(args[7]) let attestationPreferenceArg: String? = nilOrValue(args[8]) - api.register(challenge: challengeArg, relyingParty: relyingPartyArg, user: userArg, excludeCredentials: excludeCredentialsArg, pubKeyCredValues: pubKeyCredValuesArg, canBePlatformAuthenticator: canBePlatformAuthenticatorArg, canBeSecurityKey: canBeSecurityKeyArg, residentKeyPreference: residentKeyPreferenceArg, attestationPreference: attestationPreferenceArg) { result in + let extensionsArg: String? = nilOrValue(args[9]) + api.register(challenge: challengeArg, relyingParty: relyingPartyArg, user: userArg, excludeCredentials: excludeCredentialsArg, pubKeyCredValues: pubKeyCredValuesArg, canBePlatformAuthenticator: canBePlatformAuthenticatorArg, canBeSecurityKey: canBeSecurityKeyArg, residentKeyPreference: residentKeyPreferenceArg, attestationPreference: attestationPreferenceArg, extensions: extensionsArg) { result in switch result { case .success(let res): reply(wrapResult(res)) @@ -338,7 +349,8 @@ class PasskeysApiSetup { let conditionalUIArg = args[2] as! Bool let allowedCredentialsArg = args[3] as! [CredentialType] let preferImmediatelyAvailableCredentialsArg = args[4] as! Bool - api.authenticate(relyingPartyId: relyingPartyIdArg, challenge: challengeArg, conditionalUI: conditionalUIArg, allowedCredentials: allowedCredentialsArg, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentialsArg) { result in + let extensionsArg: String? = nilOrValue(args[5]) + api.authenticate(relyingPartyId: relyingPartyIdArg, challenge: challengeArg, conditionalUI: conditionalUIArg, allowedCredentials: allowedCredentialsArg, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentialsArg, extensions: extensionsArg) { result in switch result { case .success(let res): reply(wrapResult(res)) diff --git a/packages/passkeys/passkeys_darwin/lib/messages.g.dart b/packages/passkeys/passkeys_darwin/lib/messages.g.dart index 1ed55e70..dd889710 100644 --- a/packages/passkeys/passkeys_darwin/lib/messages.g.dart +++ b/packages/passkeys/passkeys_darwin/lib/messages.g.dart @@ -109,6 +109,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -126,6 +127,9 @@ class RegisterResponse { /// The supported transports for the authenticator List transports; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -133,6 +137,7 @@ class RegisterResponse { clientDataJSON, attestationObject, transports, + clientExtensionResults, ]; } @@ -144,6 +149,7 @@ class RegisterResponse { clientDataJSON: result[2]! as String, attestationObject: result[3]! as String, transports: (result[4] as List?)!.cast(), + clientExtensionResults: result[5] as String?, ); } } @@ -157,6 +163,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -176,6 +183,9 @@ class AuthenticateResponse { String? userHandle; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -184,6 +194,7 @@ class AuthenticateResponse { authenticatorData, signature, userHandle, + clientExtensionResults, ]; } @@ -196,6 +207,7 @@ class AuthenticateResponse { authenticatorData: result[3]! as String, signature: result[4]! as String, userHandle: result[5] as String?, + clientExtensionResults: result[6] as String?, ); } } @@ -227,15 +239,15 @@ class _PasskeysApiCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return AuthenticateResponse.decode(readValue(buffer)!); - case 129: + case 129: return CredentialType.decode(readValue(buffer)!); - case 130: + case 130: return RegisterResponse.decode(readValue(buffer)!); - case 131: + case 131: return RelyingParty.decode(readValue(buffer)!); - case 132: + case 132: return User.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -257,8 +269,7 @@ class PasskeysApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.canAuthenticate', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -284,8 +295,7 @@ class PasskeysApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.hasBiometrics', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -307,12 +317,32 @@ class PasskeysApi { } } - Future register(String arg_challenge, RelyingParty arg_relyingParty, User arg_user, List arg_excludeCredentials, List arg_pubKeyCredValues, bool arg_canBePlatformAuthenticator, bool arg_canBeSecurityKey, String? arg_residentKeyPreference, String? arg_attestationPreference) async { + Future register( + String arg_challenge, + RelyingParty arg_relyingParty, + User arg_user, + List arg_excludeCredentials, + List arg_pubKeyCredValues, + bool arg_canBePlatformAuthenticator, + bool arg_canBeSecurityKey, + String? arg_residentKeyPreference, + String? arg_attestationPreference, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.register', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_challenge, arg_relyingParty, arg_user, arg_excludeCredentials, arg_pubKeyCredValues, arg_canBePlatformAuthenticator, arg_canBeSecurityKey, arg_residentKeyPreference, arg_attestationPreference]) as List?; + final List? replyList = await channel.send([ + arg_challenge, + arg_relyingParty, + arg_user, + arg_excludeCredentials, + arg_pubKeyCredValues, + arg_canBePlatformAuthenticator, + arg_canBeSecurityKey, + arg_residentKeyPreference, + arg_attestationPreference, + arg_extensions + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -334,12 +364,24 @@ class PasskeysApi { } } - Future authenticate(String arg_relyingPartyId, String arg_challenge, bool arg_conditionalUI, List arg_allowedCredentials, bool arg_preferImmediatelyAvailableCredentials) async { + Future authenticate( + String arg_relyingPartyId, + String arg_challenge, + bool arg_conditionalUI, + List arg_allowedCredentials, + bool arg_preferImmediatelyAvailableCredentials, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.authenticate', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_relyingPartyId, arg_challenge, arg_conditionalUI, arg_allowedCredentials, arg_preferImmediatelyAvailableCredentials]) as List?; + final List? replyList = await channel.send([ + arg_relyingPartyId, + arg_challenge, + arg_conditionalUI, + arg_allowedCredentials, + arg_preferImmediatelyAvailableCredentials, + arg_extensions + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -363,10 +405,10 @@ class PasskeysApi { Future cancelCurrentAuthenticatorOperation() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.cancelCurrentAuthenticatorOperation', codec, + 'dev.flutter.pigeon.passkeys_darwin.PasskeysApi.cancelCurrentAuthenticatorOperation', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/passkeys/passkeys_darwin/lib/passkeys_darwin.dart b/packages/passkeys/passkeys_darwin/lib/passkeys_darwin.dart index d3b57e5a..4d8c2bcb 100644 --- a/packages/passkeys/passkeys_darwin/lib/passkeys_darwin.dart +++ b/packages/passkeys/passkeys_darwin/lib/passkeys_darwin.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:passkeys_darwin/messages.g.dart'; import 'package:passkeys_platform_interface/passkeys_platform_interface.dart'; @@ -44,6 +46,7 @@ class PasskeysDarwin extends PasskeysPlatform { request.authSelectionType!.authenticatorAttachment != 'platform', request.authSelectionType?.residentKey, request.attestation, + request.extensions != null ? jsonEncode(request.extensions) : null, ); return RegisterResponseType( @@ -52,6 +55,9 @@ class PasskeysDarwin extends PasskeysPlatform { clientDataJSON: r.clientDataJSON, attestationObject: r.attestationObject, transports: r.transports.whereType().toList(), + clientExtensionResults: r.clientExtensionResults != null + ? jsonDecode(r.clientExtensionResults!) as Map? + : null, ); } @@ -74,6 +80,7 @@ class PasskeysDarwin extends PasskeysPlatform { .toList() ?? [], request.preferImmediatelyAvailableCredentials, + request.extensions != null ? jsonEncode(request.extensions) : null, ); return AuthenticateResponseType( @@ -83,6 +90,9 @@ class PasskeysDarwin extends PasskeysPlatform { authenticatorData: r.authenticatorData, signature: r.signature, userHandle: r.userHandle ?? '', + clientExtensionResults: r.clientExtensionResults != null + ? jsonDecode(r.clientExtensionResults!) as Map? + : null, ); } diff --git a/packages/passkeys/passkeys_darwin/pigeons/messages.dart b/packages/passkeys/passkeys_darwin/pigeons/messages.dart index af2dcd14..890716f3 100644 --- a/packages/passkeys/passkeys_darwin/pigeons/messages.dart +++ b/packages/passkeys/passkeys_darwin/pigeons/messages.dart @@ -58,6 +58,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -74,6 +75,9 @@ class RegisterResponse { /// The supported transports for the authenticator final List transports; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } /// Represents an authenticate response @@ -86,6 +90,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -104,6 +109,9 @@ class AuthenticateResponse { final String signature; final String? userHandle; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } @HostApi() @@ -123,6 +131,7 @@ abstract class PasskeysApi { bool canBeSecurityKey, String? residentKeyPreference, String? attestationPreference, + String? extensions, ); @async @@ -132,6 +141,7 @@ abstract class PasskeysApi { bool conditionalUI, List allowedCredentials, bool preferImmediatelyAvailableCredentials, + String? extensions, ); @async diff --git a/packages/passkeys/passkeys_doctor/lib/passkeys_doctor.dart b/packages/passkeys/passkeys_doctor/lib/passkeys_doctor.dart index 86daafbe..ddb718f8 100644 --- a/packages/passkeys/passkeys_doctor/lib/passkeys_doctor.dart +++ b/packages/passkeys/passkeys_doctor/lib/passkeys_doctor.dart @@ -4,4 +4,4 @@ export 'src/types/exceptions.dart'; export 'src/types/checkpoint.dart'; export 'src/types/result.dart'; export 'src/passkeys_doctor.dart'; -export 'src/types/exception_messages.dart'; \ No newline at end of file +export 'src/types/exception_messages.dart'; diff --git a/packages/passkeys/passkeys_doctor/lib/src/messages.g.dart b/packages/passkeys/passkeys_doctor/lib/src/messages.g.dart index 1f905fdf..bd3b736f 100644 --- a/packages/passkeys/passkeys_doctor/lib/src/messages.g.dart +++ b/packages/passkeys/passkeys_doctor/lib/src/messages.g.dart @@ -15,7 +15,6 @@ PlatformException _createConnectionError(String channelName) { ); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -41,9 +40,11 @@ class WebCredentialsApi { /// Constructor for [WebCredentialsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - WebCredentialsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + WebCredentialsApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -51,8 +52,10 @@ class WebCredentialsApi { final String pigeonVar_messageChannelSuffix; Future> getFingerprints() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.corbado_auth_doctor.WebCredentialsApi.getFingerprints$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.corbado_auth_doctor.WebCredentialsApi.getFingerprints$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, diff --git a/packages/passkeys/passkeys_doctor/lib/src/types/checkpoint.dart b/packages/passkeys/passkeys_doctor/lib/src/types/checkpoint.dart index 12bf94ec..98e3cb9b 100644 --- a/packages/passkeys/passkeys_doctor/lib/src/types/checkpoint.dart +++ b/packages/passkeys/passkeys_doctor/lib/src/types/checkpoint.dart @@ -13,9 +13,9 @@ class Checkpoint { String toString() { return 'Checkpoint: $name\n' - 'Description: $description\n' - 'Type: $type\n' - 'Documentation Link: ${documentationLink ?? "N/A"}'; + 'Description: $description\n' + 'Type: $type\n' + 'Documentation Link: ${documentationLink ?? "N/A"}'; } Map toJson() { @@ -28,4 +28,4 @@ class Checkpoint { } } -enum CheckpointType { success, warning, error } \ No newline at end of file +enum CheckpointType { success, warning, error } diff --git a/packages/passkeys/passkeys_doctor/lib/src/types/result.dart b/packages/passkeys/passkeys_doctor/lib/src/types/result.dart index adfa1da5..1503143d 100644 --- a/packages/passkeys/passkeys_doctor/lib/src/types/result.dart +++ b/packages/passkeys/passkeys_doctor/lib/src/types/result.dart @@ -9,4 +9,4 @@ class Result { required this.checkpoints, this.exception, }); -} \ No newline at end of file +} diff --git a/packages/passkeys/passkeys_doctor/pigeons/messages.dart b/packages/passkeys/passkeys_doctor/pigeons/messages.dart index b860da07..03034614 100644 --- a/packages/passkeys/passkeys_doctor/pigeons/messages.dart +++ b/packages/passkeys/passkeys_doctor/pigeons/messages.dart @@ -3,13 +3,13 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/messages.g.dart', - javaOut: 'android/src/main/java/com/corbado/corbado_auth_doctor/Messages.java', + javaOut: + 'android/src/main/java/com/corbado/corbado_auth_doctor/Messages.java', javaOptions: JavaOptions( package: 'com.corbado.passkeys_doctor', ), ), ) - @HostApi() abstract class WebCredentialsApi { List getFingerprints(); diff --git a/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_request.dart b/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_request.dart index e5e402f4..ad7c0d17 100644 --- a/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_request.dart +++ b/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_request.dart @@ -15,6 +15,7 @@ class AuthenticateRequestType { this.timeout, this.userVerification, this.allowCredentials, + this.extensions, }); /// Constructs a new instance from a JSON string. @@ -57,6 +58,7 @@ class AuthenticateRequestType { mediation: mediation, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentials, + extensions: json['extensions'] as Map?, ); } @@ -96,6 +98,12 @@ class AuthenticateRequestType { /// immediately available, such as those that are stored on the device. final bool preferImmediatelyAvailableCredentials; + /// WebAuthn extensions to include in the authentication request. + /// Supports both single-value extensions (e.g. appid) and structured + /// extensions (e.g. prf). The keys are extension identifiers and values + /// are the extension inputs as defined by the WebAuthn spec. + final Map? extensions; + /// Converts this instance to a JSON string. String toJsonString() => jsonEncode(toJson()); @@ -108,6 +116,7 @@ class AuthenticateRequestType { if (allowCredentials != null && allowCredentials!.isNotEmpty) 'allowCredentials': allowCredentials!.map((e) => e.toJson()).toList(), if (userVerification != null) 'userVerification': userVerification, + if (extensions != null) 'extensions': extensions, }; } } diff --git a/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_response.dart b/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_response.dart index 4106eb25..6d539627 100644 --- a/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_response.dart +++ b/packages/passkeys/passkeys_platform_interface/lib/types/authenticate_response.dart @@ -8,7 +8,8 @@ class AuthenticateResponseType { required this.clientDataJSON, required this.authenticatorData, required this.signature, - required this.userHandle}); + required this.userHandle, + this.clientExtensionResults}); /// Constructs a new instance from a JSON string. factory AuthenticateResponseType.fromJsonString(String jsonString) { @@ -37,6 +38,8 @@ class AuthenticateResponseType { authenticatorData: response['authenticatorData'] as String? ?? '', signature: response['signature'] as String? ?? '', userHandle: (response['userHandle'] as String?) ?? '', + clientExtensionResults: + json['clientExtensionResults'] as Map?, ); } @@ -48,6 +51,8 @@ class AuthenticateResponseType { authenticatorData: json['authenticatorData'] as String? ?? '', signature: json['signature'] as String? ?? '', userHandle: (json['userHandle'] as String?) ?? '', + clientExtensionResults: + json['clientExtensionResults'] as Map?, ); } @@ -69,6 +74,11 @@ class AuthenticateResponseType { /// The user handle. Can be empty if the user handle is not available. final String userHandle; + /// The client extension results returned by the authenticator. + /// Contains the results of any WebAuthn extensions that were requested + /// during authentication (e.g. appid, prf). + final Map? clientExtensionResults; + /// Converts this instance to a JSON string. String toJsonString() => jsonEncode(toJson()); @@ -92,7 +102,7 @@ class AuthenticateResponseType { 'rawId': rawId, 'type': 'public-key', 'response': response, - 'clientExtensionResults': {}, + 'clientExtensionResults': clientExtensionResults ?? {}, }; } } diff --git a/packages/passkeys/passkeys_platform_interface/lib/types/register_request.dart b/packages/passkeys/passkeys_platform_interface/lib/types/register_request.dart index 49ac409c..2c6ba5f7 100644 --- a/packages/passkeys/passkeys_platform_interface/lib/types/register_request.dart +++ b/packages/passkeys/passkeys_platform_interface/lib/types/register_request.dart @@ -19,6 +19,7 @@ class RegisterRequestType { this.pubKeyCredParams, this.timeout, this.attestation, + this.extensions, }); /// Constructs a new instance from a JSON string. @@ -67,6 +68,7 @@ class RegisterRequestType { : null, timeout: json['timeout'] as int?, attestation: json['attestation'] as String?, + extensions: json['extensions'] as Map?, ); } @@ -100,6 +102,12 @@ class RegisterRequestType { /// - "direct"/"enterprise": Conveys unaltered attestation information final String? attestation; + /// WebAuthn extensions to include in the registration request. + /// Supports both single-value extensions (e.g. credProps) and structured + /// extensions (e.g. prf). The keys are extension identifiers and values + /// are the extension inputs as defined by the WebAuthn spec. + final Map? extensions; + /// Converts this instance to a JSON string. String toJsonString() => jsonEncode(toJson()); @@ -118,6 +126,7 @@ class RegisterRequestType { if (authSelectionType != null) 'authenticatorSelection': authSelectionType!.toJson(), if (attestation != null) 'attestation': attestation, + if (extensions != null) 'extensions': extensions, }; } } diff --git a/packages/passkeys/passkeys_platform_interface/lib/types/register_response.dart b/packages/passkeys/passkeys_platform_interface/lib/types/register_response.dart index b4e4a091..85a9c85e 100644 --- a/packages/passkeys/passkeys_platform_interface/lib/types/register_response.dart +++ b/packages/passkeys/passkeys_platform_interface/lib/types/register_response.dart @@ -7,6 +7,7 @@ class RegisterResponseType { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// Constructs a new instance from a JSON string. @@ -33,6 +34,8 @@ class RegisterResponseType { clientDataJSON: response['clientDataJSON'] as String? ?? '', attestationObject: response['attestationObject'] as String? ?? '', transports: transports?.map((e) => e as String?).toList() ?? [], + clientExtensionResults: + json['clientExtensionResults'] as Map?, ); } @@ -42,6 +45,11 @@ class RegisterResponseType { final String attestationObject; final List transports; + /// The client extension results returned by the authenticator. + /// Contains the results of any WebAuthn extensions that were requested + /// during registration (e.g. credProps, prf). + final Map? clientExtensionResults; + /// Converts this instance to a JSON string. String toJsonString() => jsonEncode(toJson()); @@ -63,7 +71,7 @@ class RegisterResponseType { 'rawId': rawId, 'type': 'public-key', 'response': response, - 'clientExtensionResults': {}, + 'clientExtensionResults': clientExtensionResults ?? {}, }; } } diff --git a/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.dart b/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.dart index fd3c4065..9f318a32 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.dart @@ -18,6 +18,7 @@ class PasskeyLoginRequest { String? platformUserVerification, List? platformAllowCredentials, MediationType platformMediation, + Map? extensions, ) { final allowCredentials = platformAllowCredentials?.map((e) { return PasskeyLoginAllowCredentialType( @@ -37,6 +38,7 @@ class PasskeyLoginRequest { platformUserVerification) : null, allowCredentials: allowCredentials, + extensions: extensions, ); final mediation = @@ -58,7 +60,7 @@ class PasskeyLoginPublicKey { this.rpId, this.allowCredentials, this.userVerification, - this.loginExtensions}); + this.extensions}); factory PasskeyLoginPublicKey.fromJson(Map json) => _$PasskeyLoginPublicKeyFromJson(json); @@ -68,7 +70,7 @@ class PasskeyLoginPublicKey { final String? rpId; final List? allowCredentials; final UserVerificationRequirement? userVerification; - final LoginExtensions? loginExtensions; + final Map? extensions; Map toJson() => _$PasskeyLoginPublicKeyToJson(this); } @@ -163,17 +165,3 @@ enum UserVerificationRequirement { } } } - -@JsonSerializable(explicitToJson: true) -class LoginExtensions { - LoginExtensions(this.appid, this.appidExclude, this.credProps); - - factory LoginExtensions.fromJson(Map json) => - _$LoginExtensionsFromJson(json); - - final String? appid; - final String? appidExclude; - final String? credProps; - - Map toJson() => _$LoginExtensionsToJson(this); -} diff --git a/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.g.dart b/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.g.dart index 7d0b97a1..ce53107a 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.g.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeyLoginRequest.g.dart @@ -38,10 +38,7 @@ PasskeyLoginPublicKey _$PasskeyLoginPublicKeyFromJson( .toList(), userVerification: $enumDecodeNullable( _$UserVerificationRequirementEnumMap, json['userVerification']), - loginExtensions: json['loginExtensions'] == null - ? null - : LoginExtensions.fromJson( - json['loginExtensions'] as Map), + extensions: json['extensions'] as Map?, ); Map _$PasskeyLoginPublicKeyToJson( @@ -54,7 +51,7 @@ Map _$PasskeyLoginPublicKeyToJson( instance.allowCredentials?.map((e) => e.toJson()).toList(), 'userVerification': _$UserVerificationRequirementEnumMap[instance.userVerification], - 'loginExtensions': instance.loginExtensions?.toJson(), + 'extensions': instance.extensions, }; const _$UserVerificationRequirementEnumMap = { @@ -90,17 +87,3 @@ const _$AuthenticatorTransportEnumMap = { AuthenticatorTransport.Usb: 'usb', AuthenticatorTransport.Bluetooth: 'bluetooth', }; - -LoginExtensions _$LoginExtensionsFromJson(Map json) => - LoginExtensions( - json['appid'] as String?, - json['appidExclude'] as String?, - json['credProps'] as String?, - ); - -Map _$LoginExtensionsToJson(LoginExtensions instance) => - { - 'appid': instance.appid, - 'appidExclude': instance.appidExclude, - 'credProps': instance.credProps, - }; diff --git a/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.dart b/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.dart index 0270bfb6..d7ecc21c 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.dart @@ -8,11 +8,13 @@ class PasskeyLoginResponse { factory PasskeyLoginResponse.fromJson(Map json) => _$PasskeyLoginResponseFromJson(json); - PasskeyLoginResponse(this.id, this.rawId, this.response); + PasskeyLoginResponse( + this.id, this.rawId, this.response, this.clientExtensionResults); final String id; final String rawId; final AssertionResponse response; + final Map? clientExtensionResults; Map toJson() => _$PasskeyLoginResponseToJson(this); @@ -24,6 +26,7 @@ class PasskeyLoginResponse { userHandle: response.userHandle ?? '', id: id, rawId: rawId, + clientExtensionResults: clientExtensionResults, ); } diff --git a/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.g.dart b/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.g.dart index c2516a9c..eb964843 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.g.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeyLoginResponse.g.dart @@ -12,6 +12,7 @@ PasskeyLoginResponse _$PasskeyLoginResponseFromJson( json['id'] as String, json['rawId'] as String, AssertionResponse.fromJson(json['response'] as Map), + json['clientExtensionResults'] as Map?, ); Map _$PasskeyLoginResponseToJson( @@ -20,6 +21,7 @@ Map _$PasskeyLoginResponseToJson( 'id': instance.id, 'rawId': instance.rawId, 'response': instance.response, + 'clientExtensionResults': instance.clientExtensionResults, }; AssertionResponse _$AssertionResponseFromJson(Map json) => diff --git a/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.dart b/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.dart index 745c3922..357028eb 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.dart @@ -25,6 +25,7 @@ class PublicKey { this.excludeCredentials, this.timeout, this.attestation, + this.extensions, ); final RelyingPartyType rp; @@ -35,6 +36,7 @@ class PublicKey { final List excludeCredentials; final int? timeout; final String? attestation; + final Map? extensions; Map toJson() => _$PublicKeyToJson(this); } diff --git a/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.g.dart b/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.g.dart index a90c6e67..50c4bddb 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.g.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeySignUpRequest.g.dart @@ -23,4 +23,5 @@ Map _$PublicKeyToJson(PublicKey instance) => { instance.excludeCredentials.map((e) => e.toJson()).toList(), 'timeout': instance.timeout, 'attestation': instance.attestation, + 'extensions': instance.extensions, }; diff --git a/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.dart b/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.dart index 6b8da6a5..7fbde9d6 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.dart @@ -7,11 +7,13 @@ class PasskeySignUpResponse { factory PasskeySignUpResponse.fromJson(Map json) => _$PasskeySignUpResponseFromJson(json); - PasskeySignUpResponse(this.id, this.rawId, this.response); + PasskeySignUpResponse( + this.id, this.rawId, this.response, this.clientExtensionResults); final String id; final String rawId; final AttestationResponse response; + final Map? clientExtensionResults; Map toJson() => _$PasskeySignUpResponseToJson(this); } @@ -22,10 +24,10 @@ class AttestationResponse { _$AttestationResponseFromJson(json); AttestationResponse( - this.clientDataJSON, - this.attestationObject, - this.transports, - ); + this.clientDataJSON, + this.attestationObject, + this.transports, + ); final String clientDataJSON; final String attestationObject; diff --git a/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.g.dart b/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.g.dart index aaacffd5..37e88255 100644 --- a/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.g.dart +++ b/packages/passkeys/passkeys_web/lib/models/passkeySignUpResponse.g.dart @@ -12,6 +12,7 @@ PasskeySignUpResponse _$PasskeySignUpResponseFromJson( json['id'] as String, json['rawId'] as String, AttestationResponse.fromJson(json['response'] as Map), + json['clientExtensionResults'] as Map?, ); Map _$PasskeySignUpResponseToJson( @@ -20,6 +21,7 @@ Map _$PasskeySignUpResponseToJson( 'id': instance.id, 'rawId': instance.rawId, 'response': instance.response, + 'clientExtensionResults': instance.clientExtensionResults, }; AttestationResponse _$AttestationResponseFromJson(Map json) => diff --git a/packages/passkeys/passkeys_web/lib/passkeys_web.dart b/packages/passkeys/passkeys_web/lib/passkeys_web.dart index 425088a4..2263472b 100644 --- a/packages/passkeys/passkeys_web/lib/passkeys_web.dart +++ b/packages/passkeys/passkeys_web/lib/passkeys_web.dart @@ -45,6 +45,7 @@ class PasskeysWeb extends PasskeysPlatform { request.excludeCredentials, request.timeout, request.attestation, + request.extensions, ), ); @@ -62,6 +63,7 @@ class PasskeysWeb extends PasskeysPlatform { clientDataJSON: typedResponse.response.clientDataJSON, attestationObject: typedResponse.response.attestationObject, transports: typedResponse.response.transports, + clientExtensionResults: typedResponse.clientExtensionResults, ); } catch (e) { final exception = _parseException(e.toString()); @@ -79,6 +81,7 @@ class PasskeysWeb extends PasskeysPlatform { request.userVerification, request.allowCredentials, request.mediation, + request.extensions, ); try { diff --git a/packages/passkeys/passkeys_windows/lib/messages.g.dart b/packages/passkeys/passkeys_windows/lib/messages.g.dart index 99ce0f23..87b512de 100644 --- a/packages/passkeys/passkeys_windows/lib/messages.g.dart +++ b/packages/passkeys/passkeys_windows/lib/messages.g.dart @@ -220,6 +220,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -237,6 +238,9 @@ class RegisterResponse { /// The supported transports for the authenticator List transports; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -244,6 +248,7 @@ class RegisterResponse { clientDataJSON, attestationObject, transports, + clientExtensionResults, ]; } @@ -255,6 +260,7 @@ class RegisterResponse { clientDataJSON: result[2]! as String, attestationObject: result[3]! as String, transports: (result[4] as List?)!.cast(), + clientExtensionResults: result[5] as String?, ); } } @@ -268,6 +274,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, required this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -288,6 +295,9 @@ class AuthenticateResponse { /// The user handle String userHandle; + /// JSON-encoded client extension results + String? clientExtensionResults; + Object encode() { return [ id, @@ -296,6 +306,7 @@ class AuthenticateResponse { authenticatorData, signature, userHandle, + clientExtensionResults, ]; } @@ -308,6 +319,7 @@ class AuthenticateResponse { authenticatorData: result[3]! as String, signature: result[4]! as String, userHandle: result[5]! as String, + clientExtensionResults: result[6] as String?, ); } } @@ -442,7 +454,8 @@ class PasskeysApi { List? arg_pubKeyCredParams, int? arg_timeout, String? arg_attestation, - List arg_excludeCredentials) async { + List arg_excludeCredentials, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_windows.PasskeysApi.register', codec, binaryMessenger: _binaryMessenger); @@ -454,7 +467,8 @@ class PasskeysApi { arg_pubKeyCredParams, arg_timeout, arg_attestation, - arg_excludeCredentials + arg_excludeCredentials, + arg_extensions ]) as List?; if (replyList == null) { throw PlatformException( @@ -483,7 +497,8 @@ class PasskeysApi { int? arg_timeout, String? arg_userVerification, List? arg_allowCredentials, - bool? arg_preferImmediatelyAvailableCredentials) async { + bool? arg_preferImmediatelyAvailableCredentials, + String? arg_extensions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.passkeys_windows.PasskeysApi.authenticate', codec, binaryMessenger: _binaryMessenger); @@ -493,7 +508,8 @@ class PasskeysApi { arg_timeout, arg_userVerification, arg_allowCredentials, - arg_preferImmediatelyAvailableCredentials + arg_preferImmediatelyAvailableCredentials, + arg_extensions ]) as List?; if (replyList == null) { throw PlatformException( diff --git a/packages/passkeys/passkeys_windows/lib/passkeys_windows.dart b/packages/passkeys/passkeys_windows/lib/passkeys_windows.dart index f528795c..8f888b04 100644 --- a/packages/passkeys/passkeys_windows/lib/passkeys_windows.dart +++ b/packages/passkeys/passkeys_windows/lib/passkeys_windows.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:passkeys_platform_interface/passkeys_platform_interface.dart'; import 'package:passkeys_platform_interface/types/types.dart'; @@ -34,6 +36,7 @@ class PasskeysWindows extends PasskeysPlatform { ) .toList(), request.preferImmediatelyAvailableCredentials, + request.extensions != null ? jsonEncode(request.extensions) : null, ); return AuthenticateResponseType( @@ -43,6 +46,11 @@ class PasskeysWindows extends PasskeysPlatform { authenticatorData: authenticateResponse.authenticatorData, signature: authenticateResponse.signature, userHandle: authenticateResponse.userHandle, + clientExtensionResults: + authenticateResponse.clientExtensionResults != null + ? jsonDecode(authenticateResponse.clientExtensionResults!) + as Map? + : null, ); } @@ -95,6 +103,7 @@ class PasskeysWindows extends PasskeysPlatform { request.excludeCredentials .map((e) => ExcludeCredential(type: e.type, id: e.id)) .toList(), + request.extensions != null ? jsonEncode(request.extensions) : null, ); return RegisterResponseType( @@ -103,6 +112,10 @@ class PasskeysWindows extends PasskeysPlatform { clientDataJSON: registerResponse.clientDataJSON, attestationObject: registerResponse.attestationObject, transports: registerResponse.transports.whereType().toList(), + clientExtensionResults: registerResponse.clientExtensionResults != null + ? jsonDecode(registerResponse.clientExtensionResults!) + as Map? + : null, ); } diff --git a/packages/passkeys/passkeys_windows/pigeons/messages.dart b/packages/passkeys/passkeys_windows/pigeons/messages.dart index 68531dcf..3d7d11e7 100644 --- a/packages/passkeys/passkeys_windows/pigeons/messages.dart +++ b/packages/passkeys/passkeys_windows/pigeons/messages.dart @@ -109,6 +109,7 @@ class RegisterResponse { required this.clientDataJSON, required this.attestationObject, required this.transports, + this.clientExtensionResults, }); /// The ID @@ -125,6 +126,9 @@ class RegisterResponse { /// The supported transports for the authenticator final List transports; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } /// Represents an authenticate response @@ -137,6 +141,7 @@ class AuthenticateResponse { required this.authenticatorData, required this.signature, required this.userHandle, + this.clientExtensionResults, }); /// The ID @@ -156,6 +161,9 @@ class AuthenticateResponse { /// The user handle final String userHandle; + + /// JSON-encoded client extension results + final String? clientExtensionResults; } @HostApi() @@ -176,6 +184,7 @@ abstract class PasskeysApi { int? timeout, String? attestation, List excludeCredentials, + String? extensions, ); @async @@ -186,6 +195,7 @@ abstract class PasskeysApi { String? userVerification, List? allowCredentials, bool? preferImmediatelyAvailableCredentials, + String? extensions, ); @async diff --git a/packages/passkeys/passkeys_windows/windows/CMakeLists.txt b/packages/passkeys/passkeys_windows/windows/CMakeLists.txt index 90600ada..38b0d358 100644 --- a/packages/passkeys/passkeys_windows/windows/CMakeLists.txt +++ b/packages/passkeys/passkeys_windows/windows/CMakeLists.txt @@ -6,6 +6,16 @@ project(${PROJECT_NAME} LANGUAGES CXX) # not be changed set(PLUGIN_NAME "passkeys_windows_plugin") +# Fetch nlohmann/json for JSON parsing +include(FetchContent) +FetchContent_Declare( + nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz +) +set(JSON_BuildTests OFF CACHE INTERNAL "") +set(JSON_Install OFF CACHE INTERNAL "") +FetchContent_MakeAvailable(nlohmann_json) + add_library(${PLUGIN_NAME} SHARED "passkeys_windows.cpp" "passkeys_windows_plugin.cpp" @@ -27,6 +37,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE WebAuthN crypt32 bcrypt + nlohmann_json::nlohmann_json ) # WebAuthn API is loaded dynamically from webauthn.dll at runtime diff --git a/packages/passkeys/passkeys_windows/windows/messages.g.cpp b/packages/passkeys/passkeys_windows/windows/messages.g.cpp index 8f4440a6..3fad4c0d 100644 --- a/packages/passkeys/passkeys_windows/windows/messages.g.cpp +++ b/packages/passkeys/passkeys_windows/windows/messages.g.cpp @@ -376,6 +376,20 @@ RegisterResponse::RegisterResponse( attestation_object_(attestation_object), transports_(transports) {} +RegisterResponse::RegisterResponse( + const std::string& id, + const std::string& raw_id, + const std::string& client_data_j_s_o_n, + const std::string& attestation_object, + const EncodableList& transports, + const std::string* client_extension_results) + : id_(id), + raw_id_(raw_id), + client_data_j_s_o_n_(client_data_j_s_o_n), + attestation_object_(attestation_object), + transports_(transports), + client_extension_results_(client_extension_results ? std::optional(*client_extension_results) : std::nullopt) {} + const std::string& RegisterResponse::id() const { return id_; } @@ -421,14 +435,28 @@ void RegisterResponse::set_transports(const EncodableList& value_arg) { } +const std::string* RegisterResponse::client_extension_results() const { + return client_extension_results_ ? &(*client_extension_results_) : nullptr; +} + +void RegisterResponse::set_client_extension_results(const std::string_view* value_arg) { + client_extension_results_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void RegisterResponse::set_client_extension_results(std::string_view value_arg) { + client_extension_results_ = value_arg; +} + + EncodableList RegisterResponse::ToEncodableList() const { EncodableList list; - list.reserve(5); + list.reserve(6); list.push_back(EncodableValue(id_)); list.push_back(EncodableValue(raw_id_)); list.push_back(EncodableValue(client_data_j_s_o_n_)); list.push_back(EncodableValue(attestation_object_)); list.push_back(EncodableValue(transports_)); + list.push_back(client_extension_results_ ? EncodableValue(*client_extension_results_) : EncodableValue()); return list; } @@ -439,6 +467,10 @@ RegisterResponse RegisterResponse::FromEncodableList(const EncodableList& list) std::get(list[2]), std::get(list[3]), std::get(list[4])); + auto& encodable_client_extension_results = list[5]; + if (!encodable_client_extension_results.IsNull()) { + decoded.set_client_extension_results(std::get(encodable_client_extension_results)); + } return decoded; } @@ -458,6 +490,22 @@ AuthenticateResponse::AuthenticateResponse( signature_(signature), user_handle_(user_handle) {} +AuthenticateResponse::AuthenticateResponse( + const std::string& id, + const std::string& raw_id, + const std::string& client_data_j_s_o_n, + const std::string& authenticator_data, + const std::string& signature, + const std::string& user_handle, + const std::string* client_extension_results) + : id_(id), + raw_id_(raw_id), + client_data_j_s_o_n_(client_data_j_s_o_n), + authenticator_data_(authenticator_data), + signature_(signature), + user_handle_(user_handle), + client_extension_results_(client_extension_results ? std::optional(*client_extension_results) : std::nullopt) {} + const std::string& AuthenticateResponse::id() const { return id_; } @@ -512,15 +560,29 @@ void AuthenticateResponse::set_user_handle(std::string_view value_arg) { } +const std::string* AuthenticateResponse::client_extension_results() const { + return client_extension_results_ ? &(*client_extension_results_) : nullptr; +} + +void AuthenticateResponse::set_client_extension_results(const std::string_view* value_arg) { + client_extension_results_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AuthenticateResponse::set_client_extension_results(std::string_view value_arg) { + client_extension_results_ = value_arg; +} + + EncodableList AuthenticateResponse::ToEncodableList() const { EncodableList list; - list.reserve(6); + list.reserve(7); list.push_back(EncodableValue(id_)); list.push_back(EncodableValue(raw_id_)); list.push_back(EncodableValue(client_data_j_s_o_n_)); list.push_back(EncodableValue(authenticator_data_)); list.push_back(EncodableValue(signature_)); list.push_back(EncodableValue(user_handle_)); + list.push_back(client_extension_results_ ? EncodableValue(*client_extension_results_) : EncodableValue()); return list; } @@ -532,6 +594,10 @@ AuthenticateResponse AuthenticateResponse::FromEncodableList(const EncodableList std::get(list[3]), std::get(list[4]), std::get(list[5])); + auto& encodable_client_extension_results = list[6]; + if (!encodable_client_extension_results.IsNull()) { + decoded.set_client_extension_results(std::get(encodable_client_extension_results)); + } return decoded; } @@ -703,7 +769,9 @@ void PasskeysApi::SetUp( return; } const auto& exclude_credentials_arg = std::get(encodable_exclude_credentials_arg); - api->Register(challenge_arg, relying_party_arg, user_arg, authenticator_selection_arg, pub_key_cred_params_arg, timeout_arg, attestation_arg, exclude_credentials_arg, [reply](ErrorOr&& output) { + const auto& encodable_extensions_arg = args.at(8); + const auto* extensions_arg = std::get_if(&encodable_extensions_arg); + api->Register(challenge_arg, relying_party_arg, user_arg, authenticator_selection_arg, pub_key_cred_params_arg, timeout_arg, attestation_arg, exclude_credentials_arg, extensions_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; @@ -747,7 +815,9 @@ void PasskeysApi::SetUp( const auto* allow_credentials_arg = std::get_if(&encodable_allow_credentials_arg); const auto& encodable_prefer_immediately_available_credentials_arg = args.at(5); const auto* prefer_immediately_available_credentials_arg = std::get_if(&encodable_prefer_immediately_available_credentials_arg); - api->Authenticate(relying_party_id_arg, challenge_arg, timeout_arg, user_verification_arg, allow_credentials_arg, prefer_immediately_available_credentials_arg, [reply](ErrorOr&& output) { + const auto& encodable_extensions_arg = args.at(6); + const auto* extensions_arg = std::get_if(&encodable_extensions_arg); + api->Authenticate(relying_party_id_arg, challenge_arg, timeout_arg, user_verification_arg, allow_credentials_arg, prefer_immediately_available_credentials_arg, extensions_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; diff --git a/packages/passkeys/passkeys_windows/windows/messages.g.h b/packages/passkeys/passkeys_windows/windows/messages.g.h index f84d9d5d..f146ac0e 100644 --- a/packages/passkeys/passkeys_windows/windows/messages.g.h +++ b/packages/passkeys/passkeys_windows/windows/messages.g.h @@ -285,7 +285,7 @@ class AuthenticatorSelection { // Generated class from Pigeon that represents data sent in messages. class RegisterResponse { public: - // Constructs an object setting all fields. + // Constructs an object setting all non-nullable fields. explicit RegisterResponse( const std::string& id, const std::string& raw_id, @@ -293,6 +293,15 @@ class RegisterResponse { const std::string& attestation_object, const flutter::EncodableList& transports); + // Constructs an object setting all fields. + explicit RegisterResponse( + const std::string& id, + const std::string& raw_id, + const std::string& client_data_j_s_o_n, + const std::string& attestation_object, + const flutter::EncodableList& transports, + const std::string* client_extension_results); + // The ID const std::string& id() const; void set_id(std::string_view value_arg); @@ -313,6 +322,11 @@ class RegisterResponse { const flutter::EncodableList& transports() const; void set_transports(const flutter::EncodableList& value_arg); + // JSON-encoded client extension results + const std::string* client_extension_results() const; + void set_client_extension_results(const std::string_view* value_arg); + void set_client_extension_results(std::string_view value_arg); + private: static RegisterResponse FromEncodableList(const flutter::EncodableList& list); @@ -324,6 +338,7 @@ class RegisterResponse { std::string client_data_j_s_o_n_; std::string attestation_object_; flutter::EncodableList transports_; + std::optional client_extension_results_; }; @@ -333,7 +348,7 @@ class RegisterResponse { // Generated class from Pigeon that represents data sent in messages. class AuthenticateResponse { public: - // Constructs an object setting all fields. + // Constructs an object setting all non-nullable fields. explicit AuthenticateResponse( const std::string& id, const std::string& raw_id, @@ -342,6 +357,16 @@ class AuthenticateResponse { const std::string& signature, const std::string& user_handle); + // Constructs an object setting all fields. + explicit AuthenticateResponse( + const std::string& id, + const std::string& raw_id, + const std::string& client_data_j_s_o_n, + const std::string& authenticator_data, + const std::string& signature, + const std::string& user_handle, + const std::string* client_extension_results); + // The ID const std::string& id() const; void set_id(std::string_view value_arg); @@ -366,6 +391,11 @@ class AuthenticateResponse { const std::string& user_handle() const; void set_user_handle(std::string_view value_arg); + // JSON-encoded client extension results + const std::string* client_extension_results() const; + void set_client_extension_results(const std::string_view* value_arg); + void set_client_extension_results(std::string_view value_arg); + private: static AuthenticateResponse FromEncodableList(const flutter::EncodableList& list); @@ -378,6 +408,7 @@ class AuthenticateResponse { std::string authenticator_data_; std::string signature_; std::string user_handle_; + std::optional client_extension_results_; }; @@ -417,6 +448,7 @@ class PasskeysApi { const int64_t* timeout, const std::string* attestation, const flutter::EncodableList& exclude_credentials, + const std::string* extensions, std::function reply)> result) = 0; virtual void Authenticate( const std::string& relying_party_id, @@ -425,6 +457,7 @@ class PasskeysApi { const std::string* user_verification, const flutter::EncodableList* allow_credentials, const bool* prefer_immediately_available_credentials, + const std::string* extensions, std::function reply)> result) = 0; virtual void CancelCurrentAuthenticatorOperation(std::function reply)> result) = 0; diff --git a/packages/passkeys/passkeys_windows/windows/passkeys_windows_plugin.cpp b/packages/passkeys/passkeys_windows/windows/passkeys_windows_plugin.cpp index 8cc48e3e..b9d9064e 100644 --- a/packages/passkeys/passkeys_windows/windows/passkeys_windows_plugin.cpp +++ b/packages/passkeys/passkeys_windows/windows/passkeys_windows_plugin.cpp @@ -7,7 +7,9 @@ #include #include +#include +#include #include #include #include @@ -52,6 +54,7 @@ namespace passkeys_windows result(version >= WEBAUTHN_API_VERSION_1); } + // TODO: currently Register() accepts an extensions parameter for compatibility with the API, but doesn't actually do anything with it. void Register( const std::string &challenge, const RelyingParty &relying_party, @@ -61,6 +64,7 @@ namespace passkeys_windows const int64_t *timeout, const std::string *attestation, const flutter::EncodableList &exclude_credentials, + const std::string *extensions, std::function reply)> result) override { @@ -342,6 +346,7 @@ namespace passkeys_windows const std::string *user_verification, const flutter::EncodableList *allow_credentials, const bool *prefer_immediately_available_credentials, + const std::string *extensions, std::function reply)> result) override { @@ -411,6 +416,77 @@ namespace passkeys_windows allow_list.ppCredentials = allow_ptrs.data(); } + // Parse PRF extension if provided + bool has_prf = false; + std::vector prf_global_first, prf_global_second; + WEBAUTHN_HMAC_SECRET_SALT prf_global_salt = {}; + WEBAUTHN_HMAC_SECRET_SALT_VALUES prf_salt_values = {}; + + struct PerCredPrf { std::vector credId, first, second; }; + std::vector per_cred_prf; + std::vector prf_cred_salts; + std::vector prf_cred_list; + + if (extensions && !extensions->empty()) { + try { + auto ext_json = nlohmann::json::parse(*extensions); + if (ext_json.contains("prf")) { + const auto &prf_node = ext_json.at("prf"); + // Global salts + if (prf_node.contains("eval")) { + const auto &eval = prf_node.at("eval"); + if (eval.contains("first")) { + prf_global_first = DecodeBase64Url(eval.at("first").get()); + if (eval.contains("second")) { + prf_global_second = DecodeBase64Url(eval.at("second").get()); + } + prf_global_salt.cbFirst = static_cast(prf_global_first.size()); + prf_global_salt.pbFirst = prf_global_first.data(); + prf_global_salt.cbSecond = static_cast(prf_global_second.size()); + prf_global_salt.pbSecond = prf_global_second.empty() ? nullptr : prf_global_second.data(); + prf_salt_values.pGlobalHmacSalt = &prf_global_salt; + has_prf = true; + } + } + // Per-credential salts + if (prf_node.contains("evalByCredential")) { + for (const auto &[cred_id_str, cred_eval] : prf_node.at("evalByCredential").items()) { + if (!cred_eval.contains("first")) continue; + PerCredPrf p; + p.credId = DecodeBase64Url(cred_id_str); + p.first = DecodeBase64Url(cred_eval.at("first").get()); + if (cred_eval.contains("second")) { + p.second = DecodeBase64Url(cred_eval.at("second").get()); + } + per_cred_prf.push_back(std::move(p)); + has_prf = true; + } + prf_cred_salts.resize(per_cred_prf.size()); + prf_cred_list.resize(per_cred_prf.size()); + for (size_t ci = 0; ci < per_cred_prf.size(); ci++) { + auto &pc = per_cred_prf[ci]; + auto &salt = prf_cred_salts[ci]; + salt.cbFirst = static_cast(pc.first.size()); + salt.pbFirst = pc.first.data(); + salt.cbSecond = static_cast(pc.second.size()); + salt.pbSecond = pc.second.empty() ? nullptr : pc.second.data(); + auto &entry = prf_cred_list[ci]; + entry.cbCredID = static_cast(pc.credId.size()); + entry.pbCredID = pc.credId.data(); + entry.pHmacSecretSalt = &salt; + } + if (!prf_cred_list.empty()) { + prf_salt_values.cCredWithHmacSecretSaltList = static_cast(prf_cred_list.size()); + prf_salt_values.pCredWithHmacSecretSaltList = prf_cred_list.data(); + } + } + } + } catch (const nlohmann::json::exception &) { + // Malformed extensions JSON — proceed without PRF + has_prf = false; + } + } + // Setup options std::wstring rp_id_wide = Utf8ToWide(relying_party_id); @@ -421,6 +497,7 @@ namespace passkeys_windows options.dwUserVerificationRequirement = WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED; options.pCancellationId = &cancellation_id_; options.pAllowCredentialList = allow_creds.empty() ? nullptr : &allow_list; + options.pHmacSecretSaltValues = has_prf ? &prf_salt_values : nullptr; if (user_verification) { @@ -483,6 +560,18 @@ namespace passkeys_windows AuthenticateResponse response( id, id, client_data_json_b64, authenticator_data, signature, user_handle); + // Extract PRF/hmac-secret output if available (requires WebAuthN 3+) + if (assertion->dwVersion >= WEBAUTHN_ASSERTION_VERSION_3 && + assertion->pHmacSecret && assertion->pHmacSecret->cbFirst > 0) { + auto *h = assertion->pHmacSecret; + nlohmann::json results = {{"first", EncodeBase64Url(h->pbFirst, h->cbFirst)}}; + if (h->cbSecond > 0 && h->pbSecond) { + results["second"] = EncodeBase64Url(h->pbSecond, h->cbSecond); + } + nlohmann::json prf_output = {{"prf", {{"results", results}}}}; + response.set_client_extension_results(prf_output.dump()); + } + // Memory freed automatically by unique_ptr deleter result(response);