From 07caef25550e320c9d2b408a251dce66cbf64cf1 Mon Sep 17 00:00:00 2001 From: Chenx221 Date: Tue, 19 Mar 2024 17:11:41 +0800 Subject: [PATCH] =?UTF-8?q?Web=20Authn(3/3)=20=E4=BF=AE=E6=94=B9=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=80=BB=E8=BE=91,=E5=AE=9E=E7=8E=B0WebAuthn=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/UserController.php | 129 ++++++++++++++++++++++++--------- models/User.php | 4 +- views/user/_login1.php | 54 ++++++++++++++ views/user/_login2.php | 45 ++++++++++++ views/user/login.php | 39 +++------- web/js/login-core.js | 47 ++++++++++++ 6 files changed, 251 insertions(+), 67 deletions(-) create mode 100644 views/user/_login1.php create mode 100644 views/user/_login2.php create mode 100644 web/js/login-core.js diff --git a/controllers/UserController.php b/controllers/UserController.php index 1c93901..9861e78 100644 --- a/controllers/UserController.php +++ b/controllers/UserController.php @@ -16,7 +16,6 @@ use Webauthn\AuthenticatorAssertionResponse; use Webauthn\AuthenticatorAssertionResponseValidator; use Webauthn\AuthenticatorAttestationResponse; use Webauthn\AuthenticatorAttestationResponseValidator; -use Webauthn\CeremonyStep\CeremonyStepManager; use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Denormalizer\WebauthnSerializerFactory; use Webauthn\Exception\AuthenticatorResponseVerificationException; @@ -63,12 +62,12 @@ class UserController extends Controller ], [ 'allow' => true, - 'actions' => ['login', 'register', 'verify-two-factor'], + 'actions' => ['login', 'register', 'verify-two-factor', 'verify-assertion', 'request-assertion-options'], 'roles' => ['?', '@'], // everyone can access public share ], [ 'allow' => true, - 'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor', 'set-theme', 'change-name', 'create-credential-options', 'create-credential', 'request-assertion-options', 'verify-assertion', 'credential-list', 'credential-delete'], + 'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor', 'set-theme', 'change-name', 'create-credential-options', 'create-credential', 'credential-list', 'credential-delete'], 'roles' => ['@'], // only logged-in user can do these ( admin included ) ] ], @@ -174,14 +173,16 @@ class UserController extends Controller } /** - * Displays the login page. - * visit via https://devs.chenx221.cyou:8081/index.php?r=user%2Flogin + * 显示登录页并接收登录请求 + * GET时显示登录页,如果带有username参数则直接到输入密码页 + * POST时验证用户名密码,如果用户启用了二步验证则重定向到二步验证页面 * + * @param string|null $username * @return string|Response * @throws InvalidConfigException * @throws \yii\httpclient\Exception */ - public function actionLogin(): Response|string + public function actionLogin(string $username = null): Response|string { if (!Yii::$app->user->isGuest) { Yii::$app->session->setFlash('error', '账户已登录,请不要重复登录'); @@ -190,36 +191,69 @@ class UserController extends Controller $model = new User(['scenario' => 'login']); - if ($model->load(Yii::$app->request->post()) && $model->validate()) { - // 根据 verifyProvider 的值选择使用哪种验证码服务 - list($verifyProvider, $captchaResponse, $isCaptchaValid) = $this->checkCaptcha(); - - if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) { - // 验证用户名和密码 + if (($model->load(Yii::$app->request->post()) && $model->validate()) | $username !== null) { + if ($model->password === null) { + if ($username !== null) { + $model->username = $username; + } $user = User::findOne(['username' => $model->username]); - if ($user !== null && $user->validatePassword($model->password)) { - // 如果用户启用了二步验证,将用户重定向到二步验证页面 - if ($user->is_otp_enabled) { - Yii::$app->session->set('login_verification', ['id' => $user->id]); - return $this->redirect(['user/verify-two-factor']); - } else { - //login without 2FA - $user->last_login = date('Y-m-d H:i:s'); - $user->last_login_ip = Yii::$app->request->userIP; - if (!$user->save(false)) { - Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误'); - } - Yii::$app->user->login($user, $model->rememberMe ? 3600 * 24 * 30 : 0); - return $this->goHome(); - } - } else { - Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用'); + if ($user === null) { + Yii::$app->session->setFlash('error', '用户不存在'); + return $this->render('login', [ + 'model' => $model, + ]); + } elseif ($user->status === 0) { + Yii::$app->session->setFlash('error', '用户已停用,请联系管理员获取支持'); + return $this->render('login', [ + 'model' => $model, + ]); + } + $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository(); + $fidoCredentials = $publicKeyCredentialSourceRepository->findAllForUserEntity($user); + if (empty($fidoCredentials) | $username !== null) { //未设置FIDO + return $this->render('_login1', [ + 'model' => $model, + ]); + } else { //已设置FIDO + return $this->render('_login2', [ + 'model' => $model, + ]); } } else { - Yii::$app->session->setFlash('error', '请等待验证码加载并完成验证'); + // 根据 verifyProvider 的值选择使用哪种验证码服务 + list($verifyProvider, $captchaResponse, $isCaptchaValid) = $this->checkCaptcha(); + + if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) { + // 验证用户名和密码 + $user = User::findOne(['username' => $model->username]); + if ($user !== null && $user->validatePassword($model->password)) { + // 如果用户启用了二步验证,将用户重定向到二步验证页面 + if ($user->is_otp_enabled) { + Yii::$app->session->set('login_verification', ['id' => $user->id]); + return $this->redirect(['user/verify-two-factor']); + } else { + //login without 2FA + $user->last_login = date('Y-m-d H:i:s'); + $user->last_login_ip = Yii::$app->request->userIP; + if (!$user->save(false)) { + Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误'); + } + Yii::$app->user->login($user, $model->rememberMe ? 3600 * 24 * 30 : 0); + return $this->goHome(); + } + } else { + Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用'); + } + } else { + Yii::$app->session->setFlash('error', '请等待验证码加载并完成验证'); + } } + } else { + return $this->render('login', [ + 'model' => $model, + ]); } - return $this->render('login', [ + return $this->render('_login1', [ 'model' => $model, ]); } @@ -703,13 +737,24 @@ class UserController extends Controller /** * 请求验证选项 + * @param string|null $username * @return Response * @throws RandomException */ - public function actionRequestAssertionOptions(): Response + public function actionRequestAssertionOptions(string $username = null): Response { - $user = Yii::$app->user->identity; - + $user = null; + if ($username !== null) { + $user = User::findOne(['username' => $username]); + if ($user === null) { + return $this->asJson(['message' => 'User not found']); + } + } else { + $user = Yii::$app->user->identity; + } + if ($user === null) { + return $this->asJson(['message' => 'Guest? Sure?']); + } $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository(); $registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($user); @@ -736,10 +781,12 @@ class UserController extends Controller /** * 验证断言 * 用于已登录情况下验证fifo设置是否成功 + * @param int $is_login + * @param int $remember * @return Response * @throws JsonException */ - public function actionVerifyAssertion(): Response + public function actionVerifyAssertion(int $is_login = 0,int $remember = 0): Response { $data = Yii::$app->request->getRawBody(); @@ -787,9 +834,19 @@ class UserController extends Controller return $this->asJson(['message' => $e->getMessage(), 'verified' => false]); } + + if ($is_login === 1) { + $user = User::findOne(['id' => $publicKeyCredentialSourceRepository1->user_id]); + $user->last_login = date('Y-m-d H:i:s'); + $user->last_login_ip = Yii::$app->request->userIP; + if (!$user->save(false)) { + Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误'); + } + Yii::$app->user->login($user, $remember===1 ? 3600 * 24 * 30 : 0); + } // Optional, but highly recommended, you can save the credential source as it may be modified // during the verification process (counter may be higher). - $publicKeyCredentialSourceRepository1->saveCredential($publicKeyCredentialSource, '',false); + $publicKeyCredentialSourceRepository1->saveCredential($publicKeyCredentialSource, '', false); return $this->asJson(['verified' => true]); } diff --git a/models/User.php b/models/User.php index efc484b..238bcae 100644 --- a/models/User.php +++ b/models/User.php @@ -69,10 +69,10 @@ class User extends ActiveRecord implements IdentityInterface ['input_vault_secret', 'string', 'min' => 6, 'max' => 24], [['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255], [['last_login_ip'], 'string', 'max' => 45], - [['username', 'password'], 'required', 'on' => 'login'], + [['username'], 'required', 'on' => 'login'], [['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'], ['username', 'string', 'min' => 3, 'max' => 12], - ['password', 'string', 'min' => 6, 'max' => 24, 'on' => 'register'], + ['password', 'string', 'min' => 6, 'max' => 24], ['password2', 'compare', 'compareAttribute' => 'password', 'on' => 'register'], ['email', 'email', 'on' => 'register'], ['username', 'unique', 'on' => 'register'], diff --git a/views/user/_login1.php b/views/user/_login1.php new file mode 100644 index 0000000..7d2ef9a --- /dev/null +++ b/views/user/_login1.php @@ -0,0 +1,54 @@ +title = '用户登录'; +$this->params['breadcrumbs'][] = $this->title; +$verifyProvider = Yii::$app->params['verifyProvider']; +if ($verifyProvider === 'reCAPTCHA') { + $this->registerJsFile('https://www.recaptcha.net/recaptcha/api.js?hl=zh-CN', ['async' => true, 'defer' => true]); +} elseif ($verifyProvider === 'hCaptcha') { + $this->registerJsFile('https://js.hcaptcha.com/1/api.js?hl=zh-CN', ['async' => true, 'defer' => true]); +} elseif ($verifyProvider === 'Turnstile') { + $this->registerJsFile('https://challenges.cloudflare.com/turnstile/v0/api.js', ['async' => true, 'defer' => true]); +} ?> +
+

title) ?>

