feat(webauthn): install web-auth/webauthn-lib + credential storage
Add web-auth/webauthn-lib ^5.3, the webauthn_credentials table (per-user unique credential_id), the WebauthnCredential model and User hasMany relation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
eef79d1758
commit
78b1017844
|
|
@ -6,6 +6,7 @@ use Database\Factories\UserFactory;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
@ -95,4 +96,14 @@ class User extends Authenticatable
|
||||||
{
|
{
|
||||||
return ! $this->must_change_password && $this->hasTwoFactorEnabled();
|
return ! $this->must_change_password && $this->hasTwoFactorEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function webauthnCredentials(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(WebauthnCredential::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasWebauthnCredentials(): bool
|
||||||
|
{
|
||||||
|
return $this->webauthnCredentials()->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class WebauthnCredential extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'transports' => 'array',
|
||||||
|
'sign_count' => 'integer',
|
||||||
|
'last_used_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"livewire/livewire": "^3.6",
|
"livewire/livewire": "^3.6",
|
||||||
"phpseclib/phpseclib": "^3.0",
|
"phpseclib/phpseclib": "^3.0",
|
||||||
"pragmarx/google2fa-qrcode": "^4.0",
|
"pragmarx/google2fa-qrcode": "^4.0",
|
||||||
|
"web-auth/webauthn-lib": "^5.3",
|
||||||
"wire-elements/modal": "^2.0"
|
"wire-elements/modal": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('webauthn_credentials', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('credential_id', 512); // base64url of the raw credential id (indexed)
|
||||||
|
$table->text('public_key'); // serialized PublicKeyCredentialSource (JSON)
|
||||||
|
$table->string('aaguid')->nullable();
|
||||||
|
$table->json('transports')->nullable();
|
||||||
|
$table->unsignedBigInteger('sign_count')->default(0);
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'credential_id'], 'webauthn_user_credential_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('webauthn_credentials');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WebauthnCredential;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class WebauthnCredentialTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_user_has_webauthn_credentials(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->assertFalse($user->hasWebauthnCredentials());
|
||||||
|
|
||||||
|
WebauthnCredential::create([
|
||||||
|
'user_id' => $user->id, 'name' => 'YubiKey 5C', 'credential_id' => 'abc',
|
||||||
|
'public_key' => '{}', 'sign_count' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($user->fresh()->hasWebauthnCredentials());
|
||||||
|
$this->assertCount(1, $user->fresh()->webauthnCredentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue