Skip to content

Commit 83719c2

Browse files
authored
Merge pull request #119 from chav170/feature/port-2fa-features-2
Feature/port 2fa features 2
2 parents e7012e2 + 308ffc2 commit 83719c2

File tree

9 files changed

+383
-62
lines changed

9 files changed

+383
-62
lines changed

composer.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"php": ">=8.1",
3131
"ext-json": "*",
3232
"cakephp/cakephp": "^5.0",
33-
"cakedc/users": "^14.3",
33+
"cakedc/users": "^14.3.5",
3434
"lcobucci/jwt": "~4.0.0",
3535
"firebase/php-jwt": "^6.3"
3636
},
@@ -43,7 +43,7 @@
4343
"phpstan/phpstan": "^1.8",
4444
"robthree/twofactorauth": "^1.6",
4545
"vlucas/phpdotenv": "^3.3",
46-
"web-auth/webauthn-lib": "^5.0"
46+
"web-auth/webauthn-lib": "^4.4"
4747
},
4848
"autoload": {
4949
"psr-4": {
@@ -58,7 +58,6 @@
5858
}
5959
},
6060
"prefer-stable": true,
61-
"minimum-stability": "dev",
6261
"config": {
6362
"sort-packages": true,
6463
"allow-plugins": {

src/Webauthn/AuthenticateAdapter.php

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
namespace CakeDC\Api\Webauthn;
1515

16-
use Cake\Log\Log;
16+
use Cake\Http\Exception\BadRequestException;
17+
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
18+
use Webauthn\AuthenticatorAssertionResponse;
19+
use Webauthn\AuthenticatorAssertionResponseValidator;
1720
use Webauthn\PublicKeyCredentialRequestOptions;
1821
use Webauthn\PublicKeyCredentialSource;
1922

@@ -25,22 +28,20 @@ class AuthenticateAdapter extends BaseAdapter
2528
public function getOptions(): PublicKeyCredentialRequestOptions
2629
{
2730
$userEntity = $this->getUserEntity();
28-
$allowed = array_map(function (PublicKeyCredentialSource $credential) {
31+
$allowedCredentials = array_map(function (PublicKeyCredentialSource $credential) {
2932
return $credential->getPublicKeyCredentialDescriptor();
3033
}, $this->repository->findAllForUserEntity($userEntity));
31-
Log::error(print_r($allowed, true));
3234

33-
$options = $this->server->generatePublicKeyCredentialRequestOptions(
34-
PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED,
35-
$allowed
36-
);
35+
$options = (new PublicKeyCredentialRequestOptions(random_bytes(32)))
36+
->setRpId($this->rpEntity->getId())
37+
->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED)
38+
->allowCredentials(...$allowedCredentials)
39+
->setExtensions(new AuthenticationExtensionsClientInputs());
40+
3741
$storeEntity = $this->readStore();
38-
Log::error(print_r($storeEntity, true));
3942
$storeEntity['store'] = [];
4043
$storeEntity = $this->patchStore($storeEntity, 'authenticateOptions', base64_encode(serialize($options)));
4144
$res = $this->store->save($storeEntity);
42-
Log::error(print_r($storeEntity, true));
43-
Log::error(print_r($res, true));
4445

4546
return $options;
4647
}
@@ -49,31 +50,44 @@ public function getOptions(): PublicKeyCredentialRequestOptions
4950
* Verify the registration response
5051
*
5152
* @return \Webauthn\PublicKeyCredentialSource
53+
* @throws \Throwable
5254
*/
5355
public function verifyResponse(): \Webauthn\PublicKeyCredentialSource
5456
{
5557
$storeEntity = $this->readStore();
56-
Log::error(print_r($storeEntity, true));
5758
$options = $this->getStore($storeEntity, 'authenticateOptions');
5859
if ($options) {
5960
$options = unserialize(base64_decode($options));
6061
}
61-
Log::error(print_r($options, true));
6262

63-
return $this->loadAndCheckAssertionResponse($options);
63+
$publicKeyCredentialLoader = $this->createPublicKeyCredentialLoader();
64+
$publicKeyCredential = $publicKeyCredentialLoader->loadArray($this->request->getData());
65+
$authenticatorAssertionResponse = $publicKeyCredential->getResponse();
66+
if ($authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
67+
$authenticatorAssertionResponseValidator = $this->createAssertionResponseValidator();
68+
69+
return $authenticatorAssertionResponseValidator->check(
70+
$publicKeyCredential->getRawId(),
71+
$authenticatorAssertionResponse,
72+
$options,
73+
$this->request,
74+
$this->getUserEntity()->getId(),
75+
);
76+
}
77+
78+
throw new BadRequestException(__d('cake_d_c/api', 'Could not validate credential response for authentication'));
6479
}
6580

6681
/**
67-
* @param \Webauthn\PublicKeyCredentialRequestOptions $options request options
68-
* @return \Webauthn\PublicKeyCredentialSource
82+
* @return \Webauthn\AuthenticatorAssertionResponseValidator
6983
*/
70-
protected function loadAndCheckAssertionResponse($options): PublicKeyCredentialSource
84+
protected function createAssertionResponseValidator(): AuthenticatorAssertionResponseValidator
7185
{
72-
return $this->server->loadAndCheckAssertionResponse(
73-
json_encode($this->request->getData()),
74-
$options,
75-
$this->getUserEntity(),
76-
$this->request
86+
return new AuthenticatorAssertionResponseValidator(
87+
$this->repository,
88+
null,
89+
$this->createExtensionOutputCheckerHandler(),
90+
$this->getAlgorithmManager()
7791
);
7892
}
7993
}

src/Webauthn/Base64Utility.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2010 - 2023, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2010 - 2023, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\Api\Webauthn;
15+
16+
use InvalidArgumentException;
17+
use ParagonIE\ConstantTime\Base64UrlSafe;
18+
19+
/**
20+
* Base64Utility class
21+
*/
22+
class Base64Utility
23+
{
24+
/**
25+
* @param string $data The data to encode
26+
* @return string
27+
*/
28+
public static function complyEncodedNoPadding(string $data): string
29+
{
30+
return Base64UrlSafe::encodeUnpadded(static::basicDecode($data));
31+
}
32+
33+
/**
34+
* @param string $data The data to encode
35+
* @param bool $usePadding If true, the "=" padding at end of the encoded value are kept, else it is removed
36+
* @return string The data encoded
37+
*/
38+
public static function basicEncode(string $data, bool $usePadding = false): string
39+
{
40+
$encoded = strtr(base64_encode($data), '+/', '-_');
41+
42+
return $usePadding === true ? $encoded : rtrim($encoded, '=');
43+
}
44+
45+
/**
46+
* @param string $data The data to decode
47+
* @throws \InvalidArgumentException
48+
* @return string The data decoded
49+
*/
50+
public static function basicDecode(string $data): string
51+
{
52+
$decoded = base64_decode(strtr($data, '-_', '+/'), true);
53+
if ($decoded === false) {
54+
throw new InvalidArgumentException('Invalid data provided');
55+
}
56+
57+
return $decoded;
58+
}
59+
}

src/Webauthn/BaseAdapter.php

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,25 @@
2020
use CakeDC\Api\Utility\RequestParser;
2121
use CakeDC\Api\Webauthn\Repository\UserCredentialSourceRepository;
2222
use CakeDC\Users\Model\Table\UsersTable;
23+
use Cose\Algorithm\Manager;
24+
use Cose\Algorithm\Signature\ECDSA\ES256;
25+
use Cose\Algorithm\Signature\ECDSA\ES256K;
26+
use Cose\Algorithm\Signature\ECDSA\ES384;
27+
use Cose\Algorithm\Signature\ECDSA\ES512;
28+
use Cose\Algorithm\Signature\EdDSA\Ed256;
29+
use Cose\Algorithm\Signature\EdDSA\Ed512;
30+
use Cose\Algorithm\Signature\RSA\PS256;
31+
use Cose\Algorithm\Signature\RSA\PS384;
32+
use Cose\Algorithm\Signature\RSA\PS512;
33+
use Cose\Algorithm\Signature\RSA\RS256;
34+
use Cose\Algorithm\Signature\RSA\RS384;
35+
use Cose\Algorithm\Signature\RSA\RS512;
36+
use Webauthn\AttestationStatement\AttestationObjectLoader;
37+
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
38+
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
39+
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
2340
use Webauthn\PublicKeyCredentialRpEntity;
2441
use Webauthn\PublicKeyCredentialUserEntity;
25-
use Webauthn\Server;
2642

2743
class BaseAdapter
2844
{
@@ -39,20 +55,30 @@ class BaseAdapter
3955
protected $repository;
4056

4157
/**
42-
* @var \Webauthn\Server
58+
* @var \Cake\Datasource\EntityInterface|\CakeDC\Users\Model\Entity\User
4359
*/
44-
protected $server;
60+
private $user;
4561

4662
/**
47-
* @var \Cake\Datasource\EntityInterface|\CakeDC\Users\Model\Entity\User
63+
* @var \Webauthn\PublicKeyCredentialRpEntity
4864
*/
49-
private $user;
65+
protected PublicKeyCredentialRpEntity $rpEntity;
5066

5167
/**
5268
* @var \CakeDC\Api\Model\Table\AuthStoreTable
5369
*/
5470
protected $store;
5571

72+
/**
73+
* @var \Webauthn\AttestationStatement\AttestationStatementSupportManager|null
74+
*/
75+
protected ?AttestationStatementSupportManager $attestationStatementSupportManager = null;
76+
77+
/**
78+
* @var \Cose\Algorithm\Manager|null
79+
*/
80+
protected ?Manager $algorithmManager = null;
81+
5682
/**
5783
* Constructor.
5884
*
@@ -67,7 +93,7 @@ public function __construct(ServerRequest $request, ?UsersTable $usersTable, $us
6793
$store = TableRegistry::getTableLocator()->get('CakeDC/Api.AuthStore');
6894
$this->store = $store;
6995
$session = $this->readStore();
70-
$rpEntity = new PublicKeyCredentialRpEntity(
96+
$this->rpEntity = new PublicKeyCredentialRpEntity(
7197
Configure::read('Api.Webauthn2fa.' . $this->getDomain() . '.appName'), // The application name
7298
Configure::read('Api.Webauthn2fa.' . $this->getDomain() . '.id')
7399
);
@@ -81,11 +107,6 @@ public function __construct(ServerRequest $request, ?UsersTable $usersTable, $us
81107
$this->user,
82108
$usersTable
83109
);
84-
85-
$this->server = new Server(
86-
$rpEntity,
87-
$this->repository
88-
);
89110
}
90111

91112
/**
@@ -107,7 +128,7 @@ protected function getUserEntity(): PublicKeyCredentialUserEntity
107128
/**
108129
* Get the user.
109130
*
110-
* @return array|mixed|null
131+
* @return mixed|array|null
111132
*/
112133
public function getUser()
113134
{
@@ -229,4 +250,75 @@ public function getDomain($replace = true)
229250
{
230251
return RequestParser::getDomain($this->request, $replace);
231252
}
253+
254+
/**
255+
* @param \Webauthn\AttestationStatement\AttestationStatementSupportManager $attestationStatementSupportManager manager instance
256+
* @return void
257+
*/
258+
public function setAttestationStatementSupportManager(
259+
AttestationStatementSupportManager $attestationStatementSupportManager
260+
): void {
261+
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
262+
}
263+
264+
/**
265+
* @return \Webauthn\AttestationStatement\AttestationStatementSupportManager
266+
*/
267+
protected function getAttestationStatementSupportManager(): AttestationStatementSupportManager
268+
{
269+
if ($this->attestationStatementSupportManager === null) {
270+
$this->attestationStatementSupportManager = new AttestationStatementSupportManager();
271+
$this->attestationStatementSupportManager
272+
->add(new NoneAttestationStatementSupport());
273+
}
274+
275+
return $this->attestationStatementSupportManager;
276+
}
277+
278+
/**
279+
* @return \CakeDC\Api\Webauthn\PublicKeyCredentialLoader
280+
*/
281+
protected function createPublicKeyCredentialLoader(): PublicKeyCredentialLoader
282+
{
283+
$attestationObjectLoader = new AttestationObjectLoader(
284+
$this->getAttestationStatementSupportManager()
285+
);
286+
287+
return new PublicKeyCredentialLoader(
288+
$attestationObjectLoader
289+
);
290+
}
291+
292+
/**
293+
* @return \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler
294+
*/
295+
protected function createExtensionOutputCheckerHandler(): ExtensionOutputCheckerHandler
296+
{
297+
return new ExtensionOutputCheckerHandler();
298+
}
299+
300+
/**
301+
* @return \Cose\Algorithm\Manager
302+
*/
303+
protected function getAlgorithmManager(): Manager
304+
{
305+
if ($this->algorithmManager === null) {
306+
$this->algorithmManager = Manager::create()->add(
307+
ES256::create(),
308+
ES256K::create(),
309+
ES384::create(),
310+
ES512::create(),
311+
RS256::create(),
312+
RS384::create(),
313+
RS512::create(),
314+
PS256::create(),
315+
PS384::create(),
316+
PS512::create(),
317+
Ed256::create(),
318+
Ed512::create(),
319+
);
320+
}
321+
322+
return $this->algorithmManager;
323+
}
232324
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2010 - 2023, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2010 - 2023, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\Api\Webauthn;
15+
16+
use Webauthn\PublicKeyCredential;
17+
18+
class PublicKeyCredentialLoader extends \Webauthn\PublicKeyCredentialLoader
19+
{
20+
/**
21+
* @inheritDoc
22+
*/
23+
public function loadArray(array $json): PublicKeyCredential
24+
{
25+
if (isset($json['response']['clientDataJSON']) && is_string($json['response']['clientDataJSON'])) {
26+
$json['response']['clientDataJSON'] =
27+
Base64Utility::complyEncodedNoPadding($json['response']['clientDataJSON']);
28+
}
29+
30+
if (isset($json['response']['authenticatorData']) && is_string($json['response']['authenticatorData'])) {
31+
$json['response']['authenticatorData'] =
32+
Base64Utility::complyEncodedNoPadding($json['response']['authenticatorData']);
33+
}
34+
35+
return parent::loadArray($json);
36+
}
37+
}

0 commit comments

Comments
 (0)