Web Authn(2/3)

后端工作基本完成
*上一条git提交备注写错了
*应该是添加相关依赖以及对配置文件的微调
This commit is contained in:
Chenx221 2024-03-14 20:29:45 +08:00
parent 67923425f8
commit e74492dbb7
Signed by: chenx221
GPG Key ID: D7A9EC07024C3021
5 changed files with 201 additions and 169 deletions

View File

@ -8,6 +8,6 @@ class SimpleWebAuthnBrowser extends AssetBundle
{ {
public $sourcePath = '@npm/simplewebauthn--browser/dist/bundle'; public $sourcePath = '@npm/simplewebauthn--browser/dist/bundle';
public $js = [ public $js = [
'index.js', 'index.umd.min.js',
]; ];
} }

View File

@ -2,7 +2,7 @@
namespace app\controllers; namespace app\controllers;
use app\models\PublicKeyCredentialSource; use app\models\PublicKeyCredentialSourceRepository;
use app\models\User; use app\models\User;
use app\utils\FileSizeHelper; use app\utils\FileSizeHelper;
use OTPHP\TOTP; use OTPHP\TOTP;
@ -10,19 +10,32 @@ use Random\RandomException;
use ReCaptcha\ReCaptcha; use ReCaptcha\ReCaptcha;
use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Serializer;
use Throwable; use Throwable;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport; use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse; use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator; use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse; use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\CeremonyStep\CeremonyStepManager;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\Denormalizer\AttestationObjectDenormalizer;
use Webauthn\Denormalizer\AttestationStatementDenormalizer;
use Webauthn\Denormalizer\AuthenticatorAssertionResponseDenormalizer;
use Webauthn\Denormalizer\AuthenticatorAttestationResponseDenormalizer;
use Webauthn\Denormalizer\AuthenticatorDataDenormalizer;
use Webauthn\Denormalizer\AuthenticatorResponseDenormalizer;
use Webauthn\Denormalizer\CollectedClientDataDenormalizer;
use Webauthn\Denormalizer\PublicKeyCredentialDenormalizer;
use Webauthn\Denormalizer\WebauthnSerializerFactory;
use Webauthn\Exception\AuthenticatorResponseVerificationException; use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredential; use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\PublicKeyCredentialUserEntity;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder;
@ -65,7 +78,7 @@ class UserController extends Controller
], ],
[ [
'allow' => true, 'allow' => true,
'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor', 'set-theme', 'change-name'], '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'],
'roles' => ['@'], // only logged-in user can do these ( admin included ) 'roles' => ['@'], // only logged-in user can do these ( admin included )
] ]
], ],
@ -85,6 +98,10 @@ class UserController extends Controller
'verify-two-factor' => ['GET', 'POST'], 'verify-two-factor' => ['GET', 'POST'],
'set-theme' => ['POST'], 'set-theme' => ['POST'],
'change-name' => ['POST'], 'change-name' => ['POST'],
'create-credential-options' => ['GET'],
'create-credential' => ['POST'],
'request-assertion-options' => ['GET'],
'verify-assertion' => ['POST']
], ],
], ],
] ]
@ -570,12 +587,13 @@ class UserController extends Controller
} }
/** /**
* 创建公钥凭证选项
* @return Response * @return Response
* @throws RandomException * @throws RandomException
*/ */
public function actionCreateCredentialOptions(): Response public function actionCreateCredentialOptions(): Response
{ {
$id = Yii::$app->params['domain'] ?? null; $id = Yii::$app->params['domain'];
$user = Yii::$app->user->identity; $user = Yii::$app->user->identity;
$rpEntity = PublicKeyCredentialRpEntity::create( $rpEntity = PublicKeyCredentialRpEntity::create(
@ -583,14 +601,10 @@ class UserController extends Controller
$id $id
); );
$hash = md5(strtolower(trim($user->email)));
$gravatarUrl = "https://www.gravatar.com/avatar/$hash";
$userEntity = PublicKeyCredentialUserEntity::create( $userEntity = PublicKeyCredentialUserEntity::create(
$user->username, $user->username,
$user->id, $user->id,
$user->name, $user->name,
$gravatarUrl
); );
$challenge = random_bytes(16); $challenge = random_bytes(16);
@ -609,47 +623,57 @@ class UserController extends Controller
} }
/** /**
* @return void * 创建公钥凭证
* @return Response
*/ */
public function actionCreateCredential(): void public function actionCreateCredential(): Response
{ {
$data = Yii::$app->request->post('publicKeyCredential'); $data = Yii::$app->request->getRawBody();
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); $attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$webauthnSerializerFactory = new WebauthnSerializerFactory($attestationStatementSupportManager);
$serializer = $webauthnSerializerFactory->create();
$publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json'); $publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
$authenticatorAttestationResponse = $publicKeyCredential->response; $authenticatorAttestationResponse = $publicKeyCredential->response;
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) { if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
//e.g. process here with a redirection to the public key creation page. return $this->asJson(['message' => 'Invalid response type']);
} }
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create()); $ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
$extensionOutputCheckerHandler = ExtensionOutputCheckerHandler::create(); $ceremonyStepManager = $ceremonyStepManagerFactory->creationCeremony();
// PHP Deprecated:
// Since web-auth/webauthn-lib 4.8.0:
// The parameter "$attestationStatementSupportManager" is deprecated since 4.8.0 will be removed in 5.0.0.
// Please set a CheckAttestationFormatIsKnownAndValid object into CeremonyStepManager object instead.
// in /vendor/symfony/deprecation-contracts/function.php on line 25
// NMD, 这个问题在文档更新之前我是不会去解决的
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create( $authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
$attestationStatementSupportManager, $attestationStatementSupportManager,
null, //Deprecated Public Key Credential Source Repository. Please set null. null, //Deprecated Public Key Credential Source Repository. Please set null.
null, //Deprecated Token Binding Handler. Please set null. null, //Deprecated Token Binding Handler. Please set null.
$extensionOutputCheckerHandler null,
null,
$ceremonyStepManager
); );
$publicKeyCredentialCreationOptions = Yii::$app->session->get('publicKeyCredentialCreationOptions'); $publicKeyCredentialCreationOptions = Yii::$app->session->get('publicKeyCredentialCreationOptions');
try { try {
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check( $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check( //response -> source
$authenticatorAttestationResponse, $authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions, $publicKeyCredentialCreationOptions,
Yii::$app->params['domain'] ?? null Yii::$app->params['domain']
); );
// 创建一个PublicKeyCredentialSourceRepository对象 $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource(); $publicKeyCredentialSourceRepository->saveCredential($publicKeyCredentialSource, 'test'); //receive source
return $this->asJson(['verified' => true]);
// 保存PublicKeyCredentialSource对象到数据库
$publicKeyCredentialSourceRepository->saveCredential($publicKeyCredentialSource);
Yii::$app->session->setFlash('success', '公钥凭证已创建');
} catch (Throwable $e) { } catch (Throwable $e) {
//e.g. process here with a redirection to the public key creation page. return $this->asJson(['message' => $e->getMessage(), 'verified' => false]);
//show error message
} }
} }
/** /**
* 请求验证选项
* @return Response * @return Response
* @throws RandomException * @throws RandomException
*/ */
@ -657,12 +681,15 @@ class UserController extends Controller
{ {
$user = Yii::$app->user->identity; $user = Yii::$app->user->identity;
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource(); $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($user); $registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($user);
$allowedCredentials = array_map( $allowedCredentials = array_map(
static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor { static function (PublicKeyCredentialSourceRepository $credential): PublicKeyCredentialDescriptor {
return $credential->getPublicKeyCredentialDescriptor(); $data = $credential->data;
$webauthnSerializerFactory = new WebauthnSerializerFactory(new AttestationStatementSupportManager());
$publicKeyCredentialSource = $webauthnSerializerFactory->create()->deserialize($data, PublicKeyCredentialSource::class, 'json');
return $publicKeyCredentialSource->getPublicKeyCredentialDescriptor();
}, },
$registeredAuthenticators $registeredAuthenticators
); );
@ -676,43 +703,55 @@ class UserController extends Controller
return $this->asJson($publicKeyCredentialRequestOptions); return $this->asJson($publicKeyCredentialRequestOptions);
} }
/** /**
* * 验证断言
* @return void * @return Response
* @throws \JsonException
*/ */
public function actionVerifyAssertion(): void public function actionVerifyAssertion(): Response
{ {
$data = Yii::$app->request->post('publicKeyCredential'); $data = Yii::$app->request->getRawBody();
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$webauthnSerializerFactory = new WebauthnSerializerFactory($attestationStatementSupportManager);
$serializer = $webauthnSerializerFactory->create();
$publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json'); $publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
$authenticatorAssertionResponse = $publicKeyCredential->response; $authenticatorAssertionResponse = $publicKeyCredential->response;
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) { if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
//e.g. process here with a redirection to the public key login/MFA page. return $this->asJson(['message' => 'Invalid response type']);
} }
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSource();
$publicKeyCredentialSource = $publicKeyCredentialSourceRepository->findOneByCredentialId( $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
$publicKeyCredential->rawId $publicKeyCredentialSourceRepository1 = $publicKeyCredentialSourceRepository->findOneByCredentialId(
$publicKeyCredential->id
); );
if ($publicKeyCredentialSource === null) { if ($publicKeyCredentialSourceRepository1 === null) {
// Throw an exception if the credential is not found. $this->asJson(['message' => 'Invalid credential']);
// It can also be rejected depending on your security policy (e.g. disabled by the user because of loss)
} }
$PKCS = $webauthnSerializerFactory->create()->deserialize($publicKeyCredentialSourceRepository1->data, PublicKeyCredentialSource::class, 'json');
$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create(); $authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create();
$publicKeyCredentialRequestOptions = Yii::$app->session->get('publicKeyCredentialRequestOptions'); $publicKeyCredentialRequestOptions = Yii::$app->session->get('publicKeyCredentialRequestOptions');
try { try {
$publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check( $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
$publicKeyCredentialSource, $PKCS,
$authenticatorAssertionResponse, $authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions, $publicKeyCredentialRequestOptions,
Yii::$app->params['domain'] ?? null, Yii::$app->params['domain'],
null //Deprecated Token Binding Handler. Please set null. null //Deprecated Token Binding Handler. Please set null.
); );
} catch (AuthenticatorResponseVerificationException $e) { } catch (AuthenticatorResponseVerificationException $e) {
return $this->asJson(['message' => $e->getMessage(), 'verified' => false]);
} }
// Optional, but highly recommended, you can save the credential source as it may be modified // Optional, but highly recommended, you can save the credential source as it may be modified
// during the verification process (counter may be higher). // during the verification process (counter may be higher).
$publicKeyCredentialSourceRepository->saveCredential($publicKeyCredentialSource); $publicKeyCredentialSourceRepository1->saveCredential($publicKeyCredentialSource,'test');
return $this->asJson(['verified' => true]);
} }
} }

