Flutter Passkey Service

pub package License: MIT

A comprehensive Flutter plugin that provides seamless integration with Passkeys (WebAuthn) for both iOS and Android platforms. Enable passwordless authentication in your Flutter applications using biometric authentication, device PINs, or security keys.

โœจ Features

  • ๐Ÿ” Passwordless Authentication - Secure biometric and device-based authentication
  • ๐Ÿ“ฑ Cross-Platform Support - Native implementation for iOS 16.0+ and Android API 28+
  • ๐Ÿ›ก๏ธ WebAuthn Compliant - Full compliance with W3C WebAuthn standards
  • ๐Ÿ”„ Cross-Device Sync - Passkeys sync across user's devices via platform providers
  • ๐Ÿš€ Type-Safe API - Generated with Pigeon for reliable Flutter-to-native communication
  • ๐ŸŽฏ Easy Integration - Simple, developer-friendly API with comprehensive error handling
  • ๐Ÿ“š Well Documented - Complete API documentation with examples

๐Ÿš€ Quick Start

Installation

Add flutter_passkey_service to your pubspec.yaml:

dependencies:
  flutter_passkey_service: ^0.0.1

Run the following command:

flutter pub get

Platform Setup

iOS Setup

  1. Minimum Requirements: iOS 16.0+

  2. Add Capability: In Xcode, add "Associated Domains" capability

    • Open your project in Xcode
    • Select your target โ†’ "Signing & Capabilities"
    • Click "+" and add "Associated Domains"
  3. Configure Domain: Add your domain with webcredentials prefix:

    webcredentials:yourdomain.com
    
  4. Domain Verification: Create an apple-app-site-association file on your server:

    {
      "webcredentials": {
        "apps": ["TEAMID.com.yourcompany.yourapp"]
      }
    }
    
    • Host at: https://yourdomain.com/.well-known/apple-app-site-association
    • No file extension required
    • Content-Type: application/json
    • Verification Tool: Apple App Site Association Validator

Android Setup

  1. Minimum Requirements: Android API 28+ (Android 9.0)

  2. Add Dependencies: The plugin automatically includes required dependencies

  3. Configure Digital Asset Links: Create an assetlinks.json file:

    [{
      "relation": ["delegate_permission/common.handle_all_urls"],
      "target": {
        "namespace": "android_app",
        "package_name": "com.yourcompany.yourapp",
        "sha256_cert_fingerprints": ["SHA256_FINGERPRINT_OF_YOUR_APP"]
      }
    }]
    
  4. Host Asset Links File:

    • Upload to: https://yourdomain.com/.well-known/assetlinks.json
    • Content-Type: application/json
    • Must be accessible via HTTPS
  5. Get SHA256 Fingerprint:

    # For debug keystore
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
       
    # For release keystore
    keytool -list -v -keystore /path/to/your/keystore.jks -alias your_key_alias
    
  6. Verification Tools:

Basic Usage

import 'package:flutter_passkey_service/flutter_passkey_service.dart';

class PasskeyAuth {
  
  // Register a new passkey
  Future<void> registerPasskey() async {
    try {
      final options = FlutterPasskeyService.createRegistrationOptions(
        challenge: 'your-server-challenge',
        rpName: 'Your App Name',
        rpId: 'yourdomain.com',
        userId: 'user-123',
        username: 'user@example.com',
        displayName: 'John Doe',
      );
      
      final response = await FlutterPasskeyService.register(options);
      
      // Send response to your server for verification
      print('Registration successful: ${response.id}');
      
    } on PasskeyException catch (e) {
      print('Registration failed: ${e.message}');
    }
  }
  
  // Authenticate with existing passkey
  Future<void> authenticateWithPasskey() async {
    try {
      final request = FlutterPasskeyService.createAuthenticationOptions(
        challenge: 'your-server-challenge',
        rpId: 'yourdomain.com',
      );
      
      final response = await FlutterPasskeyService.authenticate(request);
      
      // Send response to your server for verification
      print('Authentication successful: ${response.id}');
      
    } on PasskeyException catch (e) {
      print('Authentication failed: ${e.message}');
    }
  }
}

๐Ÿ“š Comprehensive Guide

Domain Verification Setup

Proper domain verification is essential for passkey functionality. Both platforms require your app to be associated with your web domain.

