jbtronics / 2fa-webauthn
为 scheb/2fa 添加 Webauthn 二次认证插件
v2.2.2
2024-07-21 19:24 UTC
Requires
- php: ^8.1
- ext-json: *
- nyholm/psr7: ^1.5
- psr/log: ^3.0.0|^2.0.0
- scheb/2fa-bundle: ^6.0.0|^7.0.0
- symfony/framework-bundle: ^6.0|^7.0
- symfony/psr-http-message-bridge: ^2.1|^6.1|^7.0
- symfony/uid: ^6.0|^7.0
- web-auth/webauthn-lib: ^4.7
Requires (Dev)
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-latest
README
此仓库包含一个插件,为 scheb/2fa 添加对 Webauthn 验证器(如 Yubikey)作为第二因素的支持。
功能
- 支持所有 Webauthn 验证器作为第二因素
- 支持每个用户多个验证器
- 对现有已注册的 U2F 密钥(来自 r/u2f-two-factor-bundle)的向后兼容性
要求
- Symfony 6
- PHP 8.1 或更高版本
- webauthn/webauthn-lib 4.0 或更高版本
如果您想使用 symfony 5.* 和 PHP 7.4,请使用此捆绑包的 1.0.0 版本。
安装
- 安装捆绑包
composer require jbtronics/2fa-webauthn
- 在您的
config/bundles.php
中启用捆绑包(通常由 Symfony flex 自动完成) - 如果您想使用 symfony 5.* 和 PHP 7.4,使用此捆绑包的 1.0.0 版本。您不需要运行社区食谱,因为我们只使用捆绑包中的 doctrine 类型定义。将
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
添加到您的config/bundles.php
中。
设置和使用
在遵循安装步骤后,请执行以下步骤来设置库
- 将
Jbtronics\TFAWebauthn\Model\TwoFactorInterface
接口添加到您的用户实体
use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface; class User implements WebauthnTwoFactorInterface { /** * @var Collection<int, PublicKeyCredentialSource> * @ORM\OneToMany(targetEntity="App\Entity\WebauthnKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) */ private $webauthnKeys; /** * Determines whether the user has 2FA using Webauthn enabled * @return bool True if the webauthn 2FA is enabled, false otherwise */ public function isWebAuthnAuthenticatorEnabled(): bool { //Return true to enable webauthn 2FA return count($this->webauthnKeys) > 0; } /** * Returns a list of all legacy U2F keys, associated with this user * Return an empty array, if this user does not have any legacy U2F keys. * @return iterable<LegacyU2FKeyInterface> */ public function getLegacyU2FKeys(): iterable { return []; //If you have no legacy U2F keys, return just an empty array //return $this->u2f_keys; //Otherwise return the legacy keys (see migration section below) } /** * Returns a list of all webauthn keys, associated with this user * @return iterable<PublicKeyCredentialSource> */ public function getWebauthnKeys(): iterable { return $this->webauthnKeys; } /** * Returns the webauthn user entity that should be used for this user. * @return PublicKeyCredentialUserEntity */ public function getWebAuthnUser(): PublicKeyCredentialUserEntity { //Return webauthn user definition for this user. As we just use it as an two-factor authentication, the values here are most likely not that important return new PublicKeyCredentialUserEntity( $this->getUsername(), // The Webauthn Name (like a username) $this->getID(), // A unique identifier for this user $this->getDisplayName() // The display name of this user (optional, otherwise null) ); } }
- 为 webauthn 密钥创建一个新的实体。为了简单起见,我们使用 web-auth/webauthn-symfony-bundle 的模板(有关更多信息,请参阅 此处)
declare(strict_types=1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource; use Webauthn\TrustPath\TrustPath; /** * @ORM\Table(name="webauthn_keys") * @ORM\Entity() */ class WebAuthnKey extends BasePublicKeyCredentialSource { /** * @var string * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="webauthnKeys") **/ protected ?User $user = null; //You can declare additional fields too, if you want to store additional information about the key (like a name) private $name; public function getId(): string { return $this->id; } public static function fromRegistration(BasePublicKeyCredentialSource $registration): self { return new static( $registration->getPublicKeyCredentialId(), $registration->getType(), $registration->getTransports(), $registration->getAttestationType(), $registration->getTrustPath(), $registration->getAaguid(), $registration->getCredentialPublicKey(), $registration->getUserHandle(), $registration->getCounter(), $registration->getOtherUI() ); } }
-
将前端 JavaScript 代码包含到您的项目中:对于 webauthn,我们需要一些 JavaScript 代码来与验证器交互。将文件从
src/Resources/assets/tfa_webauthn.js
复制到您的项目中,并通过<script>
标签加载或通过在 webpack 中使用.addEntry()
包含它。 -
添加配置文件
config/packages/jbtronics_2fa_webauthn.yaml
tfa_webauthn: enabled: true # Optional configuration options: # timeout: 60000 # The timeout in millisceconds to allow the user to interact with the authenticator. Default: 60000 # template: '' # The template to use for the login form # rpID: null # The relying party ID of your application. If null, the current host will be used. Default: null # U2FAppID: null # The U2F AppID of your application. If null, the current host will be used. Default: null # These settings are most likely not important for two-factor authentication: # rpName: 'My Application' # The relying party name of your application, Default: 'My Application' # rpIcon: null # The relying party icon of your application. Default: null
- 自定义登录模板:类似于
scheb/2fa
捆绑包的基础登录模板,您可能需要覆盖此捆绑包的登录模板以将其集成到设计中。将模板从Resources/views/Authentication/form.html.twig
复制到您的项目中,并根据您的需求进行自定义。在捆绑包配置中配置template
设置为您的新的路径。
新密钥的注册
原则上,使用现有密钥的登录应该可以工作,但您可能需要一些注册新密钥的可能性。为了简化这个过程,有一个 Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper
服务可以帮助您完成此操作
- 创建一个新的控制器,用于处理注册,其外观应如下所示
use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;Ä class WebauthnKeyRegistrationController extends AbstractController { /** * @Route("/webauthn/register", name="webauthn_register") */ public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em) { //If form was submitted, check the auth response if ($request->getMethod() === 'POST') { $webauthnResponse = $request->request->get('_auth_code'); //Retrieve other data from the form, that you want to store with the key $keyName = $request->request->get('keyName'); try { //Check the response $new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse); } catch (Exception $exception) { // Handle errors... } //If we got here, the registration was successful. Now we can store the new key in the database //Convert our returned key into an database entity and persist it... $keyEntity = WebauthnKey::fromRegistration($new_key); $keyEntity->setName($keyName); $keyEntity->setUser($this->getUser()); $em->persist($keyEntity); $em->flush(); $this->addFlash('success', 'Key registered successfully'); //We are finished here so return to another page return $this->redirectToRoute('homepage'); } return $this->render( 'webauthn_register.html.twig', [ //Generate the registration request 'registrationRequest' => $registrationHelper->generateRegistrationRequestAsJSON(), ] ); } }
- 创建一个包含表单的模板,该表单将用于注册新密钥。表单应如下所示
<form method="post" class="form" action="{{ path('webauthn_register') }}" data-webauthn-tfa-action="register" data-webauthn-tfa-data='{{ registrationRequest|raw }}'> <input type="text" name="keyName" id="keyName" placeholder="Shown key name"/> <button type="submit" class="btn btn-success">Add new Key</button> <input type="hidden" name="_auth_code" id="_auth_code" /> </form>
data-webauthn-tfa-action
属性将表单标记为 webauthn 注册表单,并由上述包含的前端代码处理。如果表单被提交,前端代码将捕获该操作并启动注册过程。响应将被放入具有 id _auth_code
的隐藏输入字段中,并发送到我们的控制器进行解析。
从 r/u2f-two-factor-bundle 迁移
- 将您的 U2FKey 实体的
R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface
接口替换为Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface
,并删除 fromRegistrationData() 函数(因为我们不再需要它)。 - 将您的用户中
R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface
接口替换为Jbtronics\TFAWebauthn\Model\TwoFactorInterface
,配置它(见上文)并将您的getU2FKeys()
函数重命名为getLegacyU2FKeys()
。 - (可选:)如果您的应用ID与域名不同,请使用
U2FAppID
选项进行配置。但通常情况下,这通常不需要。 - 删除
r/u2f-two-factor-bundle
的旧路由、模板和设置,并将其从您的应用程序中移除 - 按照上述设置步骤进行
许可证
本软件包受 MIT 许可证的许可。有关详细信息,请参阅 LICENSE。
致谢
- Webauthn 支持由 spomky-labs webauthn-framework 提供
- 本库受到了 r/u2f-two-factor-bundle 软件包的启发