Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ class LocalRelyingPartyServer {
user
..credentialID = response.id
..transports = response.transports.isEmpty
// For iOS faceID and touchID transports returns an empty list.
? ["internal"]
// When using FaceID or TouchID, the transports are empty.
? ['internal', 'hybrid']
: response.transports as List<String>;
_users[user.name] = user;

Expand Down
8 changes: 8 additions & 0 deletions packages/passkeys/passkeys/lib/authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class PasskeyAuthenticator {
throw DomainNotAssociatedException(e.message);
case 'deviceNotSupported':
throw DeviceNotSupportedException();
case 'android-timeout':
throw TimeoutException(e.message);
case 'ios-security-key-timeout':
throw TimeoutException(e.message);
default:
rethrow;
}
Expand Down Expand Up @@ -75,6 +79,10 @@ class PasskeyAuthenticator {
throw DeviceNotSupportedException();
case 'android-no-create-option':
throw NoCreateOptionException(e.message);
case 'android-timeout':
throw TimeoutException(e.message);
case 'ios-security-key-timeout':
throw TimeoutException(e.message);
default:
if (e.code.startsWith('android-unhandled')) {
throw UnhandledAuthenticatorException(e.code, e.message, e.details);
Expand Down
15 changes: 15 additions & 0 deletions packages/passkeys/passkeys/lib/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ class NoCreateOptionException implements AuthenticatorException {
String toString() => message ?? '';
}

/// This exception is thrown when the user tries to login or register but the
/// operation times out.
///
/// Platforms: Android, iOS
///
/// Suggestions:
/// - ask the user to try again
class TimeoutException implements AuthenticatorException {
final String? message;
/// Constructor
TimeoutException(this.message);

String toString() => message ?? '';
}

/// This exception is thrown when an exception is thrown by the authenticator
/// that we do not handle so far in this package.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.corbado.passkeys_android;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.os.CancellationSignal;
import android.util.Log;

Expand All @@ -18,15 +15,19 @@
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.GetPublicKeyCredentialOption;
import androidx.credentials.PublicKeyCredential;
import androidx.credentials.exceptions.*;
import androidx.credentials.exceptions.CreateCredentialCancellationException;
import androidx.credentials.exceptions.CreateCredentialException;
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException;
import androidx.credentials.exceptions.GetCredentialCancellationException;
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.exceptions.NoCredentialException;
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException;
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException;
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException;

import com.corbado.passkeys_android.models.login.AllowCredentialType;
import com.corbado.passkeys_android.models.login.GetCredentialOptions;
import com.corbado.passkeys_android.models.signup.AuthenticatorSelectionType;
import com.corbado.passkeys_android.models.signup.CreateCredentialOptions;
import com.corbado.passkeys_android.models.login.GetCredentialOptions;
import com.corbado.passkeys_android.models.signup.ExcludeCredentialType;
import com.corbado.passkeys_android.models.signup.PubKeyCredParamType;
import com.corbado.passkeys_android.models.signup.RelyingPartyType;
Expand All @@ -35,7 +36,6 @@
import com.google.android.gms.fido.fido2.Fido2ApiClient;
import com.google.android.gms.tasks.Task;

import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -52,6 +52,7 @@ public class MessageHandler implements Messages.PasskeysApi {
private static final String MISSING_GOOGLE_SIGN_IN_ERROR = "Please sign in with a Google account first to create a new passkey.";
private static final String EXCLUDE_CREDENTIALS_MATCH_ERROR = "You can not create a credential on this device because one of the excluded credentials exists on the local device.";
private static final String MISSING_CREATION_OPTIONS = "Please make sure you enable a passwords or passkeys provider in your device settings.";
private static final String TIMEOUT_ERROR = "Passkey operation timed out, please try again";

private final FlutterPasskeysPlugin plugin;

Expand Down Expand Up @@ -161,6 +162,8 @@ public void onError(CreateCredentialException e) {
platformException = new Messages.FlutterError("android-sync-account-not-available", e.getMessage(), SYNC_ACCOUNT_NOT_AVAILABLE_ERROR);
} else if (Objects.equals(e.getMessage(), "One of the excluded credentials exists on the local device")) {
platformException = new Messages.FlutterError("exclude-credentials-match", e.getMessage(), EXCLUDE_CREDENTIALS_MATCH_ERROR);
} else if (Objects.equals(e.getMessage(), "[15] Flow has timed out.")) {
platformException = new Messages.FlutterError("android-timeout", e.getMessage(), TIMEOUT_ERROR);
} else {
platformException = new Messages.FlutterError("android-unhandled: " + e.getType(), e.getMessage(), e.getErrorMessage());
}
Expand Down Expand Up @@ -253,6 +256,8 @@ public void onError(GetCredentialException e) {
} else if (e instanceof GetPublicKeyCredentialDomException) {
if (Objects.equals(e.getMessage(), "Failed to decrypt credential.")) {
platformException = new Messages.FlutterError("android-sync-account-not-available", e.getMessage(), SYNC_ACCOUNT_NOT_AVAILABLE_ERROR);
} else if (Objects.equals(e.getMessage(), "[15] Flow has timed out.")) {
platformException = new Messages.FlutterError("android-timeout", e.getMessage(), TIMEOUT_ERROR);
} else {
platformException = new Messages.FlutterError("android-unhandled: " + e.getType(), e.getMessage(), e.getErrorMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class AuthenticateController: NSObject, ASAuthorizationControllerDelegate, ASAut
self.completion = completion;
}

func run(request: ASAuthorizationPlatformPublicKeyCredentialAssertionRequest, conditionalUI: Bool, preferImmediatelyAvailableCredentials: Bool) {
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
func run(requests: [ASAuthorizationRequest], conditionalUI: Bool, preferImmediatelyAvailableCredentials: Bool) {
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = self
authorizationController.presentationContextProvider = self

Expand All @@ -39,7 +39,19 @@ class AuthenticateController: NSObject, ASAuthorizationControllerDelegate, ASAut

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let r as ASAuthorizationPublicKeyCredentialAssertion:
case let r as ASAuthorizationSecurityKeyPublicKeyCredentialAssertion:
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()
)

completion?(.success(response))
break
case let r as ASAuthorizationPlatformPublicKeyCredentialAssertion:
let response = AuthenticateResponse(
id: r.credentialID.toBase64URL(),
rawId: r.credentialID.toBase64URL(),
Expand All @@ -62,7 +74,9 @@ class AuthenticateController: NSObject, ASAuthorizationControllerDelegate, ASAut
completion?(.failure(FlutterError(from: err)))
}

completion?(.failure(FlutterError(code: CustomErrors.unknown)))
let nsErr = error as NSError
completion?(.failure(FlutterError(fromNSError: nsErr)))
return
}

func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ extension FlutterError: Error {
var code = ""
if (error.domain == "WKErrorDomain" && error.code == 8) {
code = "exclude-credentials-match"
}else if(error.domain == "WKErrorDomain" && error.code == 31){
// This error happens when the security key prompt times out (2 minutes)
code = "ios-security-key-timeout"
} else {
code = "ios-unhandled-" + error.domain
}
Expand Down
Loading