iOS Domain Verification (Apple App Site Association)

  1. Create the Association File:

    {
      "webcredentials": {
        "apps": [
          "TEAMID.com.yourcompany.yourapp",
          "TEAMID.com.yourcompany.yourapp.staging"
        ]
      }
    }
    
  2. Host the File:

    • URL: https://yourdomain.com/.well-known/apple-app-site-association
    • Important: No .json file extension!
    • Content-Type: application/json
    • Must be served over HTTPS
    • Must return HTTP 200 status
  3. Find Your Team ID:

  4. Verify Association:

    # Test your association file
    curl -v https://yourdomain.com/.well-known/apple-app-site-association
    
  1. Get Your App's SHA256 Fingerprint:

    # Debug keystore (for development)
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA256
       
    # Release keystore (for production)
    keytool -list -v -keystore /path/to/release-key.keystore -alias release-key-alias | grep SHA256
       
    # From APK file
    keytool -printcert -jarfile app-release.apk | grep SHA256
    
  2. Create Asset Links File:

    [
      {
        "relation": ["delegate_permission/common.handle_all_urls"],
        "target": {
          "namespace": "android_app",
          "package_name": "com.yourcompany.yourapp",
          "sha256_cert_fingerprints": [
            "AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78"
          ]
        }
      }
    ]
    
  3. Host the File:

    • URL: https://yourdomain.com/.well-known/assetlinks.json
    • Content-Type: application/json
    • Must be served over HTTPS
    • Must return HTTP 200 status
  4. Verify Asset Links:

    # Test your asset links file
    curl -v https://yourdomain.com/.well-known/assetlinks.json
    

Common Domain Verification Issues

Issue Solution
File not found (404) Ensure files are in /.well-known/ directory
HTTPS required Both files must be served over HTTPS only
Wrong content-type Set Content-Type: application/json
Invalid JSON Validate JSON syntax
Wrong package name Must match your app's package identifier exactly
Case sensitivity Package names and fingerprints are case-sensitive
Caching issues Clear CDN/server cache after updating files

Testing Domain Verification

// Test domain association in your Flutter app
void testDomainVerification() async {
  try {
    // This will fail if domain verification is not set up correctly
    final request = FlutterPasskeyService.createAuthenticationOptions(
      challenge: 'test-challenge',
      rpId: 'yourdomain.com', // Must match your verified domain
    );
    
    print('Domain verification appears to be working');
  } catch (e) {
    print('Domain verification issue: $e');
  }
}

Registration Flow

The passkey registration process involves creating a new credential for the user:

Future<CreatePasskeyResponseData> registerUser({
  required String username,
  required String userId,
  required String challenge,
}) async {
  // 1. Create registration options
  final options = RegisterGenerateOptionData(
    challenge: challenge, // Base64URL encoded challenge from server
    rp: RegisterGenerateOptionRp(
      name: 'Your App Name',
      id: 'yourdomain.com',
    ),
    user: RegisterGenerateOptionUser(
      id: userId, // Unique user identifier
      name: username,
      displayName: 'Display Name',
    ),
    pubKeyCredParams: [
      RegisterGenerateOptionPublicKeyParams(alg: -7, type: 'public-key'), // ES256
      RegisterGenerateOptionPublicKeyParams(alg: -257, type: 'public-key'), // RS256
    ],
    timeout: 60000,
    attestation: 'none',
    excludeCredentials: [], // Previously registered credentials to exclude
    authenticatorSelection: RegisterGenerateOptionAuthenticatorSelection(
      residentKey: 'preferred',
      userVerification: 'required',
      requireResidentKey: false,
      authenticatorAttachment: 'platform',
    ),
    extensions: RegisterGenerateOptionExtension(credProps: true),
  );
  
  // 2. Perform registration
  final response = await FlutterPasskeyService.register(options);
  
  // 3. Send to server for verification and storage
  // response contains: id, rawId, type, authenticatorAttachment, 
  // response (attestationObject, clientDataJSON), clientExtensionResults
  
  return response;
}

Authentication Flow

Authenticate users with their existing passkeys:

Future<GetPasskeyAuthenticationResponseData> authenticateUser({
  required String challenge,
  List<String>? allowedCredentialIds,
}) async {
  // 1. Create authentication request
  final request = AuthGenerateOptionResponseData(
    rpId: 'yourdomain.com',
    challenge: challenge, // Base64URL encoded challenge from server
    allowCredentials: allowedCredentialIds?.map((id) => 
      AuthGenerateOptionAllowCredential(
        id: id,
        type: 'public-key',
        transports: ['internal', 'hybrid'],
      )
    ).toList() ?? [],
    timeout: 60000,
    userVerification: 'required',
  );
  
  // 2. Perform authentication
  final response = await FlutterPasskeyService.authenticate(request);
  
  // 3. Send to server for verification
  // response contains: id, rawId, type, authenticatorAttachment,
  // response (clientDataJSON, authenticatorData, signature, userHandle)
  
  return response;
}

Error Handling

The plugin provides comprehensive error handling through PasskeyException:

try {
  await FlutterPasskeyService.register(options);
} on PasskeyException catch (e) {
  switch (e.errorType) {
    case PasskeyErrorType.userCancelled:
      showMessage('User cancelled the operation');
      break;
    case PasskeyErrorType.noCredentialsAvailable:
      showMessage('No passkeys available for this account');
      break;
    case PasskeyErrorType.invalidParameters:
      showMessage('Invalid request parameters');
      break;
    case PasskeyErrorType.platformNotSupported:
      showMessage('Passkeys not supported on this device');
      break;
    default:
      showMessage('Authentication failed: ${e.message}');
  }
}

