Skip to content

Latest commit

ย 

History

History
496 lines (400 loc) ยท 15.4 KB

passkey.md

File metadata and controls

496 lines (400 loc) ยท 15.4 KB

Passkey ์ธ์ฆ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ์„ค๊ณ„ ๋ฌธ์„œ

๋ชฉ์ฐจ

  1. ๊ธฐ์กด ์‹œ์Šคํ…œ ๋ถ„์„
  2. Passkey ํ†ตํ•ฉ ๋ฐฉ์•ˆ
  3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ ๋ณ€๊ฒฝ
  4. ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์„ค๊ณ„
  5. ์ธ์ฆ ํ๋ฆ„ ์„ค๊ณ„
  6. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต
  7. ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„

๊ธฐ์กด ์‹œ์Šคํ…œ ๋ถ„์„

1. ์ƒค๋”ฉ ๊ตฌ์กฐ

ํ˜„์žฌ ๊ตฌ์กฐ

  • ์ƒค๋”ฉ ๋ฐฉ์‹: UUID ๊ธฐ๋ฐ˜ ํ•ด์‹œ ์ƒค๋”ฉ
  • ํ…Œ์ด๋ธ” ๋ช…๋ช… ๊ทœ์น™: users_001, users_002, ... (3์ž๋ฆฌ ํŒจ๋”ฉ)
  • ์ƒค๋“œ ํ‚ค: UUID (v4)
  • ์ƒค๋“œ ๊ฒฐ์ • ์•Œ๊ณ ๋ฆฌ์ฆ˜: CRC32 ํ•ด์‹œ โ†’ hash % shard_count + 1
  • ์„ค์ • ํŒŒ์ผ: config/shard.json

์ƒค๋”ฉ ์„œ๋น„์Šค (ShardingService)

// ์ฃผ์š” ๋ฉ”์„œ๋“œ
- getShardNumber($uuid): ์ƒค๋“œ ๋ฒˆํ˜ธ ๊ณ„์‚ฐ
- getShardTableName($uuid): ์ƒค๋“œ ํ…Œ์ด๋ธ”๋ช… ์กฐํšŒ
- getUserByUuid($uuid): UUID๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ (์ „์ฒด ์ƒค๋“œ ๊ฒ€์ƒ‰)
- getUserByEmail($email): ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ (์ธ๋ฑ์Šค ํ…Œ์ด๋ธ” ํ™œ์šฉ)
- createUser($data): ์‚ฌ์šฉ์ž ์ƒ์„ฑ (์ž๋™ ์ƒค๋“œ ๊ฒฐ์ •)
- updateUser($uuid, $data): ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ

์ธ๋ฑ์Šค ํ…Œ์ด๋ธ”

  • user_email_index: ์ด๋ฉ”์ผ โ†’ UUID ๋งคํ•‘ (๋น ๋ฅธ ์กฐํšŒ)
  • user_username_index: ์‚ฌ์šฉ์ž๋ช… โ†’ UUID ๋งคํ•‘
  • user_uuid_index: UUID โ†’ ์ƒค๋“œ ID ๋งคํ•‘ (์บ์‹œ)

2. ๊ธฐ์กด ์ธ์ฆ ๋ฐฉ์‹

์ธ์ฆ ํ๋ฆ„

  1. ๋กœ๊ทธ์ธ (Login/SubmitController)

    • ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
    • ์ƒค๋”ฉ ํ™˜๊ฒฝ: Shard::getUserByEmail() โ†’ ์ƒค๋“œ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ
    • ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ: Hash::check()
    • JWT ํ† ํฐ ์ƒ์„ฑ ๋˜๋Š” ์„ธ์…˜ ์ƒ์„ฑ
  2. ํšŒ์›๊ฐ€์ž… (Register/StoreController)

    • ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
    • ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ (์ƒค๋”ฉ ๊ณ ๋ ค)
    • UUID ์ƒ์„ฑ โ†’ ์ƒค๋“œ ๊ฒฐ์ • โ†’ ์ƒค๋“œ ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…
    • ์ธ๋ฑ์Šค ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ

