First release
This commit is contained in:
13
src/Assertion/Validator/Pipes/CheckChallengeSame.php
Normal file
13
src/Assertion/Validator/Pipes/CheckChallengeSame.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckChallengeSame as BaseChallengeSame;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckChallengeSame extends BaseChallengeSame
|
||||
{
|
||||
//
|
||||
}
|
||||
79
src/Assertion/Validator/Pipes/CheckCredentialIsForUser.php
Normal file
79
src/Assertion/Validator/Pipes/CheckCredentialIsForUser.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function hash_equals;
|
||||
|
||||
/**
|
||||
* 6. Identify the user being authenticated and verify that this user is the owner of the public
|
||||
* key credential source credentialSource identified by credential.id:
|
||||
*
|
||||
* - If the user was identified before the authentication ceremony was initiated, e.g., via a
|
||||
* username or cookie, verify that the identified user is the owner of credentialSource. If
|
||||
* response.userHandle is present, let userHandle be its value. Verify that userHandle also
|
||||
* maps to the same user.
|
||||
*
|
||||
* - If the user was not identified before the authentication ceremony was initiated, verify
|
||||
* that response.userHandle is present, and that the user identified by this value is the
|
||||
* owner of credentialSource.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckCredentialIsForUser
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($validation->user) {
|
||||
$this->validateUser($validation);
|
||||
|
||||
if ($validation->request->json('response.userHandle')) {
|
||||
$this->validateId($validation);
|
||||
}
|
||||
} else {
|
||||
$this->validateId($validation);
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user owns the Credential if it already exists in the validation procedure.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return void
|
||||
*/
|
||||
protected function validateUser(AssertionValidation $validation): void
|
||||
{
|
||||
if ($validation->credential->authenticatable()->isNot($validation->user)) {
|
||||
throw AssertionException::make('User is not owner of the stored credential.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user ID of the response.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return void
|
||||
*/
|
||||
protected function validateId(AssertionValidation $validation): void
|
||||
{
|
||||
$handle = $validation->request->json('response.userHandle');
|
||||
|
||||
if (! $handle || ! hash_equals(Uuid::fromString($validation->credential->user_id)->getHex()->toString(), $handle)) {
|
||||
throw AssertionException::make('User ID is not owner of the stored credential.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckCredentialIsWebAuthnGet
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($validation->clientDataJson->type !== 'webauthn.get') {
|
||||
throw AssertionException::make('Client Data type is not [webauthn.get].');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
}
|
||||
15
src/Assertion/Validator/Pipes/CheckOriginSecure.php
Normal file
15
src/Assertion/Validator/Pipes/CheckOriginSecure.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckOriginSecure as BaseCheckOriginSame;
|
||||
|
||||
/**
|
||||
* 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckOriginSecure extends BaseCheckOriginSame
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Events\CredentialCloned;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
|
||||
/**
|
||||
* 21. Let storedSignCount be the stored signature counter value associated with credential.id.
|
||||
* If authData.signCount is nonzero or storedSignCount is nonzero, then run the following sub-step:
|
||||
*
|
||||
* - If authData.signCount
|
||||
* -> is greater than storedSignCount:
|
||||
* Update storedSignCount to be the value of authData.signCount.
|
||||
* -> less than or equal to storedSignCount:
|
||||
* This is a signal that the authenticator may be cloned, i.e. at least two copies of the
|
||||
* credential private key may exist and are being used in parallel. Relying Parties
|
||||
* should incorporate this information into their risk scoring. Whether the Relying
|
||||
* Party updates storedSignCount in this case, or not, or fails the authentication
|
||||
* ceremony or not, is Relying Party-specific.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckPublicKeyCounterCorrect
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($this->hasCounter($validation) && $this->counterBelowStoredCredential($validation)) {
|
||||
$validation->credential->disable();
|
||||
|
||||
CredentialCloned::dispatch($validation->credential, $validation->authenticatorData->counter);
|
||||
|
||||
throw AssertionException::make('Credential counter not over stored counter.');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the incoming credential or the stored credential have a counter.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasCounter(AssertionValidation $validation): bool
|
||||
{
|
||||
return $validation->credential->counter
|
||||
|| $validation->authenticatorData->counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the credential counter is equal or higher than what the authenticator reports.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return bool
|
||||
*/
|
||||
protected function counterBelowStoredCredential(AssertionValidation $validation): bool
|
||||
{
|
||||
return $validation->authenticatorData->counter <= $validation->credential->counter;
|
||||
}
|
||||
}
|
||||
68
src/Assertion/Validator/Pipes/CheckPublicKeySignature.php
Normal file
68
src/Assertion/Validator/Pipes/CheckPublicKeySignature.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use OpenSSLAsymmetricKey;
|
||||
use function base64_decode;
|
||||
use function hash;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_verify;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckPublicKeySignature
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$publicKey = openssl_pkey_get_public($validation->credential->public_key);
|
||||
|
||||
if (!$publicKey) {
|
||||
throw AssertionException::make('Stored Public Key is invalid.');
|
||||
}
|
||||
|
||||
$signature = base64_decode($validation->request->json('response.signature', ''));
|
||||
|
||||
if (!$signature) {
|
||||
throw AssertionException::make('Signature is empty.');
|
||||
}
|
||||
|
||||
$this->validateSignature($validation, $publicKey, $signature);
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the signature from the assertion.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param string $signature
|
||||
* @param \OpenSSLAsymmetricKey $publicKey
|
||||
* @return void
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function validateSignature(
|
||||
AssertionValidation $validation,
|
||||
OpenSSLAsymmetricKey $publicKey,
|
||||
string $signature
|
||||
): void {
|
||||
$verifiable = base64_decode($validation->request->json('response.authenticatorData'))
|
||||
.hash('sha256', base64_decode($validation->request->json('response.clientDataJSON')), true);
|
||||
|
||||
if (openssl_verify($verifiable, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||
throw AssertionException::make('Signature is invalid.');
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Assertion/Validator/Pipes/CheckRelyingPartyHashSame.php
Normal file
36
src/Assertion/Validator/Pipes/CheckRelyingPartyHashSame.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\SharedPipes\CheckRelyingPartyHashSame as BaseCheckRelyingPartyHashSame;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckRelyingPartyHashSame extends BaseCheckRelyingPartyHashSame
|
||||
{
|
||||
/**
|
||||
* Return the Attestation data to check the RP ID Hash.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation|\Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return \Laragear\WebAuthn\Attestation\AuthenticatorData
|
||||
*/
|
||||
protected function authenticatorData(AssertionValidation|AttestationValidation $validation): AuthenticatorData
|
||||
{
|
||||
return $validation->authenticatorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Relying Party ID from the config or credential.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation|\Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @return string
|
||||
*/
|
||||
protected function relyingPartyId(AssertionValidation|AttestationValidation $validation): string
|
||||
{
|
||||
return $validation->credential->rp_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckRelyingPartyIdContained as BaseCheckRelyingPartyIdContained;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckRelyingPartyIdContained extends BaseCheckRelyingPartyIdContained
|
||||
{
|
||||
//
|
||||
}
|
||||
30
src/Assertion/Validator/Pipes/CheckTypeIsPublicKey.php
Normal file
30
src/Assertion/Validator/Pipes/CheckTypeIsPublicKey.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckTypeIsPublicKey
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($validation->request->json('type') !== 'public-key') {
|
||||
throw AssertionException::make('Response type is not [public-key].');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
}
|
||||
13
src/Assertion/Validator/Pipes/CheckUserInteraction.php
Normal file
13
src/Assertion/Validator/Pipes/CheckUserInteraction.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckUserInteraction as BaseCheckUserInteraction;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CheckUserInteraction extends BaseCheckUserInteraction
|
||||
{
|
||||
//
|
||||
}
|
||||
41
src/Assertion/Validator/Pipes/CompileAuthenticatorData.php
Normal file
41
src/Assertion/Validator/Pipes/CompileAuthenticatorData.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use Laragear\WebAuthn\Exceptions\DataException;
|
||||
use function base64_decode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CompileAuthenticatorData
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$data = base64_decode($validation->request->json('response.authenticatorData', ''));
|
||||
|
||||
if (!$data) {
|
||||
throw AssertionException::make('Authenticator Data does not exist or is empty.');
|
||||
}
|
||||
|
||||
try {
|
||||
$validation->authenticatorData = AuthenticatorData::fromBinary($data);
|
||||
} catch (DataException $e) {
|
||||
throw AssertionException::make($e->getMessage());
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
}
|
||||
13
src/Assertion/Validator/Pipes/CompileClientDataJson.php
Normal file
13
src/Assertion/Validator/Pipes/CompileClientDataJson.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CompileClientDataJson as BaseCompileClientDataJson;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CompileClientDataJson extends BaseCompileClientDataJson
|
||||
{
|
||||
//
|
||||
}
|
||||
39
src/Assertion/Validator/Pipes/IncrementCredentialCounter.php
Normal file
39
src/Assertion/Validator/Pipes/IncrementCredentialCounter.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
|
||||
/**
|
||||
* 21. Let storedSignCount be the stored signature counter value associated with credential.id.
|
||||
* If authData.signCount is nonzero or storedSignCount is nonzero, then run the following sub-step:
|
||||
*
|
||||
* - If authData.signCount
|
||||
* -> is greater than storedSignCount:
|
||||
* Update storedSignCount to be the value of authData.signCount.
|
||||
* -> less than or equal to storedSignCount:
|
||||
* This is a signal that the authenticator may be cloned, i.e. at least two copies of the
|
||||
* credential private key may exist and are being used in parallel. Relying Parties
|
||||
* should incorporate this information into their risk scoring. Whether the Relying
|
||||
* Party updates storedSignCount in this case, or not, or fails the authentication
|
||||
* ceremony or not, is Relying Party-specific.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IncrementCredentialCounter
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$validation->credential->syncCounter($validation->authenticatorData->counter);
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
}
|
||||
13
src/Assertion/Validator/Pipes/RetrieveChallenge.php
Normal file
13
src/Assertion/Validator/Pipes/RetrieveChallenge.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\RetrieveChallenge as BaseRetrieveChallenge;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RetrieveChallenge extends BaseRetrieveChallenge
|
||||
{
|
||||
///
|
||||
}
|
||||
58
src/Assertion/Validator/Pipes/RetrievesCredentialId.php
Normal file
58
src/Assertion/Validator/Pipes/RetrievesCredentialId.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Assertion\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AssertionException;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RetrievesCredentialId
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Assertion Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AssertionException
|
||||
*/
|
||||
public function handle(AssertionValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$id = $validation->request->json('id');
|
||||
|
||||
// First, always check the challenge credentials before finding the real one.
|
||||
if ($this->credentialNotInChallenge($id, $validation->challenge->properties)) {
|
||||
throw AssertionException::make('Credential is not on accepted list.');
|
||||
}
|
||||
|
||||
// We can now find the credential.
|
||||
$validation->credential = WebAuthnCredential::whereKey($id)->first();
|
||||
|
||||
if (!$validation->credential) {
|
||||
throw AssertionException::make('Credential ID does not exist.');
|
||||
}
|
||||
|
||||
if ($validation->credential->isDisabled()) {
|
||||
throw AssertionException::make('Credential ID is blacklisted.');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the previous Assertion request specified a credentials list to accept.
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $properties
|
||||
* @return bool
|
||||
*/
|
||||
protected function credentialNotInChallenge(string $id, array $properties): bool
|
||||
{
|
||||
return isset($properties['credentials']) && ! in_array($id, $properties['credentials'], true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user