Tr_Code/BIOMETRIC_AUTH_GUIDE.md

8.3 KiB

생체인증 가이드 (Credential Management API)

📱 개요

TWA(Trusted Web Activity) 환경에서 WebAuthn / Credential Management API를 활용한 생체인증 기능을 제공합니다.

주요 기능

1. 생체인증 등록

  • 로그인 후 생체인증(지문, 얼굴 인식 등)을 등록할 수 있습니다
  • 사용자의 사번과 이름으로 credential을 생성합니다
  • 등록된 credential은 Supabase에 안전하게 저장됩니다

2. 생체인증 로그인

  • 등록된 생체인증으로 빠르게 로그인할 수 있습니다
  • 비밀번호 입력 없이 지문/얼굴 인식만으로 인증됩니다
  • 플랫폼 인증 기능을 사용하여 보안성이 높습니다

3. 생체인증 해제

  • 필요시 생체인증을 해제할 수 있습니다
  • 해제 후 다시 등록할 수 있습니다

🏗️ 아키텍처

Frontend (Vue.js/TypeScript)

1. Composable: useBiometric.ts

// 생체인증 지원 여부 확인
await biometric.checkSupport()

// 생체인증 등록
const result = await biometric.register(employeeId, userName)

// 생체인증 로그인
const result = await biometric.authenticate()

// 생체인증 해제
const result = await biometric.unregister()

2. 상태 관리

  • isSupported: 브라우저가 WebAuthn을 지원하는지 여부
  • isAvailable: 플랫폼 생체인증이 사용 가능한지 여부
  • isEnrolled: 사용자가 생체인증을 등록했는지 여부
  • canUse: 생체인증을 사용할 수 있는지 여부
  • canRegister: 생체인증을 등록할 수 있는지 여부
  • canAuthenticate: 생체인증으로 로그인할 수 있는지 여부

Backend (Flask/Python)

1. 생체인증 등록 Challenge

POST /api/biometric/register-challenge
{
  "employeeId": "20240001"
}

Response:

{
  "success": true,
  "challenge": "random_base64_string",
  "userId": "20240001"
}

2. Credential 등록

POST /api/biometric/register
{
  "employeeId": "20240001",
  "credential": {
    "id": "credential_id",
    "rawId": "base64_raw_id",
    "response": {
      "clientDataJSON": "base64_client_data",
      "attestationObject": "base64_attestation"
    },
    "type": "public-key"
  }
}

3. 로그인 Challenge

POST /api/biometric/login-challenge
{
  "employeeId": "20240001"
}

4. 생체인증 로그인

POST /api/biometric/login
{
  "employeeId": "20240001",
  "assertion": {
    "id": "credential_id",
    "rawId": "base64_raw_id",
    "response": {
      "clientDataJSON": "base64_client_data",
      "authenticatorData": "base64_auth_data",
      "signature": "base64_signature"
    },
    "type": "public-key"
  }
}

Response:

{
  "success": true,
  "user": {
    "id": 1,
    "employee_id": "20240001",
    "name": "홍길동",
    "email": "hong@humetro.busan.kr",
    "department_id": 1
  }
}

5. 생체인증 해제

POST /api/biometric/unregister
{
  "employeeId": "20240001"
}

🗄️ 데이터베이스 스키마

biometric_credentials 테이블

CREATE TABLE IF NOT EXISTS public.biometric_credentials (
    id SERIAL PRIMARY KEY,
    employee_id VARCHAR(50) NOT NULL REFERENCES public.users(employee_id) ON DELETE CASCADE,
    credential_id VARCHAR(255) UNIQUE NOT NULL,
    credential_data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_used_at TIMESTAMPTZ,
    UNIQUE(employee_id, credential_id)
);

CREATE INDEX idx_biometric_employee_id ON public.biometric_credentials(employee_id);
CREATE INDEX idx_biometric_credential_id ON public.biometric_credentials(credential_id);

필드 설명:

  • employee_id: 사용자 사번 (외래키)
  • credential_id: WebAuthn credential ID (고유값)
  • credential_data: Credential 전체 데이터 (JSON)
  • created_at: 등록 일시
  • last_used_at: 마지막 사용 일시

🔐 보안 고려사항

1. Challenge-Response 인증

  • 서버에서 생성한 랜덤 challenge를 사용합니다
  • Challenge는 세션에 저장되고 10분간 유효합니다
  • 각 인증 시도마다 새로운 challenge를 생성합니다

