├── TwoFactorTextBundle.php ├── Model └── TwoFactorTextInterface.php ├── TextSender ├── AuthCodeTextInterface.php └── ExampleTextSender.php ├── composer.json ├── Generator ├── CodeGeneratorInterface.php └── CodeGenerator.php ├── UPGRADE.md ├── DependencyInjection ├── Compiler │ └── TextCompilerPass.php ├── Configuration.php └── TwoFactorTextExtension.php ├── LICENSE ├── README.md ├── Resources └── config │ └── two_factor_provider_text.yaml └── Provider └── TextTwoFactorProvider.php /TwoFactorTextBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new TextCompilerPass()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Model/TwoFactorTextInterface.php: -------------------------------------------------------------------------------- 1 | hasDefinition('scheb_two_factor.provider_registry')) { 16 | return; 17 | } 18 | 19 | if (!$container->hasDefinition('two_factor_text.security.provider')) { 20 | return; 21 | } 22 | 23 | if ($container->hasAlias('two_factor_text.security.auth_code_sender')) { 24 | return; 25 | } 26 | 27 | $message = 'Sender service for "two_factor_text.security.text.auth_code_sender" is not configured.'; 28 | throw new LogicException($message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Michael Erkens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /TextSender/ExampleTextSender.php: -------------------------------------------------------------------------------- 1 | request( 18 | 'POST', 19 | 'https://text-message-api/send-sms', 20 | [ 21 | 'body' => [ 22 | 'recipient' => $user->getTextAuthRecipient(), 23 | 'text' => sprintf($this->getMessageFormat(), $code ?? $user->getTextAuthCode()), 24 | ] 25 | ] 26 | ); 27 | } 28 | 29 | public function setMessageFormat(string $format): void 30 | { 31 | $this->format = $format; 32 | } 33 | 34 | public function getMessageFormat(): string 35 | { 36 | return $this->format; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 16 | 17 | /** 18 | * @psalm-suppress PossiblyNullReference 19 | * @psalm-suppress PossiblyUndefinedMethod 20 | */ 21 | $rootNode 22 | ->canBeEnabled() 23 | ->children() 24 | ->scalarNode('auth_code_sender')->defaultValue('Erkens\Security\TwoFactorTextBundle\TextSender\ExampleTextSender')->end() 25 | ->scalarNode('code_generator')->defaultValue('two_factor_text.security.default_code_generator')->end() 26 | ->scalarNode('template')->defaultValue('@SchebTwoFactor/Authentication/form.html.twig')->end() 27 | ->integerNode('digits')->defaultValue(6)->min(1)->end() 28 | ->scalarNode('text')->defaultValue('Use this code to login: %s')->end() 29 | ->end() 30 | ->end(); 31 | 32 | return $treeBuilder; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | erkens/2fa-text 2 | =============== 3 | 4 | This package extends [scheb/2fa-bundle](https://github.com/scheb/2fa-bundle) with two-factor authentication via text messages. 5 | 6 | It is based on the official [scheb/2fa-email](https://github.com/scheb/2fa-email) package. 7 | 8 | Usage 9 | ----- 10 | After you have installed and configured [scheb/2fa-bundle](https://github.com/scheb/2fa-bundle) you can install this package: 11 | 12 | ``` 13 | composer require erkens/2fa-text 14 | ``` 15 | 16 | First thing to do is make a new service that implements `Erkens\Security\TwoFactorTextBundle\TextSender\AuthCodeTextInterface` 17 | so we can actually send a sms or text message. This service can then be used in the configuration as "auth_code_sender": 18 | 19 | ``` 20 | two_factor_text: 21 | enabled: true 22 | auth_code_sender: Erkens\Security\TwoFactorTextBundle\TextSender\ExampleTextSender 23 | digits: 6 24 | text: 'To login, use this code: %s' 25 | template: '@SchebTwoFactor/Authentication/form.html.twig' 26 | ``` 27 | You can add this in its own yaml file inside `config/packages` or place it within the existing `scheb_2fa.yaml`. But 28 | make sure you have the `two_factor_text` at the root of the yaml-tree (not under `scheb_two_factor`). 29 | 30 | **Next** 31 | 32 | Your `User` entity must implement the `Erkens\Security\TwoFactorTextBundle\Model\TwoFactorTextInterface` and implement the required methods. 33 | 34 | 35 | License 36 | ------- 37 | This software is available under the [MIT license](LICENSE). 38 | -------------------------------------------------------------------------------- /Generator/CodeGenerator.php: -------------------------------------------------------------------------------- 1 | persister = $persister; 25 | $this->textSender = $textSender; 26 | $this->digits = $digits; 27 | $this->text = $text; 28 | } 29 | 30 | public function generateAndSend(TwoFactorTextInterface $user): void 31 | { 32 | $code = $this->generateCode(); 33 | $user->setTextAuthCode($code); 34 | $this->persister->persist($user); 35 | $this->send($user); 36 | } 37 | 38 | public function returnAndSendWithMessage(TwoFactorTextInterface $user, string $text): string 39 | { 40 | $code = $this->generateCode(); 41 | $this->textSender->setMessageFormat($text); 42 | $this->textSender->sendAuthCode($user, $code); 43 | return $code; 44 | } 45 | 46 | public function reSend(TwoFactorTextInterface $user): void 47 | { 48 | $this->send($user); 49 | } 50 | 51 | protected function send(TwoFactorTextInterface $user): void 52 | { 53 | $this->textSender->setMessageFormat($this->text); 54 | $this->textSender->sendAuthCode($user, $user->getTextAuthCode()); 55 | } 56 | 57 | protected function generateCode(): string 58 | { 59 | $min = 10 ** ($this->digits - 1); 60 | $max = 10 ** $this->digits - 1; 61 | return (string) random_int($min, $max); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Resources/config/two_factor_provider_text.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # Service for the default code generator 3 | two_factor_text.security.default_code_generator: 4 | class: Erkens\Security\TwoFactorTextBundle\Generator\CodeGenerator 5 | lazy: true 6 | arguments: 7 | # Argument 1: Service ID for persistence 8 | - '@scheb_two_factor.persister' 9 | # Argument 2: Service ID for sending the auth code 10 | - '@two_factor_text.security.auth_code_sender' 11 | # Argument 3: Parameter for the number of digits 12 | - '%two_factor_text.digits%' 13 | # Argument 4: Parameter for the text message format 14 | - '%two_factor_text.text%' 15 | 16 | # Alias for the Code Generator Interface (using the default implementation) 17 | Erkens\Security\TwoFactorTextBundle\Generator\CodeGeneratorInterface: 18 | alias: two_factor_text.security.code_generator 19 | public: true 20 | 21 | # Example text sender implementation (no arguments needed) 22 | Erkens\Security\TwoFactorTextBundle\TextSender\ExampleTextSender: ~ 23 | 24 | # Service for the form renderer, which uses the SchebTwoFactorBundle's default renderer 25 | two_factor_text.security.form_renderer: 26 | class: Scheb\TwoFactorBundle\Security\TwoFactor\Provider\DefaultTwoFactorFormRenderer 27 | lazy: true 28 | arguments: 29 | # Argument 1: Service ID for the Twig templating engine 30 | - '@twig' 31 | # Argument 2: Parameter for the template path 32 | - '%two_factor_text.template%' 33 | 34 | # The main two-factor provider service 35 | two_factor_text.security.provider: 36 | class: Erkens\Security\TwoFactorTextBundle\Provider\TextTwoFactorProvider 37 | arguments: 38 | # Argument 1: The code generator service 39 | - '@two_factor_text.security.code_generator' 40 | # Argument 2: The form renderer service 41 | - '@two_factor_text.security.form_renderer' 42 | tags: 43 | # Tagging the service as a two-factor provider with the alias 'text' 44 | - { name: scheb_two_factor.provider, alias: text } -------------------------------------------------------------------------------- /Provider/TextTwoFactorProvider.php: -------------------------------------------------------------------------------- 1 | codeGenerator = $codeGenerator; 21 | $this->formRenderer = $formRenderer; 22 | } 23 | 24 | public function beginAuthentication(AuthenticationContextInterface $context): bool 25 | { 26 | // Check if user can do text authentication 27 | $user = $context->getUser(); 28 | 29 | return $user instanceof TwoFactorTextInterface && $user->isTextAuthEnabled(); 30 | } 31 | 32 | public function prepareAuthentication($user): void 33 | { 34 | if ($user instanceof TwoFactorTextInterface) { 35 | $this->codeGenerator->generateAndSend($user); 36 | } 37 | } 38 | 39 | public function validateAuthenticationCode($user, string $authenticationCode): bool 40 | { 41 | if (!($user instanceof TwoFactorTextInterface)) { 42 | return false; 43 | } 44 | 45 | // Strip any user added spaces 46 | $authenticationCode = str_replace(' ', '', $authenticationCode); 47 | 48 | return $user->getTextAuthCode() === $authenticationCode; 49 | } 50 | 51 | public function getFormRenderer(): TwoFactorFormRendererInterface 52 | { 53 | return $this->formRenderer; 54 | } 55 | 56 | public function resendAuthenticationCode($user): void 57 | { 58 | if ($user instanceof TwoFactorTextInterface) { 59 | $this->codeGenerator->reSend($user); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DependencyInjection/TwoFactorTextExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 19 | 20 | // if not enabled, don't load it 21 | if (isset($config['enabled']) && true === $config['enabled']) { 22 | $this->configureTextAuthenticationProvider($container, $config); 23 | } 24 | } 25 | 26 | public function getAlias(): string 27 | { 28 | return 'two_factor_text'; 29 | } 30 | 31 | private function configureTextAuthenticationProvider(ContainerBuilder $container, array $config): void 32 | { 33 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 34 | $loader->load('two_factor_provider_text.yaml'); 35 | 36 | $container->setParameter($this->getAlias() . '.enabled', $config['enabled']); 37 | $container->setParameter($this->getAlias() . '.template', $config['template']); 38 | $container->setParameter($this->getAlias() . '.digits', $config['digits']); 39 | $container->setParameter($this->getAlias() . '.text', $config['text']); 40 | $container->setAlias($this->getAlias() . '.security.code_generator', $config['code_generator'])->setPublic(true); 41 | $container->setAlias($this->getAlias() . '.security.auth_code_sender', $config['auth_code_sender']); 42 | } 43 | 44 | public function prepend(ContainerBuilder $container): void 45 | { 46 | $configs = $container->getExtensionConfig($this->getAlias()); 47 | $config = $this->processConfiguration(new Configuration(), $configs); 48 | // Load two-factor modules 49 | if (isset($config['enabled']) && true === $config['enabled']) { 50 | $this->configureTextAuthenticationProvider($container, $config); 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------