diff --git a/src/auth/methods/BaseAuthMethod.php b/src/auth/methods/BaseAuthMethod.php index 2fefad88130..009d3f6a8a5 100644 --- a/src/auth/methods/BaseAuthMethod.php +++ b/src/auth/methods/BaseAuthMethod.php @@ -23,6 +23,19 @@ abstract class BaseAuthMethod extends Component implements AuthMethodInterface */ protected User $user; + /** + * Returns the data needed to render the authentication method’s setup form. + * + * This should be part of AuthMethodInterface, but remains here for 5.x BC + * with auth methods that implement the interface directly. + * + * @return array + */ + public function getSetupData(): array + { + return []; + } + /** * @inheritdoc */ diff --git a/src/auth/methods/TOTP.php b/src/auth/methods/TOTP.php index 8fe50a1fd84..cbed63af9aa 100644 --- a/src/auth/methods/TOTP.php +++ b/src/auth/methods/TOTP.php @@ -73,6 +73,19 @@ public function isActive(): bool return self::secretFromDb($this->user->id) !== null; } + /** + * @inheritdoc + */ + public function getSetupData(): array + { + $secret = trim($this->secret()); + + return [ + 'secret' => $secret, + 'qrCode' => $this->generateQrCode($secret), + ]; + } + /** * @inheritdoc */ diff --git a/src/controllers/AuthController.php b/src/controllers/AuthController.php index 62b9cbbda2c..59fb3e07ead 100644 --- a/src/controllers/AuthController.php +++ b/src/controllers/AuthController.php @@ -8,6 +8,8 @@ namespace craft\controllers; use Craft; +use craft\auth\methods\AuthMethodInterface; +use craft\auth\methods\BaseAuthMethod; use craft\auth\methods\RecoveryCodes; use craft\auth\methods\TOTP; use craft\helpers\Html; @@ -15,6 +17,7 @@ use craft\i18n\Locale; use craft\web\Controller; use craft\web\View; +use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; use yii\web\Response; @@ -96,6 +99,45 @@ public function actionMethodListingHtml(): ?Response ]); } + /** + * Returns the data for available authentication methods. + * + * @return Response + */ + public function actionGetAvailableMethods(): Response + { + $this->requirePostRequest(); + $this->requireAcceptsJson(); + + return $this->asJson([ + 'methods' => array_map( + fn(AuthMethodInterface $method) => $this->authMethodData($method), + Craft::$app->getAuth()->getAvailableMethods(), + ), + ]); + } + + /** + * Returns the data needed to set up an authentication method. + * + * @return Response + */ + public function actionGetSetupData(): Response + { + $this->requirePostRequest(); + $this->requireAcceptsJson(); + $this->requireElevatedSession(); + + $class = $this->request->getRequiredBodyParam('method'); + try { + $method = Craft::$app->getAuth()->getMethod($class); + } catch (InvalidArgumentException $e) { + return $this->asFailure($e->getMessage()); + } + + return $this->asJson($method instanceof BaseAuthMethod ? $method->getSetupData() : []); + } + /** * Remove auth type setup (for 2FA or Passkeys) from the database * @@ -105,14 +147,19 @@ public function actionMethodListingHtml(): ?Response */ public function actionRemoveMethod(): ?Response { - $this->requireCpRequest(); $this->requirePostRequest(); + $this->requireAcceptsJson(); $this->requireElevatedSession(); $methodClass = $this->request->getRequiredBodyParam('method'); $auth = Craft::$app->getAuth(); - $method = $auth->getMethod($methodClass); + try { + $method = $auth->getMethod($methodClass); + } catch (InvalidArgumentException $e) { + return $this->asFailure($e->getMessage()); + } + $method->remove(); // if that was the last non-Recovery Codes method, remove Recovery Codes too @@ -263,6 +310,17 @@ private function passkeyTableHtml(): string ]); } + private function authMethodData(AuthMethodInterface $method): array + { + return [ + 'class' => $method::class, + 'name' => $method::displayName(), + 'description' => $method::description(), + 'actionMenuItems' => $method->getActionMenuItems(), + 'isActive' => $method->isActive(), + ]; + } + /** * Generates new recovery codes. *