2. 플랫폼 인증

  • authenticatorAttachment: 'platform'을 사용하여 기기 내장 생체인식만 허용합니다
  • userVerification: 'required'로 사용자 검증을 필수로 합니다

3. Credential 검증

  • 서버에 저장된 credential과 비교하여 검증합니다
  • 실제 프로덕션에서는 공개키 암호화를 사용한 서명 검증이 필요합니다 (현재는 간소화된 버전)

4. 세션 관리

  • 생체인증 성공 시 Flask 세션에 사용자 정보를 저장합니다
  • 세션은 httponly 쿠키로 안전하게 관리됩니다

📱 TWA 환경에서의 동작

1. Android TWA

  • Chrome의 WebAuthn API를 통해 Android 생체인식(지문, 얼굴)을 사용합니다
  • Android Keystore에 안전하게 credential이 저장됩니다
  • Google Play Services를 통한 FIDO2 인증을 지원합니다

2. 지원 여부 확인

// PublicKeyCredential API 지원 확인
if (!window.PublicKeyCredential) {
  console.log('생체인증 미지원')
}

// 플랫폼 인증 가능 여부
const available = await PublicKeyCredential
  .isUserVerifyingPlatformAuthenticatorAvailable()

3. 사용자 경험

  1. 로그인 페이지에서 생체인증 가능 여부를 자동으로 확인합니다
  2. 등록된 생체인증이 있으면 "생체인증으로 로그인" 버튼이 표시됩니다
  3. 버튼 클릭 시 기기의 생체인증 UI가 표시됩니다
  4. 인증 성공 시 자동으로 로그인됩니다

🚀 사용 방법

1. 생체인증 등록 (로그인 후)

import { useBiometric } from '@/composables/useBiometric'

const biometric = useBiometric()

// 지원 여부 확인
await biometric.checkSupport()

if (biometric.canRegister.value) {
  // 등록
  const result = await biometric.register(employeeId, userName)
  
  if (result.success) {
    console.log('생체인증 등록 성공')
  }
}

2. 생체인증 로그인

if (biometric.canAuthenticate.value) {
  const result = await biometric.authenticate()
  
  if (result.success && result.user) {
    // 로그인 성공
    authStore.setUser(result.user)
    router.push('/')
  }
}

3. LoginView에서의 통합

<template>
  <!-- 생체인증 버튼 -->
  <button
    v-if="biometric.canAuthenticate.value"
    @click="handleBiometricLogin"
    :disabled="biometric.loading.value"
  >
    생체인증으로 로그인
  </button>

  <!-- 등록 안내 -->
  <div v-else-if="!biometric.isEnrolled.value && biometric.canUse.value">
    로그인  생체인증을 등록할  있습니다.
  </div>
</template>

<script setup>
import { useBiometric } from '@/composables/useBiometric'

const biometric = useBiometric()

onMounted(async () => {
  await biometric.checkSupport()
})

async function handleBiometricLogin() {
  const result = await biometric.authenticate()
  // 처리...
}
</script>

🔧 문제 해결

1. 생체인증이 표시되지 않음

  • 브라우저가 WebAuthn을 지원하는지 확인
  • HTTPS 환경에서만 동작 (localhost는 예외)
  • 기기에 생체인증이 설정되어 있는지 확인

2. 등록 실패

  • Challenge가 만료되었을 수 있음 (10분)
  • 네트워크 연결 확인
  • 콘솔 로그에서 에러 메시지 확인

3. 로그인 실패

  • 등록된 credential이 삭제되었을 수 있음
  • localStorage에서 biometric_credential_id 확인
  • 데이터베이스에 credential이 존재하는지 확인

📝 TODO (향후 개선사항)

  1. 서명 검증 구현

    • 현재는 credential_id만 비교하는 간소화된 버전
    • 실제 공개키 암호화 서명 검증 추가 필요
  2. 여러 Credential 지원

    • 한 사용자가 여러 기기에서 생체인증 등록 가능하도록
  3. Credential 관리 UI

    • 등록된 기기 목록 표시
    • 개별 credential 삭제 기능
  4. 마지막 사용 시간 업데이트

    • 로그인 시 last_used_at 필드 업데이트
  5. 에러 로깅 개선

    • 생체인증 관련 에러를 audit_logs에 기록

🔗 참고 자료