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
- First Signup: Device is checked if available → Account created → Device bound permanently
- First Login on Android: Account verified → Android device bound permanently
- First Login on Desktop: Account verified → Desktop device bound permanently
- 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 logicAndroidDeviceIdProvider: Retrieves Android device IDsWindowsDeviceIdProvider: Retrieves Windows device IDsFirestoreDeviceRepository: Firestore implementation for device storage
Models
DeviceBinding: Represents a device bound to a userDeviceMetadata: Additional device informationAuthResult: Result of authentication operationsValidationResult: Result of device validation
Exceptions
DeviceAlreadyBoundException: Device is registered to another accountDeviceMismatchException: User trying to login from wrong deviceDeviceIdNotFoundException: Cannot retrieve device IDPlatformNotSupportedException: Platform not supported
Contributing
This package is currently maintained for specific use cases. If you need additional features:
- Fork the repository
- Create a feature branch
- 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.