Available Error Types

Error Type Description
invalidParameters Invalid or missing parameters
userCancelled User cancelled the operation
userTimeout Operation timed out
noCredentialsAvailable No credentials available for authentication
credentialNotFound Specified credential not found
platformNotSupported Platform doesn't support passkeys
domainNotAssociated Domain not associated with app
invalidResponse Invalid response received
systemError System-level error occurred
networkError Network-related error
unknownError Unknown error occurred

๐Ÿ—๏ธ Advanced Configuration

Custom Registration Options

final customOptions = RegisterGenerateOptionData(
  challenge: challenge,
  rp: RegisterGenerateOptionRp(name: 'App', id: 'domain.com'),
  user: RegisterGenerateOptionUser(id: 'user', name: 'username', displayName: 'User'),
  pubKeyCredParams: [
    RegisterGenerateOptionPublicKeyParams(alg: -7, type: 'public-key'),
  ],
  timeout: 120000, // 2 minutes
  attestation: 'direct', // Request attestation
  excludeCredentials: [
    RegisterGenerateOptionExcludeCredential(
      id: 'existing-credential-id',
      type: 'public-key',
      transports: ['internal'],
    ),
  ],
  authenticatorSelection: RegisterGenerateOptionAuthenticatorSelection(
    residentKey: 'required', // Force resident key
    userVerification: 'preferred',
    requireResidentKey: true,
    authenticatorAttachment: 'cross-platform', // Allow external authenticators
  ),
  extensions: RegisterGenerateOptionExtension(credProps: true),
);

Server Integration

Challenge Generation

// Generate a cryptographically secure challenge on your server
String generateChallenge() {
  final bytes = List<int>.generate(32, (i) => Random.secure().nextInt(256));
  return base64Url.encode(bytes);
}

Verification

// Verify registration response on server
bool verifyRegistration(CreatePasskeyResponseData response, String challenge) {
  // 1. Decode and verify clientDataJSON
  // 2. Verify challenge matches
  // 3. Verify origin matches your domain
  // 4. Parse and verify attestationObject
  // 5. Store credential for future authentication
  return true; // Simplified
}

// Verify authentication response on server
bool verifyAuthentication(GetPasskeyAuthenticationResponseData response, String challenge) {
  // 1. Decode and verify clientDataJSON
  // 2. Verify challenge matches
  // 3. Verify origin matches your domain
  // 4. Verify signature using stored public key
  // 5. Update credential sign count
  return true; // Simplified
}

๐Ÿ”ง Platform Requirements

iOS

  • Minimum Version: iOS 16.0+
  • Frameworks: AuthenticationServices
  • Capabilities: Associated Domains
  • Features: Touch ID, Face ID, Device Passcode support

Android

  • Minimum Version: Android 9.0 (API 28)+
  • Dependencies: Credential Manager API
  • Features: Biometric authentication, Device PIN support
  • Requirements: Google Play Services

๐Ÿงช Testing

Unit Testing

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_passkey_service/flutter_passkey_service.dart';

void main() {
  group('FlutterPasskeyService', () {
    test('createRegistrationOptions returns valid options', () {
      final options = FlutterPasskeyService.createRegistrationOptions(
        challenge: 'test-challenge',
        rpName: 'Test App',
        rpId: 'test.com',
        userId: 'user-123',
        username: 'test@example.com',
      );
      
      expect(options.challenge, 'test-challenge');
      expect(options.rp.name, 'Test App');
      expect(options.user.id, 'user-123');
    });
  });
}

Integration Testing

import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Passkey Integration Tests', () {
    testWidgets('registration flow', (tester) async {
      // Test complete registration flow
      // Note: Requires physical device and user interaction
    });
  });
}

๐Ÿ” Security Considerations

  1. Challenge Generation: Always generate challenges server-side using cryptographically secure methods
  2. Origin Verification: Verify the origin in clientDataJSON matches your domain
  3. Timeout Handling: Implement appropriate timeouts for user operations
  4. Error Messages: Avoid exposing sensitive information in error messages
  5. Credential Storage: Store public keys and metadata securely on your server
  6. Sign Count: Track and validate signature counter to prevent replay attacks

๐Ÿค Contributing

We welcome contributions! Please read our Contributing Guidelines for details on:

  • Code of Conduct
  • Development setup
  • Pull request process
  • Issue reporting

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ†˜ Support

๐Ÿ”— Resources

WebAuthn & Passkeys

Platform Documentation

Domain Verification Tools

Testing & Debugging

Security Resources


Made with โค๏ธ for the Flutter community