์ธ์ฆ ๋ชจ๋ธ

  • ShardedUser: ์ƒค๋”ฉ๋œ ์‚ฌ์šฉ์ž ๋ชจ๋ธ (UUID ๊ธฐ๋ฐ˜)
  • User: ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ๋ชจ๋ธ (ID ๊ธฐ๋ฐ˜, ์ƒค๋”ฉ ๋น„ํ™œ์„ฑํ™” ์‹œ ์‚ฌ์šฉ)

3. ํ˜„์žฌ ๋ฌธ์ œ์ 

Passkey ํ†ตํ•ฉ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ

  1. ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ: webauthn_credentials ํ…Œ์ด๋ธ”์ด user_id๋ฅผ ์™ธ๋ž˜ํ‚ค๋กœ ์‚ฌ์šฉ

    • ์ƒค๋”ฉ ํ™˜๊ฒฝ์—์„œ๋Š” user_id๊ฐ€ ์ƒค๋“œ๋ณ„๋กœ ์ค‘๋ณต๋  ์ˆ˜ ์žˆ์Œ
    • UUID ๊ธฐ๋ฐ˜ ๊ด€๊ณ„๊ฐ€ ํ•„์š”ํ•จ
  2. ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ: Passkey ์ž๊ฒฉ ์ฆ๋ช…์„ ์‚ฌ์šฉ์ž UUID๋กœ ์กฐํšŒํ•ด์•ผ ํ•จ

    • ํ˜„์žฌ ๊ตฌ์กฐ๋Š” user_id ๊ธฐ๋ฐ˜ ์กฐํšŒ๋งŒ ์ง€์›
  3. ์ธ์ฆ ํ๋ฆ„ ํ†ตํ•ฉ: ๊ธฐ์กด ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ์™€ Passkey๋ฅผ ๋™์‹œ์— ์ง€์›ํ•ด์•ผ ํ•จ

    • ๋กœ๊ทธ์ธ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋‘ ๋ฐฉ์‹์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ

Passkey ํ†ตํ•ฉ ๋ฐฉ์•ˆ

1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ ๋ณ€๊ฒฝ

webauthn_credentials ํ…Œ์ด๋ธ” ์ˆ˜์ •

ํ˜„์žฌ ๊ตฌ์กฐ (๋ฌธ์ œ์ ):

$table->foreignId('user_id')->constrained('auth_schema.auth_users')->onDelete('cascade');

์ˆ˜์ •๋œ ๊ตฌ์กฐ (์ƒค๋”ฉ ์ง€์›):

Schema::create('webauthn_credentials', function (Blueprint $table) {
    $table->id();
    
    // UUID ๊ธฐ๋ฐ˜ ๊ด€๊ณ„ (์ƒค๋”ฉ ์ง€์›)
    $table->uuid('user_uuid')->index()->comment('์‚ฌ์šฉ์ž UUID (์ƒค๋”ฉ ์ง€์›)');
    
    // ๊ธฐ์กด user_id ์œ ์ง€ (ํ˜ธํ™˜์„ฑ, nullable)
    $table->unsignedBigInteger('user_id')->nullable()->comment('์‚ฌ์šฉ์ž ID (ํ˜ธํ™˜์„ฑ์šฉ)');
    
    // Passkey ์ž๊ฒฉ ์ฆ๋ช… ํ•„๋“œ
    $table->string('credential_id')->unique()->comment('WebAuthn Credential ID');
    $table->text('public_key')->comment('Public Key (Base64 ์ธ์ฝ”๋”ฉ)');
    $table->bigInteger('sign_count')->default(0)->comment('์„œ๋ช… ํšŸ์ˆ˜ (Replay ๊ณต๊ฒฉ ๋ฐฉ์ง€)');
    $table->string('device_name')->nullable()->comment('๋””๋ฐ”์ด์Šค ์ด๋ฆ„');
    $table->string('device_type')->nullable()->comment('๋””๋ฐ”์ด์Šค ํƒ€์ž… (mobile, desktop, security_key)');
    $table->timestamp('last_used_at')->nullable()->comment('๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ ์ผ์‹œ');
    $table->timestamps();
    
    // ์ธ๋ฑ์Šค
    $table->index('user_uuid'); // UUID ๊ธฐ๋ฐ˜ ์กฐํšŒ ์ตœ์ ํ™”
    $table->index('credential_id');
    $table->index('last_used_at');
    
    // ๋ณตํ•ฉ ์ธ๋ฑ์Šค (์‚ฌ์šฉ์ž๋ณ„ ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ)
    $table->index(['user_uuid', 'credential_id']);
});

