Device Restricted Auth

A Flutter package that enforces device-based login restrictions using Firebase Authentication and Firestore. Perfect for apps that need to limit account access to specific devices.

What is Device Restricted Auth?

This package permanently binds user accounts to specific devices, preventing account sharing across multiple devices. Once a user signs up or logs in from a device, that account becomes permanently linked to that device's hardware ID.

Why Use This Package?

Use Cases:

  • Streaming Apps: Prevent password sharing (like Netflix's device limits)
  • Premium Apps: Enforce "1 device per license" policies
  • Enterprise Apps: Restrict corporate accounts to company-owned devices
  • Educational Apps: Ensure students use their own devices for exams
  • Private Tools: Limit access to authorized devices only

Key Benefits:

  • Hardware-level device identification (not spoofable by users)
  • Server-side enforcement via Firestore security rules
  • Automatic device binding on first login
  • Clear error messages for device mismatches

Features

  • Permanent Device Binding: Each account is bound to 1 Android + 1 Desktop device
  • Hardware-Level Security: Uses platform-specific hardware identifiers
  • Firebase Integration: Works seamlessly with Firebase Auth and Firestore
  • Cross-Platform: Android and Windows Desktop support
  • Policy-Based: Configurable device binding policies
  • Type-Safe: Full Dart type safety with custom exceptions

Supported Platforms

Platform Support Device ID Source
Android ✅ Yes androidId from device_info_plus
Windows ✅ Yes Windows device ID from device_info_plus
iOS ❌ No Not implemented
macOS ❌ No Not implemented
Linux ❌ No Not implemented
Web ❌ No Not supported (no hardware ID)

Installation

Add this to your pubspec.yaml:

dependencies:
  device_restricted_auth: ^0.1.0
  firebase_core: ^3.8.0
  firebase_auth: ^5.3.3
  cloud_firestore: ^5.5.0

Then run:

flutter pub get

Firebase Setup

1. Initialize Firebase

Make sure Firebase is initialized in your app:

import 'package:firebase_core/firebase_core.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

2. Firestore Collections

This package requires two Firestore collections:

users/{userId} - User profile data:

{
  "email": "user@example.com",
  "username": "username",
  "name": "User Name",
  "createdAt": Timestamp
}

user_devices/{userId} - Device bindings:

{
  "android": {
    "deviceId": "abc123...",
    "boundAt": Timestamp,
    "lastActive": Timestamp,
    "isPermanent": true
  },
  "desktop": {
    "deviceId": "xyz789...",
    "boundAt": Timestamp,
    "lastActive": Timestamp,
    "isPermanent": true
  }
}

3. Firestore Security Rules (Important!)

Add these rules to enforce device restrictions server-side:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // User devices collection
    match /user_devices/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow write: if request.auth != null && request.auth.uid == userId;
    }

    // Users collection
    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null && request.auth.uid == userId;
      allow update: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Usage

Step 1: Initialize the Coordinator

Create the coordinator with platform-specific device ID provider:

import 'dart:io';
import 'package:device_restricted_auth/device_restricted_auth.dart';

// Choose provider based on platform
final deviceIdProvider = Platform.isAndroid
    ? AndroidDeviceIdProvider()
    : WindowsDeviceIdProvider();

// Create repository
final deviceRepository = FirestoreDeviceRepository();

// Create coordinator
final coordinator = DeviceRestrictionCoordinator(
  deviceRepository: deviceRepository,
  deviceIdProvider: deviceIdProvider,
);

Step 2: During User Signup

import 'package:firebase_auth/firebase_auth.dart';

Future<void> signUp(String email, String password) async {
  try {
    // 1. Check if device is available for new account
    await coordinator.verifyDeviceForSignup();

    // 2. Create Firebase account
    final credential = await FirebaseAuth.instance
        .createUserWithEmailAndPassword(
      email: email,
      password: password,
    );

    // 3. Initialize device document in Firestore
    await coordinator.initializeDeviceDocument(credential.user!.uid);

    // 4. Bind current device to this account
    await coordinator.verifyAndBindDevice(credential.user!.uid);

    print('✅ Signup successful! Device bound.');

  } on DeviceAlreadyBoundException catch (e) {
    // Device is already registered to another account
    print('Error: ${e.message}');
    // Show error to user: "This device is already linked to another account"

  } catch (e) {
    print('Signup failed: $e');
  }
}

