From 84f7422c4853bf07c0fca6e5b34fbc93125ec4b9 Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Fri, 3 Jul 2020 00:36:30 -0400 Subject: [PATCH] Fixed credential persistence and retrieval. --- ...000_create_web_authn_credentials_table.php | 2 +- src/Auth/EloquentWebAuthnProvider.php | 18 +++- src/Eloquent/WebAuthnCredential.php | 22 ----- src/Http/AssertsWebAuthn.php | 22 ++++- src/Http/AttestsWebAuthn.php | 20 +++- src/WebAuthn/WebAuthnAssertValidator.php | 2 +- tests/Auth/EloquentWebAuthnProviderTest.php | 45 ++++----- tests/Eloquent/WebAuthnAuthenticationTest.php | 2 +- tests/Http/WebAuthnLoginTest.php | 97 ++++++++++--------- tests/Http/WebAuthnRegistrationTest.php | 51 +++++----- tests/WebAuthn/WebAuthnAssertionTest.php | 14 +-- tests/WebAuthnAuthenticationTest.php | 10 +- 12 files changed, 171 insertions(+), 134 deletions(-) diff --git a/database/migrations/2020_04_02_000000_create_web_authn_credentials_table.php b/database/migrations/2020_04_02_000000_create_web_authn_credentials_table.php index 550b452..b4ad391 100644 --- a/database/migrations/2020_04_02_000000_create_web_authn_credentials_table.php +++ b/database/migrations/2020_04_02_000000_create_web_authn_credentials_table.php @@ -15,7 +15,7 @@ class CreateWebAuthnCredentialsTable extends Migration public function up() { Schema::create('web_authn_credentials', function (Blueprint $table) { - $table->string('id'); + $table->binary('id'); // Change accordingly for your users table if you need to. $table->unsignedBigInteger('user_id'); diff --git a/src/Auth/EloquentWebAuthnProvider.php b/src/Auth/EloquentWebAuthnProvider.php index a78b794..101f38c 100644 --- a/src/Auth/EloquentWebAuthnProvider.php +++ b/src/Auth/EloquentWebAuthnProvider.php @@ -51,9 +51,8 @@ public function __construct(ConfigContract $config, */ public function retrieveByCredentials(array $credentials) { - if ($this->isSignedChallenge($credentials)) { - // We will try to get the user for the given credential ID. - return call_user_func([$this->model, 'getFromCredentialId'], $credentials['id']); + if ($this->isSignedChallenge($credentials) && $id = $this->binaryID($credentials['rawId'])) { + return $this->model::getFromCredentialId($id); } if ($this->fallback) { @@ -61,6 +60,17 @@ public function retrieveByCredentials(array $credentials) } } + /** + * Transforms the raw ID string into a binary string. + * + * @param string $rawId + * @return null|string + */ + protected function binaryID(string $rawId) + { + return base64_decode(strtr($rawId, '-_', '+/'), true); + } + /** * Check if the credentials are for a public key signed challenge * @@ -82,7 +92,7 @@ protected function isSignedChallenge(array $credentials) public function validateCredentials(UserContract $user, array $credentials) { if ($this->isSignedChallenge($credentials)) { - return (bool) $this->validator->validate($credentials); + return (bool)$this->validator->validate($credentials); } if ($this->fallback) { diff --git a/src/Eloquent/WebAuthnCredential.php b/src/Eloquent/WebAuthnCredential.php index 5c7fe52..5c60e9d 100644 --- a/src/Eloquent/WebAuthnCredential.php +++ b/src/Eloquent/WebAuthnCredential.php @@ -148,28 +148,6 @@ public function getAaguidAttribute($value) return $value; } - /** - * Sets the credential public key as binary form. - * - * @param string $value - * @return void - */ - public function setPublicKeyAttribute($value) - { - $this->attributes['public_key'] = base64_decode($value); - } - - /** - * Return the credential public key as a Base64 string. - * - * @param string $value - * @return string - */ - public function getPublicKeyAttribute($value) - { - return base64_encode($value); - } - /** * Filter the credentials for those explicitly enabled. * diff --git a/src/Http/AssertsWebAuthn.php b/src/Http/AssertsWebAuthn.php index acb8571..b870b4d 100644 --- a/src/Http/AssertsWebAuthn.php +++ b/src/Http/AssertsWebAuthn.php @@ -77,13 +77,31 @@ protected function userProvider() */ public function login(Request $request) { - $credential = $request->only('rawId', 'id', 'response', 'type'); + $credential = $request->validate($this->assertionRules()); if ($authenticated = $this->attemptLogin($credential, $this->hasRemember($request))) { return $this->authenticated($request, $this->guard()->user()) ?? response()->noContent(); } - return response()->noContent(401); + return response()->noContent(422); + } + + /** + * The assertion rules to validate the incoming JSON payload. + * + * @return array|string[] + */ + protected function assertionRules() + { + return [ + 'id' => 'required|string', + 'rawId' => 'required|string', + 'response.authenticatorData' => 'required|string', + 'response.clientDataJSON' => 'required|string', + 'response.signature' => 'required|string', + 'response.userHandle' => 'required|string', + 'type' => 'required|string', + ]; } /** diff --git a/src/Http/AttestsWebAuthn.php b/src/Http/AttestsWebAuthn.php index 021c91c..4d0c73e 100644 --- a/src/Http/AttestsWebAuthn.php +++ b/src/Http/AttestsWebAuthn.php @@ -33,7 +33,7 @@ public function register(Request $request, WebAuthnAuthenticatable $user) // save it into the credentials store. If the data is invalid we will bail // out and return a non-authorized response since we can't save the data. $validCredential = WebAuthn::validateAttestation( - $request->only('id', 'rawId', 'response', 'type'), $user + $request->validate($this->attestationRules()), $user ); if ($validCredential) { @@ -44,7 +44,23 @@ public function register(Request $request, WebAuthnAuthenticatable $user) return $this->credentialRegistered($user, $validCredential) ?? response()->noContent(); } - return response()->noContent(400); + return response()->noContent(422); + } + + /** + * The attestation rules to validate the incoming JSON payload. + * + * @return array|string[] + */ + protected function attestationRules() + { + return [ + 'id' => 'required|string', + 'rawId' => 'required|string', + 'response.attestationObject' => 'required|string', + 'response.clientDataJSON' => 'required|string', + 'type' => 'required|string', + ]; } /** diff --git a/src/WebAuthn/WebAuthnAssertValidator.php b/src/WebAuthn/WebAuthnAssertValidator.php index 2f1838a..652f3c8 100644 --- a/src/WebAuthn/WebAuthnAssertValidator.php +++ b/src/WebAuthn/WebAuthnAssertValidator.php @@ -211,7 +211,7 @@ public function validate(array $data) } return $this->validator->check( - $credentials->getId(), + $credentials->getRawId(), $response, $this->retrieveAssertion(), $this->request, diff --git a/tests/Auth/EloquentWebAuthnProviderTest.php b/tests/Auth/EloquentWebAuthnProviderTest.php index 342ab2d..1a9a987 100644 --- a/tests/Auth/EloquentWebAuthnProviderTest.php +++ b/tests/Auth/EloquentWebAuthnProviderTest.php @@ -2,6 +2,7 @@ namespace Tests\Auth; +use Base64Url\Base64Url; use Tests\RegistersPackage; use Illuminate\Support\Str; use Tests\Stubs\TestWebAuthnUser; @@ -46,27 +47,27 @@ public function test_retrieves_user_using_credential_id() $user->save(); DB::table('web_authn_credentials')->insert([ - 'id' => 'test_credential_id', - 'user_id' => 1, - 'is_enabled' => true, - 'type' => 'public_key', - 'transports' => json_encode([]), - 'attestation_type' => 'none', - 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), - 'aaguid' => Str::uuid()->toString(), - 'public_key' => 'public_key', - 'counter' => 0, - 'user_handle' => 'test_user_handle', - 'created_at' => now()->toDateTimeString(), - 'updated_at' => now()->toDateTimeString(), + 'id' => 'test_credential_id', + 'user_id' => 1, + 'is_enabled' => true, + 'type' => 'public_key', + 'transports' => json_encode([]), + 'attestation_type' => 'none', + 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), + 'aaguid' => Str::uuid()->toString(), + 'public_key' => 'public_key', + 'counter' => 0, + 'user_handle' => 'test_user_handle', + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), ]); $retrieved = Auth::createUserProvider('users') ->retrieveByCredentials([ - 'id' => 'test_credential_id', - 'rawId' => 'something', + 'id' => 'test_credential_id', + 'rawId' => Base64Url::encode('test_credential_id'), 'response' => ['something'], - 'type' => 'public-key' + 'type' => 'public-key', ]); $this->assertTrue($user->is($retrieved)); @@ -84,7 +85,7 @@ public function test_retrieves_user_using_classic_credentials() $retrieved = Auth::createUserProvider('users') ->retrieveByCredentials([ - 'email' => 'john.doe@mail.com' + 'email' => 'john.doe@mail.com', ]); $this->assertTrue($user->is($retrieved)); @@ -104,7 +105,7 @@ public function test_fails_retrieving_user_using_classic_credentials_without_fal $retrieved = Auth::createUserProvider('users') ->retrieveByCredentials([ - 'email' => 'john.doe@mail.com' + 'email' => 'john.doe@mail.com', ]); $this->assertNull($retrieved); @@ -122,8 +123,8 @@ public function test_validates_user_using_password_fallback() $result = Auth::createUserProvider('users') ->validateCredentials($user, [ - 'name' => 'john', - 'password' => 'secret' + 'name' => 'john', + 'password' => 'secret', ]); $this->assertTrue($result); @@ -143,8 +144,8 @@ public function test_fails_using_password_and_fallback_disabled() $result = Auth::createUserProvider('users') ->validateCredentials($user, [ - 'name' => 'john', - 'password' => 'secret' + 'name' => 'john', + 'password' => 'secret', ]); $this->assertFalse($result); diff --git a/tests/Eloquent/WebAuthnAuthenticationTest.php b/tests/Eloquent/WebAuthnAuthenticationTest.php index 56fd350..1fcc4e9 100644 --- a/tests/Eloquent/WebAuthnAuthenticationTest.php +++ b/tests/Eloquent/WebAuthnAuthenticationTest.php @@ -57,7 +57,7 @@ public function test_saves_credential_public_key_as_binary_string() $model->public_key = $key; - $this->assertSame($key, base64_encode($model->getAttributes()['public_key'])); + $this->assertSame($key, $model->getAttributes()['public_key']); } public function test_finds_one_by_credential_id() diff --git a/tests/Http/WebAuthnLoginTest.php b/tests/Http/WebAuthnLoginTest.php index 7c05974..c48c49b 100644 --- a/tests/Http/WebAuthnLoginTest.php +++ b/tests/Http/WebAuthnLoginTest.php @@ -98,19 +98,19 @@ public function test_receives_webauthn_options_by_credentials() ])->save(); DB::table('web_authn_credentials')->insert([ - 'id' => 'test_credential_id', - 'user_id' => 1, - 'is_enabled' => true, - 'type' => 'public_key', - 'transports' => json_encode([]), - 'attestation_type' => 'none', - 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), - 'aaguid' => $uuid->toString(), - 'public_key' => 'public_key', - 'counter' => 0, - 'user_handle' => 'test_user_handle', - 'created_at' => now()->toDateTimeString(), - 'updated_at' => now()->toDateTimeString(), + 'id' => 'test_credential_id', + 'user_id' => 1, + 'is_enabled' => true, + 'type' => 'public_key', + 'transports' => json_encode([]), + 'attestation_type' => 'none', + 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), + 'aaguid' => $uuid->toString(), + 'public_key' => 'public_key', + 'counter' => 0, + 'user_handle' => 'test_user_handle', + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), ]); $this->post('webauthn/login/options', [ @@ -144,19 +144,19 @@ public function test_disabled_credential_doesnt_show() ])->save(); DB::table('web_authn_credentials')->insert([ - 'id' => 'test_credential_id', - 'user_id' => 1, - 'is_enabled' => false, - 'type' => 'public_key', - 'transports' => json_encode([]), - 'attestation_type' => 'none', - 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), - 'aaguid' => $uuid->toString(), - 'public_key' => 'public_key', - 'counter' => 0, - 'user_handle' => 'test_user_handle', - 'created_at' => now()->toDateTimeString(), - 'updated_at' => now()->toDateTimeString(), + 'id' => 'test_credential_id', + 'user_id' => 1, + 'is_enabled' => false, + 'type' => 'public_key', + 'transports' => json_encode([]), + 'attestation_type' => 'none', + 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), + 'aaguid' => $uuid->toString(), + 'public_key' => 'public_key', + 'counter' => 0, + 'user_handle' => 'test_user_handle', + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), ]); $this->post('webauthn/login/options', [ @@ -181,7 +181,7 @@ public function test_unauthenticated_when_attest_response_is_invalid() ], 'type' => 'public-key', ]) - ->assertStatus(401); + ->assertStatus(422); } public function test_user_authenticates_with_webauthn() @@ -197,28 +197,33 @@ public function test_user_authenticates_with_webauthn() $user->save(); DB::table('web_authn_credentials')->insert([ - 'id' => 'test_credential_id', - 'user_id' => 1, - 'is_enabled' => true, - 'type' => 'public_key', - 'transports' => json_encode([]), - 'attestation_type' => 'none', - 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), - 'aaguid' => $uuid->toString(), - 'public_key' => 'public_key', - 'counter' => 0, - 'user_handle' => 'test_user_handle', - 'created_at' => now()->toDateTimeString(), - 'updated_at' => now()->toDateTimeString(), + 'id' => 'test_credential_id', + 'user_id' => 1, + 'is_enabled' => true, + 'type' => 'public_key', + 'transports' => json_encode([]), + 'attestation_type' => 'none', + 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), + 'aaguid' => $uuid->toString(), + 'public_key' => 'public_key', + 'counter' => 0, + 'user_handle' => 'test_user_handle', + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), ]); $this->mock(WebAuthnAssertValidator::class) ->shouldReceive('validate') ->with($data = [ - 'id' => 'test_credential_id', - 'rawId' => 'test_raw_id', - 'type' => 'test_type', - 'response' => 'test_response', + 'id' => 'test_credential_id', + 'type' => 'test_type', + 'response' => [ + 'authenticatorData' => 'test', + 'clientDataJSON' => 'test', + 'signature' => 'test', + 'userHandle' => 'test', + ], + 'rawId' => Base64Url::encode('test_credential_id'), ]) ->andReturnUsing(function ($data) { $credentials = WebAuthnCredential::whereKey($data['id'])->first(); @@ -233,8 +238,8 @@ public function test_user_authenticates_with_webauthn() $this->assertAuthenticatedAs($user); $this->assertDatabaseHas('web_authn_credentials', [ - 'id' => 'test_credential_id', - 'counter' => 1, + 'id' => 'test_credential_id', + 'counter' => 1, ]); } diff --git a/tests/Http/WebAuthnRegistrationTest.php b/tests/Http/WebAuthnRegistrationTest.php index b584116..3ebdb3c 100644 --- a/tests/Http/WebAuthnRegistrationTest.php +++ b/tests/Http/WebAuthnRegistrationTest.php @@ -105,7 +105,7 @@ public function test_returns_attestation_options() $this->post('webauthn/register/options')->assertExactJson([ 'rp' => [ - 'id' => 'app.com', + 'id' => 'app.com', 'name' => 'test', ], 'pubKeyCredParams' => [ @@ -159,13 +159,16 @@ public function test_success_when_checks_assertion() ->shouldReceive('validate') ->with($data = [ 'id' => 'test_id', - 'rawId' => 'test_raw_id', - 'response' => 'test_response', + 'rawId' => Base64Url::encode('test_id'), + 'response' => [ + 'attestationObject' => 'test', + 'clientDataJSON' => 'test', + ], 'type' => 'test_public_key', ], $user) ->andReturnUsing(function (array $data) { return new PublicKeyCredentialSource( - $data['id'], + $data['rawId'], 'test_type', [], 'test_attestation', @@ -182,24 +185,24 @@ public function test_success_when_checks_assertion() $this->postJson('webauthn/register', $data)->assertNoContent(); $this->assertDatabaseHas('web_authn_credentials', [ - 'id' => 'test_id', - 'user_id' => 1, - 'is_enabled' => true, - 'type' => 'test_type', - 'transports' => json_encode([]), - 'attestation_type' => 'test_attestation', - 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), - 'aaguid' => '1c3c674c-2f09-4079-8e57-ddc5fe5e66eb', - 'counter' => 0, - 'user_handle' => 'test_user_handle', - 'created_at' => $now->toDateTimeString(), - 'updated_at' => $now->toDateTimeString(), - 'public_key' => base64_decode('test_public_key'), + 'id' => $data['rawId'], + 'user_id' => 1, + 'type' => 'test_type', + 'transports' => json_encode([]), + 'attestation_type' => 'test_attestation', + 'trust_path' => json_encode(['type' => EmptyTrustPath::class]), + 'aaguid' => '1c3c674c-2f09-4079-8e57-ddc5fe5e66eb', + 'counter' => 0, + 'user_handle' => 'test_user_handle', + 'created_at' => $now->toDateTimeString(), + 'updated_at' => $now->toDateTimeString(), + 'disabled_at' => null, + 'public_key' => 'test_public_key', ]); - $event->assertDispatched(AttestationSuccessful::class, function ($event) use ($user) { + $event->assertDispatched(AttestationSuccessful::class, function ($event) use ($user, $data) { return $user->is($event->user) - && 'test_id' === $event->credential->getPublicKeyCredentialId(); + && $data['rawId'] === $event->credential->getPublicKeyCredentialId(); }); } @@ -221,9 +224,9 @@ public function test_uses_resident_key() ->assertJsonFragment([ 'authenticatorSelection' => [ 'requireResidentKey' => false, - 'residentKey' => 'preferred', - 'userVerification' => 'preferred' - ] + 'residentKey' => 'preferred', + 'userVerification' => 'preferred', + ], ])->assertStatus(200); } @@ -272,10 +275,10 @@ public function test_fails_assertion() ], $user) ->andReturnFalse(); - $this->postJson('webauthn/register', $data)->assertNoContent(400); + $this->postJson('webauthn/register', $data)->assertNoContent(422); $this->assertDatabaseMissing('web_authn_credentials', [ - 'id' => 'test_credential_id', + 'id' => 'test_credential_id', ]); $event->assertNotDispatched(AttestationSuccessful::class); diff --git a/tests/WebAuthn/WebAuthnAssertionTest.php b/tests/WebAuthn/WebAuthnAssertionTest.php index 440166a..7c74cb9 100644 --- a/tests/WebAuthn/WebAuthnAssertionTest.php +++ b/tests/WebAuthn/WebAuthnAssertionTest.php @@ -15,7 +15,6 @@ use Tests\Stubs\TestWebAuthnUser; use Webauthn\PublicKeyCredential; use Illuminate\Support\Facades\DB; -use Psr\Http\Message\UriInterface; use Webauthn\AuthenticatorResponse; use Tests\RunsPublishableMigrations; use Webauthn\AttestedCredentialData; @@ -186,7 +185,7 @@ public function test_assert_validates_and_returns_credentials() $this->mock(AuthenticatorAssertionResponseValidator::class) ->shouldReceive('check') ->with( - 'test_credential_id', + 'a04f39f8ba6a4a19b98a0579bf76505ea6d55730745274616166ef827b649506', $response, $options, Mockery::type(ServerRequestInterface::class), @@ -251,7 +250,7 @@ public function test_assert_fails_when_check_fails() $this->mock(AuthenticatorAssertionResponseValidator::class) ->shouldReceive('check') ->with( - 'test_credential_id', + 'a04f39f8ba6a4a19b98a0579bf76505ea6d55730745274616166ef827b649506', $response, $options, Mockery::type(ServerRequestInterface::class), @@ -400,7 +399,7 @@ public function test_assert_fails_when_validator_throws_exception() $this->mock(AuthenticatorAssertionResponseValidator::class) ->shouldReceive('check') ->with( - 'test_credential_id', + 'a04f39f8ba6a4a19b98a0579bf76505ea6d55730745274616166ef827b649506', $response, $options, Mockery::type(ServerRequestInterface::class), @@ -467,7 +466,7 @@ public function test_assert_fails_when_throws_exception() $this->mock(AuthenticatorAssertionResponseValidator::class) ->shouldReceive('check') ->with( - 'test_credential_id', + 'a04f39f8ba6a4a19b98a0579bf76505ea6d55730745274616166ef827b649506', $response, $options, Mockery::type(ServerRequestInterface::class), @@ -515,7 +514,8 @@ public function test_attestation_reaches_repository() ->andReturnNull(); $source->shouldReceive('getAttestedCredentialData') ->andReturn( - new AttestedCredentialData(Uuid::fromBytes(base64_decode('YCiwF7HUTAK0s6/Nr8lrsg==', true)), + new AttestedCredentialData( + Uuid::fromBytes(base64_decode('YCiwF7HUTAK0s6/Nr8lrsg==', true)), base64_decode('6oRgydKXdC3LtZBDoAXxKnWte68elEQejDrYOV9x+18=', true), base64_decode('pAEDAzkBACBZAQDwn2Ee7V+9GNDn2iCU2plQnIVmZG/vOiXSHb9TQzC5806bGzLV918+1SLFhMhlX5jua2rdXt65nYw9Eln7mbmVxLBDmEm2wod6wP2HinC9HPsYwr75tMRakLMNFfH4Xx4lEsjulRmv68yl/N8XH64X8LKe2GBxjqcuJR+c3LbW4D5dWt/1pGL8fS1UbO3abA/d3IeEsP8RpEz5eVo6qBhb4r0VTo2NMeq75saBHIj4whqo6qsRqRvBmK2d9NAecBFFRIQ31NUtEQZPqXOzkbXGehDi7c3YJPBkTW9kMqcosob9Vlru+vVab+1PnFRdqaklR1UtmhrWte/wB61Hm3xdIUMBAAE=', true) ) @@ -526,7 +526,7 @@ public function test_attestation_reaches_repository() $repo = $this->mock(PublicKeyCredentialSourceRepository::class); $repo->shouldReceive('findOneByCredentialId') - ->with('6oRgydKXdC3LtZBDoAXxKnWte68elEQejDrYOV9x-18') + ->with(base64_decode('6oRgydKXdC3LtZBDoAXxKnWte68elEQejDrYOV9x+18=', true)) ->andReturn($source); $repo->shouldReceive('saveCredentialSource') diff --git a/tests/WebAuthnAuthenticationTest.php b/tests/WebAuthnAuthenticationTest.php index 197a469..a9d23cf 100644 --- a/tests/WebAuthnAuthenticationTest.php +++ b/tests/WebAuthnAuthenticationTest.php @@ -10,9 +10,11 @@ use Illuminate\Support\Facades\Date; use Webauthn\TrustPath\EmptyTrustPath; use Webauthn\PublicKeyCredentialSource; +use Illuminate\Database\Eloquent\Model; use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\PublicKeyCredentialDescriptor; use Illuminate\Database\Eloquent\Relations\HasMany; +use DarkGhostHunter\Larapass\WebAuthnAuthentication; class WebAuthnAuthenticationTest extends TestCase { @@ -81,7 +83,11 @@ protected function setUp() : void public function test_returns_relation_instance_on_method_call() { - $this->assertInstanceOf(HasMany::class, TestWebAuthnUser::make()->webAuthnCredentials()); + $model = new class extends Model { + use WebAuthnAuthentication; + }; + + $this->assertInstanceOf(HasMany::class, $model->webAuthnCredentials()); } public function test_cycles_entity_when_no_credential_exists() @@ -165,7 +171,7 @@ public function test_adds_a_new_credential() 'created_at' => $now->toDateTimeString(), 'updated_at' => $now->toDateTimeString(), 'disabled_at' => null, - 'public_key' => base64_decode('testKey'), + 'public_key' => 'testKey', ]); }