๋ณ€๊ฒฝ ์‚ฌํ•ญ:

  • user_uuid ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (ํ•„์ˆ˜, ์ธ๋ฑ์Šค)
  • user_id ์ปฌ๋Ÿผ์„ nullable๋กœ ๋ณ€๊ฒฝ (ํ˜ธํ™˜์„ฑ ์œ ์ง€)
  • ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ (์ƒค๋”ฉ ํ™˜๊ฒฝ์—์„œ๋Š” ์™ธ๋ž˜ํ‚ค ์‚ฌ์šฉ ๋ถˆ๊ฐ€)
  • user_uuid ๊ธฐ๋ฐ˜ ์ธ๋ฑ์Šค ์ถ”๊ฐ€

2. Passkey ์ธ๋ฑ์Šค ํ…Œ์ด๋ธ” ์ƒ์„ฑ (์„ ํƒ์‚ฌํ•ญ)

๋ชฉ์ : ์‚ฌ์šฉ์ž UUID๋กœ ๋น ๋ฅด๊ฒŒ Passkey ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ

Schema::create('user_passkey_index', function (Blueprint $table) {
    $table->id();
    $table->uuid('user_uuid')->index()->comment('์‚ฌ์šฉ์ž UUID');
    $table->string('credential_id')->unique()->comment('Credential ID');
    $table->integer('shard_id')->nullable()->comment('์ƒค๋“œ ๋ฒˆํ˜ธ (์บ์‹œ)');
    $table->boolean('is_active')->default(true)->comment('ํ™œ์„ฑํ™” ์—ฌ๋ถ€');
    $table->timestamps();
    
    // ์ธ๋ฑ์Šค
    $table->index('user_uuid');
    $table->index(['user_uuid', 'is_active']); // ํ™œ์„ฑ ์ž๊ฒฉ ์ฆ๋ช…๋งŒ ์กฐํšŒ
});

์žฅ์ :

  • ๋น ๋ฅธ ์กฐํšŒ ์„ฑ๋Šฅ
  • ์ƒค๋“œ ์ •๋ณด ์บ์‹ฑ

๋‹จ์ :

  • ๋ฐ์ดํ„ฐ ์ค‘๋ณต
  • ๋™๊ธฐํ™” ํ•„์š”

๊ฒฐ๋ก : ์ธ๋ฑ์Šค ํ…Œ์ด๋ธ”์€ ์„ ํƒ์‚ฌํ•ญ. ์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ๋Š” webauthn_credentials ํ…Œ์ด๋ธ”๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ์„ฑ๋Šฅ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ถ”๊ฐ€ ๊ณ ๋ ค.

3. ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์„ค๊ณ„

PasskeyService ์ƒ์„ฑ

์—ญํ• : Passkey ์ž๊ฒฉ ์ฆ๋ช… ๊ด€๋ฆฌ ๋ฐ ์ธ์ฆ ์ฒ˜๋ฆฌ

namespace Jiny\Auth\Services;

use Jiny\Auth\Facades\Shard;
use Illuminate\Support\Facades\DB;

