Web Loyihalar Integratsiyasi (React, Next.js, Node.js)

OAuth 2.0 + OpenID Connect PKCE (S256) Web Auth Protocol

Ushbu qo'llanma orqali har qanday Web loyihaga (React, Next.js, Vue, Angular, Node.js, PHP) BioID biometrik 2FA Single Sign-On tizimini integratsiya qilishingiz mumkin.

1. Web OAuth 2.0 Ketma-ketligi

[ Web App Client ] ── 1. PKCE Code Challenge (S256) ──► [ BioID Login Sahifasi ]
       │                                                         │
       │                                         2. Fuqaro Biometrik 2FA Skaneri
       │                                                         │
       ├── 4. https://app.uz/auth/callback?code=... ◄───────────┘
       │      (Redirect URI Callback)
       │
       ├── 5. POST /api/oauth/token (code + code_verifier) ────► [ BioID Server ]
       │                                                             │
       └── 6. GET /api/oauth/userinfo (Bearer Token) ───────────► [ Citizen Profile ]

2. OAuth 2.0 API Server Parametrlari

EndpointMethodManzilVazifasi
Authorize URLGET/api/oauth/authorizeAvtorizatsiya va yuz skaneri oynasiga yuborish
Token URLPOST/api/oauth/tokenCode va PKCE Verifier-ni Access Token-ga almashtirish
Userinfo URLGET/api/oauth/userinfoFuqaroning shaxsiy va biometrik ma'lumotlarini olish

3. JavaScript / TypeScript SDK Servis Kodu

Ushbu tayyor sinfni loyihangizga joylashtiring (bioid-web-sdk.ts):

// bioid-web-sdk.ts

export interface BioIDUserProfile {
  sub: string;
  pinfl: string;
  name: string;
  given_name: string;
  family_name: string;
  middle_name?: string;
  passport_series: string;
  passport_number: string;
  phone_number: string;
}

export class BioIDWebSDK {
  private baseUrl: string;
  private clientId: string;
  private redirectUri: string;

  constructor(config: { baseUrl?: string; clientId: string; redirectUri: string }) {
    this.baseUrl = config.baseUrl || 'http://localhost:3000';
    this.clientId = config.clientId;
    this.redirectUri = config.redirectUri;
  }

  /**
   * SHA-256 Code Challenge Generator (PKCE S256)
   */
  private async generatePKCE(): Promise<{ verifier: string; challenge: string }> {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    const verifier = Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');

    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');

    return { verifier, challenge };
  }

  /**
   * BioID Avtorizatsiya sahifasiga yo'naltirish
   */
  public async redirectToLogin(): Promise<void> {
    const { verifier, challenge } = await this.generatePKCE();
    const state = Math.random().toString(36).substring(2, 15);

    // PKCE verifier va state-ni brauzer sessionStorage-da saqlash
    sessionStorage.setItem('bioid_code_verifier', verifier);
    sessionStorage.setItem('bioid_auth_state', state);

    const authUrl = new URL(`${this.baseUrl}/api/oauth/authorize`);
    authUrl.searchParams.set('client_id', this.clientId);
    authUrl.searchParams.set('redirect_uri', this.redirectUri);
    authUrl.searchParams.set('response_type', 'code');
    authUrl.searchParams.set('scope', 'openid profile pinfl phone');
    authUrl.searchParams.set('state', state);
    authUrl.searchParams.set('code_challenge', challenge);
    authUrl.searchParams.set('code_challenge_method', 'S256');

    window.location.href = authUrl.toString();
  }

  /**
   * Callback sahifasida Authorization Code-ni Access Token-ga almashtirish
   */
  public async handleCallback(code: string): Promise<BioIDUserProfile> {
    const verifier = sessionStorage.getItem('bioid_code_verifier');
    if (!verifier) {
      throw new Error("PKCE Code Verifier topilmadi. Avtorizatsiya qaytadan boshlanishi kerak.");
    }

    // 1. Token olish (POST /api/oauth/token)
    const tokenRes = await fetch(`${this.baseUrl}/api/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        client_id: this.clientId,
        code,
        redirect_uri: this.redirectUri,
        code_verifier: verifier,
      }),
    });

    const tokenData = await tokenRes.json();
    if (!tokenRes.ok || !tokenData.access_token) {
      throw new Error(tokenData.error || "Access Token olishda xatolik yuz berdi");
    }

    // PKCE seansini tozalash
    sessionStorage.removeItem('bioid_code_verifier');
    sessionStorage.removeItem('bioid_auth_state');

    // 2. Fuqaro ma'lumotlarini olish (GET /api/oauth/userinfo)
    const userinfoRes = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {
      headers: {
        Authorization: `Bearer ${tokenData.access_token}`,
      },
    });

    if (!userinfoRes.ok) {
      throw new Error("Fuqaro profil ma'lumotlarini olishda xatolik");
    }

    return await userinfoRes.json();
  }
}

4. React / Next.js Sahifa va Tugma Misoli

import React, { useEffect, useState } from 'react';
import { BioIDWebSDK, BioIDUserProfile } from './bioid-web-sdk';

const bioid = new BioIDWebSDK({
  clientId: 'YOUR_BIOID_CLIENT_ID',
  redirectUri: 'http://localhost:3000/auth/callback',
});

// Kirish Tugmasi Komponenti
export function BioIDLoginButton() {
  return (
    <button
      onClick={() => bioid.redirectToLogin()}
      className="px-6 py-3 bg-[#0052FF] text-white font-bold text-sm rounded-2xl shadow-md hover:bg-blue-700 transition-all"
    >
      BioID Orqali Kirish (2FA Face ID)
    </button>
  );
}

// Callback Ishlovchi Sahifa Komponenti
export function AuthCallbackPage() {
  const [profile, setProfile] = useState<BioIDUserProfile | null>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');

    if (code) {
      bioid
        .handleCallback(code)
        .then((userData) => setProfile(userData))
        .catch((err) => setError(err.message));
    } else {
      setError("Avtorizatsiya kodi topilmadi");
    }
  }, []);

  if (error) return <div className="text-red-600 font-semibold p-4">Xatolik: {error}</div>;
  if (!profile) return <div className="text-blue-600 font-semibold p-4">Biometrik Profil Tasdiqlanmoqda...</div>;

  return (
    <div className="p-6 bg-white rounded-3xl border border-slate-200 shadow-md space-y-3">
      <h3 className="text-lg font-bold text-slate-900">Muvaffaqiyatli Kirildi</h3>
      <p><strong>F.I.SH:</strong> {profile.name}</p>
      <p><strong>JSHSHIR:</strong> {profile.pinfl}</p>
      <p><strong>Pasport:</strong> {profile.passport_series} {profile.passport_number}</p>
      <p><strong>Tel:</strong> {profile.phone_number}</p>
    </div>
  );
}