+ +

我们已经找到了你的账户,请在下方输入你的密码以继续:

+ +
+
+ [Url::to('user/login')]]); ?> + + field($model, 'username')->label('用户名')->textInput(['autofocus' => true, 'readonly' => true]) ?> + field($model, 'password')->passwordInput()->label('密码') ?> + field($model, 'rememberMe')->checkbox()->label('记住本次登录') ?> +
+ +
+ +
+ +
+ +
+
+ 'btn btn-primary']) ?> +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/views/user/_login2.php b/views/user/_login2.php new file mode 100644 index 0000000..0cd2fe2 --- /dev/null +++ b/views/user/_login2.php @@ -0,0 +1,45 @@ +title = '用户登录'; +$this->params['breadcrumbs'][] = $this->title; +?> +
+

title) ?>

+ +

我们已经找到了你的账户,请在下方验证Passkey以继续:

+ + +
+
+ '#']); ?> + field($model, 'username')->label('用户名')->textInput(['autofocus' => true, 'readonly' => true, 'id' => 'username']) ?> + field($model, 'rememberMe')->checkbox(['id' => 'rememberMe'])->label('记住本次登录') ?> +
+ 'btn btn-primary', 'id' => 'webauthn_verify']) ?> +
+ +
+ $model->username])) ?> +
+
+
+
+registerJsFile('@web/js/login-core.js', ['depends' => [JqueryAsset::class], 'position' => View::POS_END]); +?> \ No newline at end of file diff --git a/views/user/login.php b/views/user/login.php index 79d25e8..00c4722 100644 --- a/views/user/login.php +++ b/views/user/login.php @@ -2,52 +2,33 @@ use yii\bootstrap5\Html; use yii\bootstrap5\ActiveForm; +use yii\web\JqueryAsset; +use yii\web\View; +use yii\widgets\Pjax; /** @var yii\web\View $this */ /** @var app\models\User $model */ /** @var ActiveForm $form */ - $this->title = '用户登录'; $this->params['breadcrumbs'][] = $this->title; -$verifyProvider = Yii::$app->params['verifyProvider']; -if ($verifyProvider === 'reCAPTCHA') { - $this->registerJsFile('https://www.recaptcha.net/recaptcha/api.js?hl=zh-CN', ['async' => true, 'defer' => true]); -} elseif ($verifyProvider === 'hCaptcha') { - $this->registerJsFile('https://js.hcaptcha.com/1/api.js?hl=zh-CN', ['async' => true, 'defer' => true]); -} elseif ($verifyProvider === 'Turnstile') { - $this->registerJsFile('https://challenges.cloudflare.com/turnstile/v0/api.js', ['async' => true, 'defer' => true]); -} ?> +?>