class PasskeyService
{
    /**
     * ์‚ฌ์šฉ์ž UUID๋กœ Passkey ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ
     * 
     * ์ƒค๋”ฉ ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•˜์—ฌ UUID ๊ธฐ๋ฐ˜์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
     * 
     * @param string $userUuid ์‚ฌ์šฉ์ž UUID
     * @param bool $activeOnly ํ™œ์„ฑ ์ž๊ฒฉ ์ฆ๋ช…๋งŒ ์กฐํšŒํ• ์ง€ ์—ฌ๋ถ€
     * @return \Illuminate\Support\Collection
     */
    public function getCredentialsByUserUuid(string $userUuid, bool $activeOnly = true)
    {
        $query = DB::table('webauthn_credentials')
            ->where('user_uuid', $userUuid);
        
        // ํ™œ์„ฑ ์ž๊ฒฉ ์ฆ๋ช…๋งŒ ์กฐํšŒ (ํ•„์š”์‹œ)
        // if ($activeOnly) {
        //     $query->where('is_active', true);
        // }
        
        return $query->get();
    }
    
    /**
     * Credential ID๋กœ ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ
     * 
     * @param string $credentialId Credential ID
     * @return object|null
     */
    public function getCredentialById(string $credentialId)
    {
        return DB::table('webauthn_credentials')
            ->where('credential_id', $credentialId)
            ->first();
    }
    
    /**
     * Passkey ์ž๊ฒฉ ์ฆ๋ช… ๋“ฑ๋ก
     * 
     * @param string $userUuid ์‚ฌ์šฉ์ž UUID
     * @param array $credentialData ์ž๊ฒฉ ์ฆ๋ช… ๋ฐ์ดํ„ฐ
     * @return string Credential ID
     */
    public function registerCredential(string $userUuid, array $credentialData)
    {
        // ์‚ฌ์šฉ์ž ์กด์žฌ ํ™•์ธ (์ƒค๋”ฉ ํ™˜๊ฒฝ)
        $user = Shard::getUserByUuid($userUuid);
        if (!$user) {
            throw new \Exception('์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
        }
        
        // ์ž๊ฒฉ ์ฆ๋ช… ๋ฐ์ดํ„ฐ ์ค€๋น„
        $data = [
            'user_uuid' => $userUuid,
            'user_id' => $user->id ?? null, // ํ˜ธํ™˜์„ฑ (nullable)
            'credential_id' => $credentialData['credential_id'],
            'public_key' => $credentialData['public_key'],
            'sign_count' => $credentialData['sign_count'] ?? 0,
            'device_name' => $credentialData['device_name'] ?? null,
            'device_type' => $credentialData['device_type'] ?? null,
            'last_used_at' => null,
            'created_at' => now(),
            'updated_at' => now(),
        ];
        
        // ์ž๊ฒฉ ์ฆ๋ช… ์ €์žฅ
        DB::table('webauthn_credentials')->insert($data);
        
        return $credentialData['credential_id'];
    }
    
    /**
     * Passkey ์ž๊ฒฉ ์ฆ๋ช… ์—…๋ฐ์ดํŠธ (sign_count, last_used_at)
     * 
     * @param string $credentialId Credential ID
     * @param int $signCount ์„œ๋ช… ํšŸ์ˆ˜
     * @return bool
     */
    public function updateCredentialUsage(string $credentialId, int $signCount)
    {
        return DB::table('webauthn_credentials')
            ->where('credential_id', $credentialId)
            ->update([
                'sign_count' => $signCount,
                'last_used_at' => now(),
                'updated_at' => now(),
            ]) > 0;
    }
    
    /**
     * Passkey ์ž๊ฒฉ ์ฆ๋ช… ์‚ญ์ œ
     * 
     * @param string $userUuid ์‚ฌ์šฉ์ž UUID
     * @param string $credentialId Credential ID
     * @return bool
     */
    public function deleteCredential(string $userUuid, string $credentialId)
    {
        return DB::table('webauthn_credentials')
            ->where('user_uuid', $userUuid)
            ->where('credential_id', $credentialId)
            ->delete() > 0;
    }
    
    /**
     * ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  Passkey ์ž๊ฒฉ ์ฆ๋ช… ์‚ญ์ œ
     * 
     * @param string $userUuid ์‚ฌ์šฉ์ž UUID
     * @return int ์‚ญ์ œ๋œ ์ž๊ฒฉ ์ฆ๋ช… ์ˆ˜
     */
    public function deleteAllUserCredentials(string $userUuid)
    {
        return DB::table('webauthn_credentials')
            ->where('user_uuid', $userUuid)
            ->delete();
    }
}

4. ์ธ์ฆ ํ๋ฆ„ ์„ค๊ณ„

๋กœ๊ทธ์ธ ํ๋ฆ„ ํ†ตํ•ฉ

๊ธฐ์กด ํ๋ฆ„ (Login/SubmitController):

1. ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ
2. Shard::getUserByEmail() โ†’ ์‚ฌ์šฉ์ž ์กฐํšŒ
3. Hash::check() โ†’ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
4. JWT ํ† ํฐ ์ƒ์„ฑ ๋˜๋Š” ์„ธ์…˜ ์ƒ์„ฑ

ํ†ตํ•ฉ๋œ ํ๋ฆ„ (Passkey ์ถ”๊ฐ€):

1. ์ธ์ฆ ๋ฐฉ์‹ ์„ ํƒ (์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋˜๋Š” Passkey)
   
