Skip to content

Commit

Permalink
Update webauthn package client codebase to remove deprecated code
Browse files Browse the repository at this point in the history
  • Loading branch information
vokomarov committed Aug 7, 2024
1 parent 70ab65a commit 1ece351
Show file tree
Hide file tree
Showing 16 changed files with 648 additions and 271 deletions.
13 changes: 7 additions & 6 deletions app/config/passkey.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

declare(strict_types = 1);

use Cose\Algorithms;
use Webauthn\PublicKeyCredentialParameters;
use Cose\Algorithm\Signature\ECDSA\ES256K;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\RSA\RS256;

return [
'service' => [
'id' => env('AUTH_PASSKEY_SERVICE_ID', 'cash-track.app'),
'name' => env('AUTH_PASSKEY_SERVICE_NAME', 'Cash Track'),
],
'timeout' => (int) env('AUTH_PASSKEY_TIMEOUT', '300000'), // 5 minutes by default
'supported' => [
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256K),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256), // "ES256" as registered in the IANA COSE Algorithms registry
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS256), // Value registered by the specification for "RS256"
'algorithms' => [
ES256K::create(),
ES256::create(), // "ES256" as registered in the IANA COSE Algorithms registry
RS256::create(), // Value registered by the specification for "RS256"
],
];
2 changes: 1 addition & 1 deletion app/src/Auth/Jwt/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function getPayload(): array
public static function fromPayload(string $id, array $payload): Token
{
$expiresAt = null;
if ($payload['exp'] ?? null !== null) {
if (($payload['exp'] ?? null) !== null) {
$expiresAt = (new \DateTimeImmutable())->setTimestamp($payload['exp']);
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/Bootloader/RedisBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protected function resolve(): Redis
throw new \RedisException("Unable to connect to Redis");
}

if (! $redis->ping()) {
if ($redis->ping() === false) {
$this->logger->emergency("PING to a Redis instance is not successful [{$uri}]: {$redis->getLastError()}");

throw new \RedisException("Unable to connect to Redis");
Expand Down
19 changes: 16 additions & 3 deletions app/src/Config/PasskeyConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace App\Config;

use Cose\Algorithm\Algorithm;
use Spiral\Core\InjectableConfig;
use Webauthn\PublicKeyCredentialParameters;

class PasskeyConfig extends InjectableConfig
{
Expand All @@ -20,7 +22,7 @@ class PasskeyConfig extends InjectableConfig
'name' => '',
],
'timeout' => 0,
'supported' => [],
'algorithms' => [],
];

public function getServiceId(): string
Expand All @@ -45,8 +47,19 @@ public function getTimeout(): int
/**
* @return \Webauthn\PublicKeyCredentialParameters[]
*/
public function getSupported(): array
public function getSupportedPublicKeyCredentials(): array
{
return $this->config['supported'] ?? [];
return array_map(
fn(Algorithm $algorithm) => PublicKeyCredentialParameters::create('public-key', $algorithm::identifier()),
$this->config['algorithms'] ?? [],
);
}

/**
* @return \Cose\Algorithm\Algorithm[]
*/
public function getSupportedAlgorithms(): array
{
return $this->config['algorithms'] ?? [];
}
}
8 changes: 4 additions & 4 deletions app/src/Repository/ChargeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function totalByWalletPK(int $walletId, string $type = null): float
{
$query = $this->select()->where('wallet_id', $walletId);

if (! empty($type)) {
if ($type !== null && $type !== '') {
$query = $query->where('type', $type);
}

Expand All @@ -150,7 +150,7 @@ public function totalByWalletPKAndTagPKs(int $walletId, array $tagIds, string $t
'method' => AbstractLoader::LEFT_JOIN,
])->where('tags.id', 'in', new Parameter($tagIds));

if (! empty($type)) {
if ($type !== null && $type !== '') {
$query = $query->where('type', $type);
}

Expand Down Expand Up @@ -185,7 +185,7 @@ public function totalByTagPK(int $tagId, string $type = null): float
'method' => AbstractLoader::LEFT_JOIN,
])->where('tags.id', $tagId);

if (! empty($type)) {
if ($type !== null && $type !== '') {
$query = $query->where('type', $type);
}

Expand All @@ -207,7 +207,7 @@ public function totalByWalletPKGroupByTagPKs(int $walletID, array $tagIDs, strin
'method' => AbstractLoader::LEFT_JOIN,
])->where('tags.id', 'in', new Parameter($tagIDs));

if (! empty($type)) {
if ($type !== null && $type !== '') {
$query = $query->where('type', $type);
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/Service/Auth/GoogleAuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function loginOrRegister(string $idToken): Authentication
throw new \RuntimeException($exception->getMessage(), (int) $exception->getCode(), $exception);
}

if ($user instanceof User && !($data['email_verified'] ?? false)) {
if ($user instanceof User && ($data['email_verified'] ?? false) === false) {
$this->logger->error('Unable to attach Google Account to existing user, Google email is not verified.', [
'data' => json_encode($data),
]);
Expand Down
13 changes: 9 additions & 4 deletions app/src/Service/Auth/Passkey/CreationChallenge.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,31 @@
namespace App\Service\Auth\Passkey;

use Illuminate\Contracts\Support\Arrayable;
use Symfony\Component\Serializer\SerializerInterface;
use Webauthn\PublicKeyCredentialCreationOptions;

/**
* @template-implements Arrayable<string, string>
* @template-implements Arrayable<string, string|null>
*/
class CreationChallenge implements Arrayable
{
public function __construct(
private readonly SerializerInterface $serializer,
public string $name,
public string $challenge,
public PublicKeyCredentialCreationOptions|null $options = null,
) {
}

public static function fromArray(array $data): CreationChallenge
public static function fromArray(array $data, SerializerInterface $serializer): CreationChallenge
{
$instance = $serializer->deserialize($data['options'] ?? '', PublicKeyCredentialCreationOptions::class, 'json');

return new CreationChallenge(
$serializer,
(string) ($data['name'] ?? ''),
(string) ($data['challenge'] ?? ''),
PublicKeyCredentialCreationOptions::createFromString($data['options'] ?? ''),
$instance,
);
}

Expand All @@ -33,7 +38,7 @@ public function toArray(): array
return [
'name' => $this->name,
'challenge' => $this->challenge,
'options' => json_encode($this->options),
'options' => $this->serializer->serialize($this->options ?? [], 'json'),
];
}
}
82 changes: 44 additions & 38 deletions app/src/Service/Auth/Passkey/PasskeyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,24 @@
use App\Service\Auth\Passkey\Response\DataEncoder;
use App\Service\Auth\Passkey\Response\PasskeyInitResponse;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\RSA\RS256;
use Cycle\ORM\EntityManagerInterface;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Redis;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Symfony\Component\Serializer\SerializerInterface;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\Denormalizer\WebauthnSerializerFactory;
use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialSource;
Expand All @@ -55,6 +54,33 @@ public function __construct(
) {
}

protected function getAlgorithmManager(): Manager
{
$manager = Manager::create();

foreach ($this->config->getSupportedAlgorithms() as $algorithm) {
$manager->add($algorithm);
}

return $manager;
}

protected function getAttestationStatementSupportManager(): AttestationStatementSupportManager
{
$manager = new AttestationStatementSupportManager();
$manager->add(NoneAttestationStatementSupport::create());

return $manager;
}

protected function getSerializer(): SerializerInterface
{
$manager = new AttestationStatementSupportManager();
$manager->add(NoneAttestationStatementSupport::create());

return (new WebauthnSerializerFactory($this->getAttestationStatementSupportManager()))->create();
}

public function initAuth(): \JsonSerializable
{
$challenge = $this->generateChallenge();
Expand Down Expand Up @@ -86,22 +112,13 @@ public function initAuth(): \JsonSerializable
*/
public function authenticate(string $challenge, string $data): User
{
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());

$attestationObjectLoader = AttestationObjectLoader::create(
$attestationStatementSupportManager
);

$options = $this->getRequestOptions($challenge);
if (! ($options instanceof RequestChallenge) || ! ($options->options instanceof PublicKeyCredentialRequestOptions)) {
throw new InvalidChallengeException('Invalid challenge');
}

$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);

try {
$credential = $publicKeyCredentialLoader->loadArray(self::decode($data));
$credential = $this->getSerializer()->deserialize(self::decode($data), PublicKeyCredential::class, 'json');
$credential->response instanceof AuthenticatorAssertionResponse || throw new \RuntimeException('Invalid client response');
} catch (\Throwable $exception) {
throw new InvalidClientResponseException(
Expand All @@ -111,23 +128,18 @@ public function authenticate(string $challenge, string $data): User
);
}

$algorithmManager = Manager::create()->add(
ES256::create(),
RS256::create(),
);

$passkey = $this->repository->findKeyByCredential($credential);
if (!$passkey instanceof Passkey || $passkey->data === '') {
throw new PasskeyNotFoundException('Unregistered passkey');
}

$existingCredentialSource = PublicKeyCredentialSource::createFromArray($passkey->getData());
$existingCredentialSource = $this->getSerializer()->deserialize($passkey->data, PublicKeyCredentialSource::class, 'json');

$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create(
null,
null,
ExtensionOutputCheckerHandler::create(),
$algorithmManager
$this->getAlgorithmManager()
);

$credentialSource = $authenticatorAssertionResponseValidator->check(
Expand All @@ -151,20 +163,14 @@ public function authenticate(string $challenge, string $data): User

public function store(User $user, string $challenge, string $data): Passkey
{
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());

$attestationObjectLoader = AttestationObjectLoader::create($attestationStatementSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);

$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
$attestationStatementSupportManager,
$this->getAttestationStatementSupportManager(),
null,
null,
ExtensionOutputCheckerHandler::create(),
);

$credential = $publicKeyCredentialLoader->loadArray(self::decode($data));
$credential = $this->getSerializer()->deserialize(self::decode($data), PublicKeyCredential::class, 'json');
$credential->response instanceof AuthenticatorAttestationResponse || throw new \RuntimeException('Invalid response data');

$creationOptions = $this->getCreationOptions($challenge);
Expand Down Expand Up @@ -221,7 +227,7 @@ public function init(User $user, string $keyName): \JsonSerializable
rp: $this->getRelyingParty(),
user: $this->getUserEntity($user),
challenge: $challenge,
pubKeyCredParams: $this->config->getSupported(),
pubKeyCredParams: $this->config->getSupportedPublicKeyCredentials(),
authenticatorSelection: $this->getAuthenticatorSelectionCriteria(),
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
excludeCredentials: $this->getExistingCredentials($user),
Expand All @@ -247,7 +253,7 @@ protected function generateChallenge(): string

protected function storeCreationOptions(string $name, string $challenge, PublicKeyCredentialCreationOptions $options): void
{
$creation = new CreationChallenge($name, $challenge, $options);
$creation = new CreationChallenge($this->getSerializer(), $name, $challenge, $options);

$cacheKey = $this->challengeCacheKey($challenge);

Expand All @@ -257,7 +263,7 @@ protected function storeCreationOptions(string $name, string $challenge, PublicK

protected function storeRequestOptions(string $challenge, PublicKeyCredentialRequestOptions $options): void
{
$request = new RequestChallenge($challenge, $options);
$request = new RequestChallenge($this->getSerializer(), $challenge, $options);

$cacheKey = $this->challengeCacheKey($challenge);

Expand All @@ -278,7 +284,7 @@ protected function getCreationOptions(string $challenge): ?CreationChallenge
return null;
}

return CreationChallenge::fromArray($data);
return CreationChallenge::fromArray($data, $this->getSerializer());
}

protected function getRequestOptions(string $challenge): ?RequestChallenge
Expand All @@ -288,7 +294,7 @@ protected function getRequestOptions(string $challenge): ?RequestChallenge
return null;
}

return RequestChallenge::fromArray($data);
return RequestChallenge::fromArray($data, $this->getSerializer());
}

protected function challengeCacheKey(string $challenge): string
Expand Down Expand Up @@ -344,10 +350,10 @@ protected function getAuthenticatorSelectionCriteria(): AuthenticatorSelectionCr
);
}

protected function getExtensions(): AuthenticationExtensionsClientInputs
protected function getExtensions(): AuthenticationExtensions
{
return AuthenticationExtensionsClientInputs::create([
AuthenticationExtension::create('credProps', true),
return AuthenticationExtensions::create([
AuthenticationExtension::create('credProps', true)
]);
}
}
Loading

0 comments on commit 1ece351

Please sign in to comment.