View File

@ -1,115 +0,0 @@
<?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();
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace app\models;
use JsonException;
use Webauthn\PublicKeyCredentialSource;
use Yii;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
/**
* This is the model class for table "public_key_credential_source_repository".
*
* @property int $id 记录id
* @property int $user_id 对应的用户id
* @property string $name 标识名
* @property string $public_key_credential_id PKC_ID
* @property string $data PKC_DATA
*
* @property User $user
*/
class PublicKeyCredentialSourceRepository extends ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName(): string
{
return 'public_key_credential_source_repository';
}
/**
* {@inheritdoc}
*/
public function rules(): array
{
return [
[['user_id', 'name', 'public_key_credential_id', 'data'], 'required'],
[['user_id'], 'integer'],
[['data'], 'string'],
[['name'], 'string', 'max' => 64],
[['public_key_credential_id'], 'string', 'max' => 255],
[['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels(): array
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'name' => 'Name',
'public_key_credential_id' => 'Public Key Credential ID',
'data' => 'Data',
];
}
/**
* Gets query for [[User]].
*
* @return ActiveQuery
*/
public function getUser(): ActiveQuery
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}
/**
* 获取用户相关的所有公钥凭证对象
* @param User $user
* @return array
*/
public function findAllForUserEntity(User $user): array
{
return self::findAll(['user_id' => $user->id]);
}
/**
* 从数据库中获取指定$id的记录对象
* @param string $PKC_ID
* @return PublicKeyCredentialSourceRepository|null
*/
public function findOneByCredentialId(string $PKC_ID): ?PublicKeyCredentialSourceRepository
{
return self::findOne(['public_key_credential_id' => $PKC_ID]);
}
/**
* 保存PublicKeyCredentialSource对象到数据库
* @param PublicKeyCredentialSource $PKCS
* @param string $name
* @return bool
* @throws JsonException
*/
public function saveCredential(PublicKeyCredentialSource $PKCS,string $name): bool
{
$jsonSerialize = $PKCS->jsonSerialize();
$this->public_key_credential_id = $jsonSerialize['publicKeyCredentialId'];
$publicKeyCredentialSourceJson = json_encode($jsonSerialize, JSON_THROW_ON_ERROR);
$this->data = $publicKeyCredentialSourceJson;
$this->name = $name;
$this->user_id = Yii::$app->user->id;
return $this->save();
}
}