   [์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒฝ๋กœ]
   2-1. ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ
   2-2. Shard::getUserByEmail() โ†’ ์‚ฌ์šฉ์ž ์กฐํšŒ
   2-3. Hash::check() โ†’ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
   2-4. JWT ํ† ํฐ ์ƒ์„ฑ
   
   [Passkey ๊ฒฝ๋กœ]
   2-1. ์ด๋ฉ”์ผ ์ž…๋ ฅ (๋˜๋Š” ์‚ฌ์šฉ์ž๋ช…)
   2-2. Shard::getUserByEmail() โ†’ ์‚ฌ์šฉ์ž ์กฐํšŒ
   2-3. PasskeyService::getCredentialsByUserUuid() โ†’ ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ
   2-4. WebAuthn ์ธ์ฆ ์š”์ฒญ ์ƒ์„ฑ (challenge)
   2-5. ํด๋ผ์ด์–ธํŠธ์—์„œ Passkey ์ธ์ฆ ์ˆ˜ํ–‰
   2-6. ์„œ๋ฒ„์—์„œ ์ธ์ฆ ์‘๋‹ต ๊ฒ€์ฆ
   2-7. PasskeyService::updateCredentialUsage() โ†’ ์‚ฌ์šฉ ๊ธฐ๋ก ์—…๋ฐ์ดํŠธ
   2-8. JWT ํ† ํฐ ์ƒ์„ฑ

์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ • ๋ฐฉ์•ˆ

๋ฐฉ์•ˆ 1: ๊ธฐ์กด ์ปจํŠธ๋กค๋Ÿฌ ํ™•์žฅ (๊ถŒ์žฅ)

  • Login/SubmitController์— Passkey ์ธ์ฆ ๋กœ์ง ์ถ”๊ฐ€
  • auth_method ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ธ์ฆ ๋ฐฉ์‹ ๊ตฌ๋ถ„

๋ฐฉ์•ˆ 2: ๋ณ„๋„ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ

  • Login/PasskeyController ์ƒ์„ฑ
  • ๊ธฐ์กด ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌ

๊ถŒ์žฅ ๋ฐฉ์•ˆ: ๋ฐฉ์•ˆ 1 (๊ธฐ์กด ์ปจํŠธ๋กค๋Ÿฌ ํ™•์žฅ)

