diff --git a/src/controllers/AuthController.php b/src/controllers/AuthController.php index cdd586030ca..62b9cbbda2c 100644 --- a/src/controllers/AuthController.php +++ b/src/controllers/AuthController.php @@ -11,6 +11,7 @@ use craft\auth\methods\RecoveryCodes; use craft\auth\methods\TOTP; use craft\helpers\Html; +use craft\helpers\Session as SessionHelper; use craft\i18n\Locale; use craft\web\Controller; use craft\web\View; @@ -203,6 +204,8 @@ public function actionPasskeyRequestOptions(): Response $serializer = $authService->webauthnServer()->getSerializer(); $serializedData = $serializer->serialize($options, 'json'); + SessionHelper::set($authService->passkeyRequestOptionsParam, $serializedData); + return $this->asJson([ 'options' => $serializedData, ]); diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index 1f991b655a0..24c306e7521 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -319,7 +319,12 @@ public function actionLoginWithPasskey(): ?Response $duration = Craft::$app->getConfig()->getGeneral()->userSessionDuration; // PublicKeyCredentialRequestOptions - $requestOptions = $this->request->getRequiredBodyParam('requestOptions'); + $requestOptions = SessionHelper::remove(Craft::$app->getAuth()->passkeyRequestOptionsParam); + + if (!$requestOptions) { + return $this->asFailure(Craft::t('app', 'Passkey authentication failed.')); + } + // PublicKeyCredential $response = $this->request->getRequiredBodyParam('response'); $credential = WebAuthnRecord::findOne(['credentialId' => Json::decode($response)['id']]); diff --git a/src/elements/User.php b/src/elements/User.php index cbba047d941..61aaaa13947 100644 --- a/src/elements/User.php +++ b/src/elements/User.php @@ -1434,20 +1434,25 @@ public function authenticateWithPasskey( return false; } + $authService = Craft::$app->getAuth(); // Validate the security key try { - $keyValid = Craft::$app->getAuth()->verifyPasskey($this, $requestOptions, $response); + $keyValid = $authService->verifyPasskey($this, $requestOptions, $response); } catch (InvalidUserHandleException $e) { - $keyValid = Craft::$app->getAuth()->verifyPasskey($this, $requestOptions, $response, true); + $keyValid = $authService->verifyPasskey($this, $requestOptions, $response, true); } catch (InvalidArgumentException) { $keyValid = false; } + $updatedPublicKeyCredentialSource = Session::remove($authService->passkeyCredSourceParam); + if (!$keyValid) { $this->handleInvalidLoginParam(); return false; } + $authService->webauthnServer()->getCredentialRepository()->saveCredentialSource($updatedPublicKeyCredentialSource); + $this->authError = $this->_getAuthError(); return !isset($this->authError); } diff --git a/src/services/Auth.php b/src/services/Auth.php index c7ccd4c7643..9447d03abcb 100644 --- a/src/services/Auth.php +++ b/src/services/Auth.php @@ -71,6 +71,16 @@ class Auth extends Component */ public string $passkeyCreationOptionsParam; + /** + * @var string The session variable name used to store passkey request options. + */ + public string $passkeyRequestOptionsParam; + + /** + * @var string The session variable name used to store updated passkey credential source. + */ + public string $passkeyCredSourceParam; + /** * @var AuthMethodInterface[][] All user authentication methods * @see getAllMethods() @@ -111,6 +121,12 @@ public function init(): void if (!isset($this->passkeyCreationOptionsParam)) { $this->passkeyCreationOptionsParam = sprintf('%s__pkCredCreationOptions', $stateKeyPrefix); } + if (!isset($this->passkeyRequestOptionsParam)) { + $this->passkeyRequestOptionsParam = sprintf('%s__pkReqOptions', $stateKeyPrefix); + } + if (!isset($this->passkeyCredSourceParam)) { + $this->passkeyCredSourceParam = sprintf('%s__pkCredSource', $stateKeyPrefix); + } } /** @@ -600,13 +616,18 @@ public function verifyPasskey( } try { - $this->webauthnServer()->getAuthenticatorAssertionResponseValidator()->check( + $updatedPublicKeyCredentialSource = $this->webauthnServer()->getAuthenticatorAssertionResponseValidator()->check( $publicKeyCredentialSource, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, Craft::$app->getRequest()->getHostName(), $userEntity->id, ); + + // we can't save the updated public key credential source to db here as in User::authenticateWithPasskey() + // we might need to call this method (Auth::verifyPasskey()) again, with checkOldUserHandle set to true; + // so, we're going to store it in the session and then save from the User::authenticateWithPasskey() method + SessionHelper::set($this->passkeyCredSourceParam, $updatedPublicKeyCredentialSource); } catch (InvalidUserHandleException $e) { throw $e; } catch (Throwable $e) {