Web Authn(1/3)
This commit is contained in:
parent
9cd39aae91
commit
e103c0bdee
@ -2,10 +2,30 @@
|
||||
|
||||
namespace app\controllers;
|
||||
|
||||
use app\models\PublicKeyCredentialSource;
|
||||
use app\models\User;
|
||||
use app\utils\FileSizeHelper;
|
||||
use OTPHP\TOTP;
|
||||
use Random\RandomException;
|
||||
use ReCaptcha\ReCaptcha;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Throwable;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAssertionResponseValidator;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponseValidator;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredential;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Yii;
|
||||
use yii\base\Exception;
|
||||
use yii\base\InvalidConfigException;
|
||||
@ -129,13 +149,13 @@ class UserController extends Controller
|
||||
$captchaResponse = null;
|
||||
$isCaptchaValid = false;
|
||||
if ($verifyProvider === 'reCAPTCHA') {
|
||||
$captchaResponse = Yii::$app->request->post('g-recaptcha-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('g-recaptcha-response');
|
||||
$isCaptchaValid = $this->validateRecaptcha($captchaResponse);
|
||||
} elseif ($verifyProvider === 'hCaptcha') {
|
||||
$captchaResponse = Yii::$app->request->post('h-captcha-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('h-captcha-response');
|
||||
$isCaptchaValid = $this->validateHcaptcha($captchaResponse);
|
||||
} elseif ($verifyProvider === 'Turnstile') {
|
||||
$captchaResponse = Yii::$app->request->post('cf-turnstile-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('cf-turnstile-response');
|
||||
$isCaptchaValid = $this->validateTurnstile($captchaResponse);
|
||||
}
|
||||
|
||||
@ -324,13 +344,13 @@ class UserController extends Controller
|
||||
$captchaResponse = null;
|
||||
$isCaptchaValid = false;
|
||||
if ($verifyProvider === 'reCAPTCHA') {
|
||||
$captchaResponse = Yii::$app->request->post('g-recaptcha-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('g-recaptcha-response');
|
||||
$isCaptchaValid = $this->validateRecaptcha($captchaResponse);
|
||||
} elseif ($verifyProvider === 'hCaptcha') {
|
||||
$captchaResponse = Yii::$app->request->post('h-captcha-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('h-captcha-response');
|
||||
$isCaptchaValid = $this->validateHcaptcha($captchaResponse);
|
||||
} elseif ($verifyProvider === 'Turnstile') {
|
||||
$captchaResponse = Yii::$app->request->post('cf-turnstile-response', null);
|
||||
$captchaResponse = Yii::$app->request->post('cf-turnstile-response');
|
||||
$isCaptchaValid = $this->validateTurnstile($captchaResponse);
|
||||
}
|
||||
|
||||
@ -346,7 +366,7 @@ class UserController extends Controller
|
||||
if (!is_dir($userFolder)) {
|
||||
mkdir($userFolder);
|
||||
}
|
||||
$secretFolder = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . $model->id.'.secret';
|
||||
$secretFolder = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . $model->id . '.secret';
|
||||
if (!is_dir($secretFolder)) {
|
||||
mkdir($secretFolder);
|
||||
}
|
||||
@ -548,4 +568,151 @@ class UserController extends Controller
|
||||
}
|
||||
return $this->redirect(['user/info']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function actionCreateCredentialOptions(): Response
|
||||
{
|
||||
$id = Yii::$app->params['domain'] ?? null;
|
||||
$user = Yii::$app->user->identity;
|
||||
|
||||
$rpEntity = PublicKeyCredentialRpEntity::create(
|
||||
'NetDisk Application',
|
||||
$id
|
||||
);
|
||||
|
||||
$hash = md5(strtolower(trim($user->email)));
|
||||
$gravatarUrl = "https://www.gravatar.com/avatar/$hash";
|
||||
|
||||
$userEntity = PublicKeyCredentialUserEntity::create(
|
||||
$user->username,
|
||||
$user->id,
|
||||
$user->name,
|
||||
$gravatarUrl
|
||||
);
|
||||
|
||||
$challenge = random_bytes(16);
|
||||
$publicKeyCredentialCreationOptions =
|
||||
PublicKeyCredentialCreationOptions::create(
|
||||
$rpEntity,
|
||||
$userEntity,
|
||||
$challenge
|
||||
);
|
||||
|
||||
// 将选项存储在会话中,以便在后续的验证步骤中使用
|
||||
Yii::$app->session->set('publicKeyCredentialCreationOptions', $publicKeyCredentialCreationOptions);
|
||||
|
||||
// 将选项发送给客户端
|
||||
return $this->asJson($publicKeyCredentialCreationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function actionCreateCredential(): void
|
||||
{
|
||||
$data = Yii::$app->request->post('publicKeyCredential');
|
||||
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
|
||||
$publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
|
||||
$authenticatorAttestationResponse = $publicKeyCredential->response;
|
||||
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
|
||||
//e.g. process here with a redirection to the public key creation page.
|
||||
}
|
||||
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
|
||||
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
|
||||
$extensionOutputCheckerHandler = ExtensionOutputCheckerHandler::create();
|
||||
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
|
||||
$attestationStatementSupportManager,
|
||||
null, //Deprecated Public Key Credential Source Repository. Please set null.
|
||||
null, //Deprecated Token Binding Handler. Please set null.
|
||||
$extensionOutputCheckerHandler
|
||||
);
|
||||
$publicKeyCredentialCreationOptions = Yii::$app->session->get('publicKeyCredentialCreationOptions');
|
||||
try {
|
||||
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
Yii::$app->params['domain'] ?? null
|
||||
);
|
||||
// 创建一个PublicKeyCredentialSourceRepository对象
|
||||
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource();
|
||||
|
||||
// 保存PublicKeyCredentialSource对象到数据库
|
||||
$publicKeyCredentialSourceRepository->saveCredential($publicKeyCredentialSource);
|
||||
|
||||
Yii::$app->session->setFlash('success', '公钥凭证已创建');
|
||||
} catch (Throwable $e) {
|
||||
//e.g. process here with a redirection to the public key creation page.
|
||||
//show error message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function actionRequestAssertionOptions(): Response
|
||||
{
|
||||
$user = Yii::$app->user->identity;
|
||||
|
||||
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource();
|
||||
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($user);
|
||||
|
||||
$allowedCredentials = array_map(
|
||||
static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor {
|
||||
return $credential->getPublicKeyCredentialDescriptor();
|
||||
},
|
||||
$registeredAuthenticators
|
||||
);
|
||||
$publicKeyCredentialRequestOptions =
|
||||
PublicKeyCredentialRequestOptions::create(
|
||||
random_bytes(32), // Challenge
|
||||
allowCredentials: $allowedCredentials
|
||||
);
|
||||
Yii::$app->session->set('publicKeyCredentialRequestOptions', $publicKeyCredentialRequestOptions);
|
||||
|
||||
return $this->asJson($publicKeyCredentialRequestOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function actionVerifyAssertion(): void
|
||||
{
|
||||
$data = Yii::$app->request->post('publicKeyCredential');
|
||||
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
|
||||
$publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
|
||||
$authenticatorAssertionResponse = $publicKeyCredential->response;
|
||||
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
|
||||
//e.g. process here with a redirection to the public key login/MFA page.
|
||||
}
|
||||
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource();
|
||||
$publicKeyCredentialSource = $publicKeyCredentialSourceRepository->findOneByCredentialId(
|
||||
$publicKeyCredential->rawId
|
||||
);
|
||||
if ($publicKeyCredentialSource === null) {
|
||||
// Throw an exception if the credential is not found.
|
||||
// It can also be rejected depending on your security policy (e.g. disabled by the user because of loss)
|
||||
}
|
||||
$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create();
|
||||
$publicKeyCredentialRequestOptions = Yii::$app->session->get('publicKeyCredentialRequestOptions');
|
||||
try {
|
||||
$publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
Yii::$app->params['domain'] ?? null,
|
||||
null //Deprecated Token Binding Handler. Please set null.
|
||||
);
|
||||
} catch (AuthenticatorResponseVerificationException $e) {
|
||||
}
|
||||
|
||||
// Optional, but highly recommended, you can save the credential source as it may be modified
|
||||
// during the verification process (counter may be higher).
|
||||
$publicKeyCredentialSourceRepository->saveCredential($publicKeyCredentialSource);
|
||||
}
|
||||
|
||||
}
|
||||
|
115
models/PublicKeyCredentialSource.php
Normal file
115
models/PublicKeyCredentialSource.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\Denormalizer\WebauthnSerializerFactory;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* This is the model class for table "public_key_credential_source".
|
||||
*
|
||||
* @property int $id 记录id
|
||||
* @property int $user_id 对应的用户id
|
||||
* @property string $public_key_credential_source PKCS
|
||||
*
|
||||
* @property User $user
|
||||
*/
|
||||
class PublicKeyCredentialSource extends \yii\db\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function tableName()
|
||||
{
|
||||
return 'public_key_credential_source';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
[['user_id', 'public_key_credential_source'], 'required'],
|
||||
[['user_id'], 'integer'],
|
||||
[['public_key_credential_source'], 'string'],
|
||||
[['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function attributeLabels()
|
||||
{
|
||||
return [
|
||||
'id' => 'ID',
|
||||
'user_id' => 'User ID',
|
||||
'public_key_credential_source' => 'Public Key Credential Source',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets query for [[User]].
|
||||
*
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->hasOne(User::class, ['id' => 'user_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户相关的所有公钥凭证
|
||||
* @param User $user
|
||||
* @return \Webauthn\PublicKeyCredentialSource[]
|
||||
*/
|
||||
public function findAllForUserEntity(User $user): array
|
||||
{
|
||||
$records = self::findAll(['user_id' => $user->id]);
|
||||
$publicKeyCredentialSources = [];
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(new AttestationStatementSupportManager());
|
||||
foreach ($records as $record) {
|
||||
$publicKeyCredentialSource = $webauthnSerializerFactory->create()->deserialize($record->public_key_credential_source, \Webauthn\PublicKeyCredentialSource::class, 'json');
|
||||
$publicKeyCredentialSources[] = $publicKeyCredentialSource;
|
||||
}
|
||||
return $publicKeyCredentialSources;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库中获取指定$id的记录,转为PublicKeyCredentialSource对象
|
||||
* @param int $id
|
||||
* @return \Webauthn\PublicKeyCredentialSource|null
|
||||
*/
|
||||
public function findOneByCredentialId(int $id): ?\Webauthn\PublicKeyCredentialSource
|
||||
{
|
||||
$record = self::findOne(['id' => $id]);
|
||||
if ($record === null) {
|
||||
return null;
|
||||
}
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(new AttestationStatementSupportManager());
|
||||
return $webauthnSerializerFactory->create()->deserialize($record->public_key_credential_source, \Webauthn\PublicKeyCredentialSource::class, 'json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存PublicKeyCredentialSource对象到数据库
|
||||
* @param \Webauthn\PublicKeyCredentialSource $PKCS
|
||||
* @return void
|
||||
*/
|
||||
public function saveCredential(\Webauthn\PublicKeyCredentialSource $PKCS): void
|
||||
{
|
||||
// Create an instance of WebauthnSerializerFactory
|
||||
$webauthnSerializerFactory = new WebauthnSerializerFactory(new AttestationStatementSupportManager());
|
||||
|
||||
// Serialize the PublicKeyCredentialSource object into a JSON string
|
||||
$publicKeyCredentialSourceJson = $webauthnSerializerFactory->create()->serialize($PKCS, 'json');
|
||||
|
||||
// Save the JSON string to the public_key_credential_source field in the database
|
||||
$this->public_key_credential_source = $publicKeyCredentialSourceJson;
|
||||
$this->user_id = Yii::$app->user->id;
|
||||
// Save the record to the database
|
||||
$this->save();
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ use yii\web\IdentityInterface;
|
||||
* @property string|null $vault_salt 保险箱加密密钥盐
|
||||
*
|
||||
* @property CollectionTasks[] $collectionTasks
|
||||
* @property PublicKeyCredentialSource[] $publicKeyCredentialSources
|
||||
* @property Share[] $shares
|
||||
*/
|
||||
class User extends ActiveRecord implements IdentityInterface
|
||||
@ -64,7 +65,7 @@ class User extends ActiveRecord implements IdentityInterface
|
||||
return [
|
||||
[['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'],
|
||||
[['created_at', 'last_login'], 'safe'],
|
||||
[['bio', 'totp_input', 'recoveryCode_input', 'name','vault_salt'], 'string'],
|
||||
[['bio', 'totp_input', 'recoveryCode_input', 'name', 'vault_salt'], 'string'],
|
||||
['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],
|
||||
@ -228,6 +229,16 @@ class User extends ActiveRecord implements IdentityInterface
|
||||
return $this->hasMany(CollectionTasks::class, ['user_id' => 'id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets query for [[PublicKeyCredentialSources]].
|
||||
*
|
||||
* @return ActiveQuery
|
||||
*/
|
||||
public function getPublicKeyCredentialSources(): ActiveQuery
|
||||
{
|
||||
return $this->hasMany(PublicKeyCredentialSource::class, ['user_id' => 'id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets query for [[Shares]].
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user