  • ์ฝ”๋“œ ์ค‘๋ณต ์ตœ์†Œํ™”
  • ์ธ์ฆ ๋กœ์ง ํ†ตํ•ฉ ๊ด€๋ฆฌ ์šฉ์ด

5. ๋ชจ๋ธ ์„ค๊ณ„

WebAuthnCredential ๋ชจ๋ธ ์ƒ์„ฑ

namespace Jiny\Auth\Models;

use Illuminate\Database\Eloquent\Model;

class WebAuthnCredential extends Model
{
    protected $table = 'webauthn_credentials';
    
    protected $fillable = [
        'user_uuid',
        'user_id',
        'credential_id',
        'public_key',
        'sign_count',
        'device_name',
        'device_type',
        'last_used_at',
    ];
    
    protected $casts = [
        'sign_count' => 'integer',
        'last_used_at' => 'datetime',
    ];
    
    /**
     * ์‚ฌ์šฉ์ž UUID๋กœ ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ (์Šค์ฝ”ํ”„)
     */
    public function scopeForUser($query, string $userUuid)
    {
        return $query->where('user_uuid', $userUuid);
    }
    
    /**
     * ํ™œ์„ฑ ์ž๊ฒฉ ์ฆ๋ช…๋งŒ ์กฐํšŒ (์Šค์ฝ”ํ”„)
     */
    public function scopeActive($query)
    {
        // ํ•„์š”์‹œ is_active ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ํ›„ ์‚ฌ์šฉ
        return $query;
    }
}

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต

1. ๋‹จ๊ณ„๋ณ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

Phase 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ ๋ณ€๊ฒฝ

  1. webauthn_credentials ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

    • user_uuid ์ปฌ๋Ÿผ ์ถ”๊ฐ€
    • user_id ์ปฌ๋Ÿผ์„ nullable๋กœ ๋ณ€๊ฒฝ
    • ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ
    • ์ธ๋ฑ์Šค ์ถ”๊ฐ€
  2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์žˆ๋Š” ๊ฒฝ์šฐ)

    • user_id โ†’ user_uuid ๋งคํ•‘
    • ์ƒค๋”ฉ๋œ ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์—์„œ UUID ์กฐํšŒ

Phase 2: ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๊ตฌํ˜„

  1. PasskeyService ์ƒ์„ฑ
  2. WebAuthn ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ (์˜ˆ: web-auth/webauthn-lib)

Phase 3: ์ปจํŠธ๋กค๋Ÿฌ ํ†ตํ•ฉ

  1. Login/SubmitController ์ˆ˜์ •
  2. Passkey ๋“ฑ๋ก ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ
  3. Passkey ๊ด€๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ (๋ชฉ๋ก, ์‚ญ์ œ)

Phase 4: ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ

  1. Passkey ๋“ฑ๋ก UI
  2. Passkey ๋กœ๊ทธ์ธ UI
  3. Passkey ๊ด€๋ฆฌ UI

2. ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€

๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ์˜ ํ˜ธํ™˜์„ฑ:

  • user_id ์ปฌ๋Ÿผ ์œ ์ง€ (nullable)
  • ๊ธฐ์กด ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฆ ์œ ์ง€
  • Passkey๋Š” ์„ ํƒ์  ๊ธฐ๋Šฅ์œผ๋กœ ์ œ๊ณต

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ:

// ๊ธฐ์กด user_id ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฅผ user_uuid๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
public function migrateUserIdToUuid()
{
    // ์ƒค๋”ฉ๋œ ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์—์„œ UUID ์กฐํšŒ
    $shardConfig = $this->loadShardConfig();
    if ($shardConfig['enable'] ?? false) {
        $shardCount = $shardConfig['shard_count'] ?? 2;
        $tablePrefix = $shardConfig['table_prefix'] ?? 'users_';
        
        for ($i = 1; $i <= $shardCount; $i++) {
            $shardNumber = str_pad($i, 3, '0', STR_PAD_LEFT);
            $shardTableName = "{$tablePrefix}{$shardNumber}";
            
            if (Schema::hasTable($shardTableName)) {
                $users = DB::table($shardTableName)
                    ->select('id', 'uuid')
                    ->whereNotNull('uuid')
                    ->get();
                
                foreach ($users as $user) {
                    DB::table('webauthn_credentials')
                        ->where('user_id', $user->id)
                        ->whereNull('user_uuid')
                        ->update(['user_uuid' => $user->uuid]);
                }
            }
        }
    }
}

๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„

1๋‹จ๊ณ„: ํ•ต์‹ฌ ๊ธฐ๋Šฅ (ํ•„์ˆ˜)

  • webauthn_credentials ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (user_uuid ์ถ”๊ฐ€)
  • PasskeyService ๊ธฐ๋ณธ ๊ตฌํ˜„
  • Passkey ๋“ฑ๋ก API
  • Passkey ๋กœ๊ทธ์ธ API

2๋‹จ๊ณ„: ํ†ตํ•ฉ ๊ธฐ๋Šฅ (์ค‘์š”)

  • Login/SubmitController Passkey ์ง€์› ์ถ”๊ฐ€
  • Passkey ์ž๊ฒฉ ์ฆ๋ช… ๊ด€๋ฆฌ API (๋ชฉ๋ก, ์‚ญ์ œ)
  • ์‚ฌ์šฉ์ž ํ”„๋กœํ•„์— Passkey ์„ค์ • UI

3๋‹จ๊ณ„: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (์„ ํƒ)

  • Passkey ์ธ๋ฑ์Šค ํ…Œ์ด๋ธ” (user_passkey_index)
  • Passkey ์‚ฌ์šฉ ํ†ต๊ณ„
  • ๋‹ค์ค‘ Passkey ๊ด€๋ฆฌ UI
  • Passkey ๋ฐฑ์—…/๋ณต๊ตฌ ๊ธฐ๋Šฅ

์ฃผ์˜์‚ฌํ•ญ

1. ์ƒค๋”ฉ ํ™˜๊ฒฝ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ๋ถˆ๊ฐ€: ์ƒค๋”ฉ ํ™˜๊ฒฝ์—์„œ๋Š” ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
  • UUID ๊ธฐ๋ฐ˜ ๊ด€๊ณ„: ๋ชจ๋“  ๊ด€๊ณ„๋Š” UUID ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๊ณ„ํ•ด์•ผ ํ•จ
  • ์ธ๋ฑ์Šค ์ตœ์ ํ™”: user_uuid ๊ธฐ๋ฐ˜ ์ธ๋ฑ์Šค ํ•„์ˆ˜

2. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

  • Credential ID ์ค‘๋ณต ๋ฐฉ์ง€: credential_id๋Š” ์ „์—ญ์ ์œผ๋กœ ๊ณ ์œ ํ•ด์•ผ ํ•จ
  • Sign Count ๊ฒ€์ฆ: Replay ๊ณต๊ฒฉ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด sign_count ๊ฒ€์ฆ ํ•„์ˆ˜
  • Public Key ๊ฒ€์ฆ: WebAuthn ํ‘œ์ค€์— ๋”ฐ๋ฅธ ๊ฒ€์ฆ ๋กœ์ง ๊ตฌํ˜„

3. ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์ธ๋ฑ์Šค ์ตœ์ ํ™”: user_uuid, credential_id ์ธ๋ฑ์Šค ํ•„์ˆ˜
  • ์บ์‹ฑ: ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ์ž๊ฒฉ ์ฆ๋ช…์€ ์บ์‹ฑ ๊ณ ๋ ค
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ: ๋Œ€๋Ÿ‰์˜ ์ž๊ฒฉ ์ฆ๋ช… ์กฐํšŒ ์‹œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ณ ๋ ค

์ฐธ๊ณ  ์ž๋ฃŒ