First release
This commit is contained in:
187
tests/Assertion/CreatorTest.php
Normal file
187
tests/Assertion/CreatorTest.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Assertion;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Laragear\WebAuthn\Assertion\Creator\AssertionCreation;
|
||||
use Laragear\WebAuthn\Assertion\Creator\AssertionCreator;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function config;
|
||||
use function in_array;
|
||||
use function now;
|
||||
use function session;
|
||||
|
||||
class CreatorTest extends TestCase
|
||||
{
|
||||
protected Request $request;
|
||||
protected WebAuthnAuthenticatableUser $user;
|
||||
protected AssertionCreation $creation;
|
||||
protected AssertionCreator $creator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->request = Request::create('https://test.app/webauthn/create', 'POST');
|
||||
$this->user = WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => 'test',
|
||||
'email' => 'test@email.com',
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
$this->creator = new AssertionCreator($this->app);
|
||||
$this->creation = new AssertionCreation($this->request);
|
||||
|
||||
$this->startSession();
|
||||
$this->request->setLaravelSession($this->app->make('session.store'));
|
||||
}
|
||||
|
||||
protected function response(): TestResponse
|
||||
{
|
||||
return $this->createTestResponse(
|
||||
$this->creator->send($this->creation)->thenReturn()->json->toResponse($this->request)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_uses_config_timeout(): void
|
||||
{
|
||||
config(['webauthn.challenge.timeout' => 120]);
|
||||
|
||||
$this->freezeSecond();
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
return now()->addMinutes(2)->getTimestamp() === $challenge->timeout;
|
||||
})
|
||||
->assertJson([
|
||||
'timeout' => 120000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_defaults_without_credentials(): void
|
||||
{
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Uuid::NIL,
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost:8000',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'public_key' => 'test_key',
|
||||
'attestation_format' => 'none',
|
||||
]);
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', function (Challenge $challenge): bool {
|
||||
static::assertSame(now()->addMinute()->getTimestamp(), $challenge->timeout);
|
||||
static::assertFalse($challenge->verify);
|
||||
|
||||
return true;
|
||||
})
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_doesnt_add_credentials_if_user_has_no_credentials(): void
|
||||
{
|
||||
$this->creation->user = $this->user;
|
||||
|
||||
$this->response()
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_adds_accepted_credentials_if_there_is_credentials(): void
|
||||
{
|
||||
$this->creation->user = $this->user;
|
||||
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Uuid::NIL,
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost:8000',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'public_key' => 'test_key',
|
||||
'attestation_format' => 'none',
|
||||
])->save();
|
||||
|
||||
$this->response()
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
'allowCredentials' => [
|
||||
['id' => 'test_id', 'type' => 'public-key']
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_doesnt_add_credentials_blacklisted(): void
|
||||
{
|
||||
$this->creation->user = $this->user;
|
||||
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Uuid::NIL,
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost:8000',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'public_key' => 'test_key',
|
||||
'attestation_format' => 'none',
|
||||
'disabled_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->response()
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_forces_user_verification(): void
|
||||
{
|
||||
$this->creation->userVerification = WebAuthn::USER_VERIFICATION_REQUIRED;
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', function (Challenge $challenge): bool {
|
||||
return $challenge->verify;
|
||||
})
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
'userVerification' => WebAuthn::USER_VERIFICATION_REQUIRED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_challenge_includes_accepted_credentials(): void
|
||||
{
|
||||
$this->creation->user = $this->user;
|
||||
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Uuid::NIL,
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost:8000',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'public_key' => 'test_key',
|
||||
'attestation_format' => 'none',
|
||||
])->save();
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', function (Challenge $challenge): bool {
|
||||
return in_array('test_id', $challenge->properties['credentials'], true);
|
||||
});
|
||||
}
|
||||
}
|
||||
623
tests/Assertion/ValidationTest.php
Normal file
623
tests/Assertion/ValidationTest.php
Normal file
@@ -0,0 +1,623 @@
|
||||
<?php /** @noinspection JsonEncodingApiUsageInspection */
|
||||
|
||||
namespace Tests\Assertion;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||
use Laragear\WebAuthn\Assertion\Validator\Pipes\CheckPublicKeyCounterCorrect;
|
||||
use Laragear\WebAuthn\Assertion\Validator\Pipes\CheckUserInteraction;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Events\CredentialCloned;
|
||||
use Laragear\WebAuthn\Events\CredentialDisabled;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Mockery;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use Throwable;
|
||||
use function base64_decode;
|
||||
use function base64_encode;
|
||||
use function json_encode;
|
||||
use function now;
|
||||
use function session;
|
||||
|
||||
class ValidationTest extends TestCase
|
||||
{
|
||||
protected Request $request;
|
||||
protected WebAuthnAuthenticatableUser $user;
|
||||
protected AssertionValidation $validation;
|
||||
protected AssertionValidator $validator;
|
||||
protected Challenge $challenge;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->request = Request::create(
|
||||
'https://test.app/webauthn/create', 'POST', content: json_encode(FakeAuthenticator::assertionResponse())
|
||||
);
|
||||
|
||||
$this->user = WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
$this->validator = new AssertionValidator($this->app);
|
||||
$this->validation = new AssertionValidation($this->request);
|
||||
|
||||
$this->freezeSecond();
|
||||
|
||||
$this->challenge = new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
);
|
||||
|
||||
$this->session(['_webauthn' => $this->challenge]);
|
||||
|
||||
$this->request->setLaravelSession($this->app->make('session.store'));
|
||||
|
||||
$this->credential = DB::table('webauthn_credentials')->insert([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'eyJpdiI6Imp0U0NVeFNNbW45KzEvMXpad2p2SUE9PSIsInZhbHVlIjoic0VxZ2I1WnlHM2lJakhkWHVkK2kzMWtibk1IN2ZlaExGT01qOElXMDdRTjhnVlR0TDgwOHk1S0xQUy9BQ1JCWHRLNzRtenNsMml1dVQydWtERjFEU0h0bkJGT2RwUXE1M1JCcVpablE2Y2VGV2YvVEE2RGFIRUE5L0x1K0JIQXhLVE1aNVNmN3AxeHdjRUo2V0hwREZSRTJYaThNNnB1VnozMlVXZEVPajhBL3d3ODlkTVN3bW54RTEwSG0ybzRQZFFNNEFrVytUYThub2IvMFRtUlBZamoyZElWKzR1bStZQ1IwU3FXbkYvSm1FU2FlMTFXYUo0SG9kc1BDME9CNUNKeE9IelE5d2dmNFNJRXBKNUdlVzJ3VHUrQWJZRFluK0hib0xvVTdWQ0ZISjZmOWF3by83aVJES1dxbU9Zd1lhRTlLVmhZSUdlWmlBOUFtcTM2ZVBaRWNKNEFSQUhENk5EaC9hN3REdnVFbm16WkRxekRWOXd4cVcvZFdKa2tlWWJqZWlmZnZLS0F1VEVCZEZQcXJkTExiNWRyQmxsZWtaSDRlT3VVS0ZBSXFBRG1JMjRUMnBKRXZxOUFUa2xxMjg2TEplUzdscVo2UytoVU5SdXk1OE1lcFN6aU05ZkVXTkdIM2tKM3Q5bmx1TGtYb1F5bGxxQVR3K3BVUVlia1VybDFKRm9lZDViNzYraGJRdmtUb2FNTEVGZmZYZ3lYRDRiOUVjRnJpcTVvWVExOHJHSTJpMnVBZ3E0TmljbUlKUUtXY2lSWDh1dE5MVDNRUzVRSkQrTjVJUU8rSGhpeFhRRjJvSEdQYjBoVT0iLCJtYWMiOiI5MTdmNWRkZGE5OTEwNzQ3MjhkYWVhYjRlNjk0MWZlMmI5OTQ4YzlmZWI1M2I4OGVkMjE1MjMxNjUwOWRmZTU2IiwidGFnIjoiIn0=',
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function validate(): AssertionValidation
|
||||
{
|
||||
return $this->validator->send($this->validation)->thenReturn();
|
||||
}
|
||||
|
||||
public function test_assertion_allows_user_instance(): void
|
||||
{
|
||||
$this->validation->user = WebAuthnAuthenticatableUser::query()->first();
|
||||
|
||||
static::assertInstanceOf(AssertionValidation::class, $this->validator->send($this->validation)->thenReturn());
|
||||
}
|
||||
|
||||
public function test_assertion_allows_user_instance_without_user_handle(): void
|
||||
{
|
||||
$this->validation->user = WebAuthnAuthenticatableUser::query()->first();
|
||||
|
||||
$response = FakeAuthenticator::assertionResponse();
|
||||
|
||||
unset($response['response']['userHandle']);
|
||||
|
||||
$this->request->setJson(new ParameterBag($response));
|
||||
|
||||
static::assertInstanceOf(AssertionValidation::class, $this->validator->send($this->validation)->thenReturn());
|
||||
}
|
||||
|
||||
public function test_assertion_increases_counter(): void
|
||||
{
|
||||
static::assertInstanceOf(AssertionValidation::class, $this->validator->send($this->validation)->thenReturn());
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'counter' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_assertion_credential_without_zero_counter_is_valid_and_not_incremented(): void
|
||||
{
|
||||
$this->app->resolving(CheckPublicKeyCounterCorrect::class, function (): void {
|
||||
$this->validation->authenticatorData->counter = 0;
|
||||
});
|
||||
|
||||
$this->validate();
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'counter' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_challenge_fails_if_not_found(): void
|
||||
{
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Challenge does not exist.');
|
||||
|
||||
$this->session(['_webauthn' => null]);
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_fails_if_challenge_exists_but_is_expired(): void
|
||||
{
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Challenge does not exist.');
|
||||
|
||||
$this->travelTo(now()->addMinute()->addSecond());
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_challenge_is_pulled_from_session(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
static::assertNull(session('_webauthn'));
|
||||
}
|
||||
|
||||
public function test_credential_id_check_fail_if_not_in_request_array(): void
|
||||
{
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Credential is not on accepted list.');
|
||||
|
||||
$this->challenge->properties['credentials'] = ['4bde1e58dba94de4ab307f46611165cb'];
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_credential_id_check_fails_if_doesnt_exist(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->delete();
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Credential ID does not exist.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_credential_id_check_fails_if_disabled(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'disabled_at' => now(),
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Credential ID is blacklisted.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_credential_check_if_not_for_user_id(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'user_id' => '4bde1e58dba94de4ab307f46611165cb',
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: User ID is not owner of the stored credential.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_credential_check_fails_if_not_for_user_instance(): void
|
||||
{
|
||||
$this->user->setAttribute('id', 2)->save();
|
||||
|
||||
$this->validation->user = $this->user;
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: User is not owner of the stored credential.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_type_check_fails_if_not_public_key(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['type'] = 'invalid';
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response type is not [public-key].');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_authenticator_data_fails_if_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['authenticatorData'] = '';
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Authenticator Data does not exist or is empty.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_authenticator_data_fails_if_invalid(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['authenticatorData'] = 'invalid';
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Authenticator Data: Invalid input.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_invalid(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = 'foo';
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data JSON is invalid or malformed.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode([]));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data JSON is empty.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_type_missing(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['origin' => '', 'challenge' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data JSON does not contain the [type] key.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_origin_missing(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['type' => '', 'challenge' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data JSON does not contain the [origin] key.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_challenge_missing(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['type' => '', 'origin' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data JSON does not contain the [challenge] key.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_action_checks_fails_if_not_webauthn_create(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'invalid', 'origin' => '', 'challenge' => ''])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Client Data type is not [webauthn.get].');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_challenge_fails_if_challenge_is_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.get', 'origin' => 'https://localhost', 'challenge' => ''])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response has an empty challenge.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_challenge_fails_if_challenge_is_not_equal(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.get', 'origin' => 'https://localhost', 'challenge' => 'invalid'])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response challenge is not equal.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.get', 'origin' => '', 'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response has an empty origin.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_invalid_host(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.get', 'origin' => 'invalid', 'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response origin is invalid.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_unsecure(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
/** @noinspection HttpUrlsUsage */
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.get', 'origin' => 'http://unsecure.com', 'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response not made to a secure server (localhost or HTTPS).');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.get',
|
||||
'origin' => '',
|
||||
'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response has an empty origin.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_not_equal(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.get',
|
||||
'origin' => 'https://otherhost.com',
|
||||
'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Relaying Party ID not scoped to current.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_not_contained(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.get',
|
||||
'origin' => 'https://invalidlocalhost',
|
||||
'challenge' => FakeAuthenticator::ASSERTION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Relaying Party ID not scoped to current.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_hash_not_same(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'rp_id' => 'https://otherorigin.com',
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response has different Relying Party ID hash.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_user_interaction_fails_if_user_not_present(): void
|
||||
{
|
||||
$this->app->resolving(CheckUserInteraction::class, function (): void {
|
||||
$this->validation->authenticatorData = Mockery::mock(AuthenticatorData::class);
|
||||
|
||||
$this->validation->authenticatorData->expects('wasUserAbsent')->andReturnTrue();
|
||||
});
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response did not have the user present.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_user_interaction_fails_if_user_verification_was_required(): void
|
||||
{
|
||||
$this->challenge->verify = true;
|
||||
|
||||
$this->app->resolving(CheckUserInteraction::class, function (): void {
|
||||
$this->validation->authenticatorData = Mockery::mock(AuthenticatorData::class);
|
||||
|
||||
$this->validation->authenticatorData->expects('wasUserAbsent')->andReturnFalse();
|
||||
$this->validation->authenticatorData->expects('wasUserNotVerified')->andReturnTrue();
|
||||
});
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Response did not verify the user.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_signature_fails_if_credential_public_key_invalid(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'public_key' => Crypt::encryptString('invalid')
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Stored Public Key is invalid.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_signature_fails_if_response_signature_empty(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::assertionResponse();
|
||||
|
||||
$invalid['response']['signature'] = base64_encode('');
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Signature is empty.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_signature_fails_if_invalid(): void
|
||||
{
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'public_key' => Crypt::encryptString("-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnBadZo+CnNdUHvzCWuLN
|
||||
TFsXTCjsHH5A+aUtIImsJsbTKmYsYtOuiOwEgcGglKEJV0MwzV4v2SDQzSirwLEr
|
||||
isis4qV6Q3a0ZyZcYhgyMzvkk5CtDhpzxhsmFwiMSGt9gVRE8cOxGDQX2jTPfqyk
|
||||
xZTkoXKEHevq8kl5PBCPsaWskrWsySw9mmqNCmIjhE2Evgarm0Xq7yq5h62H2ZzF
|
||||
T3U5C0H32I9cTPk6f/SVke+GMseVRiLleltJMNl0CAcKGBmJpQfeLFlKmOc15Wql
|
||||
wuMegjGULD9dPQvZS5uX+P0bHYfXq5V/HTwrR9FmkEdhq5YB9nE6RkE6Fbs5f+LI
|
||||
hQIDAQAB
|
||||
-----END PUBLIC KEY-----")
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Signature is invalid.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_counter_fails_if_authenticator_counts_same_as_stored_counter(): void
|
||||
{
|
||||
$event = Event::fake([CredentialCloned::class, CredentialDisabled::class]);
|
||||
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'counter' => 1
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Credential counter not over stored counter.');
|
||||
|
||||
try {
|
||||
$this->validate();
|
||||
} catch (Throwable $e) {
|
||||
$event->assertDispatched(CredentialCloned::class);
|
||||
$event->assertDispatched(CredentialDisabled::class);
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'disabled_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function test_counter_fails_if_authenticator_counts_below_as_stored_counter(): void
|
||||
{
|
||||
$event = Event::fake([CredentialCloned::class, CredentialDisabled::class]);
|
||||
|
||||
DB::table('webauthn_credentials')->where('id', FakeAuthenticator::CREDENTIAL_ID)->update([
|
||||
'counter' => 2
|
||||
]);
|
||||
|
||||
$this->expectException(AssertionException::class);
|
||||
$this->expectExceptionMessage('Assertion Error: Credential counter not over stored counter.');
|
||||
|
||||
try {
|
||||
$this->validate();
|
||||
} catch (Throwable $e) {
|
||||
$event->assertDispatched(CredentialCloned::class);
|
||||
$event->assertDispatched(CredentialDisabled::class);
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'disabled_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
tests/Attestation/CreatorTest.php
Normal file
191
tests/Attestation/CreatorTest.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Attestation;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreator;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function config;
|
||||
use function now;
|
||||
use function session;
|
||||
|
||||
class CreatorTest extends TestCase
|
||||
{
|
||||
protected Request $request;
|
||||
protected WebAuthnAuthenticatableUser $user;
|
||||
protected AttestationCreation $creation;
|
||||
protected AttestationCreator $creator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->request = Request::create('https://test.app/webauthn/create', 'POST');
|
||||
$this->user = WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => 'test',
|
||||
'email' => 'test@email.com',
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
$this->creator = new AttestationCreator($this->app);
|
||||
$this->creation = new AttestationCreation($this->user, $this->request);
|
||||
|
||||
$this->startSession();
|
||||
$this->request->setLaravelSession($this->app->make('session.store'));
|
||||
}
|
||||
|
||||
protected function response(): TestResponse
|
||||
{
|
||||
return $this->createTestResponse(
|
||||
$this->creator->send($this->creation)->thenReturn()->json->toResponse($this->request)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_base_structure(): void
|
||||
{
|
||||
$this->freezeSecond();
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', function (Challenge $challenge): bool {
|
||||
static::assertSame(now()->addMinute()->getTimestamp(), $challenge->timeout);
|
||||
static::assertTrue(Uuid::isValid(Uuid::fromString($challenge->properties['user_uuid'])));
|
||||
static::assertSame('test@email.com', $challenge->properties['user_handle']);
|
||||
static::assertFalse($challenge->verify);
|
||||
|
||||
return true;
|
||||
})
|
||||
->assertJson([
|
||||
'rp' => [
|
||||
'name' => 'Laravel'
|
||||
],
|
||||
'user' => [
|
||||
'name' => 'test@email.com',
|
||||
'displayName' => 'test',
|
||||
'id' => session('_webauthn')->properties['user_uuid'],
|
||||
],
|
||||
'pubKeyCredParams' => [
|
||||
['type' => 'public-key', 'alg' => -7],
|
||||
['type' => 'public-key', 'alg' => -257],
|
||||
],
|
||||
'attestation' => 'none',
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_uses_relaying_party_config(): void
|
||||
{
|
||||
config(['webauthn.relying_party' => [
|
||||
'id' => 'https://foo.bar',
|
||||
'name' => 'foo',
|
||||
]]);
|
||||
|
||||
$this->response()->assertJsonFragment([
|
||||
'rp' => [
|
||||
'id' => 'https://foo.bar',
|
||||
'name' => 'foo',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asks_for_user_verification(): void
|
||||
{
|
||||
$this->creation->userVerification = WebAuthn::USER_VERIFICATION_REQUIRED;
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
return $challenge->verify;
|
||||
})
|
||||
->assertJsonFragment([
|
||||
'authenticatorSelection' => [
|
||||
'userVerification' => 'required'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asks_for_user_presence(): void
|
||||
{
|
||||
$this->creation->userVerification = WebAuthn::USER_VERIFICATION_DISCOURAGED;
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
return ! $challenge->verify;
|
||||
})
|
||||
->assertJsonFragment([
|
||||
'authenticatorSelection' => [
|
||||
'userVerification' => 'discouraged'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asks_for_resident_key(): void
|
||||
{
|
||||
$this->creation->residentKey = WebAuthn::RESIDENT_KEY_REQUIRED;
|
||||
|
||||
$this->response()
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
return $challenge->verify;
|
||||
})
|
||||
->assertJsonFragment([
|
||||
'authenticatorSelection' => [
|
||||
'residentKey' => 'required',
|
||||
'requireResidentKey' => true,
|
||||
'userVerification' => 'required'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_user_reuses_uuid_from_other_credential(): void
|
||||
{
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => $uuid = Str::uuid(),
|
||||
'rp_id' => 'test',
|
||||
'origin' => 'test',
|
||||
'public_key' => 'test',
|
||||
])->save();
|
||||
|
||||
$this->response()
|
||||
->assertJsonPath('user.id', $uuid->toString());
|
||||
}
|
||||
|
||||
public function test_adds_existing_credentials_if_unique_by_default(): void
|
||||
{
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Str::uuid(),
|
||||
'rp_id' => 'test',
|
||||
'origin' => 'test',
|
||||
'public_key' => 'test',
|
||||
])->save();
|
||||
|
||||
$this->response()
|
||||
->assertJsonFragment([
|
||||
'excludeCredentials' => [
|
||||
['id'=> 'test_id', 'type' => 'public-key',]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_doesnt_adds_credentials_if_allows_duplicates(): void
|
||||
{
|
||||
$this->creation->uniqueCredentials = false;
|
||||
|
||||
$this->user->webAuthnCredentials()->make()->forceFill([
|
||||
'id' => 'test_id',
|
||||
'user_id' => Str::uuid(),
|
||||
'rp_id' => 'test',
|
||||
'origin' => 'test',
|
||||
'public_key' => 'test',
|
||||
])->save();
|
||||
|
||||
$this->response()->assertJsonMissingPath('excludeCredentials');
|
||||
}
|
||||
}
|
||||
621
tests/Attestation/ValidationTest.php
Normal file
621
tests/Attestation/ValidationTest.php
Normal file
@@ -0,0 +1,621 @@
|
||||
<?php /** @noinspection JsonEncodingApiUsageInspection */
|
||||
|
||||
namespace Tests\Attestation;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigContract;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laragear\WebAuthn\Attestation\AttestationObject;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\Attestation\Formats\None;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidator;
|
||||
use Laragear\WebAuthn\Attestation\Validator\Pipes\CheckRelyingPartyHashSame;
|
||||
use Laragear\WebAuthn\Attestation\Validator\Pipes\CheckUserInteraction;
|
||||
use Laragear\WebAuthn\Attestation\Validator\Pipes\CredentialIdShouldNotBeDuplicated;
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Exceptions\AttestationException;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Mockery;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function base64_decode;
|
||||
use function base64_encode;
|
||||
use function hex2bin;
|
||||
use function json_encode;
|
||||
use function now;
|
||||
use function session;
|
||||
use function tap;
|
||||
|
||||
/**
|
||||
* CBOR Encoded strings where done in "cbor.me"
|
||||
*
|
||||
* @see https://cbor.me
|
||||
*/
|
||||
class ValidationTest extends TestCase
|
||||
{
|
||||
protected Request $request;
|
||||
protected WebAuthnAuthenticatableUser $user;
|
||||
protected AttestationValidation $validation;
|
||||
protected AttestationValidator $validator;
|
||||
protected Challenge $challenge;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->request = Request::create(
|
||||
'https://test.app/webauthn/create', 'POST', content: json_encode(FakeAuthenticator::attestationResponse())
|
||||
);
|
||||
|
||||
$this->user = WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
$this->validator = new AttestationValidator($this->app);
|
||||
$this->validation = new AttestationValidation($this->user, $this->request);
|
||||
|
||||
$this->freezeSecond();
|
||||
|
||||
$this->challenge = new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
);
|
||||
|
||||
$this->session(['_webauthn' => $this->challenge]);
|
||||
|
||||
$this->request->setLaravelSession($this->app->make('session.store'));
|
||||
}
|
||||
|
||||
protected function validate(): AttestationValidation
|
||||
{
|
||||
return $this->validator->send($this->validation)->thenReturn();
|
||||
}
|
||||
|
||||
public function test_validates_attestation_and_instances_webauthn_credential(): void
|
||||
{
|
||||
$validation = $this->validator->send($this->validation)->thenReturn();
|
||||
|
||||
static::assertInstanceOf(AttestationValidation::class, $validation);
|
||||
|
||||
static::assertFalse($validation->credential->exists);
|
||||
|
||||
$validation->credential->save();
|
||||
|
||||
$this->assertModelExists($validation->credential);
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => $validation->credential->id,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => $validation->credential->user_id,
|
||||
'alias' => null,
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'transports' => null,
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'certificates' => null,
|
||||
'disabled_at' => null,
|
||||
]);
|
||||
|
||||
$key = DB::table('webauthn_credentials')->value('public_key');
|
||||
|
||||
static::assertSame($validation->credential->public_key, Crypt::decryptString($key));
|
||||
}
|
||||
|
||||
public function test_validates_attestation_for_scoped_origin(): void
|
||||
{
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.create',
|
||||
'origin' => 'https://scoped.localhost',
|
||||
'challenge' => $this->challenge->data->toBase64Url()
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
static::assertInstanceOf(AttestationValidation::class, $this->validator->send($this->validation)->thenReturn());
|
||||
}
|
||||
|
||||
public function test_fails_if_challenge_does_not_exists(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Challenge does not exist.');
|
||||
|
||||
$this->session(['_webauthn' => null]);
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_fails_if_challenge_exists_but_is_expired(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Challenge does not exist.');
|
||||
|
||||
$this->travelTo(now()->addMinute()->addSecond());
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_challenge_is_pulled_from_session(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
static::assertNull(session('_webauthn'));
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_invalid(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Client Data JSON is invalid or malformed.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = 'foo';
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_empty(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Client Data JSON is empty.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode([]));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_type_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Client Data JSON does not contain the [type] key.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['origin' => '', 'challenge' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_origin_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Client Data JSON does not contain the [origin] key.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['type' => '', 'challenge' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_client_data_json_fails_if_challenge_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Client Data JSON does not contain the [challenge] key.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(json_encode(['type' => '', 'origin' => '']));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_invalid(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: CBOR Object is anything but an array.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(hex2bin('1A499602D2')); // 1234567890 in CBOR
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_fmt_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Format is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(hex2bin('A26761747453746D746068617574684461746160'));
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_fmt_not_string(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Format is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D74016761747453746D746068617574684461746160')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_attStmt_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Statement is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A263666D74647465737468617574684461746160')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_attStmt_not_array(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Statement is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D746474657374686175746844617461606761747453746D7400')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_authData_missing(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Authenticator Data is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A263666D7464746573746761747453746D7480')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_authData_not_byte_buffer(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Authenticator Data is missing or invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D7464746573746761747453746D7481006861757468446174611A001E8480')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_attestation_object_fails_if_fmt_not_none(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Format name [invalid] is invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D7467696E76616C69646761747453746D74A068617574684461746159016749960DE5880E8C687434170F6476605B8FE4AEB9A28632C7995CF3BA831D97634500000000000000000000000000000000000000000020ECFE96F4E099C876F2EA374218CAF33001E97DFDF8FC7F657257FC2E0BC9AF9DA401030339010020590100C3BD9A5E8971F49A2A88A6A161441E61A514BD63E77C1BE40AAAC08D3DFE4070FC37A0B739954A5150AA88A35E562E962B6D77B8EFACBACD90D2C6F93C3C5CBFD0194FA370713C673B1E0B3CEAC4A94B95C5D41EF0E0078309E0CAF6E3F1D10EF8418B4761842AC61F2B7C9F99595076C7BEEFE41E786BC9C013663054A0B3D3F0BE4FEA906696317BE1E2BD2FF299D6FA430E1A762AF69D0F0BC4CAF2FD16AB6EFA685055933FDE65E2C2232C344BE80EEB309975CBE55772887E7ADBFC38E9F68860DB11B466663FCF40C1F7529E274D6687EB237D41B62838540528CE6943664464ADA55B0F510782D5837AF07780BB7A675EF1D3FA29F39D1B472A7B80852143010001')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_authenticator_data_fails_if_invalid_binary(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Authenticator Data: Invalid input.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D74646E6F6E656761747453746D74A06861757468446174614849960DE5880E8C6D')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_compiling_authenticator_data_fails_if_invalid_length(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: ByteBuffer: Invalid offset or length.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['attestationObject'] = base64_encode(
|
||||
hex2bin('A363666D74646E6F6E656761747453746D74A068617574684461746159016649960DE5880E8C687434170F6476605B8FE4AEB9A28632C7995CF3BA831D97634500000000000000000000000000000000000000000020ECFE96F4E099C876F2EA374218CAF33001E97DFDF8FC7F657257FC2E0BC9AF9DA401030339010020590100C3BD9A5E8971F49A2A88A6A161441E61A514BD63E77C1BE40AAAC08D3DFE4070FC37A0B739954A5150AA88A35E562E962B6D77B8EFACBACD90D2C6F93C3C5CBFD0194FA370713C673B1E0B3CEAC4A94B95C5D41EF0E0078309E0CAF6E3F1D10EF8418B4761842AC61F2B7C9F99595076C7BEEFE41E786BC9C013663054A0B3D3F0BE4FEA906696317BE1E2BD2FF299D6FA430E1A762AF69D0F0BC4CAF2FD16AB6EFA685055933FDE65E2C2232C344BE80EEB309975CBE55772887E7ADBFC38E9F68860DB11B466663FCF40C1F7529E274D6687EB237D41B62838540528CE6943664464ADA55B0F510782D5837AF07780BB7A675EF1D3FA29F39D1B472A7B808521430100')
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_action_checks_fails_if_not_webauthn_create(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response is not for creating WebAuthn Credentials.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'invalid', 'origin' => '', 'challenge' => ''])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_challenge_fails_if_challenge_is_empty(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response has an empty challenge.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.create', 'origin' => '', 'challenge' => ''])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_challenge_fails_if_challenge_is_not_equal(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response challenge is not equal.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.create', 'origin' => '', 'challenge' => 'invalid'])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_empty(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response has an empty origin.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.create', 'origin' => '', 'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_invalid_host(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response origin is invalid.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.create', 'origin' => 'invalid', 'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_origin_fails_if_unsecure(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response not made to a secure server (localhost or HTTPS).');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode(['type' => 'webauthn.create', 'origin' => 'http://unsecure.com', 'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_empty(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response has an empty origin.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.create',
|
||||
'origin' => '',
|
||||
'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_not_equal(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Relaying Party ID not scoped to current.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.create',
|
||||
'origin' => 'https://otherhost.com',
|
||||
'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_not_contained(): void
|
||||
{
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Relaying Party ID not scoped to current.');
|
||||
|
||||
$invalid = FakeAuthenticator::attestationResponse();
|
||||
|
||||
$invalid['response']['clientDataJSON'] = base64_encode(
|
||||
json_encode([
|
||||
'type' => 'webauthn.create',
|
||||
'origin' => 'https://invalidlocalhost',
|
||||
'challenge' => FakeAuthenticator::ATTESTATION_CHALLENGE
|
||||
])
|
||||
);
|
||||
|
||||
$this->request->setJson(new ParameterBag($invalid));
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_rp_id_fails_if_hash_not_same(): void
|
||||
{
|
||||
$this->app->when(CheckRelyingPartyHashSame::class)
|
||||
->needs(ConfigContract::class)
|
||||
->give(static function (): Repository {
|
||||
return tap(new Repository())->set('webauthn.relaying_party.id', 'https://otherhost.com');
|
||||
});
|
||||
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response has different Relying Party ID hash.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_user_interaction_fails_if_user_not_present(): void
|
||||
{
|
||||
$this->app->resolving(CheckUserInteraction::class, function (): void {
|
||||
$this->validation->attestationObject = new AttestationObject(
|
||||
$auth = Mockery::mock(AuthenticatorData::class),
|
||||
new None([], $auth),
|
||||
'none',
|
||||
);
|
||||
|
||||
$auth->expects('wasUserAbsent')->andReturnTrue();
|
||||
});
|
||||
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response did not have the user present.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_check_user_interaction_fails_if_user_verification_was_required(): void
|
||||
{
|
||||
$this->challenge->verify = true;
|
||||
|
||||
$this->app->resolving(CheckUserInteraction::class, function (): void {
|
||||
$this->validation->attestationObject = new AttestationObject(
|
||||
$auth = Mockery::mock(AuthenticatorData::class),
|
||||
new None([], $auth),
|
||||
'none',
|
||||
);
|
||||
|
||||
$auth->expects('wasUserAbsent')->andReturnFalse();
|
||||
$auth->expects('wasUserNotVerified')->andReturnTrue();
|
||||
});
|
||||
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Response did not verify the user.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function test_credential_duplicate_check_fails_if_already_exists(): void
|
||||
{
|
||||
$this->app->resolving(CredentialIdShouldNotBeDuplicated::class, static function (): void {
|
||||
DB::table('webauthn_credentials')->insert([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'test_key',
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->expectException(AttestationException::class);
|
||||
$this->expectExceptionMessage('Attestation Error: Credential ID already exists in the database.');
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
}
|
||||
148
tests/Auth/EloquentWebAuthnProviderTest.php
Normal file
148
tests/Auth/EloquentWebAuthnProviderTest.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php /** @noinspection JsonEncodingApiUsageInspection */
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Mockery;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EloquentWebAuthnProviderTest extends TestCase
|
||||
{
|
||||
protected function defineEnvironment($app): void
|
||||
{
|
||||
$app->make('config')->set('auth.providers.users.driver', 'eloquent-webauthn');
|
||||
$app->make('config')->set('auth.providers.users.model', WebAuthnAuthenticatableUser::class);
|
||||
}
|
||||
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => '$2y$10$c/yQW6o.mEiCfys7enU29.4ETjmg/jdw.4puMTWbceEFGijejPkSW', // password
|
||||
]);
|
||||
|
||||
WebAuthnCredential::forceCreate([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'test_key',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_retrieves_user_using_webauthn(): void
|
||||
{
|
||||
$provider = Auth::createUserProvider('users');
|
||||
|
||||
$retrieved = $provider->retrieveByCredentials([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'rawId' => 'raw',
|
||||
'response' => ['something'],
|
||||
'type' => 'public-key',
|
||||
]);
|
||||
|
||||
static::assertTrue(WebAuthnAuthenticatableUser::query()->first()->is($retrieved));
|
||||
|
||||
$retrieved = $provider->retrieveByCredentials([
|
||||
'id' => '27EdS6eTDHCTa9Y73G9gY1b81yVJuuiu1TTyorFicBf',
|
||||
'rawId' => 'raw',
|
||||
'response' => ['something'],
|
||||
'type' => 'public-key',
|
||||
]);
|
||||
|
||||
static::assertNull($retrieved);
|
||||
}
|
||||
|
||||
public function test_retrieves_user_using_credentials(): void
|
||||
{
|
||||
$provider = Auth::createUserProvider('users');
|
||||
|
||||
$retrieved = $provider->retrieveByCredentials([
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
]);
|
||||
|
||||
static::assertTrue(WebAuthnAuthenticatableUser::query()->first()->is($retrieved));
|
||||
|
||||
$retrieved = $provider->retrieveByCredentials([
|
||||
'email' => 'invalid@invalid.com',
|
||||
]);
|
||||
|
||||
static::assertNull($retrieved);
|
||||
}
|
||||
|
||||
public function test_retrieves_user_using_classic_credentials_without_fallback(): void
|
||||
{
|
||||
$this->app->make('config')->set('auth.providers.users.password_fallback', false);
|
||||
|
||||
$this->test_retrieves_user_using_credentials();
|
||||
}
|
||||
|
||||
public function test_validates_webauthn(): void
|
||||
{
|
||||
$this->mock(AssertionValidator::class)
|
||||
->expects('send->thenReturn')
|
||||
->andReturn();
|
||||
|
||||
$valid = Auth::createUserProvider('users')
|
||||
->validateCredentials(WebAuthnAuthenticatableUser::first(), FakeAuthenticator::assertionResponse());
|
||||
|
||||
static::assertTrue($valid);
|
||||
}
|
||||
|
||||
public function test_validates_webauthn_to_false(): void
|
||||
{
|
||||
$this->mock(AssertionValidator::class)
|
||||
->expects('send->thenReturn')
|
||||
->andThrow(AssertionException::make('invalid'));
|
||||
|
||||
$this->instance('log', $logger = Mockery::mock(LoggerInterface::class));
|
||||
|
||||
$logger->expects('debug')
|
||||
->with('Assertion Error: invalid', [])
|
||||
->andReturn();
|
||||
|
||||
$valid = Auth::createUserProvider('users')
|
||||
->validateCredentials(WebAuthnAuthenticatableUser::first(), FakeAuthenticator::assertionResponse());
|
||||
|
||||
static::assertFalse($valid);
|
||||
}
|
||||
|
||||
public function test_validates_password(): void
|
||||
{
|
||||
$valid = Auth::createUserProvider('users')
|
||||
->validateCredentials(WebAuthnAuthenticatableUser::first(), ['password' => 'password']);
|
||||
|
||||
static::assertTrue($valid);
|
||||
}
|
||||
|
||||
public function test_validates_password_to_false(): void
|
||||
{
|
||||
$valid = Auth::createUserProvider('users')
|
||||
->validateCredentials(WebAuthnAuthenticatableUser::first(), ['password' => 'invalid']);
|
||||
|
||||
static::assertFalse($valid);
|
||||
}
|
||||
|
||||
public function test_doesnt_validates_password_when_fallback_is_false(): void
|
||||
{
|
||||
$this->app->make('config')->set('auth.providers.users.password_fallback', false);
|
||||
|
||||
$valid = Auth::createUserProvider('users')
|
||||
->validateCredentials(WebAuthnAuthenticatableUser::first(), ['password' => 'password']);
|
||||
|
||||
static::assertFalse($valid);
|
||||
}
|
||||
}
|
||||
60
tests/FakeAuthenticator.php
Normal file
60
tests/FakeAuthenticator.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
|
||||
class FakeAuthenticator
|
||||
{
|
||||
public const CREDENTIAL_ID = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
|
||||
public const CREDENTIAL_ID_RAW = '+VOLFKPY+/FuMI/sJ7gMllK76L3VoRUINj6lL/Z3qDg=';
|
||||
|
||||
public const ATTESTATION_USER = [
|
||||
'id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'name' => 'john@doe.com',
|
||||
'displayName' => 'John Doe',
|
||||
];
|
||||
|
||||
public const ATTESTATION_CHALLENGE = 'H04uZbKiqSx+xEcJ7TIYDg==';
|
||||
public const ASSERTION_CHALLENGE = 'iXozmynKi+YD2iRvKNbSPA==';
|
||||
|
||||
/**
|
||||
* Returns a correct attestation response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[ArrayShape(['id' => "string", 'type' => "string", 'rawId' => "string", 'response' => "string[]"])]
|
||||
public static function attestationResponse(): array
|
||||
{
|
||||
return [
|
||||
'id' => static::CREDENTIAL_ID,
|
||||
'type' => 'public-key',
|
||||
'rawId' => static::CREDENTIAL_ID_RAW,
|
||||
'response' => [
|
||||
'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSDA0dVpiS2lxU3gteEVjSjdUSVlEZyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==',
|
||||
'attestationObject' => 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZ0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAAAAAAAAAAAAAAAAAAAAAAAACD5U4sUo9j78W4wj+wnuAyWUrvovdWhFQg2PqUv9neoOKQBAwM5AQAgWQEAnBadZo+CnNdUHvzCWuLNTFsXTCjsHH5A+aUtIImsJsbTKmYsYtOuiOwEgcGglKEJV0MwzV4v2SDQzSirwLErisis4qV6Q3a0ZyZcYhgyMzvkk5CtDhpzxhsmFwiMSGt9gVRE8cOxGDQX2jTPfqykxZTkoXKEHevq8kl5PBCPsaWskrWsySw9mmqNCmIjhE2Evgarm0Xq7yq5h62H2ZzFT3U5C0H32I9cTPk6f/SVke+GMseVRiLleltJMNl0C0cKGBmJpQfeLFlKmOc15WqlwuMegjGULD9dPQvZS5uX+P0bHYfXq5V/HTwrR9FmkEdhq5YB9nE6RkE6Fbs5f+LIhSFDAQAB',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a correct Assertion response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[ArrayShape(['id' => "string", 'type' => "string", 'rawId' => "string", 'response' => "string[]"])]
|
||||
public static function assertionResponse(): array
|
||||
{
|
||||
return [
|
||||
'id' => static::CREDENTIAL_ID,
|
||||
'type' => 'public-key',
|
||||
'rawId' => static::CREDENTIAL_ID_RAW,
|
||||
'response' => [
|
||||
'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
|
||||
'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
|
||||
'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
|
||||
'userHandle' => static::ATTESTATION_USER['id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
65
tests/Http/Controllers/StubControllersTest.php
Normal file
65
tests/Http/Controllers/StubControllersTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Http\Controllers;
|
||||
|
||||
use Laragear\WebAuthn\Http\Requests\AssertedRequest;
|
||||
use Laragear\WebAuthn\Http\Requests\AssertionRequest;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
||||
use Laragear\WebAuthn\JsonTransport;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StubControllersTest extends TestCase
|
||||
{
|
||||
protected function defineWebRoutes($router): void
|
||||
{
|
||||
$router->group([], __DIR__ . '/../../../routes/webauthn.php');
|
||||
}
|
||||
|
||||
public function test_uses_attestation_request(): void
|
||||
{
|
||||
$request = $this->mock(AttestationRequest::class);
|
||||
|
||||
$request->expects('fastRegistration')->andReturnSelf();
|
||||
$request->expects('toCreate')->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('webauthn/register/options')->assertOk();
|
||||
}
|
||||
|
||||
public function test_uses_attested_request(): void
|
||||
{
|
||||
$this->mock(AttestedRequest::class)->expects('save')->andReturn();
|
||||
|
||||
$this->postJson('webauthn/register')->assertNoContent();
|
||||
}
|
||||
|
||||
public function test_uses_assertion_request(): void
|
||||
{
|
||||
$request = $this->mock(AssertionRequest::class);
|
||||
|
||||
$request->expects('validate')
|
||||
->with(['email' => 'sometimes|email|string'])
|
||||
->andReturn(['email' => 'email@email.com']);
|
||||
|
||||
$request->expects('toVerify')
|
||||
->with(['email' => 'email@email.com'])
|
||||
->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('webauthn/login/options')->assertOk();
|
||||
}
|
||||
|
||||
public function test_uses_asserted_request(): void
|
||||
{
|
||||
$this->mock(AssertedRequest::class)->expects('login')->andReturn(new WebAuthnAuthenticatableUser());
|
||||
|
||||
$this->postJson('webauthn/login')->assertNoContent();
|
||||
}
|
||||
|
||||
public function test_uses_asserted_request_and_fails_login_with_422(): void
|
||||
{
|
||||
$this->mock(AssertedRequest::class)->expects('login')->andReturnNull();
|
||||
|
||||
$this->postJson('webauthn/login')->assertStatus(422);
|
||||
}
|
||||
}
|
||||
278
tests/Http/Requests/AssertedRequestTest.php
Normal file
278
tests/Http/Requests/AssertedRequestTest.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Http\Requests;
|
||||
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Http\Requests\AssertedRequest;
|
||||
use Mockery;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function array_merge;
|
||||
use function base64_decode;
|
||||
use function config;
|
||||
use function now;
|
||||
use function session;
|
||||
|
||||
class AssertedRequestTest extends TestCase
|
||||
{
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
DB::table('webauthn_credentials')->insert([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'eyJpdiI6Imp0U0NVeFNNbW45KzEvMXpad2p2SUE9PSIsInZhbHVlIjoic0VxZ2I1WnlHM2lJakhkWHVkK2kzMWtibk1IN2ZlaExGT01qOElXMDdRTjhnVlR0TDgwOHk1S0xQUy9BQ1JCWHRLNzRtenNsMml1dVQydWtERjFEU0h0bkJGT2RwUXE1M1JCcVpablE2Y2VGV2YvVEE2RGFIRUE5L0x1K0JIQXhLVE1aNVNmN3AxeHdjRUo2V0hwREZSRTJYaThNNnB1VnozMlVXZEVPajhBL3d3ODlkTVN3bW54RTEwSG0ybzRQZFFNNEFrVytUYThub2IvMFRtUlBZamoyZElWKzR1bStZQ1IwU3FXbkYvSm1FU2FlMTFXYUo0SG9kc1BDME9CNUNKeE9IelE5d2dmNFNJRXBKNUdlVzJ3VHUrQWJZRFluK0hib0xvVTdWQ0ZISjZmOWF3by83aVJES1dxbU9Zd1lhRTlLVmhZSUdlWmlBOUFtcTM2ZVBaRWNKNEFSQUhENk5EaC9hN3REdnVFbm16WkRxekRWOXd4cVcvZFdKa2tlWWJqZWlmZnZLS0F1VEVCZEZQcXJkTExiNWRyQmxsZWtaSDRlT3VVS0ZBSXFBRG1JMjRUMnBKRXZxOUFUa2xxMjg2TEplUzdscVo2UytoVU5SdXk1OE1lcFN6aU05ZkVXTkdIM2tKM3Q5bmx1TGtYb1F5bGxxQVR3K3BVUVlia1VybDFKRm9lZDViNzYraGJRdmtUb2FNTEVGZmZYZ3lYRDRiOUVjRnJpcTVvWVExOHJHSTJpMnVBZ3E0TmljbUlKUUtXY2lSWDh1dE5MVDNRUzVRSkQrTjVJUU8rSGhpeFhRRjJvSEdQYjBoVT0iLCJtYWMiOiI5MTdmNWRkZGE5OTEwNzQ3MjhkYWVhYjRlNjk0MWZlMmI5OTQ4YzlmZWI1M2I4OGVkMjE1MjMxNjUwOWRmZTU2IiwidGFnIjoiIn0=',
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function defineEnvironment($app): void
|
||||
{
|
||||
$app->make('config')->set('auth.providers.users.driver', 'eloquent-webauthn');
|
||||
$app->make('config')->set('auth.providers.users.model', WebAuthnAuthenticatableUser::class);
|
||||
}
|
||||
|
||||
protected function defineWebRoutes($router): void
|
||||
{
|
||||
$router->post('test', static function (AssertedRequest $request): void {
|
||||
$request->login();
|
||||
});
|
||||
}
|
||||
|
||||
public function test_verifies_and_logs_in_user(): void
|
||||
{
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
|
||||
$this->assertAuthenticatedAs(WebAuthnAuthenticatableUser::find(1));
|
||||
}
|
||||
|
||||
public function test_pulls_challenge_session_key(): void
|
||||
{
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
|
||||
static::assertNull(session('_webauthn'));
|
||||
}
|
||||
|
||||
public function test_logs_in_with_remember_header_x_webauthn_remember(): void
|
||||
{
|
||||
$event = Event::fake(Login::class);
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::assertionResponse(), [
|
||||
'X-WebAuthn-Remember' => 1
|
||||
])->assertOk();
|
||||
|
||||
$event->assertDispatched(Login::class, static function (Login $event): bool {
|
||||
return $event->remember;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_logs_in_with_remember_header_webauthn_remember(): void
|
||||
{
|
||||
$event = Event::fake(Login::class);
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::assertionResponse(), [
|
||||
'WebAuthn-Remember' => 1
|
||||
])->assertOk();
|
||||
|
||||
$event->assertDispatched(Login::class, static function (Login $event): bool {
|
||||
return $event->remember;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_logs_in_with_remember_input(): void
|
||||
{
|
||||
$event = Event::fake(Login::class);
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', array_merge(FakeAuthenticator::assertionResponse(), [
|
||||
'remember' => 'on'
|
||||
]))->assertOk();
|
||||
|
||||
$event->assertDispatched(Login::class, static function (Login $event): bool {
|
||||
return $event->remember;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_logs_in_with_manual_remember_true(): void
|
||||
{
|
||||
$event = Event::fake(Login::class);
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
Route::middleware('web')->post('remember', function (AssertedRequest $request) {
|
||||
$request->login(null, true);
|
||||
});
|
||||
|
||||
$this->postJson('remember', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
|
||||
$event->assertDispatched(Login::class, static function (Login $event): bool {
|
||||
return $event->remember;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_logs_in_with_manual_remember_false(): void
|
||||
{
|
||||
$event = Event::fake(Login::class);
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
Route::middleware('web')->post('remember', function (AssertedRequest $request) {
|
||||
$request->login(null, false);
|
||||
});
|
||||
|
||||
$this->postJson('remember', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
|
||||
$event->assertDispatched(Login::class, static function (Login $event): bool {
|
||||
return ! $event->remember;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_uses_custom_session_key(): void
|
||||
{
|
||||
config(['webauthn.challenge.key' => 'foo']);
|
||||
|
||||
$this->session(['foo' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('test', array_merge(FakeAuthenticator::assertionResponse(), [
|
||||
'remember' => 'on'
|
||||
]))->assertOk();
|
||||
|
||||
$this->assertAuthenticatedAs(WebAuthnAuthenticatableUser::find(1));
|
||||
}
|
||||
|
||||
public function test_logs_in_with_custom_guard(): void
|
||||
{
|
||||
$guard = Auth::guard('web');
|
||||
|
||||
Auth::expects('guard')->with('foo')->twice()->andReturn($guard);
|
||||
|
||||
Route::middleware('web')->post('custom', function (AssertedRequest $request) {
|
||||
$request->login('foo');
|
||||
});
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('custom', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
|
||||
$this->assertAuthenticatedAs(WebAuthnAuthenticatableUser::find(1), 'foo');
|
||||
}
|
||||
|
||||
public function test_logs_in_returns_user(): void
|
||||
{
|
||||
Route::middleware('web')->post('custom', function (AssertedRequest $request) {
|
||||
static::assertTrue(WebAuthnAuthenticatableUser::find(1)->is($request->login()));
|
||||
});
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
$this->postJson('custom', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
}
|
||||
|
||||
public function test_fails_validation_if_a_member_is_missing(): void
|
||||
{
|
||||
$this->postJson('test', [
|
||||
'id' => 'test_id',
|
||||
'response' => [
|
||||
'authenticatorData' => 'test',
|
||||
'clientDataJSON' => 'test',
|
||||
'signature' => 'test',
|
||||
'userHandle' => 'test',
|
||||
],
|
||||
'type' => 'test'
|
||||
])
|
||||
->assertJsonValidationErrorFor('rawId');
|
||||
}
|
||||
|
||||
public function test_asserts_with_missing_user_handle(): void
|
||||
{
|
||||
$response = FakeAuthenticator::assertionResponse();
|
||||
|
||||
Arr::forget($response, 'response.userHandle');
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ASSERTION_CHALLENGE)), 60, false,
|
||||
)]);
|
||||
|
||||
// We know it's going to fail since the user is not in the validation object, this is just for show.
|
||||
$this->mock(AssertionValidator::class)
|
||||
->expects('send->thenReturn')
|
||||
->andReturn();
|
||||
|
||||
$this->postJson('test', $response)->assertOk();
|
||||
|
||||
$this->assertAuthenticatedAs(WebAuthnAuthenticatableUser::find(1));
|
||||
}
|
||||
|
||||
public function test_destroy_session_on_regeneration(): void
|
||||
{
|
||||
Route::middleware('web')->post('custom', function (AssertedRequest $request) {
|
||||
$request->login(destroySession: true);
|
||||
});
|
||||
|
||||
$session = Mockery::mock(\Illuminate\Contracts\Session\Session::class);
|
||||
|
||||
$session->expects('regenerate')->with(true)->andReturn();
|
||||
|
||||
$this->app->resolving(AssertedRequest::class, function (AssertedRequest $request) use ($session): void {
|
||||
$request->setLaravelSession($session);
|
||||
});
|
||||
|
||||
$this->mock(AssertionValidator::class)
|
||||
->expects('send->thenReturn')
|
||||
->andReturn();
|
||||
|
||||
$this->postJson('custom', FakeAuthenticator::assertionResponse())->assertOk();
|
||||
}
|
||||
}
|
||||
229
tests/Http/Requests/AssertionRequestTest.php
Normal file
229
tests/Http/Requests/AssertionRequestTest.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laragear\WebAuthn\Assertion\Creator\AssertionCreation;
|
||||
use Laragear\WebAuthn\Assertion\Creator\AssertionCreator;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Http\Requests\AssertionRequest;
|
||||
use Laragear\WebAuthn\JsonTransport;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function config;
|
||||
use function session;
|
||||
use function strlen;
|
||||
|
||||
class AssertionRequestTest extends TestCase
|
||||
{
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function defineEnvironment($app): void
|
||||
{
|
||||
$app->make('config')->set('auth.providers.users.driver', 'eloquent-webauthn');
|
||||
$app->make('config')->set('auth.providers.users.model', WebAuthnAuthenticatableUser::class);
|
||||
}
|
||||
|
||||
public function test_creates_assertion(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertSessionHas('_webauthn', function (Challenge $challenge): bool {
|
||||
return !$challenge->verify;
|
||||
})
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_uses_custom_timeout(): void
|
||||
{
|
||||
config(['webauthn.challenge.timeout' => 120]);
|
||||
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertJson([
|
||||
'timeout' => 120000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_uses_custom_length(): void
|
||||
{
|
||||
config(['webauthn.challenge.bytes' => 32]);
|
||||
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
|
||||
static::assertSame(64, strlen(session('_webauthn')->data->toHex()));
|
||||
}
|
||||
|
||||
public function test_uses_custom_session_key(): void
|
||||
{
|
||||
config(['webauthn.challenge.key' => 'foo']);
|
||||
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertSessionHas('foo');
|
||||
}
|
||||
|
||||
public function test_uses_custom_guard(): void
|
||||
{
|
||||
$guard = Auth::guard('web');
|
||||
|
||||
Auth::partialMock()->expects('guard')->with('foo')->andReturn($guard);
|
||||
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->guard('foo')->toVerify(['email' => FakeAuthenticator::ATTESTATION_USER['name']]);
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'timeout' => 60000,
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_guard_fails_if_user_not_webauthn_authenticatable(): void
|
||||
{
|
||||
config(['auth.providers.users.driver' => 'eloquent']);
|
||||
config(['auth.providers.users.model' => User::class]);
|
||||
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->guard('web')->toVerify(['email' => FakeAuthenticator::ATTESTATION_USER['name']]);
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertStatus(500)
|
||||
->assertSee('The user found for the [web] auth guard is not an instance of [WebAuthnAuthenticatable].');
|
||||
}
|
||||
|
||||
public function test_uses_fast_login(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->fastLogin()->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertJson([
|
||||
'timeout' => 60000,
|
||||
'userVerification' => 'discouraged',
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
|
||||
static::assertFalse(session('_webauthn')->verify);
|
||||
}
|
||||
|
||||
public function test_uses_secure_login(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->secureLogin()->toVerify();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertJson([
|
||||
'timeout' => 60000,
|
||||
'userVerification' => 'required',
|
||||
'challenge' => session('_webauthn')->data->toBase64Url()
|
||||
]);
|
||||
|
||||
static::assertTrue(session('_webauthn')->verify);
|
||||
}
|
||||
|
||||
public function test_uses_verify_with_already_instanced_user(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify(WebAuthnAuthenticatableUser::find(1));
|
||||
});
|
||||
|
||||
$creator = $this->mock(AssertionCreator::class);
|
||||
|
||||
$creator->expects('send')->withArgs(function (AssertionCreation $creation): bool {
|
||||
return $creation->user->getKey() === 1;
|
||||
})
|
||||
->andReturnSelf();
|
||||
|
||||
$creator->expects('then')->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('test')->assertOk();
|
||||
}
|
||||
|
||||
public function test_uses_verify_with_user_id(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify(1);
|
||||
});
|
||||
|
||||
$creator = $this->mock(AssertionCreator::class);
|
||||
|
||||
$creator->expects('send')->withArgs(function (AssertionCreation $creation): bool {
|
||||
return $creation->user->getKey() === 1;
|
||||
})
|
||||
->andReturnSelf();
|
||||
|
||||
$creator->expects('then')->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('test')->assertOk();
|
||||
}
|
||||
|
||||
public function test_uses_verify_with_bad_id_returning_null_user(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify(99);
|
||||
});
|
||||
|
||||
$creator = $this->mock(AssertionCreator::class);
|
||||
|
||||
$creator->expects('send')->withArgs(function (AssertionCreation $creation): bool {
|
||||
return ! $creation->user;
|
||||
})
|
||||
->andReturnSelf();
|
||||
|
||||
$creator->expects('then')->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('test')->assertOk();
|
||||
}
|
||||
|
||||
public function test_uses_verify_with_bad_credentials_returning_null_user(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AssertionRequest $request) {
|
||||
return $request->toVerify(['email' => 'invalid@email.com']);
|
||||
});
|
||||
|
||||
$creator = $this->mock(AssertionCreator::class);
|
||||
|
||||
$creator->expects('send')->withArgs(function (AssertionCreation $creation): bool {
|
||||
return ! $creation->user;
|
||||
})
|
||||
->andReturnSelf();
|
||||
|
||||
$creator->expects('then')->andReturn(new JsonTransport());
|
||||
|
||||
$this->postJson('test')->assertOk();
|
||||
}
|
||||
}
|
||||
149
tests/Http/Requests/AttestationRequestTest.php
Normal file
149
tests/Http/Requests/AttestationRequestTest.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function config;
|
||||
|
||||
class AttestationRequestTest extends TestCase
|
||||
{
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
$this->be(
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_forbidden_if_user_not_authenticated(): void
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_forbidden_if_user_not_webauthn_authenticatable(): void
|
||||
{
|
||||
$this->be(new User());
|
||||
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_returns_response_and_saves_challenge(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
static::assertFalse($challenge->verify);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_uses_custom_session_key(): void
|
||||
{
|
||||
config(['webauthn.challenge.key' => 'foo']);
|
||||
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')->assertSessionHas('foo');
|
||||
}
|
||||
|
||||
public function test_uses_fast_registration(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->fastRegistration()->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
static::assertFalse($challenge->verify);
|
||||
|
||||
return true;
|
||||
})
|
||||
->assertJsonPath('authenticatorSelection.userVerification', 'discouraged');
|
||||
}
|
||||
|
||||
public function test_uses_secure_registration(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->secureRegistration()->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
static::assertTrue($challenge->verify);
|
||||
|
||||
return true;
|
||||
})
|
||||
->assertJsonPath('authenticatorSelection.userVerification', 'required');
|
||||
}
|
||||
|
||||
public function test_uses_userless_and_verifies_user(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->userless()->toCreate();
|
||||
});
|
||||
|
||||
$this->postJson('test')
|
||||
->assertSessionHas('_webauthn', static function (Challenge $challenge): bool {
|
||||
static::assertTrue($challenge->verify);
|
||||
|
||||
return true;
|
||||
})
|
||||
->assertJsonFragment([
|
||||
'authenticatorSelection' => [
|
||||
'residentKey' => 'required',
|
||||
'requireResidentKey' => true,
|
||||
'userVerification' => 'required'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_allows_duplicates(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', function (AttestationRequest $request) {
|
||||
return $request->allowDuplicates()->toCreate();
|
||||
});
|
||||
|
||||
WebAuthnCredential::forceCreate([
|
||||
'id' => 'test_id',
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'test_key',
|
||||
]);
|
||||
|
||||
$this->postJson('test')->assertJsonMissingPath('excludeCredentials');
|
||||
}
|
||||
}
|
||||
176
tests/Http/Requests/AttestedRequestTest.php
Normal file
176
tests/Http/Requests/AttestedRequestTest.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\Events\CredentialCreated;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function base64_decode;
|
||||
use function config;
|
||||
|
||||
class AttestedRequestTest extends TestCase
|
||||
{
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
$this->be(
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
protected function defineWebRoutes($router): void
|
||||
{
|
||||
$router->post('test', static function (AttestedRequest $request): void {
|
||||
$request->save();
|
||||
});
|
||||
}
|
||||
|
||||
public function test_forbidden_if_user_not_authenticated(): void
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
$this->postJson('test')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_forbidden_if_user_not_webauthn_authenticatable(): void
|
||||
{
|
||||
$this->be(new User());
|
||||
|
||||
$this->postJson('test')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_invalid_if_web_authn_response_not_structured(): void
|
||||
{
|
||||
$this->postJson('test', [
|
||||
'id' => 'test',
|
||||
'rawId' => 'test',
|
||||
'response' => [
|
||||
'clientDataJSON' => 'test',
|
||||
],
|
||||
'type' => 'test',
|
||||
])->assertJsonValidationErrorFor('response.attestationObject');
|
||||
}
|
||||
|
||||
public function test_calls_validator_if_valid_and_authorized(): void
|
||||
{
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(
|
||||
base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
)
|
||||
]);
|
||||
|
||||
$event = Event::fake(CredentialCreated::class);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::attestationResponse())->assertOk();
|
||||
|
||||
$event->assertDispatched(CredentialCreated::class, function (CredentialCreated $event): bool {
|
||||
return FakeAuthenticator::CREDENTIAL_ID === $event->credential->getKey();
|
||||
});
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_uses_custom_session_key(): void
|
||||
{
|
||||
config(['webauthn.challenge.key' => 'foo']);
|
||||
|
||||
$this->session(['foo' => new Challenge(
|
||||
new ByteBuffer(
|
||||
base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
)
|
||||
]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::attestationResponse())->assertOk();
|
||||
}
|
||||
|
||||
public function test_saves_with_array(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', static function (AttestedRequest $request): void {
|
||||
$request->save(['alias' => 'foo']);
|
||||
});
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(
|
||||
base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
)
|
||||
]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::attestationResponse())->assertOk();
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'alias' => 'foo',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_saves_with_callable(): void
|
||||
{
|
||||
Route::middleware('web')->post('test', static function (AttestedRequest $request): void {
|
||||
$request->save(function ($credential) {
|
||||
$credential->alias = 'foo';
|
||||
});
|
||||
});
|
||||
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
)
|
||||
]);
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::attestationResponse())->assertOk();
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'alias' => 'foo',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_saves_return_credential_key(): void
|
||||
{
|
||||
$this->session(['_webauthn' => new Challenge(
|
||||
new ByteBuffer(base64_decode(FakeAuthenticator::ATTESTATION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
['user_uuid' => FakeAuthenticator::ATTESTATION_USER['id']]
|
||||
)
|
||||
]);
|
||||
|
||||
Route::middleware('web')->post('test', static function (AttestedRequest $request): array {
|
||||
return [$request->save()];
|
||||
});
|
||||
|
||||
$this->postJson('test', FakeAuthenticator::attestationResponse())
|
||||
->assertJson([FakeAuthenticator::CREDENTIAL_ID]);
|
||||
}
|
||||
}
|
||||
156
tests/Models/WebAuthnCredentialTest.php
Normal file
156
tests/Models/WebAuthnCredentialTest.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php /** @noinspection JsonEncodingApiUsageInspection */
|
||||
|
||||
namespace Tests\Models;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laragear\WebAuthn\Events\CredentialDisabled;
|
||||
use Laragear\WebAuthn\Events\CredentialEnabled;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\FakeAuthenticator;
|
||||
use Tests\Stubs\WebAuthnAuthenticatableUser;
|
||||
use Tests\TestCase;
|
||||
use function array_merge;
|
||||
use function json_encode;
|
||||
use function now;
|
||||
|
||||
class WebAuthnCredentialTest extends TestCase
|
||||
{
|
||||
protected function afterRefreshingDatabase(): void
|
||||
{
|
||||
WebAuthnAuthenticatableUser::forceCreate([
|
||||
'name' => FakeAuthenticator::ATTESTATION_USER['displayName'],
|
||||
'email' => FakeAuthenticator::ATTESTATION_USER['name'],
|
||||
'password' => 'test_password',
|
||||
]);
|
||||
|
||||
$base = static function (array $override = []): array {
|
||||
return array_merge([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'authenticatable_type' => WebAuthnAuthenticatableUser::class,
|
||||
'authenticatable_id' => 1,
|
||||
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||
'counter' => 0,
|
||||
'rp_id' => 'http://localhost',
|
||||
'origin' => 'http://localhost',
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'public_key' => 'test_key',
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
], $override);
|
||||
};
|
||||
|
||||
DB::table('webauthn_credentials')->insert($base());
|
||||
|
||||
DB::table('webauthn_credentials')->insert($base([
|
||||
'id' => '27EdS6eTDHCTa9Y73G9gY1b81yVJuuiu1TTyorFicBf'
|
||||
]));
|
||||
|
||||
DB::table('webauthn_credentials')->insert($base([
|
||||
'id' => 'HLs22xpFT7ilSbYvbARFNf9Q3nVyfczTT9LFhtFT89D',
|
||||
'disabled_at' => now()->toDateTimeString(),
|
||||
]));
|
||||
}
|
||||
|
||||
public function test_queries_enabled_credentials(): void
|
||||
{
|
||||
static::assertSame(2, WebAuthnCredential::query()->whereEnabled()->count());
|
||||
}
|
||||
|
||||
public function test_queries_disabled_credentials(): void
|
||||
{
|
||||
static::assertSame(1, WebAuthnCredential::query()->whereDisabled()->count());
|
||||
}
|
||||
|
||||
public function test_is_enabled(): void
|
||||
{
|
||||
$credential = WebAuthnCredential::find(FakeAuthenticator::CREDENTIAL_ID);
|
||||
|
||||
static::assertTrue($credential->isEnabled());
|
||||
static::assertFalse($credential->isDisabled());
|
||||
}
|
||||
|
||||
public function test_is_disabled(): void
|
||||
{
|
||||
$credential = WebAuthnCredential::find('HLs22xpFT7ilSbYvbARFNf9Q3nVyfczTT9LFhtFT89D');
|
||||
|
||||
static::assertTrue($credential->isDisabled());
|
||||
static::assertFalse($credential->isEnabled());
|
||||
}
|
||||
|
||||
public function test_disables_credential(): void
|
||||
{
|
||||
$event = Event::fake();
|
||||
|
||||
$credential = WebAuthnCredential::find(FakeAuthenticator::CREDENTIAL_ID);
|
||||
|
||||
$this->freezeSecond();
|
||||
|
||||
$credential->disable();
|
||||
$credential->disable();
|
||||
|
||||
$event->assertDispatchedTimes(CredentialDisabled::class);
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'disabled_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_enables_credential(): void
|
||||
{
|
||||
$event = Event::fake();
|
||||
|
||||
$credential = WebAuthnCredential::find('HLs22xpFT7ilSbYvbARFNf9Q3nVyfczTT9LFhtFT89D');
|
||||
|
||||
$this->freezeSecond();
|
||||
|
||||
$credential->enable();
|
||||
$credential->enable();
|
||||
|
||||
$event->assertDispatchedTimes(CredentialEnabled::class);
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'disabled_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_syncs_counter(): void
|
||||
{
|
||||
$credential = WebAuthnCredential::find(FakeAuthenticator::CREDENTIAL_ID);
|
||||
|
||||
$credential->syncCounter(20);
|
||||
|
||||
$this->assertDatabaseHas(WebAuthnCredential::class, [
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'counter' => 20,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_authenticatable(): void
|
||||
{
|
||||
$user = WebAuthnCredential::find(FakeAuthenticator::CREDENTIAL_ID)->authenticatable;
|
||||
|
||||
static::assertInstanceOf(WebAuthnAuthenticatableUser::class, $user);
|
||||
}
|
||||
|
||||
public function test_shows_serializes_few_columns(): void
|
||||
{
|
||||
$json = WebAuthnCredential::find(FakeAuthenticator::CREDENTIAL_ID)->toJson();
|
||||
|
||||
static::assertJsonStringEqualsJsonString(
|
||||
json_encode([
|
||||
'id' => FakeAuthenticator::CREDENTIAL_ID,
|
||||
'origin' => 'http://localhost',
|
||||
'alias' => null,
|
||||
'aaguid' => Uuid::NIL,
|
||||
'attestation_format' => 'none',
|
||||
'disabled_at' => null,
|
||||
]),
|
||||
$json
|
||||
);
|
||||
}
|
||||
}
|
||||
19
tests/Stubs/WebAuthnAuthenticatableUser.php
Normal file
19
tests/Stubs/WebAuthnAuthenticatableUser.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Stubs;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||
|
||||
/**
|
||||
* @method static static forceCreate(array $attributes = [])
|
||||
*/
|
||||
class WebAuthnAuthenticatableUser extends User implements WebAuthnAuthenticatable
|
||||
{
|
||||
use HasFactory;
|
||||
use WebAuthnAuthentication;
|
||||
|
||||
protected $table = 'users';
|
||||
}
|
||||
23
tests/TestCase.php
Normal file
23
tests/TestCase.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laragear\WebAuthn\WebAuthnServiceProvider;
|
||||
use Orchestra\Testbench\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function defineDatabaseMigrations(): void
|
||||
{
|
||||
$this->loadLaravelMigrations();
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app): array
|
||||
{
|
||||
return [WebAuthnServiceProvider::class];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user