Flutter Mobil Ilova Integratsiyasi
OAuth 2.0 PKCE (S256) Protocol & In-App Biometric Web Browser Auth
Ushbu yo'riqnoma orqali har qanday Flutter (Android va iOS) mobil ilovasiga BioID Milliy Yagona Identifikatsiya Tizimini integratsiya qilishingiz mumkin.
1. Integratsiya Jarayoni (OAuth 2.0 PKCE)
[ Flutter Mobile App ] ── 1. PKCE Challenge (S256) ──► [ In-App Browser Tab ]
│ │
│ 2. BioID Yuz Skaneri (2FA)
│ │
├── 4. bioidapp://oauth/callback?code=... ◄─────────────┘
│ (Deep Link Callback)
│
├── 5. POST /api/oauth/token (code + code_verifier) ──► [ BioID Server ]
│ │
└── 6. Return Access Token & User Profile ◄─────────────────┘2. Pubspec.yaml Bog'liqliklarini Sozlash
Flutter loyihangizning pubspec.yaml fayliga quyidagi paketlarni qo'shing:
dependencies:
flutter:
sdk: flutter
flutter_web_auth_2: ^3.1.2
http: ^1.2.0
crypto: ^3.0.33. Android va iOS Deep Link Sozlamalari
Androidandroid/app/src/main/AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<!-- Custom Scheme for BioID OAuth Callback -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bioidapp" android:host="oauth" android:path="/callback" />
</intent-filter>
</activity>iOSios/Runner/Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>uz.bioid.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bioidapp</string>
</array>
</dict>
</array>4. Tayyor Dart Servis Kodu (`bio_id_service.dart`)
Ushbu servis avtomatik PKCE SHA-256 challenge yaratadi, in-app brauzerni ochadi va avtorizatsiya kodini tokenhamda fuqaro profiliga almashtiradi:
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http/http.dart' as http;
class BioIDCitizenProfile {
final String sub;
final String pinfl;
final String name;
final String givenName;
final String familyName;
final String passportSeries;
final String passportNumber;
final String phoneNumber;
BioIDCitizenProfile({
required this.sub,
required this.pinfl,
required this.name,
required this.givenName,
required this.familyName,
required this.passportSeries,
required this.passportNumber,
required this.phoneNumber,
});
factory BioIDCitizenProfile.fromJson(Map<String, dynamic> json) {
return BioIDCitizenProfile(
sub: json['sub'] ?? '',
pinfl: json['pinfl'] ?? '',
name: json['name'] ?? '',
givenName: json['given_name'] ?? '',
familyName: json['family_name'] ?? '',
passportSeries: json['passport_series'] ?? '',
passportNumber: json['passport_number'] ?? '',
phoneNumber: json['phone_number'] ?? '',
);
}
}
class BioIDAuthService {
// BioID Server Manzili (Production: https://bioid.gov.uz yoki Emulator: http://10.0.2.2:3000)
static const String baseUrl = 'http://localhost:3000';
// Admin panelda yaratilgan OAuth Client ID
static const String clientId = 'YOUR_BIOID_CLIENT_ID';
static const String redirectUri = 'bioidapp://oauth/callback';
static const String scopes = 'openid profile pinfl phone';
/// PKCE Code Verifier va Challenge Yaratish
static String _generateRandomString(int length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
final random = Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)]).join();
}
static String _base64UrlEncode(List<int> bytes) {
return base64Url.encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
}
/// BioID SSO Orqali Avtorizatsiyadan O'tish
static Future<BioIDCitizenProfile?> loginWithBioID() async {
try {
// 1. PKCE verifier va challenge yaratish (S256)
final codeVerifier = _generateRandomString(64);
final challengeBytes = sha256.convert(utf8.encode(codeVerifier)).bytes;
final codeChallenge = _base64UrlEncode(challengeBytes);
final state = _generateRandomString(16);
// 2. Avtorizatsiya URL manzilini shakllantirish
final authorizeUrl = Uri.parse('$baseUrl/api/oauth/authorize').replace(
queryParameters: {
'client_id': clientId,
'redirect_uri': redirectUri,
'response_type': 'code',
'scope': scopes,
'state': state,
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
},
);
// 3. In-App Web Browser oynasini ochish va Callback kutish
final resultUrlString = await FlutterWebAuth2.authenticate(
url: authorizeUrl.toString(),
callbackUrlScheme: 'bioidapp',
);
final resultUri = Uri.parse(resultUrlString);
final code = resultUri.queryParameters['code'];
if (code == null) {
throw Exception("BioID avtorizatsiya kodi olinmadi");
}
// 4. Token almashtirish (POST /api/oauth/token)
final tokenResponse = await http.post(
Uri.parse('$baseUrl/api/oauth/token'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'grant_type': 'authorization_code',
'client_id': clientId,
'code': code,
'redirect_uri': redirectUri,
'code_verifier': codeVerifier,
}),
);
if (tokenResponse.statusCode != 200) {
throw Exception("Token olishda xatolik: ${tokenResponse.body}");
}
final tokenData = jsonDecode(tokenResponse.body);
final accessToken = tokenData['access_token'];
// 5. Fuqaro profil ma'lumotlarini olish (GET /api/oauth/userinfo)
final userInfoResponse = await http.get(
Uri.parse('$baseUrl/api/oauth/userinfo'),
headers: {'Authorization': 'Bearer $accessToken'},
);
if (userInfoResponse.statusCode != 200) {
throw Exception("Fuqaro ma'lumotlarini olishda xatolik");
}
final userData = jsonDecode(userInfoResponse.body);
return BioIDCitizenProfile.fromJson(userData);
} catch (e) {
print("BioID Login Error: $e");
rethrow;
}
}
}5. Flutter UI Tugmasi Qo'llanishi (`main.dart`)
import 'package:flutter/material.dart';
import 'services/bio_id_service.dart';
class BioIDLoginWidget extends StatefulWidget {
const BioIDLoginWidget({super.key});
@override
State<BioIDLoginWidget> createState() => _BioIDLoginWidgetState();
}
class _BioIDLoginWidgetState extends State<BioIDLoginWidget> {
BioIDCitizenProfile? citizen;
bool isLoading = false;
Future<void> _handleBioIDLogin() async {
setState(() => isLoading = true);
try {
final profile = await BioIDAuthService.loginWithBioID();
setState(() => citizen = profile);
} catch (err) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Xatolik: $err')),
);
} finally {
setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BioID SSO Verification')),
body: Center(
child: isLoading
? const CircularProgressIndicator()
: citizen != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.verified, color: Colors.blue, size: 64),
const SizedBox(height: 16),
Text(citizen!.name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text('JSHSHIR: ${citizen!.pinfl}'),
Text('Pasport: ${citizen!.passportSeries} ${citizen!.passportNumber}'),
Text('Tel: ${citizen!.phoneNumber}'),
],
)
: ElevatedButton.icon(
onPressed: _handleBioIDLogin,
icon: const Icon(Icons.fingerprint),
label: const Text('BioID Orqali Kirish (2FA Face ID)'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0052FF),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
),
),
),
);
}
}