title) ?>

-

请在下方输入你的用户凭证:

+

请在下方输入你的用户名:

field($model, 'username')->label('用户名')->textInput(['autofocus' => true]) ?> - field($model, 'password')->passwordInput()->label('密码') ?> - field($model, 'rememberMe')->checkbox()->label('记住本次登录') ?>
- -
- -
- -
- -
-
- 'btn btn-primary']) ?> -
-
- -
-
- + 'btn btn-primary']) ?>
+
+ +
-
+ \ No newline at end of file diff --git a/web/js/login-core.js b/web/js/login-core.js new file mode 100644 index 0000000..755f8a3 --- /dev/null +++ b/web/js/login-core.js @@ -0,0 +1,47 @@ +const elemSuccess = document.getElementById('webauthn_success'); +const elemError = document.getElementById('webauthn_error'); +const { startAuthentication } = SimpleWebAuthnBrowser; +const elemBegin_v = document.getElementById('webauthn_verify'); +const username_input = document.getElementById('username'); +const remember = document.getElementById('rememberMe'); + +elemBegin_v.addEventListener('click', async () => { + elemSuccess.innerHTML = ''; + elemError.innerHTML = ''; + elemSuccess.parentElement.hidden = true; + elemError.parentElement.hidden = true; + const username = encodeURIComponent(username_input.value); + const resp = await fetch(`index.php?r=user%2Frequest-assertion-options&username=${username}`); + let asseResp; + try { + asseResp = await startAuthentication(await resp.json()); + } catch (error) { + elemError.innerText = error; + elemError.parentElement.hidden = false; + throw error; + } + const isChecked = remember.checked? 1 : 0; + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + const verificationResp = await fetch(`index.php?r=user%2Fverify-assertion&is_login=1&remember=${isChecked}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(asseResp), + }); + const verificationJSON = await verificationResp.json(); + if (verificationJSON && verificationJSON.verified) { + elemSuccess.innerHTML = '登录成功!1s后跳转到首页'; + elemSuccess.parentElement.hidden = false; + setTimeout(() => { + window.location.href = 'index.php'; + }, 1000); + + } else { + elemError.innerHTML = `Oh no, something went wrong! Response:
${JSON.stringify(
+            verificationJSON,
+        )}
`; + elemError.parentElement.hidden = false; + } +}); \ No newline at end of file