Step 3: During User Login

Future<void> login(String email, String password) async {
  try {
    // 1. Sign in with Firebase
    final credential = await FirebaseAuth.instance
        .signInWithEmailAndPassword(
      email: email,
      password: password,
    );

    // 2. Verify device binding (binds on first login, validates on subsequent)
    await coordinator.verifyAndBindDevice(credential.user!.uid);

    print('✅ Login successful!');

  } on DeviceMismatchException catch (e) {
    // User is trying to login from a different device
    print('Error: ${e.message}');
    // Show error: "This account is bound to another device"
    await FirebaseAuth.instance.signOut(); // Sign out immediately

  } catch (e) {
    print('Login failed: $e');
  }
}

Error Handling

The package throws specific exceptions for different scenarios:

DeviceAlreadyBoundException

Thrown during signup when the device is already registered to another account.

try {
  await coordinator.verifyDeviceForSignup();
} on DeviceAlreadyBoundException catch (e) {
  // Show: "This device is already linked to another account"
  // Action: Ask user to login instead of signup
}

DeviceMismatchException

Thrown during login when the user tries to access their account from a different device.

try {
  await coordinator.verifyAndBindDevice(userId);
} on DeviceMismatchException catch (e) {
  // Show: "This account is bound to another device"
  // Action: Sign out and prevent access
  await FirebaseAuth.instance.signOut();
}

DeviceIdNotFoundException

Thrown when the device ID cannot be retrieved from the platform.

try {
  final deviceId = await deviceIdProvider.getDeviceId();
} on DeviceIdNotFoundException catch (e) {
  // Show: "Unable to identify this device"
  // Action: Check device permissions or platform support
}

PlatformNotSupportedException

Thrown when using a provider on an unsupported platform.

try {
  final provider = AndroidDeviceIdProvider();
  await provider.getDeviceId(); // On iOS/Windows
} on PlatformNotSupportedException catch (e) {
  // Show: "This platform is not supported"
}

How Device Binding Works

  1. First Signup: Device is checked if available → Account created → Device bound permanently
  2. First Login on Android: Account verified → Android device bound permanently
  3. First Login on Desktop: Account verified → Desktop device bound permanently
  4. Subsequent Logins: Device ID is verified against bound device → Access granted/denied

Important Notes:

  • Each account can have 1 Android device + 1 Desktop device
  • Device bindings are permanent and cannot be changed
  • Users cannot login from a different device once bound
  • Device IDs are hardware-based and survive app reinstalls

Limitations

  • No Device Replacement: Once bound, devices cannot be changed (by design)
  • Platform-Specific: Only Android and Windows Desktop are supported
  • Requires Firebase: This package is tightly coupled with Firebase services
  • No Web Support: Web browsers don't have reliable hardware IDs
  • Privacy Consideration: Device IDs are stored in Firestore (ensure compliance with privacy laws)

Example App

See the example directory for a complete working app demonstrating:

  • Firebase initialization
  • Signup with device verification
  • Login with device binding
  • Error handling for all scenarios

API Reference

Core Classes

  • DeviceRestrictionCoordinator: Main orchestrator for device restriction logic
  • AndroidDeviceIdProvider: Retrieves Android device IDs
  • WindowsDeviceIdProvider: Retrieves Windows device IDs
  • FirestoreDeviceRepository: Firestore implementation for device storage

Models

  • DeviceBinding: Represents a device bound to a user
  • DeviceMetadata: Additional device information
  • AuthResult: Result of authentication operations
  • ValidationResult: Result of device validation

Exceptions

  • DeviceAlreadyBoundException: Device is registered to another account
  • DeviceMismatchException: User trying to login from wrong device
  • DeviceIdNotFoundException: Cannot retrieve device ID
  • PlatformNotSupportedException: Platform not supported

Contributing

This package is currently maintained for specific use cases. If you need additional features:

  1. Fork the repository
  2. Create a feature branch
  3. Submit a pull request with clear documentation

License

MIT License - see LICENSE file for details.

Support

For issues, questions, or feature requests, please visit the GitHub repository.