mbarbey / u2f-security-bundle
Symfony 扩展,允许使用 U2F 安全密钥作为双重认证
Requires
- php: ^5.5.9 || ^7.0
- samyoul/u2f-php-server: ^1.1
- symfony/doctrine-bridge: ^2.8 || ^3.0 || ^4.0
- symfony/framework-bundle: ^2.8 || ^3.0 || ^4.0
- symfony/security-bundle: ^2.8 || ^3.0 || ^4.0
- symfony/validator: ^2.8 || ^3.0 || ^4.0
Requires (Dev)
- phpunit/phpunit: ^4.8.35 || ^5.7.11 || ^6.5
- symfony/phpunit-bridge: ^2.8 || ^3.0 || ^4.0
- symfony/yaml: ^2.8 || ^3.0 || ^4.0
This package is auto-updated.
Last update: 2020-09-01 15:46:55 UTC
README
正在进行中。包几乎完成。代码已准备就绪,现在正在测试和编写README。
U2F 安全包
此 Symfony 扩展旨在为您的 Symfony 项目添加双重安全级别。
概览
如果您想使用 U2F 安全密钥作为第二级安全,您有 3 个选项
- 您是一位真正的战士,从头开始构建一切
- 您使用第三方库并围绕它创建包装器
- 您使用此包 :-)
此 U2F 安全包是围绕 samyoul/u2f-php-server 库的包装器。
它将所有复杂性移至包装器,您需要做的只是创建实体、从控制器中调用一些操作,并在美观的页面上显示一个漂亮的表单。就这样(对于基本功能)。
要求
在安装此包之前,您需要有一个已经通过 Symfony 方式(即安全包、防火墙和用户实体)运行的登录和受保护区域。
重要的一点,U2F 密钥仅在 HTTPS 请求上工作。因此,您将需要一个 SSL 证书,即使在本地主机上也是如此。好消息是您可以使用自签名证书而不会出现问题。
安装和配置
总体而言,安装过程可以分为三个部分
- Composer 安装和包配置
- 创建实体和模型
- 创建控制器
现在我们一起来吧!
首先,您需要通过 composer 安装包!
composer require mbarbey/u2f-security-bundle
安装配方后,不要忘记编辑文件 config/packages/mbarbey_u2f_security.yaml
以匹配您自己的路由。
-
键
authentication_route
是必需的。它是用户在成功使用 U2F 安全密钥认证之前被囚禁的路由。它必须是执行 U2F 认证的路由。 -
键
whitelist_routes
是一个可选的路由列表,在这些路由上用户登录后仍然可以访问,而无需通过双重安全进行认证。例如,您可以列出登录和注销路由。这些给定的路由将自动添加到以下已列出白名单路由中_wdt
_profiler_home
_profiler_search
_profiler_search_bar
_profiler_phpinfo
_profiler_search_results
_profiler_open_file
_profiler
_profiler_router
_profiler_exception
_profiler_exception_css
太棒了!很简单,对吧?现在让我们创建一些实体。
实体和模型
首先,我们将创建一个 "key" 实体,它将存储 U2F 密钥的数据。您有两个选项
- 创建一个新的实体并扩展类
Mbarbey\U2fSecurityBundle\Model\Key\U2fKey
。 - 创建一个新的实体并实现接口
Mbarbey\U2fSecurityBundle\Model\Key\U2fKeyInterface
。如果您选择了这个选项,请务必将变量$keyHandle
、$publicKey
、$certificate
和$counter
设置为公开。这实际上是所使用的库所需要的。如果您需要一些灵感,可以查看Mbarbey\U2fSecurityBundle\Model\Key\U2fKey
类。
做得好,现在让我们更深入地处理这些实体。
您需要编辑一下防火墙所使用的现有 user
实体,使其与新创建的 U2F 密钥实体相关联。同样,您有两个选项
- 扩展类
Mbarbey\U2fSecurityBundle\Model\User\U2fUser
。 - 实现接口
Mbarbey\U2fSecurityBundle\Model\User\U2fUserInterface
并添加缺失的函数(getU2fKeys
、addU2fKey
和removeU2fKey
)。
太棒了,您已经创建了所有必需的实体。现在我们需要两个模型来存储表单数据。
您需要创建一个空的认证模型,该模型扩展了 Mbarbey\U2fSecurityBundle\Model\U2fAuthentication\U2fAuthentication
类。如果您需要,可以向该模型添加额外的数据,这些数据将存储由 U2F 密钥生成的挑战响应,在登录后显示的认证页面。
接下来,您需要创建一个空的注册模型,该模型扩展了 Mbarbey\U2fSecurityBundle\Model\U2fRegistration\U2fRegistration
类。同样,如果您需要,可以向该模型添加额外的数据,这些数据将存储将附加到用户的 U2F 密钥的标识。例如,您可以给密钥命名,以便用户能够识别它们;-)
太棒了,您已经完成了 80% 的工作。现在让我们做一些更简单的任务。
注册控制器
我们首先允许用户注册安全密钥。
为此,您需要一个控制器(新或现有)和一个 registration
动作,该动作只能在用用户名和密码登录后(经典方式)使用。
在这个动作中,您需要做以下事情
- 将服务
Mbarbey\U2fSecurityBundle\Service\U2fSecurity
注入为动作的参数。 - 创建一个注册表单并使用您在上一部分创建的模型。对于表单,字段
response
必须隐藏。 - 如果您的表单已提交且有效
- 您需要使用您之前创建的实体创建一个新的密钥,并调用您刚刚注入的服务中的
validateRegistration
函数,并传递以下参数- 当前用户(例如 $this->getUser())
- 由表单填充的注册数据
- 新创建的密钥
- 如果发生错误,此函数将抛出异常,因此您需要使用 try catch 来处理它,并告知用户他的注册失败原因。
- 如果没有错误,您新创建的密钥已被填充并准备持久化。
- 否则
- 您需要存储函数
createRegistration
的结果,并将其作为参数传递您的 appId。appId 必须始终是 HTTP 协议和您的域名。例如:[https://example.com](https://example.com)。对于更动态的系统,您可以使用 HTTP 请求中的函数getSchemeAndHttpHost
。以下是一个使用示例:$registrationData = $service->createRegistration($request->getSchemeAndHttpHost());
。这些数据是发送给用户的注册请求,包含两个部分:请求和签名。 - 使用以下内容渲染视图
- 您的表单
- 您的注册请求的
request
部分(例如$registrationData['request']
) - 您的注册请求的
signatures
部分(例如$registrationData['signatures']
)
以下是这个控制器动作的完整示例
public function u2fRegistration(Request $request, U2fSecurity $service) { $registration = new U2fRegistration(); $form = $this->createForm(U2fRegistrationType::class, $registration); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $key = new Key(); $service->validateRegistration($this->getUser(), $registration, $key); $em = $this->getDoctrine()->getManager(); $em->persist($key); $em->flush(); return $this->redirectToRoute('user_keys_list'); } catch (\Exception $e) { $this->addFlash('danger', $e->getMessage()); } } $registrationData = $service->createRegistration($request->getSchemeAndHttpHost()); return $this->render('user/key/register.html.twig', array( 'jsRequest' => $registrationData['request'], 'jsSignatures' => $registrationData['signatures'], 'form' => $form->createView(), )); }
对于前端部分,由您自行决定。您需要的东西有两个:表单和一些JavaScript。以下是您必须使用的JavaScript代码。请随意修改。
<script src="{{ asset('bundles/mbarbeyu2fsecurity/u2f.js') }}"></script> <script type="text/javascript"> setTimeout(function() { // A magic JS function that talks to the USB device. This function will keep polling for the USB device until it finds one. u2f.register("{{ jsRequest.appId }}", [{version: "{{jsRequest.version}}", challenge: "{{ jsRequest.challenge }}"}], {{ jsSignatures|raw }}, function(data) { // Handle returning error data if(data.errorCode && data.errorCode != 0) { alert("registration failed with error: " + data.errorCode); // Or handle the error however you'd like. return; } // On success process the data from USB device to send to the server var registration_response = data; // Get the form items so we can send data back to the server var form = document.getElementsByTagName('form')[0]; var response = document.getElementById('{{ form.response.vars.id }}'); // Fill and submit form. response.value = JSON.stringify(registration_response); form.submit(); }); }, 1000); </script>
哇塞!用户可以注册安全密钥并将其链接到他们的账户了!
但是!注册密钥很酷,但用它进行认证会更好。
认证控制器
现在让我们为认证做同样的事情。请记住,这个操作必须与你在包配置中定义的认证路由相匹配。
在这个第二个操作中,你需要做
- 将服务
Mbarbey\U2fSecurityBundle\Service\U2fSecurity
注入为动作的参数。 - 创建一个认证表单,并使用上一部分创建的模型。对于表单,字段
response
必须隐藏。 - 如果您的表单已提交且有效
- 你需要从服务中调用函数
validateAuthentication
,并给出以下参数- 用户(例如:$this->getUser())
- 表单填写的数据
- 这个函数将返回用户的已使用密钥并更新计数器,或者抛出异常。因此,你需要try-catch来处理它,并告知用户认证失败的原因。
- 如果没有错误,你可以更新/保存接收到的密钥。
- 否则
- 你需要存储函数
createAuthentication
的结果,并将其作为参数传递给你的appId和要检查的用户。这里是一个用法示例:$authenticationRequest = $service->createAuthentication($request->getSchemeAndHttpHost(), $this->getUser());
。这些数据是认证请求,将发送给用户。 - 使用以下内容渲染视图
- 您的表单
- 认证请求
以下是这个控制器动作的完整示例
public function u2fAuthentication(Request $request, U2fSecurity $service) { $authentication = new U2fAuthentication(); $form = $this->createForm(U2fAuthenticationType::class, $authentication); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $updatedKey = $service->validateAuthentication($this->getUser(), $authentication); $em = $this->getDoctrine()->getManager(); $em->persist($updatedKey); $em->flush(); return $this->redirectToRoute('user_list'); } catch (\Exception $e) { $this->addFlash('danger', $e->getMessage()); } } $authenticationRequest = $service->createAuthentication($request->getSchemeAndHttpHost(), $this->getUser()); return $this->render('user/key/authenticate.html.twig', array( 'authenticationRequest' => $authenticationRequest, 'form' => $form->createView(), )); }
对于前端部分,由您自行决定。您需要的东西有两个:表单和一些JavaScript。以下是您必须使用的JavaScript代码。请随意修改。
<script src="{{ asset('bundles/mbarbeyu2fsecurity/u2f.js') }}"></script> <script type="text/javascript"> setTimeout(function() { // Magic JavaScript talking to your HID u2f.sign("{{ authenticationRequest.appId }}", "{{ authenticationRequest.challenge }}", {{ authenticationRequest.registeredKeys|raw }}, function(data) { // Handle returning error data if(data.errorCode && data.errorCode != 0) { alert("Authentication failed with error: " + data.errorCode); // Or handle the error however you'd like. return; } // On success process the data from USB device to send to the server var authentication_response = data; // Get the form items so we can send data back to the server var form = document.getElementsByTagName('form')[0]; var response = document.getElementById('{{ form.response.vars.id }}'); // Fill and submit form. response.value = JSON.stringify(authentication_response); form.submit(); }); }, 1000); </script>
恭喜你(背景播放成功音乐)。现在你的用户可以注册一些安全密钥,当他们登录时,将被重定向到认证页面,直到他们成功使用安全密钥进行认证。
高级用法
在U2F工作流程中,你可以监听以下9个事件
注册
- U2fPreRegistrationEvent
你可以监听事件 U2fPreRegistrationEvent::getName()
,当你在控制器中调用服务 U2fSecurity
的函数 canRegister
时触发。
在你的监听器中,你可以使用任何标准来决定给定的用户是否允许注册安全密钥。你可以使用事件的函数 abort
来拒绝使用安全密钥。在这种情况下,你控制器中调用的函数 canRegister
将返回 U2fPreRegistrationEvent
事件。你可以使用该函数的 isAborted
来知道你是否应该继续该过程。
这里是一个用法示例
public function u2fRegistration(Request $request, U2fSecurity $service) { /* * We check if there is any listener somewhere which have an objection for the current user registering a new key */ $canRegister = $service->canRegister($this->getUser(), $request->getSchemeAndHttpHost()); if ($canRegister->isAborted()) { $this->addFlash('warning', $canRegister->getReason()); return $this->redirectToRoute('user_register_u2f_denied'); } /* * The user is allowed to register a key :-) */ $registration = new U2fRegistration(); $form = $this->createForm(U2fRegistrationType::class, $registration); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $key = new Key(); $key->setName($registration->getName())->setUser($this->getUser()); $service->validateRegistration($this->getUser(), $registration, $key); $em = $this->getDoctrine()->getManager(); $em->persist($key); $em->flush(); return $this->redirectToRoute('user_details', ['userId' => $this->getUser()->getId()]); } catch (\Exception $e) { $form->get('name')->addError(new FormError($e->getMessage())); } } $registrationData = $service->createRegistration($request->getSchemeAndHttpHost()); return $this->render('security/u2fRegistration.html.twig', array( 'jsRequest' => $registrationData['request'], 'jsSignatures' => $registrationData['signatures'], 'form' => $form->createView(), )); }
- U2fRegistrationSuccessEvent
你可以监听事件 U2fRegistrationSuccessEvent::getName()
,当用户成功注册新的安全密钥时触发。事件包含用户和刚刚注册的新密钥。
- U2fRegistrationFailureEvent
你可以监听事件 U2fRegistrationFailureEvent::getName()
,当用户尝试注册新的安全密钥失败时触发。事件包含用户和引起失败的PHP异常。
- U2fPostRegistrationEvent
你可以监听事件 U2fPostRegistrationEvent::getName()
,在每次密钥注册后触发,无论成功或失败。事件包含用户,如果注册成功,还可以包含新注册的密钥。
认证
- U2fAuthenticationRequiredEvent
您可以监听事件 U2fAuthenticationRequiredEvent::getName()
,该事件在用户使用其凭证正确登录后立即触发,并且如果用户已注册至少一个安全密钥。在您的监听器中,您可以使用任何标准来决定是否必须将给定用户移入U2F监狱,直到他证明自己的身份。您可以使用事件的 abort
函数来阻止使用U2F监狱。
- U2fPreAuthenticationEvent
您可以监听事件 U2fPreAuthenticationEvent::getName()
,该事件在您从控制器调用 U2fSecurity
服务的 canAuthenticate
函数时触发。
在您的监听器中,您可以使用任何标准来决定给定用户是否允许使用安全密钥作为双因素认证。您可以使用事件的 abort
函数来拒绝使用安全密钥。在这种情况下,控制器中调用的 canAuthenticate
函数将返回 U2fPreAuthenticationEvent
事件。您可以使用 isAborted
函数来判断是否应继续该过程。在“停止双因素认证”的情况下,您可以使用 U2fSecurity
服务的 stopRequestingAuthentication
函数将用户从U2F监狱中移除。
这里是一个用法示例
public function u2fAuthentication(Request $request, U2fSecurity $service) { /* * We check if there is any listener somewhere which have an objection for the current user authenticating with a key */ $canAuthenticate = $service->canAuthenticate($request->getSchemeAndHttpHost(), $this->getUser()); if ($canAuthenticate->isAborted()) { $service->stopRequestingAuthentication(); $this->addFlash('warning', 'Your connection was not secured by a security key'); return $this->redirectToRoute('user_list'); } /* * The user is allowed to use a key :-) */ $authentication = new U2fAuthentication(); $form = $this->createForm(U2fAuthenticationType::class, $authentication); $form->handleRequest($request); if ($form->isSubmitted()) { try { $updatedKey = $service->validateAuthentication($this->getUser(), $authentication); $em = $this->getDoctrine()->getManager(); $em->persist($updatedKey); $em->flush(); if ($request->getSession()->has(U2fSubscriber::U2F_SECURITY_KEY)) { $request->getSession()->remove(U2fSubscriber::U2F_SECURITY_KEY); } return $this->redirectToRoute('user_list'); } catch (\Exception $e) { $this->addFlash('danger', 'Authentication failed'); } } $authenticationRequest = $service->createAuthentication($request->getSchemeAndHttpHost(), $this->getUser()); return $this->render('security/u2fAuthentication.html.twig', array( 'authenticationRequest' => $authenticationRequest, 'form' => $form->createView() )); }
- U2fAuthenticationSuccessEvent
您可以监听事件 U2fAuthenticationSuccessEvent::getName()
,该事件在用户成功使用安全密钥进行认证时触发。该事件包含用户和所使用的密钥。
- U2fAuthenticationFailureEvent
您可以监听事件 U2fAuthenticationFailureEvent::getName()
,该事件在用户未能使用安全密钥进行认证时触发。该事件包含用户、引发失败的PHP异常和认证失败计数器。当用户最终正确认证自己时,此计数器将被清除。
- U2fPostAuthenticationEvent
您可以监听事件 U2fPostAuthenticationEvent::getName()
,该事件在每次U2F认证之后触发,无论成功与否。该事件包含用户、一个 success
标志,如果认证成功,还可以包含用户密钥。