View File

@ -33,7 +33,7 @@ use yii\web\IdentityInterface;
* @property string|null $vault_salt 保险箱加密密钥盐 * @property string|null $vault_salt 保险箱加密密钥盐
* *
* @property CollectionTasks[] $collectionTasks * @property CollectionTasks[] $collectionTasks
* @property PublicKeyCredentialSource[] $publicKeyCredentialSources * @property PublicKeyCredentialSourceRepository[] $publicKeyCredentialSourcesRepository
* @property Share[] $shares * @property Share[] $shares
*/ */
class User extends ActiveRecord implements IdentityInterface class User extends ActiveRecord implements IdentityInterface
@ -230,13 +230,13 @@ class User extends ActiveRecord implements IdentityInterface
} }
/** /**
* Gets query for [[PublicKeyCredentialSources]]. * Gets query for [[PublicKeyCredentialSourcesRepository]].
* *
* @return ActiveQuery * @return ActiveQuery
*/ */
public function getPublicKeyCredentialSources(): ActiveQuery public function getPublicKeyCredentialSourcesRepository(): ActiveQuery
{ {
return $this->hasMany(PublicKeyCredentialSource::class, ['user_id' => 'id']); return $this->hasMany(PublicKeyCredentialSourceRepository::class, ['user_id' => 'id']);
} }
/** /**