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.3

3. 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),
                    ),
                  ),
      ),
    );
  }
}