From e38f9b77a2137540474dc3086e583d2408c2c054 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Fri, 6 Oct 2023 00:04:17 +0300 Subject: [PATCH 01/16] 2fa for cakephp api --- .../Migrations/20231003201241_auth_store.php | 34 ++++ src/Model/Entity/AuthStore.php | 33 ++++ src/Model/Table/AuthStoreTable.php | 78 ++++++++ src/Rbac/Rules/TwoFactorPassedScope.php | 49 +++++ src/Rbac/Rules/TwoFactorScope.php | 49 +++++ src/Service/Action/Auth/JwtLoginAction.php | 2 +- src/Service/Action/Auth/JwtRefreshAction.php | 6 +- src/Service/Action/Auth/JwtTokenTrait.php | 42 +++- src/Service/Action/Auth/OtpVerifyAction.php | 82 ++++++++ .../Action/Auth/OtpVerifyCheckAction.php | 61 ++++++ .../Action/Auth/OtpVerifyGetAction.php | 74 +++++++ .../Action/Auth/TwoFactorAuthAction.php | 49 +++++ src/Service/Action/Auth/Webauthn2faAction.php | 45 +++++ .../Action/Auth/Webauthn2faAuthAction.php | 51 +++++ .../Auth/Webauthn2faAuthOptionsAction.php | 42 ++++ .../Action/Auth/Webauthn2faRegisterAction.php | 51 +++++ .../Auth/Webauthn2faRegisterOptionsAction.php | 49 +++++ src/Service/AuthService.php | 23 +++ src/Webauthn/AuthenticateAdapter.php | 79 ++++++++ src/Webauthn/BaseAdapter.php | 186 ++++++++++++++++++ src/Webauthn/RegisterAdapter.php | 72 +++++++ .../UserCredentialSourceRepository.php | 120 +++++++++++ 22 files changed, 1266 insertions(+), 11 deletions(-) create mode 100644 config/Migrations/20231003201241_auth_store.php create mode 100644 src/Model/Entity/AuthStore.php create mode 100644 src/Model/Table/AuthStoreTable.php create mode 100644 src/Rbac/Rules/TwoFactorPassedScope.php create mode 100644 src/Rbac/Rules/TwoFactorScope.php create mode 100644 src/Service/Action/Auth/OtpVerifyAction.php create mode 100644 src/Service/Action/Auth/OtpVerifyCheckAction.php create mode 100644 src/Service/Action/Auth/OtpVerifyGetAction.php create mode 100644 src/Service/Action/Auth/TwoFactorAuthAction.php create mode 100644 src/Service/Action/Auth/Webauthn2faAction.php create mode 100644 src/Service/Action/Auth/Webauthn2faAuthAction.php create mode 100644 src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php create mode 100644 src/Service/Action/Auth/Webauthn2faRegisterAction.php create mode 100644 src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php create mode 100644 src/Webauthn/AuthenticateAdapter.php create mode 100644 src/Webauthn/BaseAdapter.php create mode 100644 src/Webauthn/RegisterAdapter.php create mode 100644 src/Webauthn/Repository/UserCredentialSourceRepository.php diff --git a/config/Migrations/20231003201241_auth_store.php b/config/Migrations/20231003201241_auth_store.php new file mode 100644 index 0000000..fcc98ec --- /dev/null +++ b/config/Migrations/20231003201241_auth_store.php @@ -0,0 +1,34 @@ +table('auth_store', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'string', [ + 'limit' => 500, + 'null' => false, + ]) + ->addColumn('store', 'text', [ + 'null' => true, + ]) + ->addColumn('created', 'datetime', [ + 'null' => false, + ]) + ->addColumn('modified', 'datetime', [ + 'null' => false, + ]) + ->create(); + } +} diff --git a/src/Model/Entity/AuthStore.php b/src/Model/Entity/AuthStore.php new file mode 100644 index 0000000..dc1a323 --- /dev/null +++ b/src/Model/Entity/AuthStore.php @@ -0,0 +1,33 @@ + + */ + protected $_accessible = [ + 'id' => true, + 'store' => true, + 'created' => true, + 'modified' => true, + ]; +} diff --git a/src/Model/Table/AuthStoreTable.php b/src/Model/Table/AuthStoreTable.php new file mode 100644 index 0000000..ff90533 --- /dev/null +++ b/src/Model/Table/AuthStoreTable.php @@ -0,0 +1,78 @@ +setTable('auth_store'); + $this->setDisplayField('id'); + $this->setPrimaryKey('id'); + + $this->addBehavior('Timestamp'); + } + + /** + * Default validation rules. + * + * @param \Cake\Validation\Validator $validator Validator instance. + * @return \Cake\Validation\Validator + */ + public function validationDefault(Validator $validator): Validator + { + $validator + ->scalar('store') + ->allowEmptyString('store'); + + return $validator; + } + + + /** + * Field additional_data is json + * + * @param \Cake\Database\Schema\TableSchemaInterface $schema The table definition fetched from database. + * @return \Cake\Database\Schema\TableSchemaInterface the altered schema + */ + protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface + { + $schema->setColumnType('store', 'json'); + + return parent::_initializeSchema($schema); + } +} diff --git a/src/Rbac/Rules/TwoFactorPassedScope.php b/src/Rbac/Rules/TwoFactorPassedScope.php new file mode 100644 index 0000000..785a46a --- /dev/null +++ b/src/Rbac/Rules/TwoFactorPassedScope.php @@ -0,0 +1,49 @@ +getAttribute('authentication'); + if ($authentication === null) { + return false; + } + $provider = $authentication->getAuthenticationProvider(); + if ($provider === null || !($provider instanceof JwtAuthenticator)) { + return false; + } + $payload = $provider->getPayload(); + $aud = Router::url('/', true); + + return $payload->aud == $aud; + } +} diff --git a/src/Rbac/Rules/TwoFactorScope.php b/src/Rbac/Rules/TwoFactorScope.php new file mode 100644 index 0000000..e9c80f6 --- /dev/null +++ b/src/Rbac/Rules/TwoFactorScope.php @@ -0,0 +1,49 @@ +getAttribute('authentication'); + if ($authentication === null) { + return false; + } + $provider = $authentication->getAuthenticationProvider(); + if ($provider === null || !($provider instanceof JwtAuthenticator)) { + return false; + } + $payload = $provider->getPayload(); + $aud = Router::url('/2fa', true); + + return $payload->aud == $aud; + } +} diff --git a/src/Service/Action/Auth/JwtLoginAction.php b/src/Service/Action/Auth/JwtLoginAction.php index 0627714..1d79b96 100644 --- a/src/Service/Action/Auth/JwtLoginAction.php +++ b/src/Service/Action/Auth/JwtLoginAction.php @@ -39,6 +39,6 @@ protected function _afterIdentifyUser($user, $socialLogin = false) return false; } - return $this->generateTokenResponse($user); + return $this->generateTokenResponse($user, 'login'); } } diff --git a/src/Service/Action/Auth/JwtRefreshAction.php b/src/Service/Action/Auth/JwtRefreshAction.php index 3b1ef27..252c337 100644 --- a/src/Service/Action/Auth/JwtRefreshAction.php +++ b/src/Service/Action/Auth/JwtRefreshAction.php @@ -35,6 +35,8 @@ class JwtRefreshAction extends Action */ protected $user; + protected $payload; + /** * Initialize an action instance * @@ -94,7 +96,7 @@ public function validates(): bool throw new ValidationException('Invalid token provided', 401); } - $payload = $auth->getPayload(); + $this->payload = $auth->getPayload(); return true; } @@ -110,6 +112,6 @@ public function execute() return false; } - return $this->generateTokenResponse($this->user); + return $this->generateRefreshTokenResponse($this->user, $this->payload); } } diff --git a/src/Service/Action/Auth/JwtTokenTrait.php b/src/Service/Action/Auth/JwtTokenTrait.php index 837192a..e952636 100644 --- a/src/Service/Action/Auth/JwtTokenTrait.php +++ b/src/Service/Action/Auth/JwtTokenTrait.php @@ -25,20 +25,32 @@ trait JwtTokenTrait { + /** * Generates token response. * * @param \Cake\Datasource\EntityInterface|array $user User info. * @return array */ - public function generateTokenResponse($user) + public function generateTokenResponse($user, $type) + { + $timestamp = new DateTimeImmutable('-1 second'); + unset($user['additional_data'], $user['secret'], $user['secret_verified']); + + return Hash::merge($user, [ + 'access_token' => $this->generateAccessToken($user, $timestamp, $type), + 'refresh_token' => $this->generateRefreshToken($user, $timestamp, $type), + 'expired' => $this->accessTokenLifeTime($timestamp), + ]); + } + + public function generateRefreshTokenResponse($user, $payload) { - //$timestamp = time(); $timestamp = new DateTimeImmutable(); return Hash::merge($user, [ - 'access_token' => $this->generateAccessToken($user, $timestamp), - 'refresh_token' => $this->generateRefreshToken($user, $timestamp), + 'access_token' => $this->generateAccessToken($user, $timestamp, null, $payload), + 'refresh_token' => $this->generateRefreshToken($user, $timestamp, null, $payload), 'expired' => $this->accessTokenLifeTime($timestamp), ]); } @@ -50,14 +62,14 @@ public function generateTokenResponse($user) * @param \DateTimeImmutable $timestamp Timestamp. * @return bool|string */ - public function generateAccessToken($user, $timestamp) + public function generateAccessToken($user, $timestamp, $type, $payload = null) { if (empty($user)) { return false; } $subject = $user['id']; - $audience = Router::url('/', true); + $audience = $this->getAudience($type, $payload); $issuer = Router::url('/', true); $signer = new Sha512(); $secret = Configure::read('Api.Jwt.AccessToken.secret'); @@ -75,6 +87,20 @@ public function generateAccessToken($user, $timestamp) return $token->toString(); } + public function getAudience($type, $payload) + { + if ($type === null && is_array($payload) && isset($payload['aud'])) { + return $payload['aud']; + } + if ($type == 'login' && Configure::read('Api.2fa.enabled')) { + $audience = Router::url('/2fa', true); + } else { + $audience = Router::url('/', true); + } + + return $audience; + } + /** * Generates refresh token. * @@ -82,14 +108,14 @@ public function generateAccessToken($user, $timestamp) * @param \DateTimeImmutable $timestamp Timestamp. * @return bool|string */ - public function generateRefreshToken($user, $timestamp) + public function generateRefreshToken($user, $timestamp, $type, $payload = null) { if (empty($user)) { return false; } $subject = $user['id']; - $audience = Router::url('/', true); + $audience = $this->getAudience($type, $payload); $issuer = Router::url('/', true); $signer = new Sha512(); $secret = Configure::read('Api.Jwt.RefreshToken.secret'); diff --git a/src/Service/Action/Auth/OtpVerifyAction.php b/src/Service/Action/Auth/OtpVerifyAction.php new file mode 100644 index 0000000..0f626af --- /dev/null +++ b/src/Service/Action/Auth/OtpVerifyAction.php @@ -0,0 +1,82 @@ +tfa = new TwoFactorAuth( + Configure::read('OneTimePasswordAuthenticator.issuer'), + Configure::read('OneTimePasswordAuthenticator.digits'), + Configure::read('OneTimePasswordAuthenticator.period'), + Configure::read('OneTimePasswordAuthenticator.algorithm'), + Configure::read('OneTimePasswordAuthenticator.qrcodeprovider'), + Configure::read('OneTimePasswordAuthenticator.rngprovider') + ); + } + + /** + * createSecret + * + * @return string base32 shared secret stored in users table + */ + public function createSecret() + { + return $this->tfa->createSecret(); + } + + /** + * verifyCode + * Verifying tfa code with shared secret + * + * @param string $secret of the user + * @param string $code from verification form + * @return bool + */ + public function verifyCode($secret, $code) + { + return $this->tfa->verifyCode($secret, $code); + } + + /** + * getQRCodeImageAsDataUri + * + * @param string $issuer issuer + * @param string $secret secret + * @return string base64 string containing QR code for shared secret + */ + public function getQRCodeImageAsDataUri($issuer, $secret) + { + return $this->tfa->getQRCodeImageAsDataUri($issuer, $secret); + } + +} diff --git a/src/Service/Action/Auth/OtpVerifyCheckAction.php b/src/Service/Action/Auth/OtpVerifyCheckAction.php new file mode 100644 index 0000000..16bbb8f --- /dev/null +++ b/src/Service/Action/Auth/OtpVerifyCheckAction.php @@ -0,0 +1,61 @@ +getData('code'); + $user = $this->getIdentity(); + $entity = $this->getUsersTable()->get($user['id']); + + if (!empty($entity['secret'])) { + $codeVerified = $this->verifyCode($entity['secret'], $verificationCode); + } + + if (!$codeVerified) { + throw new \Exception(__d('cake_d_c/api', 'Verification code is invalid. Try again')); + } + + unset($user['secret']); + + if (!$user['secret_verified']) { + $this->getUsersTable()->query()->update() + ->set(['secret_verified' => true]) + ->where(['id' => $user['id']]) + ->execute(); + } + + return $this->generateTokenResponse($user->toArray(), '2fa'); + } + +} diff --git a/src/Service/Action/Auth/OtpVerifyGetAction.php b/src/Service/Action/Auth/OtpVerifyGetAction.php new file mode 100644 index 0000000..b585d54 --- /dev/null +++ b/src/Service/Action/Auth/OtpVerifyGetAction.php @@ -0,0 +1,74 @@ +getIdentity(); + $secretVerified = $user['secret_verified'] ?? null; + // showing QR-code until shared secret is verified + if (!$secretVerified) { + $secret = $this->onVerifyGetSecret($user); + if (empty($secret)) { + throw new \Exception('Secret generation issue, please try again'); + } else { + $secretDataUri = $this->getQRCodeImageAsDataUri($user['email'], $secret); + $result = ['secretDataUri' => $secretDataUri, 'verified' => false]; + } + } else { + $result = ['verified' => true]; + } + + return $result; + } + + protected function onVerifyGetSecret($user) + { + if (isset($user['secret']) && $user['secret']) { + return $user['secret']; + } + + $secret = $this->createSecret(); + try { + $query = $this->getUsersTable()->query(); + $query->update() + ->set(['secret' => $secret]) + ->where(['id' => $user['id']]); + $query->execute(); + } catch (\Exception $e) { + $message = __d('cake_d_c/api', 'Could not verify, please try again'); + + throw new \Exception($message); + } + + return $secret; + } +} diff --git a/src/Service/Action/Auth/TwoFactorAuthAction.php b/src/Service/Action/Auth/TwoFactorAuthAction.php new file mode 100644 index 0000000..3fe44ec --- /dev/null +++ b/src/Service/Action/Auth/TwoFactorAuthAction.php @@ -0,0 +1,49 @@ +getIdentity(); + $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + $hasWebauthn = $adapter->hasCredential(); + $hasOtp = $user['secret'] && $user['secret_verified']; + + return [ + 'hasRegistered2fa' => $hasWebauthn && $hasOtp, + 'hasOtp' => $hasOtp, + 'hasWebauthn' => $hasWebauthn, + ]; + } + +} diff --git a/src/Service/Action/Auth/Webauthn2faAction.php b/src/Service/Action/Auth/Webauthn2faAction.php new file mode 100644 index 0000000..5c70789 --- /dev/null +++ b/src/Service/Action/Auth/Webauthn2faAction.php @@ -0,0 +1,45 @@ +getIdentity(); + $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + + return [ + 'isRegister' => !$adapter->hasCredential(), + 'username' => $user->webauthn_username ?? $user->username, + ]; + } +} diff --git a/src/Service/Action/Auth/Webauthn2faAuthAction.php b/src/Service/Action/Auth/Webauthn2faAuthAction.php new file mode 100644 index 0000000..30de0c7 --- /dev/null +++ b/src/Service/Action/Auth/Webauthn2faAuthAction.php @@ -0,0 +1,51 @@ +getIdentity(); + $adapter = new AuthenticateAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + $adapter->verifyResponse(); + $adapter->deleteStore(); + + return $this->generateTokenResponse($user->toArray(), '2fa'); + } catch (\Throwable $e) { + $user = $this->getIdentity(); + \Cake\Log\Log::debug(__d('cake_d_c/api', 'Register error with webauthn for user id: {0}', $user['id'] ?? 'empty')); + throw $e; + } + } +} diff --git a/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php b/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php new file mode 100644 index 0000000..8f3293b --- /dev/null +++ b/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php @@ -0,0 +1,42 @@ +getService()->getRequest(), $this->getUsersTable(), $this->getIdentity()); + + return $adapter->getOptions(); + } +} diff --git a/src/Service/Action/Auth/Webauthn2faRegisterAction.php b/src/Service/Action/Auth/Webauthn2faRegisterAction.php new file mode 100644 index 0000000..149d3aa --- /dev/null +++ b/src/Service/Action/Auth/Webauthn2faRegisterAction.php @@ -0,0 +1,51 @@ +getIdentity(); + $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + + if (!$adapter->hasCredential()) { + $adapter->verifyResponse(); + + return ['success' => true]; + } + + throw new BadRequestException( + __d('cake_d_c/api', 'User already has configured webauthn2fa') + ); + + } +} diff --git a/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php b/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php new file mode 100644 index 0000000..93fc969 --- /dev/null +++ b/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php @@ -0,0 +1,49 @@ +getIdentity(); + $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + + if (!$adapter->hasCredential()) { + return $adapter->getOptions(); + } + throw new BadRequestException( + __d('cake_d_c/api', 'User already has configured webauthn2fa') + ); + + } +} diff --git a/src/Service/AuthService.php b/src/Service/AuthService.php index b182a34..eef5b2e 100644 --- a/src/Service/AuthService.php +++ b/src/Service/AuthService.php @@ -17,6 +17,8 @@ use CakeDC\Api\Service\Action\Auth\JwtLoginAction; use CakeDC\Api\Service\Action\Auth\JwtRefreshAction; use CakeDC\Api\Service\Action\Auth\JwtSocialLoginAction; +use CakeDC\Api\Service\Action\Auth\OtpVerifyGetAction; +use CakeDC\Api\Service\Action\Auth\OtpVerifyCheckAction; use CakeDC\Api\Service\Action\Auth\LoginAction; use CakeDC\Api\Service\Action\Auth\RegisterAction; use CakeDC\Api\Service\Action\Auth\ResetPasswordAction; @@ -24,6 +26,12 @@ use CakeDC\Api\Service\Action\Auth\SocialLoginAction; use CakeDC\Api\Service\Action\Auth\ValidateAccountAction; use CakeDC\Api\Service\Action\Auth\ValidateAccountRequestAction; +use CakeDC\Api\Service\Action\Auth\TwoFactorAuthAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterOptionsAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faAuthOptionsAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faAuthAction; /** * Class AuthService @@ -38,14 +46,29 @@ class AuthService extends Service public function initialize(): void { parent::initialize(); + $getMethods = ['method' => ['GET'], 'mapCors' => true]; $methods = ['method' => ['POST'], 'mapCors' => true]; + + $this->mapAction('webauthn2fa', Webauthn2faAction::class, $getMethods); + $this->mapAction('two_factor_auth', TwoFactorAuthAction::class, $getMethods); + $this->mapAction('webauthn2fa_register_options', Webauthn2faRegisterOptionsAction::class, $getMethods); + $this->mapAction('webauthn2fa_register', Webauthn2faRegisterAction::class, $methods); + $this->mapAction('webauthn2fa_auth_options', Webauthn2faAuthOptionsAction::class, $getMethods); + $this->mapAction('webauthn2fa_auth', Webauthn2faAuthAction::class, $methods); + + $this->mapAction('otp_verify', OtpVerifyGetAction::class, $getMethods); + $this->mapAction('otp_verify_check', OtpVerifyCheckAction::class, $methods); + $this->mapAction('jwt_login', JwtLoginAction::class, $methods); $this->mapAction('jwt_refresh', JwtRefreshAction::class, $methods); $this->mapAction('jwt_social_login', JwtSocialLoginAction::class, $methods); + $this->mapAction('login', LoginAction::class, $methods); $this->mapAction('register', RegisterAction::class, $methods); + $this->mapAction('reset_password_request', ResetPasswordRequestAction::class, $methods); $this->mapAction('reset_password', ResetPasswordAction::class, $methods); + $this->mapAction('validate_account_request', ValidateAccountRequestAction::class, $methods); $this->mapAction('validate_account', ValidateAccountAction::class, $methods); $this->mapAction('social_login', SocialLoginAction::class, $methods); diff --git a/src/Webauthn/AuthenticateAdapter.php b/src/Webauthn/AuthenticateAdapter.php new file mode 100644 index 0000000..f8b8032 --- /dev/null +++ b/src/Webauthn/AuthenticateAdapter.php @@ -0,0 +1,79 @@ +getUserEntity(); + $allowed = array_map(function (PublicKeyCredentialSource $credential) { + return $credential->getPublicKeyCredentialDescriptor(); + }, $this->repository->findAllForUserEntity($userEntity)); + \Cake\Log\Log::error(print_r($allowed, true)); + + $options = $this->server->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, + $allowed + ); + $storeEntity = $this->readStore(); + \Cake\Log\Log::error(print_r($storeEntity, true)); + $storeEntity['store'] = []; + $storeEntity = $this->patchStore($storeEntity, 'authenticateOptions', base64_encode(serialize($options))); + $res = $this->store->save($storeEntity); + \Cake\Log\Log::error(print_r($storeEntity, true)); + \Cake\Log\Log::error(print_r($res, true)); + + return $options; + } + + /** + * Verify the registration response + * + * @return \Webauthn\PublicKeyCredentialSource + */ + public function verifyResponse(): \Webauthn\PublicKeyCredentialSource + { + $storeEntity = $this->readStore(); + \Cake\Log\Log::error(print_r($storeEntity, true)); + $options = $this->getStore($storeEntity, 'authenticateOptions'); + if ($options) { + $options = unserialize(base64_decode($options)); + } + \Cake\Log\Log::error(print_r($options, true)); + + return $this->loadAndCheckAssertionResponse($options); + } + + /** + * @param \Webauthn\PublicKeyCredentialRequestOptions $options request options + * @return \Webauthn\PublicKeyCredentialSource + */ + protected function loadAndCheckAssertionResponse($options): PublicKeyCredentialSource + { + return $this->server->loadAndCheckAssertionResponse( + json_encode($this->request->getData()), + $options, + $this->getUserEntity(), + $this->request + ); + } +} diff --git a/src/Webauthn/BaseAdapter.php b/src/Webauthn/BaseAdapter.php new file mode 100644 index 0000000..a7ac441 --- /dev/null +++ b/src/Webauthn/BaseAdapter.php @@ -0,0 +1,186 @@ +request = $request; + $this->store = TableRegistry::getTableLocator()->get('CakeDC/Api.AuthStore'); + $session = $this->readStore(); + $rpEntity = new PublicKeyCredentialRpEntity( + Configure::read('Api.Webauthn2fa.' . $this->getDomain() . '.appName'), // The application name + Configure::read('Api.Webauthn2fa.' . $this->getDomain() . '.id') + ); + /** + * @var \Cake\ORM\Entity $userSession + */ + $userSession = $userData; + $this->user = $usersTable->get($userSession->id); + $this->repository = new UserCredentialSourceRepository( + $request, + $this->user, + $usersTable + ); + + $this->server = new Server( + $rpEntity, + $this->repository + ); + } + + /** + * @return \Webauthn\PublicKeyCredentialUserEntity + */ + protected function getUserEntity(): PublicKeyCredentialUserEntity + { + $user = $this->getUser(); + + return new PublicKeyCredentialUserEntity( + $user->webauthn_username ?? $user->username, + (string)$user->id, + (string)$user->first_name + ); + } + + /** + * @return array|mixed|null + */ + public function getUser() + { + return $this->user; + } + + /** + * @return bool + */ + public function hasCredential(): bool + { + return (bool)$this->repository->findAllForUserEntity( + $this->getUserEntity() + ); + } + + public function readStore() + { + $entity = $this->store->find()->where(['id' => $this->getStoreKey()])->first(); + if ($entity === null) { + $entity = $this->store->newEmptyEntity(); + $entity->id = $this->getStoreKey(); + } + if (empty($entity->store)) { + $entity->store = []; + } + + return $entity; + } + + public function saveStore($data) + { + $entity = $this->readStore(); + $entity->store = $data; + return $this->store->save($entity); + } + + public function deleteStore() + { + $entity = $this->readStore(); + + return $this->store->delete($entity); + } + + public function getStoreKey() + { + $authHeader = $this->request->getHeader('Authorization'); + if (is_array($authHeader)) { + $authHeader = array_pop($authHeader); + } + $options = [ + 'tokenPrefix' => 'bearer', + ]; + + return str_ireplace($options['tokenPrefix'] . ' ', '', $authHeader); + } + + public function patchStore($entity, $name, $options) + { + $entity['store']['api']['Webauthn2fa'][$this->getDomain()][$name] = $options; + + return $entity; + } + + public function getStore($entity, $name) + { + $path = self::STORE_PREFIX . '.' . $this->getDomain() . '.' . $name; + + return Hash::get($entity['store'], $path, null); + } + + public function getDomain($replace = true) + { + $domain = null; + if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) { + $domain = parse_url($_SERVER['HTTP_REFERER']); + } + if ($domain !==null && $domain['host']) { + $host = $domain['host']; + } else { + $host = $this->request->domain(); + } + + if ($replace) { + return str_replace('.', '$', $host); + } else { + return $host; + } + } +} diff --git a/src/Webauthn/RegisterAdapter.php b/src/Webauthn/RegisterAdapter.php new file mode 100644 index 0000000..adaf09a --- /dev/null +++ b/src/Webauthn/RegisterAdapter.php @@ -0,0 +1,72 @@ +getUserEntity(); + $options = $this->server->generatePublicKeyCredentialCreationOptions( + $userEntity, + PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, + [] + ); + $storeEntity = $this->readStore(); + $storeEntity = $this->patchStore($storeEntity, 'registerOptions', base64_encode(serialize($options))); + $this->store->save($storeEntity); + + return $options; + } + + /** + * Verify the registration response + * + * @return \Webauthn\PublicKeyCredentialSource + */ + public function verifyResponse(): \Webauthn\PublicKeyCredentialSource + { + $storeEntity = $this->readStore(); + $options = $this->getStore($storeEntity, 'registerOptions'); + if ($options) { + $options = unserialize(base64_decode($options)); + } + + $credential = $this->loadAndCheckAttestationResponse($options); + $this->repository->saveCredentialSource($credential); + + return $credential; + } + + /** + * @param \Webauthn\PublicKeyCredentialCreationOptions $options creation options + * @return \Webauthn\PublicKeyCredentialSource + */ + protected function loadAndCheckAttestationResponse($options): \Webauthn\PublicKeyCredentialSource + { + $credential = $this->server->loadAndCheckAttestationResponse( + json_encode($this->request->getData()), + $options, + $this->request + ); + + return $credential; + } +} diff --git a/src/Webauthn/Repository/UserCredentialSourceRepository.php b/src/Webauthn/Repository/UserCredentialSourceRepository.php new file mode 100644 index 0000000..58bd352 --- /dev/null +++ b/src/Webauthn/Repository/UserCredentialSourceRepository.php @@ -0,0 +1,120 @@ +request = $request; + $this->user = $user; + $this->usersTable = $usersTable; + } + + /** + * @param string $publicKeyCredentialId Public key credential id + * @return \Webauthn\PublicKeyCredentialSource|null + */ + public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource + { + $encodedId = Base64Url::encode($publicKeyCredentialId); + $credentials = $this->getUserData($this->user); + $credential = $credentials[$encodedId] ?? null; + + return $credential + ? PublicKeyCredentialSource::createFromArray($credential) + : null; + } + + /** + * @inheritDoc + */ + public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array + { + if ($publicKeyCredentialUserEntity->getId() != $this->user->id) { + return []; + } + \Cake\Log\Log::error(print_r($this->user, true)); + $credentials = $this->getUserData($this->user); + \Cake\Log\Log::error(print_r($credentials, true)); + + $list = []; + foreach ($credentials as $credential) { + $list[] = PublicKeyCredentialSource::createFromArray($credential); + } + + return $list; + } + + /** + * @param \Webauthn\PublicKeyCredentialSource $publicKeyCredentialSource Public key credential source + */ + public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void + { + $credentials = $this->getUserData($this->user); + $id = Base64Url::encode($publicKeyCredentialSource->getPublicKeyCredentialId()); + $credentials[$id] = json_decode(json_encode($publicKeyCredentialSource), true); + $this->patchUserData($this->user, $credentials); + $res = $this->usersTable->saveOrFail($this->user); + } + + public function patchUserData($entity, $options) + { + $entity['additional_data'] = $entity['additional_data'] ?? []; + $entity['additional_data']['api'] = $entity['additional_data']['api'] ?? []; + $entity['additional_data']['api'][$this->getDomain()] = $entity['additional_data']['api'][$this->getDomain()] ?? []; + $entity['additional_data']['api'][$this->getDomain()]['webauthn_credentials'] = $options; + + return $entity; + } + + public function getUserData($entity) + { + $path = 'additional_data.api.' . $this->getDomain() . '.webauthn_credentials'; + + return Hash::get($entity, $path, []); + } + + public function getDomain() + { + $domain = null; + if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) { + $domain = parse_url($_SERVER['HTTP_REFERER']); + } + if ($domain !==null && $domain['host']) { + $host = $domain['host']; + } else { + $host = $this->request->domain(); + } + + return str_replace('.', '$', $host); + } + +} From 574f725dc6fc32b9a8f21dcf0fa360d42db64e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Wed, 30 Apr 2025 18:05:11 +0100 Subject: [PATCH 02/16] improve gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bb48951..10a3bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tmp pmip webroot/coverage .php_cs.cache +.phpunit.cache/ From f4e0d21846bd8642cfd532d91730b05b727c3177 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Thu, 19 Oct 2023 03:15:31 +0300 Subject: [PATCH 03/16] add ability to pass base url for reset password request api endpoint. --- .../Action/Auth/ResetPasswordRequestAction.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Service/Action/Auth/ResetPasswordRequestAction.php b/src/Service/Action/Auth/ResetPasswordRequestAction.php index 47d18ab..760a59a 100644 --- a/src/Service/Action/Auth/ResetPasswordRequestAction.php +++ b/src/Service/Action/Auth/ResetPasswordRequestAction.php @@ -73,14 +73,22 @@ public function execute() { $data = $this->getData(); $reference = $data['reference']; + $baseUrl = $data['baseUrl']; try { - $resetUser = $this->getUsersTable()->resetToken($reference, [ + $options = [ 'expiration' => Configure::read('Users.Token.expiration'), 'checkActive' => false, 'sendEmail' => true, 'type' => 'password', 'ensureActive' => Configure::read('Users.Registration.ensureActive'), - ]); + ]; + if (!empty($baseUrl)) { + $options['linkGenerator'] = function($token) use ($baseUrl) { + return $baseUrl . '?token=' . $token; + } + } + + $resetUser = $this->getUsersTable()->resetToken($reference, $options); if ($resetUser) { return __d('CakeDC/Api', 'Please check your email to continue with password reset process'); } else { From f14b770c3be196f708705c014d24a67e560bf607 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Thu, 19 Oct 2023 18:04:39 +0300 Subject: [PATCH 04/16] add ability to pass base url for reset password request api endpoint. --- src/Service/Action/Auth/ResetPasswordRequestAction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/Action/Auth/ResetPasswordRequestAction.php b/src/Service/Action/Auth/ResetPasswordRequestAction.php index 760a59a..4e9535e 100644 --- a/src/Service/Action/Auth/ResetPasswordRequestAction.php +++ b/src/Service/Action/Auth/ResetPasswordRequestAction.php @@ -73,7 +73,7 @@ public function execute() { $data = $this->getData(); $reference = $data['reference']; - $baseUrl = $data['baseUrl']; + $baseUrl = $data['base_url'] ?? null; try { $options = [ 'expiration' => Configure::read('Users.Token.expiration'), @@ -85,7 +85,7 @@ public function execute() if (!empty($baseUrl)) { $options['linkGenerator'] = function($token) use ($baseUrl) { return $baseUrl . '?token=' . $token; - } + }; } $resetUser = $this->getUsersTable()->resetToken($reference, $options); From cd5cbd93acc99f01b9da02412b5f98cd845d9a5d Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Sat, 21 Oct 2023 04:51:22 +0300 Subject: [PATCH 05/16] implements 2fa checker functionality --- config/api.php | 17 ++++++ src/Service/Action/Auth/JwtTokenTrait.php | 55 +++++++++++++++-- .../Action/Auth/ResetPasswordAction.php | 36 +++++++---- .../Action/Auth/TwoFactorAuthAction.php | 5 ++ ...ltOneTimePasswordAuthenticationChecker.php | 61 +++++++++++++++++++ ...DefaultWebauthn2fAuthenticationChecker.php | 44 +++++++++++++ ...mePasswordAuthenticationCheckerFactory.php | 40 ++++++++++++ ...PasswordAuthenticationCheckerInterface.php | 31 ++++++++++ ...Webauthn2fAuthenticationCheckerFactory.php | 40 ++++++++++++ ...bauthn2fAuthenticationCheckerInterface.php | 31 ++++++++++ tests/Config/api.php | 17 ++++++ 11 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 src/Service/Auth/TwoFactorAuthentication/DefaultOneTimePasswordAuthenticationChecker.php create mode 100644 src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php create mode 100644 src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php create mode 100644 src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerInterface.php create mode 100644 src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php create mode 100644 src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerInterface.php diff --git a/config/api.php b/config/api.php index 10f7544..8c48302 100644 --- a/config/api.php +++ b/config/api.php @@ -35,6 +35,23 @@ 'serviceLookupPlugins' => null, 'lookupMode' => 'underscore', + '2fa' => [ + 'enabled' => false, + ], + + 'OneTimePasswordAuthenticator' => [ + 'login' => false, + 'checker' => \CakeDC\Api\Service\Auth\TwoFactorAuthentication\DefaultOneTimePasswordAuthenticationChecker::class, + ], + 'Webauthn2fa' => [ + 'checker' => \CakeDC\Api\Service\Auth\TwoFactorAuthentication\DefaultWebauthn2fAuthenticationChecker::class, + 'localhost' => [ + 'enabled' => false, + 'appName' => 'apilocal', + 'id' => 'localhost', + ], + ], + // auth permission uses require auth strategy 'Auth' => [ 'Crud' => [ diff --git a/src/Service/Action/Auth/JwtTokenTrait.php b/src/Service/Action/Auth/JwtTokenTrait.php index e952636..8738fa8 100644 --- a/src/Service/Action/Auth/JwtTokenTrait.php +++ b/src/Service/Action/Auth/JwtTokenTrait.php @@ -17,6 +17,8 @@ use Cake\ORM\TableRegistry; use Cake\Routing\Router; use Cake\Utility\Hash; +use CakeDC\Api\Service\Auth\TwoFactorAuthentication\OneTimePasswordAuthenticationCheckerFactory; +use CakeDC\Api\Service\Auth\TwoFactorAuthentication\Webauthn2fAuthenticationCheckerFactory; use DateInterval; use DateTimeImmutable; use Lcobucci\JWT\Configuration; @@ -41,6 +43,9 @@ public function generateTokenResponse($user, $type) 'access_token' => $this->generateAccessToken($user, $timestamp, $type), 'refresh_token' => $this->generateRefreshToken($user, $timestamp, $type), 'expired' => $this->accessTokenLifeTime($timestamp), + 'enabled2FA' => $this->is2FAEnabled($user), + 'enabledWebauthn' => $this->isEnabledWebauthn2faAuthentication($user), + 'enabledOtp' => $this->isEnabledOneTimePasswordAuthentication($user), ]); } @@ -69,7 +74,7 @@ public function generateAccessToken($user, $timestamp, $type, $payload = null) } $subject = $user['id']; - $audience = $this->getAudience($type, $payload); + $audience = $this->getAudience($user, $type, $payload); $issuer = Router::url('/', true); $signer = new Sha512(); $secret = Configure::read('Api.Jwt.AccessToken.secret'); @@ -87,12 +92,12 @@ public function generateAccessToken($user, $timestamp, $type, $payload = null) return $token->toString(); } - public function getAudience($type, $payload) + public function getAudience($user, $type, $payload) { if ($type === null && is_array($payload) && isset($payload['aud'])) { return $payload['aud']; } - if ($type == 'login' && Configure::read('Api.2fa.enabled')) { + if ($type == 'login' && $this->is2FAEnabled($user)) { $audience = Router::url('/2fa', true); } else { $audience = Router::url('/', true); @@ -101,6 +106,48 @@ public function getAudience($type, $payload) return $audience; } + protected function is2FAEnabled($user) + { + return $this->isEnabledWebauthn2faAuthentication($user) || $this->isEnabledOneTimePasswordAuthentication($user); + } + + public function isEnabledWebauthn2faAuthentication($user) + { + $enabledTwoFactorVerify = Configure::read('Api.2fa.enabled'); + $webauthn2faChecker = $this->getWebauthn2fAuthenticationChecker(); + if ($enabledTwoFactorVerify && $webauthn2faChecker->isRequired((array)$user)) { + return true; + } + + return false; + } + + public function isEnabledOneTimePasswordAuthentication($user) + { + $enabledTwoFactorVerify = Configure::read('Api.2fa.enabled'); + $otpChecker = $this->getOneTimePasswordAuthenticationChecker(); + if ($enabledTwoFactorVerify && $otpChecker->isRequired((array)$user)) { + return true; + } + + return false; + } + + protected function getOneTimePasswordAuthenticationChecker() + { + return (new OneTimePasswordAuthenticationCheckerFactory())->build(); + } + + /** + * Get the configured u2f authentication checker + * + * @return \CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface + */ + protected function getWebauthn2fAuthenticationChecker() + { + return (new Webauthn2fAuthenticationCheckerFactory())->build(); + } + /** * Generates refresh token. * @@ -115,7 +162,7 @@ public function generateRefreshToken($user, $timestamp, $type, $payload = null) } $subject = $user['id']; - $audience = $this->getAudience($type, $payload); + $audience = $this->getAudience($user, $type, $payload); $issuer = Router::url('/', true); $signer = new Sha512(); $secret = Configure::read('Api.Jwt.RefreshToken.secret'); diff --git a/src/Service/Action/Auth/ResetPasswordAction.php b/src/Service/Action/Auth/ResetPasswordAction.php index 2417659..6436f26 100644 --- a/src/Service/Action/Auth/ResetPasswordAction.php +++ b/src/Service/Action/Auth/ResetPasswordAction.php @@ -56,6 +56,24 @@ public function validates(): bool $validator ->requirePresence('token', 'create') ->notBlank('token'); + + $validator + ->requirePresence('password_confirm', 'create') + ->notBlank('password_confirm'); + + $validator + ->requirePresence('password', 'create') + ->notBlank('password') + ->add('password', [ + 'password_confirm_check' => [ + 'rule' => ['compareWith', 'password_confirm'], + 'message' => __d( + 'cake_d_c/users', + 'Your password does not match your confirm password. Please try again' + ), + 'allowEmpty' => false, + ]]); + $errors = $validator->validate($this->getData()); if (!empty($errors)) { throw new ValidationException(__('Validation failed'), 0, null, $errors); @@ -103,19 +121,13 @@ protected function _changePassword($userId) $user = $this->getUsersTable()->newEntity([], ['validate' => false]); $user->id = $userId; try { - $validator = $this->getUsersTable()->validationPasswordConfirm(new Validator()); - $this->getUsersTable()->setValidator('changePassV', $validator); - $user = $this->getUsersTable()->patchEntity($user, $this->getData(), ['validate' => 'changePassV']); - if ($user->getErrors()) { - $message = __d('CakeDC/Api', 'Password could not be changed'); - throw new ValidationException($message, 0, null, $user->getErrors()); + $data = $this->getData(); + $user->password = $data['password']; + $user = $this->getUsersTable()->changePassword($user); + if ($user) { + return __d('CakeDC/Api', 'Password has been changed successfully'); } else { - $user = $this->getUsersTable()->changePassword($user); - if ($user) { - return __d('CakeDC/Api', 'Password has been changed successfully'); - } else { - throw new Exception(__d('CakeDC/Api', 'Password could not be changed'), 500); - } + throw new Exception(__d('CakeDC/Api', 'Password could not be changed'), 500); } } catch (UserNotFoundException $exception) { throw new Exception(__d('CakeDC/Api', 'User was not found'), 404, $exception); diff --git a/src/Service/Action/Auth/TwoFactorAuthAction.php b/src/Service/Action/Auth/TwoFactorAuthAction.php index 3fe44ec..a7bb9ca 100644 --- a/src/Service/Action/Auth/TwoFactorAuthAction.php +++ b/src/Service/Action/Auth/TwoFactorAuthAction.php @@ -26,6 +26,7 @@ class TwoFactorAuthAction extends Action { use CustomUsersTableTrait; + use JwtTokenTrait; /** * Execute action. @@ -37,12 +38,16 @@ public function execute() $user = $this->getIdentity(); $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); $hasWebauthn = $adapter->hasCredential(); + $user = $user->toArray(); + $hasOtp = $user['secret'] && $user['secret_verified']; return [ 'hasRegistered2fa' => $hasWebauthn && $hasOtp, 'hasOtp' => $hasOtp, 'hasWebauthn' => $hasWebauthn, + 'enabledWebauthn' => $this->isEnabledWebauthn2faAuthentication((array)$user), + 'enabledOtp' => $this->isEnabledOneTimePasswordAuthentication((array)$user), ]; } diff --git a/src/Service/Auth/TwoFactorAuthentication/DefaultOneTimePasswordAuthenticationChecker.php b/src/Service/Auth/TwoFactorAuthentication/DefaultOneTimePasswordAuthenticationChecker.php new file mode 100644 index 0000000..394c9bf --- /dev/null +++ b/src/Service/Auth/TwoFactorAuthentication/DefaultOneTimePasswordAuthenticationChecker.php @@ -0,0 +1,61 @@ +enabledKey = $enableKey; + } + } + + /** + * Check if two factor authentication is enabled + * + * @return bool + */ + public function isEnabled() + { + return Configure::read($this->enabledKey) !== false; + } + + /** + * Check if two factor authentication is required for a user + * + * @param array $user user data + * @return bool + */ + public function isRequired(?array $user = null) + { + return !empty($user) && $this->isEnabled(); + } +} diff --git a/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php new file mode 100644 index 0000000..2e52f67 --- /dev/null +++ b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php @@ -0,0 +1,44 @@ +isEnabled(); + } +} diff --git a/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php b/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php new file mode 100644 index 0000000..67b6066 --- /dev/null +++ b/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php @@ -0,0 +1,40 @@ + 'CakeDC/Api.Form', 'ServiceFallback' => \CakeDC\Api\Service\FallbackService::class, + '2fa' => [ + 'enabled' => false, + ], + + 'OneTimePasswordAuthenticator' => [ + 'login' => false, + 'checker' => \CakeDC\Api\Service\Auth\TwoFactorAuthentication\DefaultOneTimePasswordAuthenticationChecker::class, + ], + 'Webauthn2fa' => [ + 'checker' => \CakeDC\Api\Service\Auth\TwoFactorAuthentication\DefaultWebauthn2fAuthenticationChecker::class, + 'localhost' => [ + 'enabled' => false, + 'appName' => 'apilocal', + 'id' => 'localhost', + ], + ], + 'Jwt' => [ 'AccessToken' => [ 'lifetime' => 600, From bcddf4258c1e5add5f3964af8e536fbeededd623 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Wed, 15 Nov 2023 17:17:53 +0300 Subject: [PATCH 06/16] generic recaptcha integration --- .../Action/Recaptcha/ValidateAction.php | 40 ++++++++++++ src/Service/Action/Traits/ReCaptchaTrait.php | 65 +++++++++++++++++++ src/Service/RecaptchaService.php | 36 ++++++++++ src/Utility/RequestParser.php | 38 +++++++++++ src/Webauthn/BaseAdapter.php | 17 +---- .../UserCredentialSourceRepository.php | 13 +--- 6 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 src/Service/Action/Recaptcha/ValidateAction.php create mode 100644 src/Service/Action/Traits/ReCaptchaTrait.php create mode 100644 src/Service/RecaptchaService.php create mode 100644 src/Utility/RequestParser.php diff --git a/src/Service/Action/Recaptcha/ValidateAction.php b/src/Service/Action/Recaptcha/ValidateAction.php new file mode 100644 index 0000000..d374758 --- /dev/null +++ b/src/Service/Action/Recaptcha/ValidateAction.php @@ -0,0 +1,40 @@ +validateReCaptcha(); + } + +} diff --git a/src/Service/Action/Traits/ReCaptchaTrait.php b/src/Service/Action/Traits/ReCaptchaTrait.php new file mode 100644 index 0000000..d19ddf2 --- /dev/null +++ b/src/Service/Action/Traits/ReCaptchaTrait.php @@ -0,0 +1,65 @@ +getService()->getRequest()); + $reCaptcha = Configure::read('Api.reCaptcha.' . $domain); + if ($reCaptcha['disabled'] ?? false) { + return true; + } + + $recaptcha = $this->_getReCaptchaInstance(); + if ($recaptchaResponse === null) { + $recaptchaResponse = $this->getService()->getRequest()->getData('g-recaptcha-response'); + } + $resp = $recaptcha->verify($recaptchaResponse, $this->getService()->getRequest()->clientIp()); + + return $resp->isSuccess(); + } + + /** + * Create reCaptcha instance if enabled in configuration + * + * @return \ReCaptcha\ReCaptcha|null + */ + protected function _getReCaptchaInstance() + { + $domain = RequestParser::getDomain($this->getService()->getRequest()); + $reCaptchaSecret = Configure::read('Api.reCaptcha.' . $domain. '.secret'); + if (!empty($reCaptchaSecret)) { + return new \ReCaptcha\ReCaptcha($reCaptchaSecret); + } + + return null; + } +} diff --git a/src/Service/RecaptchaService.php b/src/Service/RecaptchaService.php new file mode 100644 index 0000000..9178769 --- /dev/null +++ b/src/Service/RecaptchaService.php @@ -0,0 +1,36 @@ + ['POST'], 'mapCors' => true]; + + $this->mapAction('validate', ValidateAction::class, $methods); + } +} diff --git a/src/Utility/RequestParser.php b/src/Utility/RequestParser.php new file mode 100644 index 0000000..0d68b81 --- /dev/null +++ b/src/Utility/RequestParser.php @@ -0,0 +1,38 @@ +request->domain(); + } + + if ($replace) { + return str_replace('.', '$', $host); + } else { + return $host; + } + } +} diff --git a/src/Webauthn/BaseAdapter.php b/src/Webauthn/BaseAdapter.php index a7ac441..dcbe543 100644 --- a/src/Webauthn/BaseAdapter.php +++ b/src/Webauthn/BaseAdapter.php @@ -18,6 +18,7 @@ use Cake\ORM\TableRegistry; use Cake\Utility\Hash; use CakeDC\Users\Model\Table\UsersTable; +use CakeDC\Api\Utility\RequestParser; use CakeDC\Api\Webauthn\Repository\UserCredentialSourceRepository; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialUserEntity; @@ -167,20 +168,6 @@ public function getStore($entity, $name) public function getDomain($replace = true) { - $domain = null; - if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) { - $domain = parse_url($_SERVER['HTTP_REFERER']); - } - if ($domain !==null && $domain['host']) { - $host = $domain['host']; - } else { - $host = $this->request->domain(); - } - - if ($replace) { - return str_replace('.', '$', $host); - } else { - return $host; - } + return RequestParser::getDomain($this->request, $replace); } } diff --git a/src/Webauthn/Repository/UserCredentialSourceRepository.php b/src/Webauthn/Repository/UserCredentialSourceRepository.php index 58bd352..3179821 100644 --- a/src/Webauthn/Repository/UserCredentialSourceRepository.php +++ b/src/Webauthn/Repository/UserCredentialSourceRepository.php @@ -7,6 +7,7 @@ use Cake\Datasource\EntityInterface; use Cake\Http\ServerRequest; use Cake\Utility\Hash; +use CakeDC\Api\Utility\RequestParser; use CakeDC\Users\Model\Table\UsersTable; use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSourceRepository; @@ -104,17 +105,7 @@ public function getUserData($entity) public function getDomain() { - $domain = null; - if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) { - $domain = parse_url($_SERVER['HTTP_REFERER']); - } - if ($domain !==null && $domain['host']) { - $host = $domain['host']; - } else { - $host = $this->request->domain(); - } - - return str_replace('.', '$', $host); + return RequestParser::getDomain($this->request); } } From 4020373eee8e7a8f5f0f8715e094abd20204dd47 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Tue, 5 Dec 2023 20:48:56 +0300 Subject: [PATCH 07/16] fix nesting initialization for services --- src/Service/Service.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Service/Service.php b/src/Service/Service.php index 62c28af..532fef7 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -601,8 +601,7 @@ public function buildAction(): Action } if ($serviceName === $this->getName()) { $service = $this; - } - if (in_array($serviceName, $this->_innerServices)) { + } elseif (in_array($serviceName, $this->_innerServices)) { $options = [ 'version' => $this->getVersion(), 'request' => $this->getRequest(), From c783d23f9d66403b0c2b6baf37a0092669660b87 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Fri, 8 Dec 2023 15:07:32 +0300 Subject: [PATCH 08/16] improve otp get action --- src/Service/Action/Auth/OtpVerifyGetAction.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Service/Action/Auth/OtpVerifyGetAction.php b/src/Service/Action/Auth/OtpVerifyGetAction.php index b585d54..38e7024 100644 --- a/src/Service/Action/Auth/OtpVerifyGetAction.php +++ b/src/Service/Action/Auth/OtpVerifyGetAction.php @@ -41,7 +41,11 @@ public function execute() throw new \Exception('Secret generation issue, please try again'); } else { $secretDataUri = $this->getQRCodeImageAsDataUri($user['email'], $secret); - $result = ['secretDataUri' => $secretDataUri, 'verified' => false]; + $result = [ + 'secretDataUri' => $secretDataUri, + 'secret' => $secret, + 'verified' => false, + ]; } } else { $result = ['verified' => true]; From 4779c44653a833cc648b1080d0dea42223c1da60 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Thu, 18 Jan 2024 02:30:42 +0300 Subject: [PATCH 09/16] fix table schema deprecation --- src/Model/Table/AuthStoreTable.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Model/Table/AuthStoreTable.php b/src/Model/Table/AuthStoreTable.php index ff90533..e736b62 100644 --- a/src/Model/Table/AuthStoreTable.php +++ b/src/Model/Table/AuthStoreTable.php @@ -63,16 +63,17 @@ public function validationDefault(Validator $validator): Validator } + /** - * Field additional_data is json + * Initialize schema * - * @param \Cake\Database\Schema\TableSchemaInterface $schema The table definition fetched from database. - * @return \Cake\Database\Schema\TableSchemaInterface the altered schema + * @return \Cake\Database\Schema\TableSchemaInterface */ - protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface + public function getSchema(): TableSchemaInterface { + $schema = parent::getSchema(); $schema->setColumnType('store', 'json'); - return parent::_initializeSchema($schema); + return $schema; } } From c5a5b7e345efd26896aae6391eae576197865796 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Fri, 4 Oct 2024 17:38:52 +0300 Subject: [PATCH 10/16] implements service routes command --- config/routes.php | 2 +- src/Command/ServiceRoutesCommand.php | 152 +++++++++++++++++ src/Plugin.php | 16 +- .../Command/ServiceRoutesCommandTest.php | 153 ++++++++++++++++++ 4 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 src/Command/ServiceRoutesCommand.php create mode 100644 tests/TestCase/Command/ServiceRoutesCommandTest.php diff --git a/config/routes.php b/config/routes.php index 40e96a0..dd00494 100644 --- a/config/routes.php +++ b/config/routes.php @@ -19,7 +19,7 @@ $routes->plugin('CakeDC/Api', ['path' => '/api'], function ($routes) { $useVersioning = Configure::read('Api.useVersioning'); $versionPrefix = Configure::read('Api.versionPrefix'); - $middlewares = Configure::read('Api.Middleware'); + $middlewares = Configure::read('Api.Middleware', []); $middlewareNames = array_keys($middlewares); $routes->applyMiddleware(...$middlewareNames); diff --git a/src/Command/ServiceRoutesCommand.php b/src/Command/ServiceRoutesCommand.php new file mode 100644 index 0000000..ce6eb07 --- /dev/null +++ b/src/Command/ServiceRoutesCommand.php @@ -0,0 +1,152 @@ +setDescription(__('Display all routes in a service')); + $parser->addArgument('service', [ + 'help' => __('The name of the service to display routes for.'), + 'required' => true, + ]); + + return $parser; + } + + /** + * Display all routes in an application + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int|null The exit code or null for success + */ + public function execute(Arguments $args, ConsoleIo $io): ?int + { + $serviceName = $args->getArgument('service'); + $header = ['Route name', 'Method(s)', 'URI template', 'Service', 'Action', 'Plugin']; + if ($args->getOption('verbose')) { + $header[] = 'Defaults'; + } + + $service = ServiceRegistry::getServiceLocator()->get($serviceName); + if ($service === null) { + $io->error(__('Service "{0}" not found', $serviceName)); + return Command::CODE_ERROR; + } + + $availableRoutes = $service->routes(); + + $output = $duplicateRoutesCounter = []; + + foreach ($availableRoutes as $route) { + $methods = isset($route->defaults['_method']) ? (array)$route->defaults['_method'] : ['']; + + $item = [ + $route->options['_name'] ?? $route->getName(), + implode(', ', $methods), + $route->template, + $route->defaults['controller'] ?? '', + $route->defaults['action'] ?? '', + $route->defaults['plugin'] ?? '', + ]; + + if ($args->getOption('verbose')) { + ksort($route->defaults); + $item[] = json_encode($route->defaults); + } + + $output[] = $item; + + foreach ($methods as $method) { + if (!isset($duplicateRoutesCounter[$route->template][$method])) { + $duplicateRoutesCounter[$route->template][$method] = 0; + } + + $duplicateRoutesCounter[$route->template][$method]++; + } + } + + if ($args->getOption('sort')) { + usort($output, function ($a, $b) { + return strcasecmp($a[0], $b[0]); + }); + } + + array_unshift($output, $header); + + $io->helper('table')->output($output); + $io->out(); + + $duplicateRoutes = []; + + foreach ($availableRoutes as $route) { + $methods = isset($route->defaults['_method']) ? (array)$route->defaults['_method'] : ['']; + + foreach ($methods as $method) { + if ( + $duplicateRoutesCounter[$route->template][$method] > 1 || + ($method === '' && count($duplicateRoutesCounter[$route->template]) > 1) || + ($method !== '' && isset($duplicateRoutesCounter[$route->template][''])) + ) { + $duplicateRoutes[] = [ + $route->options['_name'] ?? $route->getName(), + $route->template, + $route->defaults['plugin'] ?? '', + $route->defaults['prefix'] ?? '', + $route->defaults['controller'] ?? '', + $route->defaults['action'] ?? '', + implode(', ', $methods), + ]; + + break; + } + } + } + + if ($duplicateRoutes) { + array_unshift($duplicateRoutes, $header); + $io->warning('The following possible route collisions were detected.'); + $io->helper('table')->output($duplicateRoutes); + $io->out(); + } + + return static::CODE_SUCCESS; + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 0e33b05..ad5daff 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -13,9 +13,11 @@ namespace CakeDC\Api; +use Cake\Console\CommandCollection; use Cake\Core\BasePlugin; use Cake\Core\Configure; use Cake\Core\ContainerInterface; +use CakeDC\Api\Command\ServiceRoutesCommand; /** * Api plugin @@ -29,7 +31,7 @@ class Plugin extends BasePlugin */ public function routes($routes): void { - $middlewares = Configure::read('Api.Middleware'); + $middlewares = Configure::read('Api.Middleware', []); foreach ($middlewares as $alias => $middleware) { $class = $middleware['class']; if (array_key_exists('request', $middleware)) { @@ -58,6 +60,7 @@ public function routes($routes): void } /** +<<<<<<< HEAD * Middleware registrator and holder. * * @param \Cake\Routing\RouteBuilder $routes Routes. @@ -83,4 +86,15 @@ public function services(ContainerInterface $container): void $this->middlewares['apiParser']->setContainer($container); } } + + /** + * Add console commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands): CommandCollection + { + return $commands->add('service routes', ServiceRoutesCommand::class); + } } diff --git a/tests/TestCase/Command/ServiceRoutesCommandTest.php b/tests/TestCase/Command/ServiceRoutesCommandTest.php new file mode 100644 index 0000000..a89ab86 --- /dev/null +++ b/tests/TestCase/Command/ServiceRoutesCommandTest.php @@ -0,0 +1,153 @@ +_publicAccess(); + parent::setUp(); + $this->useCommandRunner(); + } + + /** + * tearDown method + */ + public function tearDown(): void + { + parent::tearDown(); + ServiceRegistry::getServiceLocator()->clear(); + } + + /** + * Test help output + */ + public function testServiceRoutesHelp(): void + { + $this->exec('service routes -h'); + $this->assertExitCode(Command::CODE_SUCCESS); + $this->assertOutputContains('Display all routes in a service'); + $this->assertErrorEmpty(); + } + + /** + * Test basic route listing + */ + public function testServiceRoutesList(): void + { + $this->exec('service routes Articles'); + $this->assertExitCode(Command::CODE_SUCCESS); + + // Assert table header + $this->assertOutputContainsRow($this->getHeaderRow()); + + // Assert all routes + foreach ($this->getArticleRoutes() as $route) { + $this->assertOutputContainsRow($route); + } + } + + private function getHeaderRow(): array + { + return [ + 'Route name', + 'Method(s)', + 'URI template', + 'Service', + 'Action', + 'Plugin', + ]; + } + + private function getArticleRoutes(): array + { + return [ + [ + 'articles:untag', + 'PUT, POST', + '/articles/untag/{id}', + 'articles', + 'untag', + '', + ], + [ + 'articles:tag', + 'PUT, POST', + '/articles/tag/{id}', + 'articles', + 'tag', + '', + ], + [ + 'articles:view', + 'GET', + '/articles/{id}', + 'articles', + 'view', + '', + ], + [ + 'articles:edit', + 'PUT, PATCH', + '/articles/{id}', + 'articles', + 'edit', + '', + ], + [ + 'articles:delete', + 'DELETE', + '/articles/{id}', + 'articles', + 'delete', + '', + ], + [ + 'articles:describe', + 'OPTIONS', + '/articles/{id}', + 'articles', + 'describe', + '', + ], + [ + 'articles:index', + 'GET', + '/articles', + 'articles', + 'index', + '', + ], + [ + 'articles:add', + 'POST', + '/articles', + 'articles', + 'add', + '', + ], + [ + 'articles:describe', + 'OPTIONS', + '/articles', + 'articles', + 'describe', + '', + ], + ]; + } +} From 8e59431df5da044d7184ecbb58eba1efd8594003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Thu, 1 May 2025 09:15:06 +0100 Subject: [PATCH 11/16] Remove use of not exiting method in command test --- tests/TestCase/Command/ServiceRoutesCommandTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TestCase/Command/ServiceRoutesCommandTest.php b/tests/TestCase/Command/ServiceRoutesCommandTest.php index a89ab86..dd754b8 100644 --- a/tests/TestCase/Command/ServiceRoutesCommandTest.php +++ b/tests/TestCase/Command/ServiceRoutesCommandTest.php @@ -21,7 +21,6 @@ public function setUp(): void { $this->_publicAccess(); parent::setUp(); - $this->useCommandRunner(); } /** From c6f36a3ca71ffeea2baa91d3035fb7a6c9b9364b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Thu, 1 May 2025 13:26:42 +0100 Subject: [PATCH 12/16] cleaning code --- src/Plugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin.php b/src/Plugin.php index ad5daff..802ac1f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -60,7 +60,6 @@ public function routes($routes): void } /** -<<<<<<< HEAD * Middleware registrator and holder. * * @param \Cake\Routing\RouteBuilder $routes Routes. From 97a5d9dfb6ee2042c4cd81151a91d810160f8d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Mon, 5 May 2025 09:29:30 +0100 Subject: [PATCH 13/16] update dependenc versions in pipeline --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d79ed50..095a273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: testsuite: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -26,7 +26,7 @@ jobs: if: matrix.db-type == 'pgsql' run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cakephp -p 5432:5432 -d postgres - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -45,7 +45,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} @@ -75,14 +75,14 @@ jobs: - name: Submit code coverage if: matrix.php-version == '8.1' - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 cs-stan: name: Coding Standard & Static Analysis runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -100,7 +100,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} From ee4cfff79d93b6d35ced1462794bc01683c39cce Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Fri, 4 Oct 2024 22:38:50 +0300 Subject: [PATCH 14/16] phpstan and phpcs fixes --- composer.json | 5 +- phpstan.neon | 2 +- src/Command/ServiceRoutesCommand.php | 1 + src/Model/Entity/AuthStore.php | 2 +- src/Model/Table/AuthStoreTable.php | 5 -- src/Rbac/Rules/TwoFactorPassedScope.php | 5 +- src/Rbac/Rules/TwoFactorScope.php | 5 +- src/Routing/ApiRouter.php | 2 +- .../Action/Auth/JwtSocialLoginAction.php | 3 +- src/Service/Action/Auth/JwtTokenTrait.php | 50 +++++++++++++- src/Service/Action/Auth/OtpVerifyAction.php | 9 ++- .../Action/Auth/OtpVerifyCheckAction.php | 2 - .../Action/Auth/OtpVerifyGetAction.php | 7 +- .../Auth/ResetPasswordRequestAction.php | 2 +- .../Action/Auth/TwoFactorAuthAction.php | 6 +- src/Service/Action/Auth/Webauthn2faAction.php | 8 +-- .../Action/Auth/Webauthn2faAuthAction.php | 8 +-- .../Auth/Webauthn2faAuthOptionsAction.php | 7 +- .../Action/Auth/Webauthn2faRegisterAction.php | 6 +- .../Auth/Webauthn2faRegisterOptionsAction.php | 6 +- .../Action/Recaptcha/ValidateAction.php | 2 - src/Service/Action/Traits/ReCaptchaTrait.php | 3 +- ...DefaultWebauthn2fAuthenticationChecker.php | 2 +- ...mePasswordAuthenticationCheckerFactory.php | 4 +- ...Webauthn2fAuthenticationCheckerFactory.php | 6 +- src/Service/AuthService.php | 12 ++-- src/Service/ConfigReader.php | 4 +- src/Service/RecaptchaService.php | 1 - src/Utility/RequestParser.php | 14 +++- src/Webauthn/AuthenticateAdapter.php | 1 - src/Webauthn/BaseAdapter.php | 67 +++++++++++++++++-- src/Webauthn/RegisterAdapter.php | 1 - .../UserCredentialSourceRepository.php | 30 +++++++-- 33 files changed, 208 insertions(+), 80 deletions(-) diff --git a/composer.json b/composer.json index 983c007..581807e 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,12 @@ "firebase/php-jwt": "^6.3" }, "require-dev": { - "cakephp/cakephp-codesniffer": "~4.4.0", + "cakephp/cakephp-codesniffer": "^4.5", "league/flysystem-vfs": "^1.0", + "laminas/laminas-diactoros": "^3.0", "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.8", + "robthree/twofactorauth": "^1.6", "vlucas/phpdotenv": "^3.3" }, "autoload": { diff --git a/phpstan.neon b/phpstan.neon index 634e6ee..73c259c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 4 + level: 2 bootstrapFiles: - tests/bootstrap.php ignoreErrors: diff --git a/src/Command/ServiceRoutesCommand.php b/src/Command/ServiceRoutesCommand.php index ce6eb07..a52932d 100644 --- a/src/Command/ServiceRoutesCommand.php +++ b/src/Command/ServiceRoutesCommand.php @@ -68,6 +68,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $service = ServiceRegistry::getServiceLocator()->get($serviceName); if ($service === null) { $io->error(__('Service "{0}" not found', $serviceName)); + return Command::CODE_ERROR; } diff --git a/src/Model/Entity/AuthStore.php b/src/Model/Entity/AuthStore.php index dc1a323..899c088 100644 --- a/src/Model/Entity/AuthStore.php +++ b/src/Model/Entity/AuthStore.php @@ -9,7 +9,7 @@ * AuthStore Entity * * @property string $id - * @property string|null $store + * @property array|null $store * @property \Cake\I18n\FrozenTime $created * @property \Cake\I18n\FrozenTime $modified */ diff --git a/src/Model/Table/AuthStoreTable.php b/src/Model/Table/AuthStoreTable.php index e736b62..14bfbab 100644 --- a/src/Model/Table/AuthStoreTable.php +++ b/src/Model/Table/AuthStoreTable.php @@ -4,8 +4,6 @@ namespace CakeDC\Api\Model\Table; use Cake\Database\Schema\TableSchemaInterface; -use Cake\ORM\Query; -use Cake\ORM\RulesChecker; use Cake\ORM\Table; use Cake\Validation\Validator; @@ -25,7 +23,6 @@ * @method \CakeDC\Api\Model\Entity\AuthStore[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = []) * @method \CakeDC\Api\Model\Entity\AuthStore[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = []) * @method \CakeDC\Api\Model\Entity\AuthStore[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = []) - * * @mixin \Cake\ORM\Behavior\TimestampBehavior */ class AuthStoreTable extends Table @@ -62,8 +59,6 @@ public function validationDefault(Validator $validator): Validator return $validator; } - - /** * Initialize schema * diff --git a/src/Rbac/Rules/TwoFactorPassedScope.php b/src/Rbac/Rules/TwoFactorPassedScope.php index 785a46a..776f1ee 100644 --- a/src/Rbac/Rules/TwoFactorPassedScope.php +++ b/src/Rbac/Rules/TwoFactorPassedScope.php @@ -13,10 +13,8 @@ namespace CakeDC\Api\Rbac\Rules; use Authentication\Authenticator\JwtAuthenticator; -use CakeDC\Auth\Rbac\Rules\AbstractRule; -use Cake\Utility\Hash; use Cake\Routing\Router; -use OutOfBoundsException; +use CakeDC\Auth\Rbac\Rules\AbstractRule; use Psr\Http\Message\ServerRequestInterface; /** @@ -24,7 +22,6 @@ */ class TwoFactorPassedScope extends AbstractRule { - protected $_defaultConfig = [ ]; diff --git a/src/Rbac/Rules/TwoFactorScope.php b/src/Rbac/Rules/TwoFactorScope.php index e9c80f6..a122922 100644 --- a/src/Rbac/Rules/TwoFactorScope.php +++ b/src/Rbac/Rules/TwoFactorScope.php @@ -13,10 +13,8 @@ namespace CakeDC\Api\Rbac\Rules; use Authentication\Authenticator\JwtAuthenticator; -use CakeDC\Auth\Rbac\Rules\AbstractRule; -use Cake\Utility\Hash; use Cake\Routing\Router; -use OutOfBoundsException; +use CakeDC\Auth\Rbac\Rules\AbstractRule; use Psr\Http\Message\ServerRequestInterface; /** @@ -24,7 +22,6 @@ */ class TwoFactorScope extends AbstractRule { - protected $_defaultConfig = [ ]; diff --git a/src/Routing/ApiRouter.php b/src/Routing/ApiRouter.php index 2b5f17f..b33d6d1 100644 --- a/src/Routing/ApiRouter.php +++ b/src/Routing/ApiRouter.php @@ -134,7 +134,7 @@ class ApiRouter extends Router * The stack of URL filters to apply against routing URLs before passing the * parameters to the route collection. * - * @var array<\Closure> + * @var array */ protected static array $_urlFilters = []; diff --git a/src/Service/Action/Auth/JwtSocialLoginAction.php b/src/Service/Action/Auth/JwtSocialLoginAction.php index 122004b..500a770 100644 --- a/src/Service/Action/Auth/JwtSocialLoginAction.php +++ b/src/Service/Action/Auth/JwtSocialLoginAction.php @@ -28,6 +28,7 @@ class JwtSocialLoginAction extends Action * Execute action. * * @return mixed + * @throws \Exception */ public function execute() { @@ -37,6 +38,6 @@ public function execute() return false; } - return $this->generateTokenResponse($user); + return $this->generateTokenResponse($user, 'login'); } } diff --git a/src/Service/Action/Auth/JwtTokenTrait.php b/src/Service/Action/Auth/JwtTokenTrait.php index 8738fa8..03c4892 100644 --- a/src/Service/Action/Auth/JwtTokenTrait.php +++ b/src/Service/Action/Auth/JwtTokenTrait.php @@ -18,20 +18,23 @@ use Cake\Routing\Router; use Cake\Utility\Hash; use CakeDC\Api\Service\Auth\TwoFactorAuthentication\OneTimePasswordAuthenticationCheckerFactory; -use CakeDC\Api\Service\Auth\TwoFactorAuthentication\Webauthn2fAuthenticationCheckerFactory; +use CakeDC\Api\Service\Auth\TwoFactorAuthentication\Webauthn2FAuthenticationCheckerFactory; use DateInterval; use DateTimeImmutable; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Hmac\Sha512; use Lcobucci\JWT\Signer\Key\InMemory; +/** + * JwtTokenTrait + */ trait JwtTokenTrait { - /** * Generates token response. * * @param \Cake\Datasource\EntityInterface|array $user User info. + * @param string|null $type The type of token being generated. * @return array */ public function generateTokenResponse($user, $type) @@ -49,6 +52,13 @@ public function generateTokenResponse($user, $type) ]); } + /** + * Generates refresh token response. + * + * @param \Cake\Datasource\EntityInterface|array $user User info. + * @param array $payload Additional payload data. + * @return array + */ public function generateRefreshTokenResponse($user, $payload) { $timestamp = new DateTimeImmutable(); @@ -65,6 +75,8 @@ public function generateRefreshTokenResponse($user, $payload) * * @param \Cake\Datasource\EntityInterface|array $user User info. * @param \DateTimeImmutable $timestamp Timestamp. + * @param string|null $type The type of token being generated. + * @param array|null $payload Additional payload data. * @return bool|string */ public function generateAccessToken($user, $timestamp, $type, $payload = null) @@ -92,6 +104,14 @@ public function generateAccessToken($user, $timestamp, $type, $payload = null) return $token->toString(); } + /** + * Get the audience for the token. + * + * @param \Cake\Datasource\EntityInterface|array $user User info. + * @param string|null $type The type of token being generated. + * @param array|null $payload Additional payload data. + * @return string + */ public function getAudience($user, $type, $payload) { if ($type === null && is_array($payload) && isset($payload['aud'])) { @@ -106,11 +126,23 @@ public function getAudience($user, $type, $payload) return $audience; } + /** + * Check if 2FA is enabled for the user. + * + * @param \Cake\Datasource\EntityInterface|array $user User info. + * @return bool + */ protected function is2FAEnabled($user) { return $this->isEnabledWebauthn2faAuthentication($user) || $this->isEnabledOneTimePasswordAuthentication($user); } + /** + * Check if Webauthn 2FA authentication is enabled for the user. + * + * @param \Cake\Datasource\EntityInterface|array $user User info. + * @return bool + */ public function isEnabledWebauthn2faAuthentication($user) { $enabledTwoFactorVerify = Configure::read('Api.2fa.enabled'); @@ -122,6 +154,12 @@ public function isEnabledWebauthn2faAuthentication($user) return false; } + /** + * Check if One-Time Password authentication is enabled for the user. + * + * @param \Cake\Datasource\EntityInterface|array $user User info. + * @return bool + */ public function isEnabledOneTimePasswordAuthentication($user) { $enabledTwoFactorVerify = Configure::read('Api.2fa.enabled'); @@ -133,6 +171,11 @@ public function isEnabledOneTimePasswordAuthentication($user) return false; } + /** + * Get the One-Time Password Authentication Checker. + * + * @return \CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerInterface + */ protected function getOneTimePasswordAuthenticationChecker() { return (new OneTimePasswordAuthenticationCheckerFactory())->build(); @@ -153,6 +196,8 @@ protected function getWebauthn2fAuthenticationChecker() * * @param \Cake\Datasource\EntityInterface|array $user User info. * @param \DateTimeImmutable $timestamp Timestamp. + * @param string|null $type The type of token being generated. + * @param array|null $payload Additional payload data. * @return bool|string */ public function generateRefreshToken($user, $timestamp, $type, $payload = null) @@ -184,6 +229,7 @@ public function generateRefreshToken($user, $timestamp, $type, $payload = null) $model = $UsersTable->getAlias(); $table = TableRegistry::getTableLocator()->get('CakeDC/Api.JwtRefreshTokens'); + /** @var \CakeDC\Api\Model\Entity\JwtRefreshToken $entity */ $entity = $table->find()->where([ 'model' => $model, 'foreign_key' => $subject, diff --git a/src/Service/Action/Auth/OtpVerifyAction.php b/src/Service/Action/Auth/OtpVerifyAction.php index 0f626af..24bc23f 100644 --- a/src/Service/Action/Auth/OtpVerifyAction.php +++ b/src/Service/Action/Auth/OtpVerifyAction.php @@ -13,9 +13,9 @@ namespace CakeDC\Api\Service\Action\Auth; +use Cake\Core\Configure; use CakeDC\Api\Service\Action\Action; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; use RobThree\Auth\TwoFactorAuth; /** @@ -32,6 +32,12 @@ abstract class OtpVerifyAction extends Action */ public $tfa; + /** + * initialize + * + * @param array $config Configuration. + * @return void + */ public function initialize(array $config): void { $this->tfa = new TwoFactorAuth( @@ -78,5 +84,4 @@ public function getQRCodeImageAsDataUri($issuer, $secret) { return $this->tfa->getQRCodeImageAsDataUri($issuer, $secret); } - } diff --git a/src/Service/Action/Auth/OtpVerifyCheckAction.php b/src/Service/Action/Auth/OtpVerifyCheckAction.php index 16bbb8f..470eb3c 100644 --- a/src/Service/Action/Auth/OtpVerifyCheckAction.php +++ b/src/Service/Action/Auth/OtpVerifyCheckAction.php @@ -13,7 +13,6 @@ namespace CakeDC\Api\Service\Action\Auth; -use CakeDC\Api\Service\Action\Action; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** @@ -57,5 +56,4 @@ public function execute() return $this->generateTokenResponse($user->toArray(), '2fa'); } - } diff --git a/src/Service/Action/Auth/OtpVerifyGetAction.php b/src/Service/Action/Auth/OtpVerifyGetAction.php index 38e7024..d2c6b8d 100644 --- a/src/Service/Action/Auth/OtpVerifyGetAction.php +++ b/src/Service/Action/Auth/OtpVerifyGetAction.php @@ -13,7 +13,6 @@ namespace CakeDC\Api\Service\Action\Auth; -use CakeDC\Api\Service\Action\Action; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** @@ -54,6 +53,12 @@ public function execute() return $result; } + /** + * onVerifyGetSecret + * + * @param array $user User. + * @return string + */ protected function onVerifyGetSecret($user) { if (isset($user['secret']) && $user['secret']) { diff --git a/src/Service/Action/Auth/ResetPasswordRequestAction.php b/src/Service/Action/Auth/ResetPasswordRequestAction.php index 4e9535e..953fa80 100644 --- a/src/Service/Action/Auth/ResetPasswordRequestAction.php +++ b/src/Service/Action/Auth/ResetPasswordRequestAction.php @@ -83,7 +83,7 @@ public function execute() 'ensureActive' => Configure::read('Users.Registration.ensureActive'), ]; if (!empty($baseUrl)) { - $options['linkGenerator'] = function($token) use ($baseUrl) { + $options['linkGenerator'] = function ($token) use ($baseUrl) { return $baseUrl . '?token=' . $token; }; } diff --git a/src/Service/Action/Auth/TwoFactorAuthAction.php b/src/Service/Action/Auth/TwoFactorAuthAction.php index a7bb9ca..3eb200b 100644 --- a/src/Service/Action/Auth/TwoFactorAuthAction.php +++ b/src/Service/Action/Auth/TwoFactorAuthAction.php @@ -14,9 +14,8 @@ namespace CakeDC\Api\Service\Action\Auth; use CakeDC\Api\Service\Action\Action; -use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; use CakeDC\Api\Webauthn\RegisterAdapter; +use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** * Class LoginAction @@ -49,6 +48,5 @@ public function execute() 'enabledWebauthn' => $this->isEnabledWebauthn2faAuthentication((array)$user), 'enabledOtp' => $this->isEnabledOneTimePasswordAuthentication((array)$user), ]; - } - + } } diff --git a/src/Service/Action/Auth/Webauthn2faAction.php b/src/Service/Action/Auth/Webauthn2faAction.php index 5c70789..b3196fd 100644 --- a/src/Service/Action/Auth/Webauthn2faAction.php +++ b/src/Service/Action/Auth/Webauthn2faAction.php @@ -14,9 +14,8 @@ namespace CakeDC\Api\Service\Action\Auth; use CakeDC\Api\Service\Action\Action; -use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; use CakeDC\Api\Webauthn\RegisterAdapter; +use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** * Class LoginAction @@ -35,11 +34,12 @@ class Webauthn2faAction extends Action public function execute() { $user = $this->getIdentity(); - $adapter = new RegisterAdapter($this->getService()->getRequest(), $this->getUsersTable(), $user); + $request = $this->getService()->getRequest(); + $adapter = new RegisterAdapter($request, $this->getUsersTable(), $user); return [ 'isRegister' => !$adapter->hasCredential(), 'username' => $user->webauthn_username ?? $user->username, ]; - } + } } diff --git a/src/Service/Action/Auth/Webauthn2faAuthAction.php b/src/Service/Action/Auth/Webauthn2faAuthAction.php index 30de0c7..5b57f71 100644 --- a/src/Service/Action/Auth/Webauthn2faAuthAction.php +++ b/src/Service/Action/Auth/Webauthn2faAuthAction.php @@ -14,9 +14,8 @@ namespace CakeDC\Api\Service\Action\Auth; use CakeDC\Api\Service\Action\Action; -use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; use CakeDC\Api\Webauthn\AuthenticateAdapter; +use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** * Class LoginAction @@ -44,8 +43,9 @@ public function execute() return $this->generateTokenResponse($user->toArray(), '2fa'); } catch (\Throwable $e) { $user = $this->getIdentity(); - \Cake\Log\Log::debug(__d('cake_d_c/api', 'Register error with webauthn for user id: {0}', $user['id'] ?? 'empty')); + $message = __d('cake_d_c/api', 'Register error with webauthn for user id: {0}', $user['id'] ?? 'empty'); + Log::debug($message); throw $e; } - } + } } diff --git a/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php b/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php index 8f3293b..89f666b 100644 --- a/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php +++ b/src/Service/Action/Auth/Webauthn2faAuthOptionsAction.php @@ -16,8 +16,6 @@ use CakeDC\Api\Service\Action\Action; use CakeDC\Api\Webauthn\AuthenticateAdapter; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; -use Cake\Http\Exception\BadRequestException; /** * Class LoginAction @@ -35,8 +33,9 @@ class Webauthn2faAuthOptionsAction extends Action */ public function execute() { - $adapter = new AuthenticateAdapter($this->getService()->getRequest(), $this->getUsersTable(), $this->getIdentity()); + $request = $this->getService()->getRequest(); + $adapter = new AuthenticateAdapter($request, $this->getUsersTable(), $this->getIdentity()); return $adapter->getOptions(); - } + } } diff --git a/src/Service/Action/Auth/Webauthn2faRegisterAction.php b/src/Service/Action/Auth/Webauthn2faRegisterAction.php index 149d3aa..c53a97c 100644 --- a/src/Service/Action/Auth/Webauthn2faRegisterAction.php +++ b/src/Service/Action/Auth/Webauthn2faRegisterAction.php @@ -14,9 +14,8 @@ namespace CakeDC\Api\Service\Action\Auth; use CakeDC\Api\Service\Action\Action; -use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; use CakeDC\Api\Webauthn\RegisterAdapter; +use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; /** * Class LoginAction @@ -46,6 +45,5 @@ public function execute() throw new BadRequestException( __d('cake_d_c/api', 'User already has configured webauthn2fa') ); - - } + } } diff --git a/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php b/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php index 93fc969..8dc3f36 100644 --- a/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php +++ b/src/Service/Action/Auth/Webauthn2faRegisterOptionsAction.php @@ -13,11 +13,10 @@ namespace CakeDC\Api\Service\Action\Auth; +use Cake\Http\Exception\BadRequestException; use CakeDC\Api\Service\Action\Action; use CakeDC\Api\Webauthn\RegisterAdapter; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; -use Cake\Core\Configure; -use Cake\Http\Exception\BadRequestException; /** * Class LoginAction @@ -44,6 +43,5 @@ public function execute() throw new BadRequestException( __d('cake_d_c/api', 'User already has configured webauthn2fa') ); - - } + } } diff --git a/src/Service/Action/Recaptcha/ValidateAction.php b/src/Service/Action/Recaptcha/ValidateAction.php index d374758..1f6f134 100644 --- a/src/Service/Action/Recaptcha/ValidateAction.php +++ b/src/Service/Action/Recaptcha/ValidateAction.php @@ -15,7 +15,6 @@ use CakeDC\Api\Service\Action\Action; use CakeDC\Api\Service\Action\Traits\ReCaptchaTrait; -use CakeDC\Api\Utility\RequestParser; /** * Class SocialLoginAction @@ -36,5 +35,4 @@ public function execute() { return $this->validateReCaptcha(); } - } diff --git a/src/Service/Action/Traits/ReCaptchaTrait.php b/src/Service/Action/Traits/ReCaptchaTrait.php index d19ddf2..3aff708 100644 --- a/src/Service/Action/Traits/ReCaptchaTrait.php +++ b/src/Service/Action/Traits/ReCaptchaTrait.php @@ -15,7 +15,6 @@ use Cake\Core\Configure; use CakeDC\Api\Utility\RequestParser; -use ReCaptcha\ReCaptcha; /** * Covers registration features and email token validation @@ -55,7 +54,7 @@ public function validateReCaptcha($recaptchaResponse = null) protected function _getReCaptchaInstance() { $domain = RequestParser::getDomain($this->getService()->getRequest()); - $reCaptchaSecret = Configure::read('Api.reCaptcha.' . $domain. '.secret'); + $reCaptchaSecret = Configure::read('Api.reCaptcha.' . $domain . '.secret'); if (!empty($reCaptchaSecret)) { return new \ReCaptcha\ReCaptcha($reCaptchaSecret); } diff --git a/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php index 2e52f67..06afa4b 100644 --- a/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php +++ b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php @@ -19,7 +19,7 @@ * * @package CakeDC\Auth\Auth */ -class DefaultWebauthn2fAuthenticationChecker implements Webauthn2fAuthenticationCheckerInterface +class DefaultWebauthn2fAuthenticationChecker implements Webauthn2FAuthenticationCheckerInterface { /** * Check if two factor authentication is enabled diff --git a/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php b/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php index 67b6066..edeaac4 100644 --- a/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php +++ b/src/Service/Auth/TwoFactorAuthentication/OneTimePasswordAuthenticationCheckerFactory.php @@ -35,6 +35,8 @@ public function build() if (in_array($required, $interfaces)) { return new $className(); } - throw new \InvalidArgumentException("Invalid config for 'OneTimePasswordAuthenticator.checker', '$className' does not implement '$required'"); + $message = "Invalid config for 'OneTimePasswordAuthenticator.checker', " . + "'$className' does not implement '$required'"; + throw new \InvalidArgumentException($message); } } diff --git a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php index 6410216..f83c80e 100644 --- a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php +++ b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php @@ -19,7 +19,7 @@ * * @package CakeDC\Auth\Auth */ -class Webauthn2fAuthenticationCheckerFactory +class Webauthn2FAuthenticationCheckerFactory { /** * Get the two factor authentication checker @@ -35,6 +35,8 @@ public function build() if (in_array($required, $interfaces)) { return new $className(); } - throw new \InvalidArgumentException("Invalid config for 'Webauthn2fa.checker', '$className' does not implement '$required'"); + $message = "Invalid config for 'Webauthn2fa.checker', " . + "'$className' does not implement '$required'"; + throw new \InvalidArgumentException($message); } } diff --git a/src/Service/AuthService.php b/src/Service/AuthService.php index eef5b2e..c16ee63 100644 --- a/src/Service/AuthService.php +++ b/src/Service/AuthService.php @@ -17,21 +17,21 @@ use CakeDC\Api\Service\Action\Auth\JwtLoginAction; use CakeDC\Api\Service\Action\Auth\JwtRefreshAction; use CakeDC\Api\Service\Action\Auth\JwtSocialLoginAction; -use CakeDC\Api\Service\Action\Auth\OtpVerifyGetAction; -use CakeDC\Api\Service\Action\Auth\OtpVerifyCheckAction; use CakeDC\Api\Service\Action\Auth\LoginAction; +use CakeDC\Api\Service\Action\Auth\OtpVerifyCheckAction; +use CakeDC\Api\Service\Action\Auth\OtpVerifyGetAction; use CakeDC\Api\Service\Action\Auth\RegisterAction; use CakeDC\Api\Service\Action\Auth\ResetPasswordAction; use CakeDC\Api\Service\Action\Auth\ResetPasswordRequestAction; use CakeDC\Api\Service\Action\Auth\SocialLoginAction; +use CakeDC\Api\Service\Action\Auth\TwoFactorAuthAction; use CakeDC\Api\Service\Action\Auth\ValidateAccountAction; use CakeDC\Api\Service\Action\Auth\ValidateAccountRequestAction; -use CakeDC\Api\Service\Action\Auth\TwoFactorAuthAction; use CakeDC\Api\Service\Action\Auth\Webauthn2faAction; -use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterOptionsAction; -use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterAction; -use CakeDC\Api\Service\Action\Auth\Webauthn2faAuthOptionsAction; use CakeDC\Api\Service\Action\Auth\Webauthn2faAuthAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faAuthOptionsAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterAction; +use CakeDC\Api\Service\Action\Auth\Webauthn2faRegisterOptionsAction; /** * Class AuthService diff --git a/src/Service/ConfigReader.php b/src/Service/ConfigReader.php index a09af6e..1ee01a5 100644 --- a/src/Service/ConfigReader.php +++ b/src/Service/ConfigReader.php @@ -22,7 +22,7 @@ class ConfigReader * Builds service options. * * @param string $serviceName Service name. - * @param ?string $version Version number. + * @param string|null $version Version number. * @return array */ public function serviceOptions(string $serviceName, ?string $version = null): array @@ -44,7 +44,7 @@ public function serviceOptions(string $serviceName, ?string $version = null): ar * * @param string $serviceName A Service name. * @param string $actionName An Action name. - * @param ?string $version Version number. + * @param string|null $version Version number. * @return array */ public function actionOptions(string $serviceName, string $actionName, ?string $version = null): array diff --git a/src/Service/RecaptchaService.php b/src/Service/RecaptchaService.php index 9178769..e82b0fb 100644 --- a/src/Service/RecaptchaService.php +++ b/src/Service/RecaptchaService.php @@ -13,7 +13,6 @@ namespace CakeDC\Api\Service; -use Cake\Utility\Hash; use CakeDC\Api\Service\Action\Recaptcha\ValidateAction; /** diff --git a/src/Utility/RequestParser.php b/src/Utility/RequestParser.php index 0d68b81..3483f5f 100644 --- a/src/Utility/RequestParser.php +++ b/src/Utility/RequestParser.php @@ -15,18 +15,28 @@ use Cake\Http\ServerRequest; +/** + * RequestParser class. + */ class RequestParser { + /** + * Get the domain from the request. + * + * @param \Cake\Http\ServerRequest $request The request object. + * @param bool $replace Whether to replace the domain. + * @return string + */ public static function getDomain(ServerRequest $request, $replace = true) { $domain = null; if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) { $domain = parse_url($_SERVER['HTTP_REFERER']); } - if ($domain !==null && $domain['host']) { + if ($domain !== null && $domain['host']) { $host = $domain['host']; } else { - $host = $this->request->domain(); + $host = $request->domain(); } if ($replace) { diff --git a/src/Webauthn/AuthenticateAdapter.php b/src/Webauthn/AuthenticateAdapter.php index f8b8032..16fb8e5 100644 --- a/src/Webauthn/AuthenticateAdapter.php +++ b/src/Webauthn/AuthenticateAdapter.php @@ -13,7 +13,6 @@ namespace CakeDC\Api\Webauthn; -use Cake\Utility\Hash; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; diff --git a/src/Webauthn/BaseAdapter.php b/src/Webauthn/BaseAdapter.php index dcbe543..34f6f20 100644 --- a/src/Webauthn/BaseAdapter.php +++ b/src/Webauthn/BaseAdapter.php @@ -17,46 +17,55 @@ use Cake\Http\ServerRequest; use Cake\ORM\TableRegistry; use Cake\Utility\Hash; -use CakeDC\Users\Model\Table\UsersTable; use CakeDC\Api\Utility\RequestParser; use CakeDC\Api\Webauthn\Repository\UserCredentialSourceRepository; +use CakeDC\Users\Model\Table\UsersTable; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\Server; class BaseAdapter { - const STORE_PREFIX = 'api.Webauthn2fa'; + public const STORE_PREFIX = 'api.Webauthn2fa'; /** * @var \Cake\Http\ServerRequest */ protected $request; + /** - * @var \CakeDC\Users\Webauthn\Repository\UserCredentialSourceRepository + * @var \CakeDC\Api\Webauthn\Repository\UserCredentialSourceRepository */ protected $repository; + /** * @var \Webauthn\Server */ protected $server; + /** * @var \Cake\Datasource\EntityInterface|\CakeDC\Users\Model\Entity\User */ private $user; + /** * @var \CakeDC\Api\Model\Table\AuthStoreTable */ protected $store; /** + * Constructor. + * * @param \Cake\Http\ServerRequest $request The request. * @param \CakeDC\Users\Model\Table\UsersTable|null $usersTable The users table. + * @param \Cake\Datasource\EntityInterface|\CakeDC\Users\Model\Entity\User $userData The user data. */ public function __construct(ServerRequest $request, ?UsersTable $usersTable, $userData) { $this->request = $request; - $this->store = TableRegistry::getTableLocator()->get('CakeDC/Api.AuthStore'); + /** @var \CakeDC\Api\Model\Table\AuthStoreTable $store */ + $store = TableRegistry::getTableLocator()->get('CakeDC/Api.AuthStore'); + $this->store = $store; $session = $this->readStore(); $rpEntity = new PublicKeyCredentialRpEntity( Configure::read('Api.Webauthn2fa.' . $this->getDomain() . '.appName'), // The application name @@ -80,6 +89,8 @@ public function __construct(ServerRequest $request, ?UsersTable $usersTable, $us } /** + * Get the user entity. + * * @return \Webauthn\PublicKeyCredentialUserEntity */ protected function getUserEntity(): PublicKeyCredentialUserEntity @@ -94,6 +105,8 @@ protected function getUserEntity(): PublicKeyCredentialUserEntity } /** + * Get the user. + * * @return array|mixed|null */ public function getUser() @@ -102,6 +115,8 @@ public function getUser() } /** + * Check if the user has a credential. + * * @return bool */ public function hasCredential(): bool @@ -111,8 +126,14 @@ public function hasCredential(): bool ); } + /** + * Read the store data. + * + * @return \CakeDC\Api\Model\Entity\AuthStore + */ public function readStore() { + /** @var \CakeDC\Api\Model\Entity\AuthStore|null $entity */ $entity = $this->store->find()->where(['id' => $this->getStoreKey()])->first(); if ($entity === null) { $entity = $this->store->newEmptyEntity(); @@ -125,13 +146,25 @@ public function readStore() return $entity; } + /** + * Save the store data. + * + * @param array $data The data to save. + * @return \CakeDC\Api\Model\Entity\AuthStore|false + */ public function saveStore($data) { $entity = $this->readStore(); $entity->store = $data; + return $this->store->save($entity); } + /** + * Delete the store data. + * + * @return bool + */ public function deleteStore() { $entity = $this->readStore(); @@ -139,6 +172,11 @@ public function deleteStore() return $this->store->delete($entity); } + /** + * Get the store key. + * + * @return string + */ public function getStoreKey() { $authHeader = $this->request->getHeader('Authorization'); @@ -152,6 +190,14 @@ public function getStoreKey() return str_ireplace($options['tokenPrefix'] . ' ', '', $authHeader); } + /** + * Patch the store data. + * + * @param \CakeDC\Api\Model\Entity\AuthStore $entity The entity to patch. + * @param string $name The name of the data. + * @param array $options The options to patch. + * @return \CakeDC\Api\Model\Entity\AuthStore + */ public function patchStore($entity, $name, $options) { $entity['store']['api']['Webauthn2fa'][$this->getDomain()][$name] = $options; @@ -159,6 +205,13 @@ public function patchStore($entity, $name, $options) return $entity; } + /** + * Get the store data. + * + * @param array $entity The entity to get data from. + * @param string $name The name of the data to get. + * @return mixed|null + */ public function getStore($entity, $name) { $path = self::STORE_PREFIX . '.' . $this->getDomain() . '.' . $name; @@ -166,6 +219,12 @@ public function getStore($entity, $name) return Hash::get($entity['store'], $path, null); } + /** + * Get the current domain. + * + * @param bool $replace Whether to replace the domain. + * @return string + */ public function getDomain($replace = true) { return RequestParser::getDomain($this->request, $replace); diff --git a/src/Webauthn/RegisterAdapter.php b/src/Webauthn/RegisterAdapter.php index adaf09a..7332377 100644 --- a/src/Webauthn/RegisterAdapter.php +++ b/src/Webauthn/RegisterAdapter.php @@ -13,7 +13,6 @@ namespace CakeDC\Api\Webauthn; -use Cake\Utility\Hash; use Webauthn\PublicKeyCredentialCreationOptions; class RegisterAdapter extends BaseAdapter diff --git a/src/Webauthn/Repository/UserCredentialSourceRepository.php b/src/Webauthn/Repository/UserCredentialSourceRepository.php index 3179821..69d44ca 100644 --- a/src/Webauthn/Repository/UserCredentialSourceRepository.php +++ b/src/Webauthn/Repository/UserCredentialSourceRepository.php @@ -29,8 +29,9 @@ class UserCredentialSourceRepository implements PublicKeyCredentialSourceReposit private $usersTable; /** + * @param \Cake\Http\ServerRequest $request The request. * @param \Cake\Datasource\EntityInterface $user The user. - * @param \CakeDC\Users\Model\Table\UsersTable|null $usersTable The table. + * @param \CakeDC\Users\Model\Table\UsersTable|null $usersTable The users table. */ public function __construct(ServerRequest $request, EntityInterface $user, ?UsersTable $usersTable = null) { @@ -76,6 +77,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre /** * @param \Webauthn\PublicKeyCredentialSource $publicKeyCredentialSource Public key credential source + * @return void */ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { @@ -86,16 +88,30 @@ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredent $res = $this->usersTable->saveOrFail($this->user); } + /** + * Patch user data with webauthn credentials. + * + * @param \Cake\Datasource\EntityInterface $entity The user entity + * @param array $options The webauthn credentials + * @return \Cake\Datasource\EntityInterface + */ public function patchUserData($entity, $options) { $entity['additional_data'] = $entity['additional_data'] ?? []; - $entity['additional_data']['api'] = $entity['additional_data']['api'] ?? []; - $entity['additional_data']['api'][$this->getDomain()] = $entity['additional_data']['api'][$this->getDomain()] ?? []; - $entity['additional_data']['api'][$this->getDomain()]['webauthn_credentials'] = $options; + $apiData = $entity['additional_data']['api'] ?? []; + $apiData[$this->getDomain()] = $apiData[$this->getDomain()] ?? []; + $apiData[$this->getDomain()]['webauthn_credentials'] = $options; + $entity['additional_data']['api'] = $apiData; return $entity; } + /** + * Get user data for webauthn credentials. + * + * @param \Cake\Datasource\EntityInterface $entity The user entity + * @return array + */ public function getUserData($entity) { $path = 'additional_data.api.' . $this->getDomain() . '.webauthn_credentials'; @@ -103,9 +119,13 @@ public function getUserData($entity) return Hash::get($entity, $path, []); } + /** + * Get the current domain. + * + * @return string + */ public function getDomain() { return RequestParser::getDomain($this->request); } - } From 3f8f398a7a6ce934c5fb5c5a83970117fcc34cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Mon, 5 May 2025 10:01:07 +0100 Subject: [PATCH 15/16] Fix php-cs --- src/Service/Action/Auth/JwtTokenTrait.php | 4 ++-- .../DefaultWebauthn2fAuthenticationChecker.php | 2 +- .../Webauthn2fAuthenticationCheckerFactory.php | 6 +++--- .../Webauthn2fAuthenticationCheckerInterface.php | 2 +- tests/Fixture/UsersFixture.php | 2 -- tests/bootstrap.php | 6 +++--- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Service/Action/Auth/JwtTokenTrait.php b/src/Service/Action/Auth/JwtTokenTrait.php index 03c4892..181a09c 100644 --- a/src/Service/Action/Auth/JwtTokenTrait.php +++ b/src/Service/Action/Auth/JwtTokenTrait.php @@ -18,7 +18,7 @@ use Cake\Routing\Router; use Cake\Utility\Hash; use CakeDC\Api\Service\Auth\TwoFactorAuthentication\OneTimePasswordAuthenticationCheckerFactory; -use CakeDC\Api\Service\Auth\TwoFactorAuthentication\Webauthn2FAuthenticationCheckerFactory; +use CakeDC\Api\Service\Auth\TwoFactorAuthentication\Webauthn2fAuthenticationCheckerFactory; use DateInterval; use DateTimeImmutable; use Lcobucci\JWT\Configuration; @@ -184,7 +184,7 @@ protected function getOneTimePasswordAuthenticationChecker() /** * Get the configured u2f authentication checker * - * @return \CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface + * @return \CakeDC\Auth\Authentication\Webauthn2fAuthenticationCheckerInterface */ protected function getWebauthn2fAuthenticationChecker() { diff --git a/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php index 06afa4b..2e52f67 100644 --- a/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php +++ b/src/Service/Auth/TwoFactorAuthentication/DefaultWebauthn2fAuthenticationChecker.php @@ -19,7 +19,7 @@ * * @package CakeDC\Auth\Auth */ -class DefaultWebauthn2fAuthenticationChecker implements Webauthn2FAuthenticationCheckerInterface +class DefaultWebauthn2fAuthenticationChecker implements Webauthn2fAuthenticationCheckerInterface { /** * Check if two factor authentication is enabled diff --git a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php index f83c80e..fe09cfb 100644 --- a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php +++ b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerFactory.php @@ -19,18 +19,18 @@ * * @package CakeDC\Auth\Auth */ -class Webauthn2FAuthenticationCheckerFactory +class Webauthn2fAuthenticationCheckerFactory { /** * Get the two factor authentication checker * - * @return \CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface + * @return \CakeDC\Auth\Authentication\Webauthn2fAuthenticationCheckerInterface */ public function build() { $className = Configure::read('Api.Webauthn2fa.checker'); $interfaces = class_implements($className); - $required = Webauthn2FAuthenticationCheckerInterface::class; + $required = Webauthn2fAuthenticationCheckerInterface::class; if (in_array($required, $interfaces)) { return new $className(); diff --git a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerInterface.php b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerInterface.php index 72e2976..ea31e05 100644 --- a/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerInterface.php +++ b/src/Service/Auth/TwoFactorAuthentication/Webauthn2fAuthenticationCheckerInterface.php @@ -12,7 +12,7 @@ */ namespace CakeDC\Api\Service\Auth\TwoFactorAuthentication; -interface Webauthn2FAuthenticationCheckerInterface +interface Webauthn2fAuthenticationCheckerInterface { /** * Check if two factor authentication is enabled diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php index b0e541d..c20db91 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -15,14 +15,12 @@ use Authentication\PasswordHasher\DefaultPasswordHasher; use Authentication\PasswordHasher\PasswordHasherFactory; use Cake\TestSuite\Fixture\TestFixture; -use CakeDC\Users\Webauthn\Base64Utility; /** * UsersFixture */ class UsersFixture extends TestFixture { - /** * records property * diff --git a/tests/bootstrap.php b/tests/bootstrap.php index eb81423..44df88e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -83,9 +83,9 @@ function def($name, $value) // Cake\Core\Configure::write('App.encoding', 'UTF-8'); Cake\Core\Configure::write('debug', true); -@mkdir(TMP . 'cache/models', 0777); -@mkdir(TMP . 'cache/persistent', 0777); -@mkdir(TMP . 'cache/views', 0777); +mkdir(TMP . 'cache/models', 0777); +mkdir(TMP . 'cache/persistent', 0777); +mkdir(TMP . 'cache/views', 0777); $cache = [ 'default' => [ From 9de7bd67ecd129a0dc607549b9cc9d03ab8e43fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Mon, 5 May 2025 10:41:03 +0100 Subject: [PATCH 16/16] WIP fixing stan --- composer.json | 4 +++- phpstan-baseline.neon | 7 ------- src/Model/Entity/AuthStore.php | 2 +- src/Rbac/Rules/TwoFactorPassedScope.php | 4 ++-- src/Rbac/Rules/TwoFactorScope.php | 4 ++-- src/Service/Action/Auth/OtpVerifyCheckAction.php | 2 +- src/Service/Action/Auth/OtpVerifyGetAction.php | 4 ++-- src/Service/Action/Auth/ResetPasswordAction.php | 1 + src/Service/Action/Auth/Webauthn2faAuthAction.php | 2 ++ .../Action/Auth/Webauthn2faRegisterAction.php | 1 + .../Action/Extension/AuthorizationExtension.php_ | 2 +- src/Service/Action/Recaptcha/ValidateAction.php | 1 - src/Webauthn/AuthenticateAdapter.php | 13 +++++++------ tests/bootstrap.php | 14 ++++++++++---- 14 files changed, 33 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 581807e..129a303 100644 --- a/composer.json +++ b/composer.json @@ -36,12 +36,14 @@ }, "require-dev": { "cakephp/cakephp-codesniffer": "^4.5", + "google/recaptcha": "@stable", "league/flysystem-vfs": "^1.0", "laminas/laminas-diactoros": "^3.0", "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.8", "robthree/twofactorauth": "^1.6", - "vlucas/phpdotenv": "^3.3" + "vlucas/phpdotenv": "^3.3", + "web-auth/webauthn-lib": "^5.0" }, "autoload": { "psr-4": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8c069f1..685dbdd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -17,11 +17,6 @@ parameters: count: 1 path: src\Service\Action\Auth\ResetPasswordAction.php - - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationPasswordConfirm\\(\\)\\.$#" - count: 1 - path: src\Service\Action\Auth\ResetPasswordAction.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:changePassword\\(\\)\\.$#" count: 1 @@ -46,5 +41,3 @@ parameters: message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:resetToken\\(\\)\\.$#" count: 1 path: src\Service\Action\Auth\ValidateAccountRequestAction.php - - diff --git a/src/Model/Entity/AuthStore.php b/src/Model/Entity/AuthStore.php index 899c088..2006d5f 100644 --- a/src/Model/Entity/AuthStore.php +++ b/src/Model/Entity/AuthStore.php @@ -24,7 +24,7 @@ class AuthStore extends Entity * * @var array */ - protected $_accessible = [ + protected array $_accessible = [ 'id' => true, 'store' => true, 'created' => true, diff --git a/src/Rbac/Rules/TwoFactorPassedScope.php b/src/Rbac/Rules/TwoFactorPassedScope.php index 776f1ee..af4e733 100644 --- a/src/Rbac/Rules/TwoFactorPassedScope.php +++ b/src/Rbac/Rules/TwoFactorPassedScope.php @@ -22,13 +22,13 @@ */ class TwoFactorPassedScope extends AbstractRule { - protected $_defaultConfig = [ + protected array $_defaultConfig = [ ]; /** * @inheritDoc */ - public function allowed($user, $role, ServerRequestInterface $request) + public function allowed(array|\ArrayAccess $user, string $role, ServerRequestInterface $request): bool { $authentication = $request->getAttribute('authentication'); if ($authentication === null) { diff --git a/src/Rbac/Rules/TwoFactorScope.php b/src/Rbac/Rules/TwoFactorScope.php index a122922..7b96fac 100644 --- a/src/Rbac/Rules/TwoFactorScope.php +++ b/src/Rbac/Rules/TwoFactorScope.php @@ -22,13 +22,13 @@ */ class TwoFactorScope extends AbstractRule { - protected $_defaultConfig = [ + protected array $_defaultConfig = [ ]; /** * @inheritDoc */ - public function allowed($user, $role, ServerRequestInterface $request) + public function allowed(array|\ArrayAccess $user, string $role, ServerRequestInterface $request): bool { $authentication = $request->getAttribute('authentication'); if ($authentication === null) { diff --git a/src/Service/Action/Auth/OtpVerifyCheckAction.php b/src/Service/Action/Auth/OtpVerifyCheckAction.php index 470eb3c..b22136a 100644 --- a/src/Service/Action/Auth/OtpVerifyCheckAction.php +++ b/src/Service/Action/Auth/OtpVerifyCheckAction.php @@ -48,7 +48,7 @@ public function execute() unset($user['secret']); if (!$user['secret_verified']) { - $this->getUsersTable()->query()->update() + $this->getUsersTable()->updateQuery() ->set(['secret_verified' => true]) ->where(['id' => $user['id']]) ->execute(); diff --git a/src/Service/Action/Auth/OtpVerifyGetAction.php b/src/Service/Action/Auth/OtpVerifyGetAction.php index d2c6b8d..4cbd6be 100644 --- a/src/Service/Action/Auth/OtpVerifyGetAction.php +++ b/src/Service/Action/Auth/OtpVerifyGetAction.php @@ -67,8 +67,8 @@ protected function onVerifyGetSecret($user) $secret = $this->createSecret(); try { - $query = $this->getUsersTable()->query(); - $query->update() + $query = $this->getUsersTable()->updateQuery(); + $query ->set(['secret' => $secret]) ->where(['id' => $user['id']]); $query->execute(); diff --git a/src/Service/Action/Auth/ResetPasswordAction.php b/src/Service/Action/Auth/ResetPasswordAction.php index 6436f26..71714a0 100644 --- a/src/Service/Action/Auth/ResetPasswordAction.php +++ b/src/Service/Action/Auth/ResetPasswordAction.php @@ -118,6 +118,7 @@ public function execute() */ protected function _changePassword($userId) { + /** @var \CakeDC\Users\Model\Entity\User $user */ $user = $this->getUsersTable()->newEntity([], ['validate' => false]); $user->id = $userId; try { diff --git a/src/Service/Action/Auth/Webauthn2faAuthAction.php b/src/Service/Action/Auth/Webauthn2faAuthAction.php index 5b57f71..d1c8ac0 100644 --- a/src/Service/Action/Auth/Webauthn2faAuthAction.php +++ b/src/Service/Action/Auth/Webauthn2faAuthAction.php @@ -13,6 +13,7 @@ namespace CakeDC\Api\Service\Action\Auth; +use Cake\Log\Log; use CakeDC\Api\Service\Action\Action; use CakeDC\Api\Webauthn\AuthenticateAdapter; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; @@ -31,6 +32,7 @@ class Webauthn2faAuthAction extends Action * Execute action. * * @return mixed + * @throws \Throwable */ public function execute() { diff --git a/src/Service/Action/Auth/Webauthn2faRegisterAction.php b/src/Service/Action/Auth/Webauthn2faRegisterAction.php index c53a97c..1567fde 100644 --- a/src/Service/Action/Auth/Webauthn2faRegisterAction.php +++ b/src/Service/Action/Auth/Webauthn2faRegisterAction.php @@ -13,6 +13,7 @@ namespace CakeDC\Api\Service\Action\Auth; +use Cake\Http\Exception\BadRequestException; use CakeDC\Api\Service\Action\Action; use CakeDC\Api\Webauthn\RegisterAdapter; use CakeDC\Users\Controller\Traits\CustomUsersTableTrait; diff --git a/src/Service/Action/Extension/AuthorizationExtension.php_ b/src/Service/Action/Extension/AuthorizationExtension.php_ index 0ff3b82..a540c39 100644 --- a/src/Service/Action/Extension/AuthorizationExtension.php_ +++ b/src/Service/Action/Extension/AuthorizationExtension.php_ @@ -37,7 +37,7 @@ class AuthorizationExtension extends Extension implements EventListenerInterface * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'identityAttribute' => 'identity', 'serviceAttribute' => 'authorization', 'authorizationEvent' => 'Action.Auth.onAuthorization', diff --git a/src/Service/Action/Recaptcha/ValidateAction.php b/src/Service/Action/Recaptcha/ValidateAction.php index 1f6f134..6608308 100644 --- a/src/Service/Action/Recaptcha/ValidateAction.php +++ b/src/Service/Action/Recaptcha/ValidateAction.php @@ -29,7 +29,6 @@ class ValidateAction extends Action * Execute action. * * @return mixed - * @throws \CakeDC\Api\Service\Action\Exception */ public function execute() { diff --git a/src/Webauthn/AuthenticateAdapter.php b/src/Webauthn/AuthenticateAdapter.php index 16fb8e5..f55f4c1 100644 --- a/src/Webauthn/AuthenticateAdapter.php +++ b/src/Webauthn/AuthenticateAdapter.php @@ -13,6 +13,7 @@ namespace CakeDC\Api\Webauthn; +use Cake\Log\Log; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; @@ -27,19 +28,19 @@ public function getOptions(): PublicKeyCredentialRequestOptions $allowed = array_map(function (PublicKeyCredentialSource $credential) { return $credential->getPublicKeyCredentialDescriptor(); }, $this->repository->findAllForUserEntity($userEntity)); - \Cake\Log\Log::error(print_r($allowed, true)); + Log::error(print_r($allowed, true)); $options = $this->server->generatePublicKeyCredentialRequestOptions( PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, $allowed ); $storeEntity = $this->readStore(); - \Cake\Log\Log::error(print_r($storeEntity, true)); + Log::error(print_r($storeEntity, true)); $storeEntity['store'] = []; $storeEntity = $this->patchStore($storeEntity, 'authenticateOptions', base64_encode(serialize($options))); $res = $this->store->save($storeEntity); - \Cake\Log\Log::error(print_r($storeEntity, true)); - \Cake\Log\Log::error(print_r($res, true)); + Log::error(print_r($storeEntity, true)); + Log::error(print_r($res, true)); return $options; } @@ -52,12 +53,12 @@ public function getOptions(): PublicKeyCredentialRequestOptions public function verifyResponse(): \Webauthn\PublicKeyCredentialSource { $storeEntity = $this->readStore(); - \Cake\Log\Log::error(print_r($storeEntity, true)); + Log::error(print_r($storeEntity, true)); $options = $this->getStore($storeEntity, 'authenticateOptions'); if ($options) { $options = unserialize(base64_decode($options)); } - \Cake\Log\Log::error(print_r($options, true)); + Log::error(print_r($options, true)); return $this->loadAndCheckAssertionResponse($options); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 44df88e..494e55d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -83,15 +83,21 @@ function def($name, $value) // Cake\Core\Configure::write('App.encoding', 'UTF-8'); Cake\Core\Configure::write('debug', true); -mkdir(TMP . 'cache/models', 0777); -mkdir(TMP . 'cache/persistent', 0777); -mkdir(TMP . 'cache/views', 0777); +if (!file_exists(TMP . 'cache/models')) { + mkdir(TMP . 'cache/models', 0777); +} +if (!file_exists(TMP . 'cache/persistent')) { + mkdir(TMP . 'cache/persistent', 0777); +} +if (!file_exists(TMP . 'cache/views')) { + mkdir(TMP . 'cache/views', 0777); +} $cache = [ 'default' => [ 'engine' => 'File', ], - '_cake_core_' => [ + '_cake_translations_' => [ 'className' => 'File', 'prefix' => 'api_app_cake_core_', 'path' => CACHE . 'persistent/',