├── Controller └── FormController.php ├── DependencyInjection ├── Compiler │ ├── MailerCompilerPass.php │ ├── TwoFactorFirewallConfigCompilerPass.php │ └── TwoFactorProviderCompilerPass.php ├── Configuration.php ├── Factory │ └── Security │ │ ├── TwoFactorFactory.php │ │ └── TwoFactorServicesFactory.php └── SchebTwoFactorExtension.php ├── LICENSE ├── Model ├── Persister │ ├── DoctrinePersister.php │ └── DoctrinePersisterFactory.php ├── PersisterInterface.php └── PreferredProviderInterface.php ├── README.md ├── Resources ├── config │ ├── backup_codes.php │ ├── persistence.php │ ├── security.php │ ├── trusted_device.php │ ├── two_factor.php │ ├── two_factor_provider_email.php │ ├── two_factor_provider_google.php │ └── two_factor_provider_totp.php ├── translations │ ├── SchebTwoFactorBundle.cs.yml │ ├── SchebTwoFactorBundle.de.yml │ ├── SchebTwoFactorBundle.en.yml │ ├── SchebTwoFactorBundle.es.yml │ ├── SchebTwoFactorBundle.fr.yml │ ├── SchebTwoFactorBundle.hr.yml │ ├── SchebTwoFactorBundle.hu.yml │ ├── SchebTwoFactorBundle.id.yml │ ├── SchebTwoFactorBundle.nl.yml │ ├── SchebTwoFactorBundle.pl.yml │ ├── SchebTwoFactorBundle.ro.yml │ ├── SchebTwoFactorBundle.ru.yml │ ├── SchebTwoFactorBundle.sk.yml │ ├── SchebTwoFactorBundle.sv.yml │ ├── SchebTwoFactorBundle.tr.yml │ └── SchebTwoFactorBundle.uk.yml └── views │ └── Authentication │ └── form.html.twig ├── SchebTwoFactorBundle.php ├── Security ├── Authentication │ ├── AuthenticationTrustResolver.php │ ├── Exception │ │ ├── InvalidTwoFactorCodeException.php │ │ ├── ReusedTwoFactorCodeException.php │ │ └── TwoFactorProviderNotFoundException.php │ └── Token │ │ ├── TwoFactorToken.php │ │ ├── TwoFactorTokenFactory.php │ │ ├── TwoFactorTokenFactoryInterface.php │ │ └── TwoFactorTokenInterface.php ├── Authorization │ ├── TwoFactorAccessDecider.php │ └── Voter │ │ └── TwoFactorInProgressVoter.php ├── Http │ ├── Authentication │ │ ├── AuthenticationRequiredHandlerInterface.php │ │ ├── DefaultAuthenticationFailureHandler.php │ │ ├── DefaultAuthenticationRequiredHandler.php │ │ └── DefaultAuthenticationSuccessHandler.php │ ├── Authenticator │ │ ├── Passport │ │ │ └── Credentials │ │ │ │ └── TwoFactorCodeCredentials.php │ │ └── TwoFactorAuthenticator.php │ ├── EventListener │ │ ├── AbstractCheckCodeListener.php │ │ ├── CheckTwoFactorCodeListener.php │ │ ├── CheckTwoFactorCodeReuseListener.php │ │ ├── SuppressRememberMeListener.php │ │ └── ThrowExceptionOnTwoFactorCodeReuseListener.php │ ├── Firewall │ │ ├── ExceptionListener.php │ │ └── TwoFactorAccessListener.php │ └── Utils │ │ ├── JsonRequestUtils.php │ │ ├── ParameterBagUtils.php │ │ └── RequestDataReader.php └── TwoFactor │ ├── AuthenticationContext.php │ ├── AuthenticationContextFactory.php │ ├── AuthenticationContextFactoryInterface.php │ ├── AuthenticationContextInterface.php │ ├── Condition │ ├── AuthenticatedTokenCondition.php │ ├── IpWhitelistCondition.php │ ├── TwoFactorConditionInterface.php │ └── TwoFactorConditionRegistry.php │ ├── Csrf │ └── NullCsrfTokenManager.php │ ├── Event │ ├── AuthenticationSuccessEventSuppressor.php │ ├── AuthenticationTokenListener.php │ ├── TwoFactorAuthenticationEvent.php │ ├── TwoFactorAuthenticationEvents.php │ ├── TwoFactorCodeEvent.php │ ├── TwoFactorCodeReusedEvent.php │ └── TwoFactorFormListener.php │ ├── IpWhitelist │ ├── DefaultIpWhitelistProvider.php │ └── IpWhitelistProviderInterface.php │ ├── Provider │ ├── DefaultTwoFactorFormRenderer.php │ ├── Exception │ │ ├── TwoFactorProviderLogicException.php │ │ ├── UnexpectedTokenException.php │ │ └── UnknownTwoFactorProviderException.php │ ├── PreparationRecorderInterface.php │ ├── TokenPreparationRecorder.php │ ├── TwoFactorFormRendererInterface.php │ ├── TwoFactorProviderDecider.php │ ├── TwoFactorProviderDeciderInterface.php │ ├── TwoFactorProviderInitiator.php │ ├── TwoFactorProviderInterface.php │ ├── TwoFactorProviderPreparationListener.php │ └── TwoFactorProviderRegistry.php │ ├── TwoFactorFirewallConfig.php │ └── TwoFactorFirewallContext.php └── composer.json /Controller/FormController.php: -------------------------------------------------------------------------------- 1 | getTwoFactorToken(); 42 | $this->setPreferredProvider($request, $token); 43 | 44 | $providerName = $token->getCurrentTwoFactorProvider(); 45 | if (null === $providerName) { 46 | throw new AccessDeniedException('User is not in a two-factor authentication process.'); 47 | } 48 | 49 | return $this->renderForm($providerName, $request, $token); 50 | } 51 | 52 | protected function getTwoFactorToken(): TwoFactorTokenInterface 53 | { 54 | $token = $this->tokenStorage->getToken(); 55 | if (!($token instanceof TwoFactorTokenInterface)) { 56 | throw new AccessDeniedException('User is not in a two-factor authentication process.'); 57 | } 58 | 59 | return $token; 60 | } 61 | 62 | protected function setPreferredProvider(Request $request, TwoFactorTokenInterface $token): void 63 | { 64 | $preferredProvider = (string) $request->query->get('preferProvider'); 65 | if (!$preferredProvider) { 66 | return; 67 | } 68 | 69 | try { 70 | $token->preferTwoFactorProvider($preferredProvider); 71 | } catch (UnknownTwoFactorProviderException) { 72 | // Bad user input 73 | } 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | protected function getTemplateVars(Request $request, TwoFactorTokenInterface $token): array 80 | { 81 | $config = $this->twoFactorFirewallContext->getFirewallConfig($token->getFirewallName()); 82 | $pendingTwoFactorProviders = $token->getTwoFactorProviders(); 83 | $displayTrustedOption = $this->canSetTrustedDevice($token, $request, $config); 84 | $authenticationException = $this->getLastAuthenticationException($request->getSession()); 85 | $checkPath = $config->getCheckPath(); 86 | $isRoute = !str_contains($checkPath, '/'); 87 | 88 | return [ 89 | 'twoFactorProvider' => $token->getCurrentTwoFactorProvider(), 90 | 'availableTwoFactorProviders' => $pendingTwoFactorProviders, 91 | 'authenticationError' => $authenticationException?->getMessageKey(), 92 | 'authenticationErrorData' => $authenticationException?->getMessageData(), 93 | 'displayTrustedOption' => $displayTrustedOption, 94 | 'authCodeParameterName' => $config->getAuthCodeParameterName(), 95 | 'trustedParameterName' => $config->getTrustedParameterName(), 96 | 'isCsrfProtectionEnabled' => $config->isCsrfProtectionEnabled(), 97 | 'csrfParameterName' => $config->getCsrfParameterName(), 98 | 'csrfTokenId' => $config->getCsrfTokenId(), 99 | 'checkPathRoute' => $isRoute ? $checkPath : null, 100 | 'checkPathUrl' => $isRoute ? null : $checkPath, 101 | 'logoutPath' => $this->logoutUrlGenerator->getLogoutPath(), 102 | ]; 103 | } 104 | 105 | protected function renderForm(string $providerName, Request $request, TwoFactorTokenInterface $token): Response 106 | { 107 | $renderer = $this->providerRegistry->getProvider($providerName)->getFormRenderer(); 108 | $templateVars = $this->getTemplateVars($request, $token); 109 | 110 | return $renderer->renderForm($request, $templateVars); 111 | } 112 | 113 | protected function getLastAuthenticationException(SessionInterface $session): AuthenticationException|null 114 | { 115 | $authException = $session->get(SecurityRequestAttributes::AUTHENTICATION_ERROR); 116 | if ($authException instanceof AuthenticationException) { 117 | $session->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR); 118 | 119 | return $authException; 120 | } 121 | 122 | return null; // The value does not come from the security component. 123 | } 124 | 125 | private function canSetTrustedDevice(TwoFactorTokenInterface $token, Request $request, TwoFactorFirewallConfig $config): bool 126 | { 127 | return $this->trustedFeatureEnabled 128 | && $this->trustedDeviceManager 129 | && $this->trustedDeviceManager->canSetTrustedDevice($token->getUser(), $request, $config->getFirewallName()) 130 | && (!$config->isMultiFactor() || 1 === count($token->getTwoFactorProviders())); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/MailerCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('scheb_two_factor.security.email.provider')) { 21 | // Email authentication is not enabled 22 | return; 23 | } 24 | 25 | if ($container->hasAlias('scheb_two_factor.security.email.auth_code_mailer')) { 26 | // Custom AuthCodeMailer 27 | return; 28 | } 29 | 30 | if (!$container->hasDefinition('mailer.mailer')) { 31 | throw new LogicException('Could not determine default mailer service to use. Please install symfony/mailer or create your own mailer and configure it under "scheb_two_factor.email.mailer.'); 32 | } 33 | 34 | $container->setAlias('scheb_two_factor.security.email.auth_code_mailer', 'scheb_two_factor.security.email.symfony_auth_code_mailer'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/TwoFactorFirewallConfigCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('scheb_two_factor.firewall_context')) { 22 | return; 23 | } 24 | 25 | $firewallContextDefinition = $container->getDefinition('scheb_two_factor.firewall_context'); 26 | $taggedServices = $container->findTaggedServiceIds('scheb_two_factor.firewall_config'); 27 | 28 | $references = []; 29 | foreach ($taggedServices as $id => $attributes) { 30 | if (!isset($attributes[0]['firewall'])) { 31 | throw new InvalidArgumentException('Tag "scheb_two_factor.firewall_config" requires attribute "firewall" to be set.'); 32 | } 33 | 34 | $name = $attributes[0]['firewall']; 35 | $references[$name] = new Reference($id); 36 | } 37 | 38 | $firewallContextDefinition->replaceArgument(0, $references); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/TwoFactorProviderCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('scheb_two_factor.provider_registry')) { 23 | return; 24 | } 25 | 26 | $registryDefinition = $container->getDefinition('scheb_two_factor.provider_registry'); 27 | $taggedServices = $container->findTaggedServiceIds('scheb_two_factor.provider'); 28 | 29 | $references = []; 30 | foreach ($taggedServices as $id => $attributes) { 31 | if (!isset($attributes[0]['alias'])) { 32 | throw new InvalidArgumentException('Tag "scheb_two_factor.provider" requires attribute "alias" to be set.'); 33 | } 34 | 35 | $name = $attributes[0]['alias']; 36 | $references[$name] = new Reference($id); 37 | } 38 | 39 | $registryDefinition->replaceArgument(0, new IteratorArgument($references)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 31 | 32 | /** 33 | * @psalm-suppress UndefinedMethod 34 | * @psalm-suppress UndefinedInterfaceMethod 35 | */ 36 | $rootNode 37 | ->children() 38 | ->scalarNode('persister')->defaultValue('scheb_two_factor.persister.doctrine')->end() 39 | ->scalarNode('model_manager_name')->defaultNull()->end() 40 | ->arrayNode('security_tokens') 41 | ->defaultValue([ 42 | UsernamePasswordToken::class, 43 | PostAuthenticationToken::class, 44 | ]) 45 | ->prototype('scalar')->end() 46 | ->end() 47 | ->arrayNode('ip_whitelist') 48 | ->beforeNormalization() 49 | ->ifArray() 50 | ->then(static function (array $value): array { 51 | return iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator($value)), false); 52 | }) 53 | ->end() 54 | ->defaultValue([]) 55 | ->prototype('scalar')->end() 56 | ->end() 57 | ->scalarNode('ip_whitelist_provider')->defaultValue('scheb_two_factor.default_ip_whitelist_provider')->end() 58 | ->scalarNode('two_factor_token_factory')->defaultValue('scheb_two_factor.default_token_factory')->end() 59 | ->scalarNode('two_factor_provider_decider')->defaultValue('scheb_two_factor.default_provider_decider')->end() 60 | ->scalarNode('two_factor_condition')->defaultNull()->end() 61 | ->scalarNode('code_reuse_cache')->defaultNull()->end() 62 | ->integerNode('code_reuse_cache_duration')->defaultValue(60)->end() 63 | ->scalarNode('code_reuse_default_handler')->defaultNull()->end() 64 | ->end(); 65 | 66 | /** @psalm-suppress ArgumentTypeCoercion */ 67 | $this->addExtraConfiguration($rootNode); 68 | 69 | return $treeBuilder; 70 | } 71 | 72 | private function addExtraConfiguration(ArrayNodeDefinition $rootNode): void 73 | { 74 | $this->addTrustedDeviceConfiguration($rootNode); 75 | $this->addBackupCodeConfiguration($rootNode); 76 | $this->addEmailConfiguration($rootNode); 77 | $this->addGoogleAuthenticatorConfiguration($rootNode); 78 | $this->addTotpConfiguration($rootNode); 79 | } 80 | 81 | private function addBackupCodeConfiguration(ArrayNodeDefinition $rootNode): void 82 | { 83 | if (!interface_exists(BackupCodeInterface::class)) { 84 | return; 85 | } 86 | 87 | /** 88 | * @psalm-suppress UndefinedMethod 89 | * @psalm-suppress UndefinedInterfaceMethod 90 | * @psalm-suppress PossiblyNullReference 91 | */ 92 | $rootNode 93 | ->children() 94 | ->arrayNode('backup_codes') 95 | ->canBeEnabled() 96 | ->children() 97 | ->scalarNode('enabled')->defaultValue(false)->end() 98 | ->scalarNode('manager')->defaultValue('scheb_two_factor.default_backup_code_manager')->end() 99 | ->end() 100 | ->end() 101 | ->end(); 102 | } 103 | 104 | private function addTrustedDeviceConfiguration(ArrayNodeDefinition $rootNode): void 105 | { 106 | if (!interface_exists(TrustedDeviceInterface::class)) { 107 | return; 108 | } 109 | 110 | /** 111 | * @psalm-suppress UndefinedMethod 112 | * @psalm-suppress UndefinedInterfaceMethod 113 | * @psalm-suppress PossiblyNullReference 114 | */ 115 | $rootNode 116 | ->children() 117 | ->arrayNode('trusted_device') 118 | ->canBeEnabled() 119 | ->children() 120 | ->scalarNode('enabled')->defaultValue(false)->end() 121 | ->scalarNode('manager')->defaultValue('scheb_two_factor.default_trusted_device_manager')->end() 122 | ->integerNode('lifetime')->defaultValue(60 * 24 * 3600)->min(1)->end() 123 | ->booleanNode('extend_lifetime')->defaultFalse()->end() 124 | ->scalarNode('key')->defaultNull()->end() 125 | ->scalarNode('cookie_name')->defaultValue('trusted_device')->end() 126 | ->enumNode('cookie_secure')->values([true, false, 'auto'])->defaultValue('auto')->end() 127 | ->scalarNode('cookie_domain')->defaultNull()->end() 128 | ->scalarNode('cookie_path')->defaultValue('/')->end() 129 | ->scalarNode('cookie_same_site') 130 | ->defaultValue('lax') 131 | ->validate() 132 | ->ifNotInArray(['lax', 'strict', 'none', null]) 133 | ->thenInvalid('Invalid cookie same-site value %s, must be "lax", "strict" or null') 134 | ->end() 135 | ->end() 136 | ->end() 137 | ->end() 138 | ->end(); 139 | } 140 | 141 | private function addEmailConfiguration(ArrayNodeDefinition $rootNode): void 142 | { 143 | if (!interface_exists(EMailTwoFactorInterface::class)) { 144 | return; 145 | } 146 | 147 | /** 148 | * @psalm-suppress UndefinedMethod 149 | * @psalm-suppress UndefinedInterfaceMethod 150 | * @psalm-suppress PossiblyNullReference 151 | */ 152 | $rootNode 153 | ->children() 154 | ->arrayNode('email') 155 | ->canBeEnabled() 156 | ->children() 157 | ->scalarNode('enabled')->defaultValue(false)->end() 158 | ->scalarNode('mailer')->defaultNull()->end() 159 | ->scalarNode('code_generator')->defaultValue('scheb_two_factor.security.email.default_code_generator')->end() 160 | ->scalarNode('form_renderer')->defaultNull()->end() 161 | ->scalarNode('sender_email')->defaultNull()->end() 162 | ->scalarNode('sender_name')->defaultNull()->end() 163 | ->scalarNode('template')->defaultValue('@SchebTwoFactor/Authentication/form.html.twig')->end() 164 | ->integerNode('digits')->defaultValue(4)->min(1)->end() 165 | ->end() 166 | ->end() 167 | ->end(); 168 | } 169 | 170 | private function addTotpConfiguration(ArrayNodeDefinition $rootNode): void 171 | { 172 | if (!interface_exists(TotpTwoFactorInterface::class)) { 173 | return; 174 | } 175 | 176 | /** 177 | * @psalm-suppress UndefinedMethod 178 | * @psalm-suppress UndefinedInterfaceMethod 179 | * @psalm-suppress PossiblyNullReference 180 | */ 181 | $rootNode 182 | ->children() 183 | ->arrayNode('totp') 184 | ->canBeEnabled() 185 | ->children() 186 | ->scalarNode('enabled')->defaultValue(false)->end() 187 | ->scalarNode('form_renderer')->defaultNull()->end() 188 | ->scalarNode('issuer')->defaultNull()->end() 189 | ->scalarNode('server_name')->defaultNull()->end() 190 | ->integerNode('leeway')->defaultValue(0)->min(0)->end() 191 | ->arrayNode('parameters') 192 | ->scalarPrototype()->end() 193 | ->end() 194 | ->scalarNode('template')->defaultValue('@SchebTwoFactor/Authentication/form.html.twig')->end() 195 | ->end() 196 | ->end() 197 | ->end(); 198 | } 199 | 200 | private function addGoogleAuthenticatorConfiguration(ArrayNodeDefinition $rootNode): void 201 | { 202 | if (!interface_exists(GoogleTwoFactorInterface::class)) { 203 | return; 204 | } 205 | 206 | /** 207 | * @psalm-suppress UndefinedMethod 208 | * @psalm-suppress UndefinedInterfaceMethod 209 | * @psalm-suppress PossiblyNullReference 210 | */ 211 | $rootNode 212 | ->children() 213 | ->arrayNode('google') 214 | ->canBeEnabled() 215 | ->children() 216 | ->scalarNode('enabled')->defaultValue(false)->end() 217 | ->scalarNode('form_renderer')->defaultNull()->end() 218 | ->scalarNode('issuer')->defaultNull()->end() 219 | ->scalarNode('server_name')->defaultNull()->end() 220 | ->scalarNode('template')->defaultValue('@SchebTwoFactor/Authentication/form.html.twig')->end() 221 | ->integerNode('digits')->defaultValue(6)->min(1)->end() 222 | ->integerNode('leeway')->defaultValue(0)->min(0)->end() 223 | ->end() 224 | ->end() 225 | ->end(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /DependencyInjection/Factory/Security/TwoFactorFactory.php: -------------------------------------------------------------------------------- 1 | children() 75 | ->scalarNode('check_path')->defaultValue(self::DEFAULT_CHECK_PATH)->end() 76 | ->booleanNode('post_only')->defaultValue(self::DEFAULT_POST_ONLY)->end() 77 | ->scalarNode('auth_form_path')->defaultValue(self::DEFAULT_AUTH_FORM_PATH)->end() 78 | ->booleanNode('always_use_default_target_path')->defaultValue(self::DEFAULT_ALWAYS_USE_DEFAULT_TARGET_PATH)->end() 79 | ->scalarNode('default_target_path')->defaultValue(self::DEFAULT_TARGET_PATH)->end() 80 | ->scalarNode('success_handler')->defaultNull()->end() 81 | ->scalarNode('failure_handler')->defaultNull()->end() 82 | ->scalarNode('authentication_required_handler')->defaultNull()->end() 83 | ->scalarNode('auth_code_parameter_name')->defaultValue(self::DEFAULT_AUTH_CODE_PARAMETER_NAME)->end() 84 | ->scalarNode('trusted_parameter_name')->defaultValue(self::DEFAULT_TRUSTED_PARAMETER_NAME)->end() 85 | ->scalarNode('remember_me_sets_trusted')->defaultValue(self::DEFAULT_REMEMBER_ME_SETS_TRUSTED)->end() 86 | ->booleanNode('multi_factor')->defaultValue(self::DEFAULT_MULTI_FACTOR)->end() 87 | ->booleanNode('prepare_on_login')->defaultValue(self::DEFAULT_PREPARE_ON_LOGIN)->end() 88 | ->booleanNode('prepare_on_access_denied')->defaultValue(self::DEFAULT_PREPARE_ON_ACCESS_DENIED)->end() 89 | ->scalarNode('enable_csrf')->defaultValue(self::DEFAULT_ENABLE_CSRF)->end() 90 | ->scalarNode('csrf_parameter')->defaultValue(self::DEFAULT_CSRF_PARAMETER)->end() 91 | ->scalarNode('csrf_token_id')->defaultValue(self::DEFAULT_CSRF_TOKEN_ID)->end() 92 | ->scalarNode('csrf_header')->defaultNull()->end() 93 | ->scalarNode('csrf_token_manager')->defaultValue(self::DEFAULT_CSRF_TOKEN_MANAGER)->end() 94 | // Fake node for SecurityExtension, which requires a provider to be set when multiple user providers are registered 95 | ->scalarNode('provider')->defaultNull()->end() 96 | ->end(); 97 | } 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string 103 | { 104 | $twoFactorFirewallConfigId = $this->twoFactorServicesFactory->createTwoFactorFirewallConfig($container, $firewallName, $config); 105 | $successHandlerId = $this->twoFactorServicesFactory->createSuccessHandler($container, $firewallName, $config, $twoFactorFirewallConfigId); 106 | $failureHandlerId = $this->twoFactorServicesFactory->createFailureHandler($container, $firewallName, $config, $twoFactorFirewallConfigId); 107 | $authRequiredHandlerId = $this->twoFactorServicesFactory->createAuthenticationRequiredHandler($container, $firewallName, $config, $twoFactorFirewallConfigId); 108 | $this->twoFactorServicesFactory->createKernelExceptionListener($container, $firewallName, $authRequiredHandlerId); 109 | $this->twoFactorServicesFactory->createAccessListener($container, $firewallName, $twoFactorFirewallConfigId); 110 | $this->twoFactorServicesFactory->createFormListener($container, $firewallName, $twoFactorFirewallConfigId); 111 | $this->twoFactorServicesFactory->createProviderPreparationListener($container, $firewallName, $config); 112 | $this->createAuthenticationTokenCreatedListener($container, $firewallName); 113 | 114 | return $this->createAuthenticatorService( 115 | $container, 116 | $firewallName, 117 | $twoFactorFirewallConfigId, 118 | $successHandlerId, 119 | $failureHandlerId, 120 | $authRequiredHandlerId, 121 | ); 122 | } 123 | 124 | private function createAuthenticatorService( 125 | ContainerBuilder $container, 126 | string $firewallName, 127 | string $twoFactorFirewallConfigId, 128 | string $successHandlerId, 129 | string $failureHandlerId, 130 | string $authRequiredHandlerId, 131 | ): string { 132 | $authenticatorId = self::AUTHENTICATOR_ID_PREFIX.$firewallName; 133 | $container 134 | ->setDefinition($authenticatorId, new ChildDefinition(self::AUTHENTICATOR_DEFINITION_ID)) 135 | ->replaceArgument(0, new Reference($twoFactorFirewallConfigId)) 136 | ->replaceArgument(2, new Reference($successHandlerId)) 137 | ->replaceArgument(3, new Reference($failureHandlerId)) 138 | ->replaceArgument(4, new Reference($authRequiredHandlerId)); 139 | 140 | return $authenticatorId; 141 | } 142 | 143 | private function createAuthenticationTokenCreatedListener(ContainerBuilder $container, string $firewallName): void 144 | { 145 | $listenerId = self::AUTHENTICATION_TOKEN_CREATED_LISTENER_ID_PREFIX.$firewallName; 146 | $container 147 | ->setDefinition($listenerId, new ChildDefinition(self::AUTHENTICATION_TOKEN_CREATED_LISTENER_DEFINITION_ID)) 148 | ->replaceArgument(0, $firewallName) 149 | // Important: register event only for the specific firewall 150 | ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]); 151 | } 152 | 153 | /** 154 | * {@inheritDoc} 155 | */ 156 | public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array 157 | { 158 | $accessListenerId = self::KERNEL_ACCESS_LISTENER_ID_PREFIX.$firewallName; 159 | 160 | return [$accessListenerId]; 161 | } 162 | 163 | public function getPosition(): string 164 | { 165 | return 'form'; 166 | } 167 | 168 | public function getKey(): string 169 | { 170 | return self::AUTHENTICATION_PROVIDER_KEY; 171 | } 172 | 173 | public function getPriority(): int 174 | { 175 | return 0; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /DependencyInjection/Factory/Security/TwoFactorServicesFactory.php: -------------------------------------------------------------------------------- 1 | $config 20 | */ 21 | public function createSuccessHandler(ContainerBuilder $container, string $firewallName, array $config, string $twoFactorFirewallConfigId): string 22 | { 23 | if (isset($config['success_handler'])) { 24 | return $config['success_handler']; 25 | } 26 | 27 | $successHandlerId = TwoFactorFactory::SUCCESS_HANDLER_ID_PREFIX.$firewallName; 28 | $container 29 | ->setDefinition($successHandlerId, new ChildDefinition(TwoFactorFactory::SUCCESS_HANDLER_DEFINITION_ID)) 30 | ->replaceArgument(1, new Reference($twoFactorFirewallConfigId)); 31 | 32 | return $successHandlerId; 33 | } 34 | 35 | /** 36 | * @param array $config 37 | */ 38 | public function createFailureHandler(ContainerBuilder $container, string $firewallName, array $config, string $twoFactorFirewallConfigId): string 39 | { 40 | if (isset($config['failure_handler'])) { 41 | return $config['failure_handler']; 42 | } 43 | 44 | $failureHandlerId = TwoFactorFactory::FAILURE_HANDLER_ID_PREFIX.$firewallName; 45 | $container 46 | ->setDefinition($failureHandlerId, new ChildDefinition(TwoFactorFactory::FAILURE_HANDLER_DEFINITION_ID)) 47 | ->replaceArgument(1, new Reference($twoFactorFirewallConfigId)); 48 | 49 | return $failureHandlerId; 50 | } 51 | 52 | /** 53 | * @param array $config 54 | */ 55 | public function createAuthenticationRequiredHandler(ContainerBuilder $container, string $firewallName, array $config, string $twoFactorFirewallConfigId): string 56 | { 57 | if (isset($config['authentication_required_handler'])) { 58 | return $config['authentication_required_handler']; 59 | } 60 | 61 | $authenticationRequiredHandlerId = TwoFactorFactory::AUTHENTICATION_REQUIRED_HANDLER_ID_PREFIX.$firewallName; 62 | $container 63 | ->setDefinition($authenticationRequiredHandlerId, new ChildDefinition(TwoFactorFactory::AUTHENTICATION_REQUIRED_HANDLER_DEFINITION_ID)) 64 | ->replaceArgument(1, new Reference($twoFactorFirewallConfigId)); 65 | 66 | return $authenticationRequiredHandlerId; 67 | } 68 | 69 | /** 70 | * @param array $config 71 | */ 72 | public function getCsrfTokenManagerId(array $config): string 73 | { 74 | $csrfTokenManagerId = $config['csrf_token_manager'] ?? TwoFactorFactory::DEFAULT_CSRF_TOKEN_MANAGER; 75 | 76 | /** @psalm-suppress RiskyTruthyFalsyComparison */ 77 | return $config['enable_csrf'] ?? false 78 | ? $csrfTokenManagerId 79 | : 'scheb_two_factor.null_csrf_token_manager'; 80 | } 81 | 82 | /** 83 | * @param array $config 84 | */ 85 | public function createTwoFactorFirewallConfig(ContainerBuilder $container, string $firewallName, array $config): string 86 | { 87 | $firewallConfigId = TwoFactorFactory::FIREWALL_CONFIG_ID_PREFIX.$firewallName; 88 | $container 89 | ->setDefinition($firewallConfigId, new ChildDefinition(TwoFactorFactory::FIREWALL_CONFIG_DEFINITION_ID)) 90 | ->replaceArgument(0, $config) 91 | ->replaceArgument(1, $firewallName) 92 | // The SecurityFactory doesn't have access to the service definitions of the bundle. Therefore we tag the 93 | // definition so we can find it in a compiler pass and add to the the TwoFactorFirewallContext service. 94 | ->addTag('scheb_two_factor.firewall_config', ['firewall' => $firewallName]); 95 | 96 | return $firewallConfigId; 97 | } 98 | 99 | /** 100 | * @param array $config 101 | */ 102 | public function createProviderPreparationListener(ContainerBuilder $container, string $firewallName, array $config): void 103 | { 104 | $firewallConfigId = TwoFactorFactory::PROVIDER_PREPARATION_LISTENER_ID_PREFIX.$firewallName; 105 | $container 106 | ->setDefinition($firewallConfigId, new ChildDefinition(TwoFactorFactory::PROVIDER_PREPARATION_LISTENER_DEFINITION_ID)) 107 | ->replaceArgument(3, $firewallName) 108 | ->replaceArgument(4, $config['prepare_on_login'] ?? TwoFactorFactory::DEFAULT_PREPARE_ON_LOGIN) 109 | ->replaceArgument(5, $config['prepare_on_access_denied'] ?? TwoFactorFactory::DEFAULT_PREPARE_ON_ACCESS_DENIED) 110 | ->addTag('kernel.event_subscriber'); 111 | } 112 | 113 | public function createKernelExceptionListener(ContainerBuilder $container, string $firewallName, string $authRequiredHandlerId): void 114 | { 115 | $firewallConfigId = TwoFactorFactory::KERNEL_EXCEPTION_LISTENER_ID_PREFIX.$firewallName; 116 | $container 117 | ->setDefinition($firewallConfigId, new ChildDefinition(TwoFactorFactory::KERNEL_EXCEPTION_LISTENER_DEFINITION_ID)) 118 | ->replaceArgument(0, $firewallName) 119 | ->replaceArgument(2, new Reference($authRequiredHandlerId)) 120 | ->addTag('kernel.event_subscriber'); 121 | } 122 | 123 | public function createAccessListener(ContainerBuilder $container, string $firewallName, string $twoFactorFirewallConfigId): void 124 | { 125 | $firewallConfigId = TwoFactorFactory::KERNEL_ACCESS_LISTENER_ID_PREFIX.$firewallName; 126 | $container 127 | ->setDefinition($firewallConfigId, new ChildDefinition(TwoFactorFactory::KERNEL_ACCESS_LISTENER_DEFINITION_ID)) 128 | ->replaceArgument(0, new Reference($twoFactorFirewallConfigId)); 129 | } 130 | 131 | public function createFormListener(ContainerBuilder $container, string $firewallName, string $twoFactorFirewallConfigId): void 132 | { 133 | $firewallConfigId = TwoFactorFactory::FORM_LISTENER_ID_PREFIX.$firewallName; 134 | $container 135 | ->setDefinition($firewallConfigId, new ChildDefinition(TwoFactorFactory::FORM_LISTENER_DEFINITION_ID)) 136 | ->replaceArgument(0, new Reference($twoFactorFirewallConfigId)) 137 | ->addTag('kernel.event_subscriber'); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /DependencyInjection/SchebTwoFactorExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 30 | 31 | $container->setParameter('scheb_two_factor.model_manager_name', $config['model_manager_name']); 32 | $container->setParameter('scheb_two_factor.security_tokens', $config['security_tokens']); 33 | $container->setParameter('scheb_two_factor.ip_whitelist', $config['ip_whitelist']); 34 | 35 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 36 | $loader->load('security.php'); 37 | $loader->load('persistence.php'); 38 | $loader->load('two_factor.php'); 39 | 40 | // Load two-factor modules 41 | if (isset($config['email']['enabled']) && $this->resolveFeatureFlag($container, $config['email']['enabled'])) { 42 | $this->configureEmailAuthenticationProvider($container, $config); 43 | } 44 | 45 | if (isset($config['google']['enabled']) && $this->resolveFeatureFlag($container, $config['google']['enabled'])) { 46 | $this->configureGoogleAuthenticationProvider($container, $config); 47 | } 48 | 49 | if (isset($config['totp']['enabled']) && $this->resolveFeatureFlag($container, $config['totp']['enabled'])) { 50 | $this->configureTotpAuthenticationProvider($container, $config); 51 | } 52 | 53 | // Configure custom services 54 | $this->configurePersister($container, $config); 55 | $this->configureTwoFactorConditions($container, $config); 56 | $this->configureIpWhitelistProvider($container, $config); 57 | $this->configureTokenFactory($container, $config); 58 | $this->configureProviderDecider($container, $config); 59 | $this->configureCodeReuseCache($container, $config); 60 | 61 | if (isset($config['trusted_device']['enabled']) && $this->resolveFeatureFlag($container, $config['trusted_device']['enabled'])) { 62 | $this->configureTrustedDeviceManager($container, $config); 63 | } else { 64 | $container->setParameter('scheb_two_factor.trusted_device.enabled', false); 65 | } 66 | 67 | if (!isset($config['backup_codes']['enabled']) || !$this->resolveFeatureFlag($container, $config['backup_codes']['enabled'])) { 68 | return; 69 | } 70 | 71 | $this->configureBackupCodeManager($container, $config); 72 | } 73 | 74 | /** 75 | * @param array $config 76 | */ 77 | private function configurePersister(ContainerBuilder $container, array $config): void 78 | { 79 | $container->setAlias('scheb_two_factor.persister', $config['persister']); 80 | } 81 | 82 | /** 83 | * @param array $config 84 | */ 85 | private function configureTwoFactorConditions(ContainerBuilder $container, array $config): void 86 | { 87 | $conditions = [ 88 | new Reference('scheb_two_factor.authenticated_token_condition'), 89 | new Reference('scheb_two_factor.ip_whitelist_condition'), 90 | ]; 91 | 92 | // Custom two-factor condition 93 | if (null !== $config['two_factor_condition']) { 94 | $conditions[] = new Reference($config['two_factor_condition']); 95 | } 96 | 97 | $conditionRegistryDefinition = $container->getDefinition('scheb_two_factor.condition_registry'); 98 | $conditionRegistryDefinition->setArgument(0, new IteratorArgument($conditions)); 99 | } 100 | 101 | private function addTwoFactorCondition(ContainerBuilder $container, Reference $serviceReference): void 102 | { 103 | $conditionRegistryDefinition = $container->getDefinition('scheb_two_factor.condition_registry'); 104 | $conditionsIterator = $conditionRegistryDefinition->getArgument(0); 105 | assert($conditionsIterator instanceof IteratorArgument); 106 | $conditions = $conditionsIterator->getValues(); 107 | $conditions[] = $serviceReference; 108 | $conditionsIterator->setValues($conditions); 109 | } 110 | 111 | /** 112 | * @param array $config 113 | */ 114 | private function configureTrustedDeviceManager(ContainerBuilder $container, array $config): void 115 | { 116 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 117 | $loader->load('trusted_device.php'); 118 | $container->setAlias('scheb_two_factor.trusted_device_manager', $config['trusted_device']['manager']); 119 | 120 | $this->addTwoFactorCondition($container, new Reference('scheb_two_factor.trusted_device_condition')); 121 | 122 | if (null !== $config['trusted_device']['key']) { 123 | $jwtEncodeKey = $container->getDefinition('scheb_two_factor.trusted_jwt_encoder.configuration.key'); 124 | $jwtEncodeKey->setArgument(0, $config['trusted_device']['key']); 125 | } 126 | 127 | $container->setParameter('scheb_two_factor.trusted_device.enabled', $this->resolveFeatureFlag($container, $config['trusted_device']['enabled'])); 128 | $container->setParameter('scheb_two_factor.trusted_device.cookie_name', $config['trusted_device']['cookie_name']); 129 | $container->setParameter('scheb_two_factor.trusted_device.lifetime', $config['trusted_device']['lifetime']); 130 | $container->setParameter('scheb_two_factor.trusted_device.extend_lifetime', $config['trusted_device']['extend_lifetime']); 131 | $container->setParameter('scheb_two_factor.trusted_device.cookie_secure', 'auto' === $config['trusted_device']['cookie_secure'] ? null : $config['trusted_device']['cookie_secure']); 132 | $container->setParameter('scheb_two_factor.trusted_device.cookie_same_site', $config['trusted_device']['cookie_same_site']); 133 | $container->setParameter('scheb_two_factor.trusted_device.cookie_domain', $config['trusted_device']['cookie_domain']); 134 | $container->setParameter('scheb_two_factor.trusted_device.cookie_path', $config['trusted_device']['cookie_path']); 135 | } 136 | 137 | /** 138 | * @param array $config 139 | */ 140 | private function configureBackupCodeManager(ContainerBuilder $container, array $config): void 141 | { 142 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 143 | $loader->load('backup_codes.php'); 144 | $container->setAlias('scheb_two_factor.backup_code_manager', $config['backup_codes']['manager']); 145 | } 146 | 147 | /** 148 | * @param array $config 149 | */ 150 | private function configureIpWhitelistProvider(ContainerBuilder $container, array $config): void 151 | { 152 | $container->setAlias('scheb_two_factor.ip_whitelist_provider', $config['ip_whitelist_provider']); 153 | } 154 | 155 | /** 156 | * @param array $config 157 | */ 158 | private function configureTokenFactory(ContainerBuilder $container, array $config): void 159 | { 160 | $container->setAlias('scheb_two_factor.token_factory', $config['two_factor_token_factory']); 161 | } 162 | 163 | /** 164 | * @param array $config 165 | */ 166 | private function configureProviderDecider(ContainerBuilder $container, array $config): void 167 | { 168 | $container->setAlias('scheb_two_factor.provider_decider', $config['two_factor_provider_decider']); 169 | } 170 | 171 | /** 172 | * @param array $config 173 | */ 174 | private function configureEmailAuthenticationProvider(ContainerBuilder $container, array $config): void 175 | { 176 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 177 | $loader->load('two_factor_provider_email.php'); 178 | 179 | $container->setParameter('scheb_two_factor.email.sender_email', $config['email']['sender_email']); 180 | $container->setParameter('scheb_two_factor.email.sender_name', $config['email']['sender_name']); 181 | $container->setParameter('scheb_two_factor.email.template', $config['email']['template']); 182 | $container->setParameter('scheb_two_factor.email.digits', $config['email']['digits']); 183 | $container->setAlias('scheb_two_factor.security.email.code_generator', $config['email']['code_generator'])->setPublic(true); 184 | 185 | if (null !== $config['email']['mailer']) { 186 | $container->setAlias('scheb_two_factor.security.email.auth_code_mailer', $config['email']['mailer']); 187 | } 188 | 189 | if (null === $config['email']['form_renderer']) { 190 | return; 191 | } 192 | 193 | $container->setAlias('scheb_two_factor.security.email.form_renderer', $config['email']['form_renderer']); 194 | } 195 | 196 | /** 197 | * @param array $config 198 | */ 199 | private function configureGoogleAuthenticationProvider(ContainerBuilder $container, array $config): void 200 | { 201 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 202 | $loader->load('two_factor_provider_google.php'); 203 | 204 | $container->setParameter('scheb_two_factor.google.server_name', $config['google']['server_name']); 205 | $container->setParameter('scheb_two_factor.google.issuer', $config['google']['issuer']); 206 | $container->setParameter('scheb_two_factor.google.template', $config['google']['template']); 207 | $container->setParameter('scheb_two_factor.google.digits', $config['google']['digits']); 208 | $container->setParameter('scheb_two_factor.google.leeway', $config['google']['leeway']); 209 | 210 | if (null === $config['google']['form_renderer']) { 211 | return; 212 | } 213 | 214 | $container->setAlias('scheb_two_factor.security.google.form_renderer', $config['google']['form_renderer']); 215 | } 216 | 217 | /** 218 | * @param array $config 219 | */ 220 | private function configureTotpAuthenticationProvider(ContainerBuilder $container, array $config): void 221 | { 222 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 223 | $loader->load('two_factor_provider_totp.php'); 224 | 225 | $container->setParameter('scheb_two_factor.totp.issuer', $config['totp']['issuer']); 226 | $container->setParameter('scheb_two_factor.totp.server_name', $config['totp']['server_name']); 227 | $container->setParameter('scheb_two_factor.totp.parameters', $config['totp']['parameters']); 228 | $container->setParameter('scheb_two_factor.totp.template', $config['totp']['template']); 229 | $container->setParameter('scheb_two_factor.totp.leeway', $config['totp']['leeway']); 230 | 231 | if (null === $config['totp']['form_renderer']) { 232 | return; 233 | } 234 | 235 | $container->setAlias('scheb_two_factor.security.totp.form_renderer', $config['totp']['form_renderer']); 236 | } 237 | 238 | /** 239 | * @param array $config 240 | */ 241 | private function configureCodeReuseCache(ContainerBuilder $container, array $config): void 242 | { 243 | if (($config['code_reuse_cache'] ?? null) === null) { 244 | $config['code_reuse_cache'] = ''; 245 | } 246 | 247 | $container->setParameter('scheb_two_factor.code_reuse_cache_duration', $config['code_reuse_cache_duration']); 248 | $container->setAlias('scheb_two_factor.code_reuse_cache', $config['code_reuse_cache']); 249 | 250 | if (($config['code_reuse_default_handler'] ?? null) === null) { 251 | return; 252 | } 253 | 254 | // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed 255 | if ('scheb_two_factor.security.listener.throw_exception_on_two_factor_code_reuse' !== $config['code_reuse_default_handler']) { 256 | $container->removeDefinition('scheb_two_factor.security.listener.throw_exception_on_two_factor_code_reuse'); 257 | } 258 | } 259 | 260 | private function resolveFeatureFlag(ContainerBuilder $container, bool|string $value): bool 261 | { 262 | $retValue = $container->resolveEnvPlaceholders($value, true); 263 | 264 | if (is_bool($retValue)) { 265 | return $retValue; 266 | } 267 | 268 | if (is_string($retValue)) { 269 | $retValue = trim($retValue); 270 | 271 | if ('false' === $retValue || 'off' === $retValue) { 272 | return false; 273 | } 274 | 275 | if ('true' === $retValue || 'on' === $retValue) { 276 | return true; 277 | } 278 | } 279 | 280 | return (bool) $retValue; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Christian Scheb 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 | -------------------------------------------------------------------------------- /Model/Persister/DoctrinePersister.php: -------------------------------------------------------------------------------- 1 | om->persist($user); 22 | $this->om->flush(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Model/Persister/DoctrinePersisterFactory.php: -------------------------------------------------------------------------------- 1 | managerRegistry = $managerRegistry; 32 | } 33 | 34 | public function getPersister(): PersisterInterface 35 | { 36 | $objectManager = $this->managerRegistry->getManager($this->objectManagerName); 37 | 38 | return new DoctrinePersister($objectManager); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Model/PersisterInterface.php: -------------------------------------------------------------------------------- 1 | services() 13 | ->set('scheb_two_factor.default_backup_code_manager', BackupCodeManager::class) 14 | ->args([service('scheb_two_factor.persister')]) 15 | 16 | ->set('scheb_two_factor.null_backup_code_manager', NullBackupCodeManager::class) 17 | 18 | ->set('scheb_two_factor.security.listener.check_backup_code', CheckBackupCodeListener::class) 19 | ->tag('kernel.event_subscriber') 20 | ->args([ 21 | service('scheb_two_factor.provider_preparation_recorder'), 22 | service('scheb_two_factor.backup_code_manager'), 23 | service('event_dispatcher'), 24 | ]); 25 | }; 26 | -------------------------------------------------------------------------------- /Resources/config/persistence.php: -------------------------------------------------------------------------------- 1 | services() 13 | ->set('scheb_two_factor.persister_factory.doctrine', DoctrinePersisterFactory::class) 14 | ->args([ 15 | service('doctrine')->nullOnInvalid(), 16 | '%scheb_two_factor.model_manager_name%', 17 | ]) 18 | 19 | ->set('scheb_two_factor.persister.doctrine', DoctrinePersister::class) 20 | ->lazy(true) 21 | ->factory([service('scheb_two_factor.persister_factory.doctrine'), 'getPersister']) 22 | 23 | ->alias(PersisterInterface::class, 'scheb_two_factor.persister'); 24 | }; 25 | -------------------------------------------------------------------------------- /Resources/config/security.php: -------------------------------------------------------------------------------- 1 | services() 31 | ->set('scheb_two_factor.security.authenticator', TwoFactorAuthenticator::class) 32 | ->tag('monolog.logger', ['channel' => 'security']) 33 | ->args([ 34 | abstract_arg('Two-factor firewall config'), 35 | service('security.token_storage'), 36 | abstract_arg('Authentication success handler'), 37 | abstract_arg('Authentication failure handler'), 38 | abstract_arg('Authentication required handler'), 39 | service('event_dispatcher'), 40 | service('logger')->nullOnInvalid(), 41 | ]) 42 | 43 | ->set('scheb_two_factor.security.authentication.trust_resolver', AuthenticationTrustResolver::class) 44 | ->decorate('security.authentication.trust_resolver') 45 | ->args([service('scheb_two_factor.security.authentication.trust_resolver.inner')]) 46 | 47 | ->set('scheb_two_factor.security.access.authenticated_voter', TwoFactorInProgressVoter::class) 48 | ->tag('security.voter', ['priority' => 249]) 49 | 50 | ->set('scheb_two_factor.security.access.access_decider', TwoFactorAccessDecider::class) 51 | ->args([ 52 | service('security.access_map'), 53 | service('security.access.decision_manager'), 54 | service('security.http_utils'), 55 | service('security.logout_url_generator'), 56 | ]) 57 | 58 | ->set('scheb_two_factor.security.listener.token_created', AuthenticationTokenListener::class) 59 | ->args([ 60 | abstract_arg('Firewall name'), 61 | service('scheb_two_factor.condition_registry'), 62 | service('scheb_two_factor.provider_initiator'), 63 | service('scheb_two_factor.authentication_context_factory'), 64 | service('request_stack'), 65 | ]) 66 | 67 | ->set('scheb_two_factor.security.listener.check_two_factor_code', CheckTwoFactorCodeListener::class) 68 | ->tag('kernel.event_subscriber') 69 | ->args([ 70 | service('scheb_two_factor.provider_preparation_recorder'), 71 | service('scheb_two_factor.provider_registry'), 72 | service('event_dispatcher'), 73 | ]) 74 | ->set('scheb_two_factor.security.listener.check_two_factor_code_reuse', CheckTwoFactorCodeReuseListener::class) 75 | ->tag('kernel.event_subscriber') 76 | ->args([ 77 | service('event_dispatcher'), 78 | service('scheb_two_factor.code_reuse_cache')->nullOnInvalid(), 79 | '%scheb_two_factor.code_reuse_cache_duration%', 80 | service('logger')->nullOnInvalid(), 81 | ]) 82 | 83 | ->set('scheb_two_factor.security.listener.throw_exception_on_two_factor_code_reuse', ThrowExceptionOnTwoFactorCodeReuseListener::class) 84 | ->tag('kernel.event_subscriber') 85 | 86 | ->set('scheb_two_factor.security.listener.suppress_remember_me', SuppressRememberMeListener::class) 87 | ->tag('kernel.event_subscriber') 88 | 89 | ->set('scheb_two_factor.security.provider_preparation_listener', TwoFactorProviderPreparationListener::class) 90 | ->args([ 91 | service('scheb_two_factor.provider_registry'), 92 | service('scheb_two_factor.provider_preparation_recorder'), 93 | service('logger')->nullOnInvalid(), 94 | abstract_arg('Firewall name'), 95 | false, // Prepare on login setting 96 | false, // Prepare on access denied setting 97 | ]) 98 | 99 | ->set('scheb_two_factor.security.form_listener', TwoFactorFormListener::class) 100 | ->args([ 101 | abstract_arg('Two-factor firewall config'), 102 | service('security.token_storage'), 103 | service('event_dispatcher'), 104 | ]) 105 | 106 | ->set('scheb_two_factor.security.authentication_success_event_suppressor', AuthenticationSuccessEventSuppressor::class) 107 | ->tag('kernel.event_subscriber') 108 | 109 | ->set('scheb_two_factor.security.kernel_exception_listener', ExceptionListener::class) 110 | ->args([ 111 | abstract_arg('Firewall name'), 112 | service('security.token_storage'), 113 | abstract_arg('Authentication required handler'), 114 | service('event_dispatcher'), 115 | ]) 116 | 117 | ->set('scheb_two_factor.security.access_listener', TwoFactorAccessListener::class) 118 | ->args([ 119 | abstract_arg('Two-factor firewall config'), 120 | service('security.token_storage'), 121 | service('scheb_two_factor.security.access.access_decider'), 122 | ]) 123 | 124 | ->set('scheb_two_factor.security.authentication.success_handler', DefaultAuthenticationSuccessHandler::class) 125 | ->args([ 126 | service('security.http_utils'), 127 | abstract_arg('Two-factor firewall config'), 128 | ]) 129 | 130 | ->set('scheb_two_factor.security.authentication.failure_handler', DefaultAuthenticationFailureHandler::class) 131 | ->args([ 132 | service('security.http_utils'), 133 | abstract_arg('Two-factor firewall config'), 134 | ]) 135 | 136 | ->set('scheb_two_factor.security.authentication.authentication_required_handler', DefaultAuthenticationRequiredHandler::class) 137 | ->args([ 138 | service('security.http_utils'), 139 | abstract_arg('Two-factor firewall config'), 140 | ]) 141 | 142 | ->set('scheb_two_factor.null_csrf_token_manager', NullCsrfTokenManager::class) 143 | 144 | ->set('scheb_two_factor.security.firewall_config', TwoFactorFirewallConfig::class) 145 | ->args([ 146 | [], // Firewall settings 147 | abstract_arg('Firewall name'), 148 | service('security.http_utils'), 149 | service('scheb_two_factor.security.request_data_reader'), 150 | ]) 151 | 152 | ->set('scheb_two_factor.security.request_data_reader', RequestDataReader::class) 153 | 154 | ->alias('scheb_two_factor.csrf_token_manager', 'security.csrf.token_manager'); 155 | }; 156 | -------------------------------------------------------------------------------- /Resources/config/trusted_device.php: -------------------------------------------------------------------------------- 1 | services() 21 | ->set('scheb_two_factor.trusted_jwt_encoder.configuration.algorithm', Sha256::class) 22 | 23 | ->set('scheb_two_factor.trusted_jwt_encoder.configuration.key', InMemory::class) 24 | ->factory([InMemory::class, 'plainText']) 25 | ->args(['%kernel.secret%']) 26 | 27 | ->set('scheb_two_factor.trusted_jwt_encoder.configuration', Configuration::class) 28 | ->factory([Configuration::class, 'forSymmetricSigner']) 29 | ->args([ 30 | service('scheb_two_factor.trusted_jwt_encoder.configuration.algorithm'), 31 | service('scheb_two_factor.trusted_jwt_encoder.configuration.key'), 32 | ]) 33 | 34 | ->set('scheb_two_factor.trusted_jwt_encoder', JwtTokenEncoder::class) 35 | ->args([service('scheb_two_factor.trusted_jwt_encoder.configuration')]) 36 | 37 | ->set('scheb_two_factor.trusted_token_encoder', TrustedDeviceTokenEncoder::class) 38 | ->args([ 39 | service('scheb_two_factor.trusted_jwt_encoder'), 40 | '%scheb_two_factor.trusted_device.lifetime%', 41 | ]) 42 | 43 | ->set('scheb_two_factor.trusted_token_storage', TrustedDeviceTokenStorage::class) 44 | ->tag('kernel.reset', ['method' => 'reset']) 45 | ->lazy(true) 46 | ->args([ 47 | service('request_stack'), 48 | service('scheb_two_factor.trusted_token_encoder'), 49 | '%scheb_two_factor.trusted_device.cookie_name%', 50 | ]) 51 | 52 | ->set('scheb_two_factor.trusted_device_condition', TrustedDeviceCondition::class) 53 | ->lazy(true) 54 | ->args([ 55 | service('scheb_two_factor.trusted_device_manager'), 56 | '%scheb_two_factor.trusted_device.extend_lifetime%', 57 | ]) 58 | 59 | ->set('scheb_two_factor.trusted_cookie_response_listener', TrustedCookieResponseListener::class) 60 | ->tag('kernel.event_subscriber') 61 | ->lazy(true) 62 | ->args([ 63 | service('scheb_two_factor.trusted_token_storage'), 64 | '%scheb_two_factor.trusted_device.lifetime%', 65 | '%scheb_two_factor.trusted_device.cookie_name%', 66 | '%scheb_two_factor.trusted_device.cookie_secure%', 67 | '%scheb_two_factor.trusted_device.cookie_same_site%', 68 | '%scheb_two_factor.trusted_device.cookie_path%', 69 | '%scheb_two_factor.trusted_device.cookie_domain%', 70 | ]) 71 | 72 | ->set('scheb_two_factor.security.listener.trusted_device', TrustedDeviceListener::class) 73 | ->tag('kernel.event_subscriber') 74 | ->args([service('scheb_two_factor.trusted_device_manager')]) 75 | 76 | ->set('scheb_two_factor.default_trusted_device_manager', TrustedDeviceManager::class) 77 | ->args([service('scheb_two_factor.trusted_token_storage')]) 78 | 79 | ->set('scheb_two_factor.null_trusted_device_manager', NullTrustedDeviceManager::class); 80 | }; 81 | -------------------------------------------------------------------------------- /Resources/config/two_factor.php: -------------------------------------------------------------------------------- 1 | services() 26 | ->set('scheb_two_factor.provider_registry', TwoFactorProviderRegistry::class) 27 | ->args([ 28 | abstract_arg('Two-factor providers'), 29 | ]) 30 | 31 | ->set('scheb_two_factor.default_token_factory', TwoFactorTokenFactory::class) 32 | 33 | ->set('scheb_two_factor.default_provider_decider', TwoFactorProviderDecider::class) 34 | 35 | ->set('scheb_two_factor.authentication_context_factory', AuthenticationContextFactory::class) 36 | ->args([AuthenticationContext::class]) 37 | 38 | ->set('scheb_two_factor.condition_registry', TwoFactorConditionRegistry::class) 39 | ->lazy(true) 40 | ->args([ 41 | abstract_arg('Two-factor conditions'), 42 | ]) 43 | 44 | ->set('scheb_two_factor.authenticated_token_condition', AuthenticatedTokenCondition::class) 45 | ->lazy(true) 46 | ->args(['%scheb_two_factor.security_tokens%']) 47 | 48 | ->set('scheb_two_factor.ip_whitelist_condition', IpWhitelistCondition::class) 49 | ->lazy(true) 50 | ->args([ 51 | service('scheb_two_factor.ip_whitelist_provider'), 52 | ]) 53 | 54 | ->set('scheb_two_factor.default_ip_whitelist_provider', DefaultIpWhitelistProvider::class) 55 | ->args(['%scheb_two_factor.ip_whitelist%']) 56 | 57 | ->set('scheb_two_factor.provider_initiator', TwoFactorProviderInitiator::class) 58 | ->lazy(true) 59 | ->args([ 60 | service('scheb_two_factor.provider_registry'), 61 | service('scheb_two_factor.token_factory'), 62 | service('scheb_two_factor.provider_decider'), 63 | ]) 64 | 65 | ->set('scheb_two_factor.firewall_context', TwoFactorFirewallContext::class) 66 | ->public() 67 | ->args([abstract_arg('Firewall configs')]) 68 | 69 | ->set('scheb_two_factor.provider_preparation_recorder', TokenPreparationRecorder::class) 70 | ->args([service('security.token_storage')]) 71 | 72 | ->set('scheb_two_factor.form_controller', FormController::class) 73 | ->public() 74 | ->args([ 75 | service('security.token_storage'), 76 | service('scheb_two_factor.provider_registry'), 77 | service('scheb_two_factor.firewall_context'), 78 | service('security.logout_url_generator'), 79 | service('scheb_two_factor.trusted_device_manager')->nullOnInvalid(), 80 | '%scheb_two_factor.trusted_device.enabled%', 81 | ]) 82 | 83 | ->set('scheb_two_factor.security.form_renderer', DefaultTwoFactorFormRenderer::class) 84 | ->lazy(true) 85 | ->args([ 86 | service('twig'), 87 | '@SchebTwoFactor/Authentication/form.html.twig', 88 | ]) 89 | 90 | ->alias(TwoFactorFirewallContext::class, 'scheb_two_factor.firewall_context') 91 | 92 | ->alias(TwoFactorFormRendererInterface::class, 'scheb_two_factor.security.form_renderer'); 93 | }; 94 | -------------------------------------------------------------------------------- /Resources/config/two_factor_provider_email.php: -------------------------------------------------------------------------------- 1 | services() 15 | ->set('scheb_two_factor.security.email.symfony_auth_code_mailer', SymfonyAuthCodeMailer::class) 16 | ->args([ 17 | service('mailer.mailer'), 18 | '%scheb_two_factor.email.sender_email%', 19 | '%scheb_two_factor.email.sender_name%', 20 | ]) 21 | 22 | ->set('scheb_two_factor.security.email.default_code_generator', CodeGenerator::class) 23 | ->lazy(true) 24 | ->args([ 25 | service('scheb_two_factor.persister'), 26 | service('scheb_two_factor.security.email.auth_code_mailer'), 27 | '%scheb_two_factor.email.digits%', 28 | ]) 29 | 30 | ->set('scheb_two_factor.security.email.default_form_renderer', DefaultTwoFactorFormRenderer::class) 31 | ->lazy(true) 32 | ->args([ 33 | service('twig'), 34 | '%scheb_two_factor.email.template%', 35 | ]) 36 | 37 | ->set('scheb_two_factor.security.email.provider', EmailTwoFactorProvider::class) 38 | ->tag('scheb_two_factor.provider', ['alias' => 'email']) 39 | ->args([ 40 | service('scheb_two_factor.security.email.code_generator'), 41 | service('scheb_two_factor.security.email.form_renderer'), 42 | service('event_dispatcher'), 43 | ]) 44 | 45 | ->alias(CodeGeneratorInterface::class, 'scheb_two_factor.security.email.code_generator') 46 | 47 | ->alias('scheb_two_factor.security.email.form_renderer', 'scheb_two_factor.security.email.default_form_renderer'); 48 | }; 49 | -------------------------------------------------------------------------------- /Resources/config/two_factor_provider_google.php: -------------------------------------------------------------------------------- 1 | services() 16 | 17 | ->set('scheb_two_factor.security.google_totp_factory', GoogleTotpFactory::class) 18 | ->args([ 19 | '%scheb_two_factor.google.server_name%', 20 | '%scheb_two_factor.google.issuer%', 21 | '%scheb_two_factor.google.digits%', 22 | (new ReferenceConfigurator('clock'))->nullOnInvalid(), 23 | ]) 24 | 25 | ->set('scheb_two_factor.security.google_authenticator', GoogleAuthenticator::class) 26 | ->public() 27 | ->args([ 28 | service('scheb_two_factor.security.google_totp_factory'), 29 | service('event_dispatcher'), 30 | '%scheb_two_factor.google.leeway%', 31 | ]) 32 | 33 | ->set('scheb_two_factor.security.google.default_form_renderer', DefaultTwoFactorFormRenderer::class) 34 | ->lazy(true) 35 | ->args([ 36 | service('twig'), 37 | '%scheb_two_factor.google.template%', 38 | ]) 39 | 40 | ->set('scheb_two_factor.security.google.provider', GoogleAuthenticatorTwoFactorProvider::class) 41 | ->tag('scheb_two_factor.provider', ['alias' => 'google']) 42 | ->args([ 43 | service('scheb_two_factor.security.google_authenticator'), 44 | service('scheb_two_factor.security.google.form_renderer'), 45 | ]) 46 | 47 | ->alias('scheb_two_factor.security.google.form_renderer', 'scheb_two_factor.security.google.default_form_renderer') 48 | 49 | ->alias(GoogleAuthenticatorInterface::class, 'scheb_two_factor.security.google_authenticator') 50 | 51 | ->alias(GoogleAuthenticator::class, 'scheb_two_factor.security.google_authenticator'); 52 | }; 53 | -------------------------------------------------------------------------------- /Resources/config/two_factor_provider_totp.php: -------------------------------------------------------------------------------- 1 | services() 16 | 17 | ->set('scheb_two_factor.security.totp_factory', TotpFactory::class) 18 | ->public() 19 | ->args([ 20 | '%scheb_two_factor.totp.server_name%', 21 | '%scheb_two_factor.totp.issuer%', 22 | '%scheb_two_factor.totp.parameters%', 23 | (new ReferenceConfigurator('clock'))->nullOnInvalid(), 24 | ]) 25 | 26 | ->set('scheb_two_factor.security.totp_authenticator', TotpAuthenticator::class) 27 | ->public() 28 | ->args([ 29 | service('scheb_two_factor.security.totp_factory'), 30 | service('event_dispatcher'), 31 | '%scheb_two_factor.totp.leeway%', 32 | ]) 33 | 34 | ->set('scheb_two_factor.security.totp.default_form_renderer', DefaultTwoFactorFormRenderer::class) 35 | ->lazy(true) 36 | ->args([ 37 | service('twig'), 38 | '%scheb_two_factor.totp.template%', 39 | ]) 40 | 41 | ->set('scheb_two_factor.security.totp.provider', TotpAuthenticatorTwoFactorProvider::class) 42 | ->tag('scheb_two_factor.provider', ['alias' => 'totp']) 43 | ->args([ 44 | service('scheb_two_factor.security.totp_authenticator'), 45 | service('scheb_two_factor.security.totp.form_renderer'), 46 | ]) 47 | 48 | ->alias('scheb_two_factor.security.totp.form_renderer', 'scheb_two_factor.security.totp.default_form_renderer') 49 | 50 | ->alias(TotpAuthenticatorInterface::class, 'scheb_two_factor.security.totp_authenticator') 51 | 52 | ->alias(TotpAuthenticator::class, 'scheb_two_factor.security.totp_authenticator'); 53 | }; 54 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.cs.yml: -------------------------------------------------------------------------------- 1 | auth_code: Ověřovací kód 2 | login: Přihlásit 3 | code_invalid: Ověřovací kód není správný. 4 | trusted: Jsem na důvěryhodném počítači 5 | cancel: Storno 6 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.de.yml: -------------------------------------------------------------------------------- 1 | auth_code: Bestätigungs-Code 2 | choose_provider: Authentifizierungs-Methode wechseln 3 | login: Login 4 | code_invalid: Der Bestätigungs-Code ist nicht korrekt. 5 | trusted: Dies ist ein vertrauenswürdiger Computer 6 | cancel: Abbrechen 7 | code_reused: Dieser 2FA-Code wurde bereits benutzt. 8 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.en.yml: -------------------------------------------------------------------------------- 1 | auth_code: Authentication Code 2 | choose_provider: Switch authentication method 3 | login: Login 4 | code_invalid: The verification code is not valid. 5 | trusted: I'm on a trusted device 6 | cancel: Cancel 7 | code_reused: This 2FA code has already been used. 8 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.es.yml: -------------------------------------------------------------------------------- 1 | auth_code: Código de autenticación 2 | choose_provider: Cambiar el método de autenticación 3 | login: Ingresar 4 | code_invalid: El código es inválido. 5 | trusted: Estoy en un ordenador de confianza 6 | cancel: Cancelar 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.fr.yml: -------------------------------------------------------------------------------- 1 | auth_code: Code d'authentification 2 | choose_provider: Changer de méthode d'authentification 3 | login: Connexion 4 | code_invalid: Le code de vérification n'est pas valide. 5 | trusted: Je suis sur un appareil de confiance 6 | cancel: Annuler 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.hr.yml: -------------------------------------------------------------------------------- 1 | auth_code: Kôd za provjeru autentičnosti 2 | login: Prijava 3 | code_invalid: Kontrolni kôd nije važeći. 4 | trusted: Ja sam na pouzdanom računalu 5 | cancel: Otkazati 6 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.hu.yml: -------------------------------------------------------------------------------- 1 | auth_code: Authentikációs kód 2 | choose_provider: Authentikációs mód váltása 3 | login: Belépés 4 | code_invalid: A megerősítő kód helytelen. 5 | trusted: Ez egy megbízható számítógép 6 | cancel: Mégsem 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.id.yml: -------------------------------------------------------------------------------- 1 | auth_code: Kode Otentikasi 2 | choose_provider: Ganti metode otentikasi 3 | login: Login 4 | code_invalid: Kode verifikasi tidak valid. 5 | trusted: Saya ada di perangkat yang dipercaya 6 | cancel: Batalkan 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.nl.yml: -------------------------------------------------------------------------------- 1 | auth_code: Authenticatiecode 2 | choose_provider: Kies een andere authenticatiemethode 3 | login: Login 4 | code_invalid: De verificatiecode is niet geldig. 5 | trusted: Ik gebruik een vertrouwde computer 6 | cancel: Annuleren 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.pl.yml: -------------------------------------------------------------------------------- 1 | auth_code: Kod uwierzytelnienia 2 | choose_provider: Przełącz metodę uwierzytelniania 3 | login: Zaloguj 4 | code_invalid: Kod weryfikacyjny jest niepoprawny. 5 | trusted: Jestem na zaufanym komputerze 6 | cancel: Anuluj 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.ro.yml: -------------------------------------------------------------------------------- 1 | auth_code: Cod de autentificare 2 | login: Logare 3 | code_invalid: Codul de verificare nu este valid. 4 | trusted: Sunt pe un calculator de incredere 5 | cancel: Cancel 6 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.ru.yml: -------------------------------------------------------------------------------- 1 | auth_code: Код аутентификации 2 | login: Логин 3 | code_invalid: Неправильный код проверки 4 | trusted: Доверять этому компьютеру в дальнейшем 5 | cancel: Отменить 6 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.sk.yml: -------------------------------------------------------------------------------- 1 | auth_code: Overovací kód 2 | login: Prihlásiť 3 | code_invalid: Overovací kód nie je správny. 4 | trusted: Som na dôveryhodnom počítači 5 | cancel: Cancel 6 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.sv.yml: -------------------------------------------------------------------------------- 1 | auth_code: Inloggningskod 2 | choose_provider: Växla inloggningsmetod 3 | login: Logga in 4 | code_invalid: Inloggningskoden är felaktig 5 | trusted: Jag använder en pålitlig dator 6 | cancel: Avbryt 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.tr.yml: -------------------------------------------------------------------------------- 1 | auth_code: Kimlik Doğrulama Kodu 2 | choose_provider: Kimlik doğrulama yöntemini değiştir 3 | login: Giriş yap 4 | code_invalid: Doğrulama kodu geçerli değil. 5 | trusted: Bu cihaza güven. 6 | cancel: Vazgeç. 7 | -------------------------------------------------------------------------------- /Resources/translations/SchebTwoFactorBundle.uk.yml: -------------------------------------------------------------------------------- 1 | auth_code: Код аутентифікації 2 | login: Логін 3 | code_invalid: Неправильний код перевірки 4 | trusted: Довіряти цьому комп’ютеру надалі 5 | cancel: Відмінити 6 | -------------------------------------------------------------------------------- /Resources/views/Authentication/form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This is a demo template for the authentication form. Please consider overwriting this with your own template, 3 | especially when you're using different route names than the ones used here. 4 | #} 5 | 6 | {# Authentication errors #} 7 | {% if authenticationError %} 8 |

{{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}

9 | {% endif %} 10 | 11 | {# Let the user select the authentication method #} 12 | {% if availableTwoFactorProviders|length > 1 %} 13 |

{{ "choose_provider"|trans({}, 'SchebTwoFactorBundle') }}: 14 | {% for provider in availableTwoFactorProviders %} 15 | {{ provider }} 16 | {% endfor %} 17 |

18 | {% endif %} 19 | 20 | {# Display current two-factor provider #} 21 |

22 | 23 |
24 |

25 | 38 |

39 | 40 | {% if displayTrustedOption %} 41 |

42 | {% endif %} 43 | {% if isCsrfProtectionEnabled %} 44 | 45 | {% endif %} 46 |

47 |
48 | 49 | {# The logout link gives the user a way out if they can't complete two-factor authentication #} 50 |

{{ "cancel"|trans({}, 'SchebTwoFactorBundle') }}

51 | -------------------------------------------------------------------------------- /SchebTwoFactorBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new TwoFactorProviderCompilerPass()); 27 | $container->addCompilerPass(new TwoFactorFirewallConfigCompilerPass()); 28 | $container->addCompilerPass(new MailerCompilerPass()); 29 | 30 | $extension = $container->getExtension('security'); 31 | assert($extension instanceof SecurityExtension); 32 | 33 | $securityFactory = new TwoFactorFactory(new TwoFactorServicesFactory()); 34 | $extension->addAuthenticatorFactory($securityFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Security/Authentication/AuthenticationTrustResolver.php: -------------------------------------------------------------------------------- 1 | decoratedTrustResolver->isRememberMe($token); 23 | } 24 | 25 | public function isFullFledged(TokenInterface|null $token = null): bool 26 | { 27 | return !$this->isTwoFactorToken($token) && $this->decoratedTrustResolver->isFullFledged($token); 28 | } 29 | 30 | public function isAuthenticated(TokenInterface|null $token = null): bool 31 | { 32 | return $this->decoratedTrustResolver->isAuthenticated($token); 33 | } 34 | 35 | private function isTwoFactorToken(TokenInterface|null $token): bool 36 | { 37 | return $token instanceof TwoFactorTokenInterface; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Security/Authentication/Exception/InvalidTwoFactorCodeException.php: -------------------------------------------------------------------------------- 1 | provider; 27 | } 28 | 29 | public function setProvider(string $provider): void 30 | { 31 | $this->provider = $provider; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getMessageData(): array 38 | { 39 | return ['{{ provider }}' => $this->provider]; 40 | } 41 | 42 | /** 43 | * @return mixed[] 44 | */ 45 | public function __serialize(): array 46 | { 47 | return [$this->provider, parent::__serialize()]; 48 | } 49 | 50 | /** 51 | * @param mixed[] $data 52 | */ 53 | public function __unserialize(array $data): void 54 | { 55 | [$this->provider, $parentData] = $data; 56 | 57 | parent::__unserialize($parentData); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Security/Authentication/Token/TwoFactorToken.php: -------------------------------------------------------------------------------- 1 | */ 27 | private array $attributes = []; 28 | 29 | /** @var bool[] */ 30 | private array $preparedProviders = []; 31 | 32 | /** 33 | * @param string[] $twoFactorProviders 34 | */ 35 | public function __construct( 36 | private TokenInterface $authenticatedToken, 37 | private string|null $credentials, 38 | private string $firewallName, 39 | private array $twoFactorProviders, 40 | ) { 41 | if (null === $authenticatedToken->getUser()) { 42 | throw new InvalidArgumentException('The authenticated token must have a user object set.'); 43 | } 44 | } 45 | 46 | public function getUser(): UserInterface 47 | { 48 | $user = $this->authenticatedToken->getUser(); 49 | if (null === $user) { 50 | throw new RuntimeException('The authenticated token must have a user object set, though null was returned.'); 51 | } 52 | 53 | return $user; 54 | } 55 | 56 | public function setUser(UserInterface $user): void 57 | { 58 | $this->authenticatedToken->setUser($user); 59 | } 60 | 61 | public function getUserIdentifier(): string 62 | { 63 | return $this->authenticatedToken->getUserIdentifier(); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function getRoleNames(): array 70 | { 71 | return []; 72 | } 73 | 74 | public function createWithCredentials(string $credentials): TwoFactorTokenInterface 75 | { 76 | $credentialsToken = new self($this->authenticatedToken, $credentials, $this->firewallName, $this->twoFactorProviders); 77 | foreach (array_keys($this->preparedProviders) as $preparedProviderName) { 78 | $credentialsToken->setTwoFactorProviderPrepared($preparedProviderName); 79 | } 80 | 81 | $credentialsToken->setAttributes($this->getAttributes()); 82 | 83 | return $credentialsToken; 84 | } 85 | 86 | public function getCredentials(): string|null 87 | { 88 | return $this->credentials; 89 | } 90 | 91 | public function eraseCredentials(): void 92 | { 93 | $this->credentials = null; 94 | } 95 | 96 | public function getAuthenticatedToken(): TokenInterface 97 | { 98 | return $this->authenticatedToken; 99 | } 100 | 101 | /** 102 | * {@inheritDoc} 103 | */ 104 | public function getTwoFactorProviders(): array 105 | { 106 | return $this->twoFactorProviders; 107 | } 108 | 109 | public function preferTwoFactorProvider(string $preferredProvider): void 110 | { 111 | $this->removeTwoFactorProvider($preferredProvider); 112 | array_unshift($this->twoFactorProviders, $preferredProvider); 113 | } 114 | 115 | public function getCurrentTwoFactorProvider(): string|null 116 | { 117 | $first = reset($this->twoFactorProviders); 118 | 119 | return false !== $first ? $first : null; 120 | } 121 | 122 | public function isTwoFactorProviderPrepared(string $providerName): bool 123 | { 124 | return $this->preparedProviders[$providerName] ?? false; 125 | } 126 | 127 | public function setTwoFactorProviderPrepared(string $providerName): void 128 | { 129 | $this->preparedProviders[$providerName] = true; 130 | } 131 | 132 | public function setTwoFactorProviderComplete(string $providerName): void 133 | { 134 | if (!$this->isTwoFactorProviderPrepared($providerName)) { 135 | throw new LogicException(sprintf('Two-factor provider "%s" cannot be completed because it was not prepared.', $providerName)); 136 | } 137 | 138 | $this->removeTwoFactorProvider($providerName); 139 | } 140 | 141 | private function removeTwoFactorProvider(string $providerName): void 142 | { 143 | $key = array_search($providerName, $this->twoFactorProviders, true); 144 | if (false === $key) { 145 | throw new UnknownTwoFactorProviderException(sprintf('Two-factor provider "%s" is not active.', $providerName)); 146 | } 147 | 148 | unset($this->twoFactorProviders[$key]); 149 | } 150 | 151 | public function allTwoFactorProvidersAuthenticated(): bool 152 | { 153 | return 0 === count($this->twoFactorProviders); 154 | } 155 | 156 | public function getFirewallName(): string 157 | { 158 | return $this->firewallName; 159 | } 160 | 161 | /** 162 | * @return mixed[] 163 | */ 164 | public function __serialize(): array 165 | { 166 | return [ 167 | $this->authenticatedToken, 168 | $this->credentials, 169 | $this->firewallName, 170 | $this->attributes, 171 | $this->twoFactorProviders, 172 | $this->preparedProviders, 173 | ]; 174 | } 175 | 176 | /** 177 | * @param mixed[] $data 178 | */ 179 | public function __unserialize(array $data): void 180 | { 181 | [ 182 | $this->authenticatedToken, 183 | $this->credentials, 184 | $this->firewallName, 185 | $this->attributes, 186 | $this->twoFactorProviders, 187 | $this->preparedProviders, 188 | ] = $data; 189 | } 190 | 191 | /** 192 | * @return array 193 | */ 194 | public function getAttributes(): array 195 | { 196 | return $this->attributes; 197 | } 198 | 199 | /** 200 | * @param array $attributes 201 | */ 202 | public function setAttributes(array $attributes): void 203 | { 204 | $this->attributes = $attributes; 205 | } 206 | 207 | public function hasAttribute(string $name): bool 208 | { 209 | return array_key_exists($name, $this->attributes); 210 | } 211 | 212 | public function getAttribute(string $name): mixed 213 | { 214 | if (!array_key_exists($name, $this->attributes)) { 215 | throw new InvalidArgumentException(sprintf('This token has no "%s" attribute.', $name)); 216 | } 217 | 218 | return $this->attributes[$name]; 219 | } 220 | 221 | public function setAttribute(string $name, mixed $value): void 222 | { 223 | $this->attributes[$name] = $value; 224 | } 225 | 226 | public function __toString(): string 227 | { 228 | return $this->getUserIdentifier(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Security/Authentication/Token/TwoFactorTokenFactory.php: -------------------------------------------------------------------------------- 1 | accessMap->getPatterns($request); 34 | 35 | return $this->isPubliclyAccessAttribute($attributes); 36 | } 37 | 38 | public function isAccessible(Request $request, TokenInterface $token): bool 39 | { 40 | [$attributes] = $this->accessMap->getPatterns($request); 41 | if ($this->isPubliclyAccessAttribute($attributes)) { 42 | return true; 43 | } 44 | 45 | // Let routes pass, e.g. if a route needs to be callable during two-factor authentication 46 | // Originally compatibility for Symfony < 6.0, true flag to support multiple attributes 47 | // Still needed for compatibility with Symfony 7 48 | /** @psalm-suppress TooManyArguments */ 49 | if (null !== $attributes && $this->accessDecisionManager->decide($token, $attributes, $request, true)) { 50 | return true; 51 | } 52 | 53 | // Compatibility for Symfony < 7.0 54 | // This block of code ensures requests to the logout route can pass. 55 | // The bundle's TwoFactorAccessListener prioritized after the LogoutListener. Though the Firewall class is still 56 | // sorting the LogoutListener in programmatically. When a lazy firewall is used, the LogoutListener is executed 57 | // last, because all other listeners are encapsulated into LazyFirewallContext, which is invoked first. 58 | $logoutPath = $this->removeQueryParameters( 59 | $this->makeRelativeToBaseUrl($this->logoutUrlGenerator->getLogoutPath(), $request), 60 | ); 61 | 62 | return $this->httpUtils->checkRequestPath($request, $logoutPath); // Let the logout route pass 63 | } 64 | 65 | private function isPubliclyAccessAttribute(array|null $attributes): bool 66 | { 67 | if (null === $attributes) { 68 | // No access control at all is treated "non-public" by 2fa 69 | return false; 70 | } 71 | 72 | return [AuthenticatedVoter::PUBLIC_ACCESS] === $attributes; 73 | } 74 | 75 | private function makeRelativeToBaseUrl(string $logoutPath, Request $request): string 76 | { 77 | $baseUrl = $request->getBaseUrl(); 78 | if (0 === strlen($baseUrl)) { 79 | return $logoutPath; 80 | } 81 | 82 | $pathInfo = substr($logoutPath, strlen($baseUrl)); 83 | if ('' === $pathInfo) { 84 | return '/'; 85 | } 86 | 87 | return $pathInfo; 88 | } 89 | 90 | private function removeQueryParameters(string $path): string 91 | { 92 | $queryPos = strpos($path, '?'); 93 | if (false !== $queryPos) { 94 | $path = substr($path, 0, $queryPos); 95 | } 96 | 97 | return $path; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Security/Authorization/Voter/TwoFactorInProgressVoter.php: -------------------------------------------------------------------------------- 1 | getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception); 29 | 30 | return $this->httpUtils->createRedirectResponse($request, $this->config->getAuthFormPath()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Security/Http/Authentication/DefaultAuthenticationRequiredHandler.php: -------------------------------------------------------------------------------- 1 | config->isCheckPathRequest($request) && $request->hasSession() && $request->isMethodSafe() && !$request->isXmlHttpRequest()) { 32 | $this->saveTargetPath($request->getSession(), $this->config->getFirewallName(), $request->getUri()); 33 | } 34 | 35 | return $this->httpUtils->createRedirectResponse($request, $this->config->getAuthFormPath()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php: -------------------------------------------------------------------------------- 1 | getSession()->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR); 32 | 33 | return $this->httpUtils->createRedirectResponse($request, $this->determineRedirectTargetUrl($request)); 34 | } 35 | 36 | private function determineRedirectTargetUrl(Request $request): string 37 | { 38 | if ($this->config->isAlwaysUseDefaultTargetPath()) { 39 | return $this->config->getDefaultTargetPath(); 40 | } 41 | 42 | $session = $request->getSession(); 43 | $firewallName = $this->config->getFirewallName(); 44 | $targetUrl = $this->getTargetPath($session, $firewallName); 45 | if (null !== $targetUrl) { 46 | $this->removeTargetPath($session, $firewallName); 47 | 48 | return $targetUrl; 49 | } 50 | 51 | return $this->config->getDefaultTargetPath(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Security/Http/Authenticator/Passport/Credentials/TwoFactorCodeCredentials.php: -------------------------------------------------------------------------------- 1 | twoFactorToken; 27 | } 28 | 29 | public function getCode(): string 30 | { 31 | if (null === $this->code) { 32 | throw new LogicException('The credentials are erased as another listener already verified these credentials.'); 33 | } 34 | 35 | return $this->code; 36 | } 37 | 38 | public function markResolved(): void 39 | { 40 | $this->resolved = true; 41 | $this->code = null; 42 | } 43 | 44 | public function isResolved(): bool 45 | { 46 | return $this->resolved; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Security/Http/Authenticator/TwoFactorAuthenticator.php: -------------------------------------------------------------------------------- 1 | logger = $logger ?? new NullLogger(); 54 | } 55 | 56 | public function supports(Request $request): bool|null 57 | { 58 | return $this->twoFactorFirewallConfig->isCheckPathRequest($request); 59 | } 60 | 61 | public function authenticate(Request $request): Passport 62 | { 63 | // When the firewall is lazy, the token is not initialized in the "supports" stage, so this check does only work 64 | // within the "authenticate" stage. 65 | $currentToken = $this->tokenStorage->getToken(); 66 | if (!($currentToken instanceof TwoFactorTokenInterface)) { 67 | // This should only happen when the check path is called outside of a 2fa process 68 | // access_control can't handle this, as it's called after the authenticator 69 | throw new AccessDeniedException('User is not in a two-factor authentication process.'); 70 | } 71 | 72 | $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::ATTEMPT, $request, $currentToken); 73 | 74 | $credentials = new TwoFactorCodeCredentials($currentToken, $this->twoFactorFirewallConfig->getAuthCodeFromRequest($request)); 75 | $userLoader = static function () use ($currentToken): UserInterface { 76 | return $currentToken->getUser(); 77 | }; 78 | $userBadge = new UserBadge($currentToken->getUserIdentifier(), $userLoader); 79 | $passport = new Passport($userBadge, $credentials, []); 80 | if ($currentToken->hasAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_USE_REMEMBER_ME)) { 81 | $rememberMeBadge = new RememberMeBadge(); 82 | $rememberMeBadge->enable(); 83 | $passport->addBadge($rememberMeBadge); 84 | } 85 | 86 | if ($this->twoFactorFirewallConfig->isCsrfProtectionEnabled()) { 87 | $tokenValue = $this->twoFactorFirewallConfig->getCsrfTokenFromRequest($request); 88 | $tokenId = $this->twoFactorFirewallConfig->getCsrfTokenId(); 89 | $passport->addBadge(new CsrfTokenBadge($tokenId, $tokenValue)); 90 | } 91 | 92 | // Make sure the trusted device package is installed 93 | if (class_exists(TrustedDeviceBadge::class) && $this->shouldSetTrustedDevice($request, $passport)) { 94 | $passport->addBadge(new TrustedDeviceBadge()); 95 | } 96 | 97 | return $passport; 98 | } 99 | 100 | private function shouldSetTrustedDevice(Request $request, Passport $passport): bool 101 | { 102 | return $this->twoFactorFirewallConfig->hasTrustedDeviceParameterInRequest($request) 103 | || ( 104 | $this->twoFactorFirewallConfig->isRememberMeSetsTrusted() 105 | && $passport->hasBadge(RememberMeBadge::class) 106 | ); 107 | } 108 | 109 | public function createToken(Passport $passport, string $firewallName): TokenInterface 110 | { 111 | $credentialsBadge = $passport->getBadge(TwoFactorCodeCredentials::class); 112 | assert($credentialsBadge instanceof TwoFactorCodeCredentials); 113 | $twoFactorToken = $credentialsBadge->getTwoFactorToken(); 114 | 115 | if ($this->isAuthenticationComplete($twoFactorToken)) { 116 | $authenticatedToken = $twoFactorToken->getAuthenticatedToken(); // Authentication complete, unwrap the token 117 | $authenticatedToken->setAttribute(self::FLAG_2FA_COMPLETE, true); 118 | 119 | return $authenticatedToken; 120 | } 121 | 122 | return $twoFactorToken; 123 | } 124 | 125 | private function isAuthenticationComplete(TwoFactorTokenInterface $token): bool 126 | { 127 | return !$this->twoFactorFirewallConfig->isMultiFactor() || $token->allTwoFactorProvidersAuthenticated(); 128 | } 129 | 130 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): Response|null 131 | { 132 | $this->logger->info('User has been two-factor authenticated successfully.', ['username' => $token->getUserIdentifier()]); 133 | $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::SUCCESS, $request, $token); 134 | 135 | // When it's still a TwoFactorTokenInterface, keep showing the auth form 136 | if ($token instanceof TwoFactorTokenInterface) { 137 | $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::REQUIRE, $request, $token); 138 | 139 | return $this->authenticationRequiredHandler->onAuthenticationRequired($request, $token); 140 | } 141 | 142 | $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::COMPLETE, $request, $token); 143 | 144 | return $this->successHandler->onAuthenticationSuccess($request, $token); 145 | } 146 | 147 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response|null 148 | { 149 | $currentToken = $this->tokenStorage->getToken(); 150 | assert($currentToken instanceof TwoFactorTokenInterface); 151 | $this->logger->info('Two-factor authentication request failed.', ['exception' => $exception]); 152 | $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::FAILURE, $request, $currentToken); 153 | 154 | return $this->failureHandler->onAuthenticationFailure($request, $exception); 155 | } 156 | 157 | private function dispatchTwoFactorAuthenticationEvent(string $eventType, Request $request, TokenInterface $token): void 158 | { 159 | $event = new TwoFactorAuthenticationEvent($request, $token); 160 | $this->eventDispatcher->dispatch($event, $eventType); 161 | } 162 | 163 | public function isInteractive(): bool 164 | { 165 | return true; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Security/Http/EventListener/AbstractCheckCodeListener.php: -------------------------------------------------------------------------------- 1 | getPassport(); 27 | if (!$passport->hasBadge(TwoFactorCodeCredentials::class)) { 28 | return; 29 | } 30 | 31 | $credentialsBadge = $passport->getBadge(TwoFactorCodeCredentials::class); 32 | assert($credentialsBadge instanceof TwoFactorCodeCredentials); 33 | if ($credentialsBadge->isResolved()) { 34 | return; 35 | } 36 | 37 | $credentialsBadge = $passport->getBadge(TwoFactorCodeCredentials::class); 38 | assert($credentialsBadge instanceof TwoFactorCodeCredentials); 39 | $token = $credentialsBadge->getTwoFactorToken(); 40 | $providerName = $token->getCurrentTwoFactorProvider(); 41 | if (null === $providerName || !$providerName) { 42 | throw new AuthenticationException('There is no active two-factor provider.'); 43 | } 44 | 45 | if (!$this->preparationRecorder->isTwoFactorProviderPrepared($token->getFirewallName(), $providerName)) { 46 | throw new AuthenticationException(sprintf('The two-factor provider "%s" has not been prepared.', $providerName)); 47 | } 48 | 49 | if (!$this->isValidCode($providerName, $token->getUser(), $credentialsBadge->getCode())) { 50 | return; 51 | } 52 | 53 | $token->setTwoFactorProviderComplete($providerName); 54 | $credentialsBadge->markResolved(); 55 | } 56 | 57 | abstract protected function isValidCode(string $providerName, object $user, string $code): bool; 58 | } 59 | -------------------------------------------------------------------------------- /Security/Http/EventListener/CheckTwoFactorCodeListener.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch( 35 | new TwoFactorCodeEvent($user, $code), 36 | TwoFactorAuthenticationEvents::CHECK, 37 | ); 38 | 39 | try { 40 | $authenticationProvider = $this->providerRegistry->getProvider($providerName); 41 | } catch (InvalidArgumentException) { 42 | $exception = new TwoFactorProviderNotFoundException('Two-factor provider "'.$providerName.'" not found.'); 43 | $exception->setProvider($providerName); 44 | 45 | throw $exception; 46 | } 47 | 48 | if (!$authenticationProvider->validateAuthenticationCode($user, $code)) { 49 | throw new InvalidTwoFactorCodeException(InvalidTwoFactorCodeException::MESSAGE); 50 | } 51 | 52 | return true; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public static function getSubscribedEvents(): array 59 | { 60 | return [CheckPassportEvent::class => ['checkPassport', self::LISTENER_PRIORITY]]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Security/Http/EventListener/CheckTwoFactorCodeReuseListener.php: -------------------------------------------------------------------------------- 1 | cache) { 34 | return; 35 | } 36 | 37 | if (!$this->cache instanceof CacheItemPoolInterface) { 38 | if ($this->logger instanceof LoggerInterface) { 39 | $this->logger->error('Your reuse-cache seems to be configured wrongly! Provide a CacheItemPoolInterface as the cache object if you want to disallow reusing 2FA-codes!'); 40 | } 41 | 42 | return; 43 | } 44 | 45 | $cacheKey = 'scheb_two_factor_code_reuse.' 46 | .sha1($event->getUser()->getUserIdentifier().'.'.$event->getCode()); 47 | 48 | $cacheItem = $this->cache->getItem($cacheKey); 49 | $cacheItem->expiresAfter($this->cacheDuration); 50 | $cacheItem->set(true); 51 | $this->cache->save($cacheItem); 52 | 53 | // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed 54 | if ($cacheItem->isHit()) { 55 | $this->eventDispatcher->dispatch( 56 | new TwoFactorCodeReusedEvent($event->getUser(), $event->getCode()), 57 | TwoFactorAuthenticationEvents::CODE_REUSED, 58 | ); 59 | } 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public static function getSubscribedEvents(): array 66 | { 67 | return [TwoFactorAuthenticationEvents::CHECK => ['checkForCodeReuse', self::LISTENER_PRIORITY]]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Security/Http/EventListener/SuppressRememberMeListener.php: -------------------------------------------------------------------------------- 1 | getPassport(); 24 | if (!$passport->hasBadge(RememberMeBadge::class)) { 25 | return; 26 | } 27 | 28 | $rememberMeBadge = $passport->getBadge(RememberMeBadge::class); 29 | assert($rememberMeBadge instanceof RememberMeBadge); 30 | if (!$rememberMeBadge->isEnabled()) { 31 | return; // User did not request a remember-me cookie 32 | } 33 | 34 | $token = $event->getAuthenticatedToken(); 35 | if (!($token instanceof TwoFactorTokenInterface)) { 36 | return; // We're not in a 2fa process 37 | } 38 | 39 | // Disable remember-me cookie 40 | $rememberMeBadge->disable(); 41 | $token->setAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_USE_REMEMBER_ME, true); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public static function getSubscribedEvents(): array 48 | { 49 | return [LoginSuccessEvent::class => ['onSuccessfulLogin', self::PRIORITY]]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Security/Http/EventListener/ThrowExceptionOnTwoFactorCodeReuseListener.php: -------------------------------------------------------------------------------- 1 | ['handle', self::LISTENER_PRIORITY]]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/Http/Firewall/ExceptionListener.php: -------------------------------------------------------------------------------- 1 | getThrowable(); 37 | do { 38 | if ($exception instanceof AccessDeniedException) { 39 | $this->handleAccessDeniedException($event); 40 | 41 | return; 42 | } 43 | 44 | $exception = $exception->getPrevious(); 45 | } while (null !== $exception); 46 | } 47 | 48 | private function handleAccessDeniedException(ExceptionEvent $exceptionEvent): void 49 | { 50 | $token = $this->tokenStorage->getToken(); 51 | if (!($token instanceof TwoFactorTokenInterface)) { 52 | return; 53 | } 54 | 55 | if ($token->getFirewallName() !== $this->firewallName) { 56 | return; 57 | } 58 | 59 | $request = $exceptionEvent->getRequest(); 60 | $event = new TwoFactorAuthenticationEvent($request, $token); 61 | $this->eventDispatcher->dispatch($event, TwoFactorAuthenticationEvents::REQUIRE); 62 | 63 | $response = $this->authenticationRequiredHandler->onAuthenticationRequired($request, $token); 64 | $exceptionEvent->allowCustomResponseCode(); 65 | $exceptionEvent->setResponse($response); 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | public static function getSubscribedEvents(): array 72 | { 73 | return [ 74 | KernelEvents::EXCEPTION => ['onKernelException', self::LISTENER_PRIORITY], 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Security/Http/Firewall/TwoFactorAccessListener.php: -------------------------------------------------------------------------------- 1 | twoFactorAccessDecider->isPubliclyAccessible($request); 36 | } 37 | 38 | public function authenticate(RequestEvent $event): void 39 | { 40 | $request = $event->getRequest(); 41 | if ($this->twoFactorFirewallConfig->isCheckPathRequest($request)) { 42 | return; 43 | } 44 | 45 | if ($this->twoFactorFirewallConfig->isAuthFormRequest($request)) { 46 | return; 47 | } 48 | 49 | // When the firewall is lazy, the token is not initialized in the "supports" stage, so this check does only work 50 | // within the "authenticate" stage. 51 | $token = $this->tokenStorage->getToken(); 52 | if (!($token instanceof TwoFactorTokenInterface)) { 53 | // No need to check for firewall name here, the listener is bound to the firewall context 54 | return; 55 | } 56 | 57 | if (!$this->twoFactorAccessDecider->isAccessible($request, $token)) { 58 | $exception = new AccessDeniedException('User is in a two-factor authentication process.'); 59 | $exception->setSubject($request); 60 | 61 | throw $exception; 62 | } 63 | } 64 | 65 | public static function getPriority(): int 66 | { 67 | // When the class is injected via FirewallListenerFactoryInterface 68 | // // Inject before Symfony's AccessListener (-255) and after the LogoutListener (-127) 69 | return -191; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Security/Http/Utils/JsonRequestUtils.php: -------------------------------------------------------------------------------- 1 | getContentTypeFormat(), 'json'); 29 | } 30 | 31 | /** 32 | * Read data from a JSON payload. 33 | * Paths like foo.bar will be evaluated to find deeper items in nested data structures. 34 | */ 35 | public static function getJsonPayloadValue(Request $request, string $parameterName): string|int|float|bool|null 36 | { 37 | /** @psalm-suppress RedundantCastGivenDocblockType */ 38 | $data = json_decode((string) $request->getContent()); 39 | if (!$data instanceof stdClass) { 40 | throw new BadRequestException('Invalid JSON.'); 41 | } 42 | 43 | if (null === self::$propertyAccessor) { 44 | self::$propertyAccessor = PropertyAccess::createPropertyAccessor(); 45 | } 46 | 47 | try { 48 | $value = self::$propertyAccessor->getValue($data, $parameterName); 49 | } catch (AccessException) { 50 | return null; 51 | } 52 | 53 | if (null !== $value && !is_scalar($value)) { 54 | throw new BadRequestException('Invalid JSON data, expected a scalar value.'); 55 | } 56 | 57 | return $value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Security/Http/Utils/ParameterBagUtils.php: -------------------------------------------------------------------------------- 1 | getValue($value, substr($path, $pos)); 48 | } catch (AccessException) { 49 | return null; 50 | } 51 | } 52 | 53 | private static function getFromRequest(Request $request, string $path): mixed 54 | { 55 | $value = $request->query->all()[$path] ?? null; 56 | if (null !== $value) { 57 | return $value; 58 | } 59 | 60 | return $request->request->all()[$path] ?? null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Security/Http/Utils/RequestDataReader.php: -------------------------------------------------------------------------------- 1 | token; 29 | } 30 | 31 | public function getPassport(): Passport 32 | { 33 | return $this->passport; 34 | } 35 | 36 | public function getUser(): UserInterface 37 | { 38 | return $this->passport->getUser(); 39 | } 40 | 41 | public function getRequest(): Request 42 | { 43 | return $this->request; 44 | } 45 | 46 | public function getSession(): SessionInterface 47 | { 48 | return $this->request->getSession(); 49 | } 50 | 51 | public function getFirewallName(): string 52 | { 53 | return $this->firewallName; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Security/TwoFactor/AuthenticationContextFactory.php: -------------------------------------------------------------------------------- 1 | authenticationContextClass($request, $token, $passport, $firewallName); 27 | assert($authenticationContext instanceof AuthenticationContextInterface); 28 | 29 | return $authenticationContext; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/TwoFactor/AuthenticationContextFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getToken(); 25 | 26 | return in_array($token::class, $this->supportedTokens, true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Security/TwoFactor/Condition/IpWhitelistCondition.php: -------------------------------------------------------------------------------- 1 | getRequest(); 23 | $requestIp = $request->getClientIp(); 24 | 25 | return null === $requestIp || !IpUtils::checkIp($requestIp, $this->ipWhitelistProvider->getWhitelistedIps($context)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Security/TwoFactor/Condition/TwoFactorConditionInterface.php: -------------------------------------------------------------------------------- 1 | conditions as $condition) { 24 | if (!$condition->shouldPerformTwoFactorAuthentication($context)) { 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/TwoFactor/Csrf/NullCsrfTokenManager.php: -------------------------------------------------------------------------------- 1 | getAuthenticationToken(); 24 | 25 | // We have a TwoFactorToken, make sure the security.authentication.success is not propagated to other 26 | // listeners, since we do not have a successful login (yet) 27 | if (!($token instanceof TwoFactorTokenInterface)) { 28 | return; 29 | } 30 | 31 | $event->stopPropagation(); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public static function getSubscribedEvents(): array 38 | { 39 | return [ 40 | AuthenticationEvents::AUTHENTICATION_SUCCESS => ['onLogin', self::LISTENER_PRIORITY], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Security/TwoFactor/Event/AuthenticationTokenListener.php: -------------------------------------------------------------------------------- 1 | getAuthenticatedToken(); 35 | 36 | // TwoFactorTokenInterface can be ignored 37 | if ($token instanceof TwoFactorTokenInterface) { 38 | return; 39 | } 40 | 41 | // The token has already completed 2fa 42 | if ($token->hasAttribute(TwoFactorAuthenticator::FLAG_2FA_COMPLETE)) { 43 | return; 44 | } 45 | 46 | $request = $this->getRequest(); 47 | $passport = $event->getPassport(); 48 | $context = $this->authenticationContextFactory->create($request, $token, $passport, $this->firewallName); 49 | 50 | if (!$this->twoFactorConditionRegistry->shouldPerformTwoFactorAuthentication($context)) { 51 | return; 52 | } 53 | 54 | $newToken = $this->twoFactorProviderInitiator->beginTwoFactorAuthentication($context); 55 | if (null === $newToken) { 56 | return; 57 | } 58 | 59 | $event->setAuthenticatedToken($newToken); 60 | } 61 | 62 | private function getRequest(): Request 63 | { 64 | $request = $this->requestStack->getMainRequest(); 65 | if (null === $request) { 66 | throw new RuntimeException('No request available'); 67 | } 68 | 69 | return $request; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public static function getSubscribedEvents(): array 76 | { 77 | return [AuthenticationTokenCreatedEvent::class => 'onAuthenticationTokenCreated']; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Security/TwoFactor/Event/TwoFactorAuthenticationEvent.php: -------------------------------------------------------------------------------- 1 | request; 25 | } 26 | 27 | public function getToken(): TokenInterface 28 | { 29 | return $this->token; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/TwoFactor/Event/TwoFactorAuthenticationEvents.php: -------------------------------------------------------------------------------- 1 | user; 23 | } 24 | 25 | public function getCode(): string 26 | { 27 | return $this->code; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Security/TwoFactor/Event/TwoFactorCodeReusedEvent.php: -------------------------------------------------------------------------------- 1 | user; 23 | } 24 | 25 | public function getCode(): string 26 | { 27 | return $this->code; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Security/TwoFactor/Event/TwoFactorFormListener.php: -------------------------------------------------------------------------------- 1 | getRequest(); 30 | if (!$request->hasSession()) { 31 | return; 32 | } 33 | 34 | if (!$this->twoFactorFirewallConfig->isAuthFormRequest($request)) { 35 | return; 36 | } 37 | 38 | $token = $this->tokenStorage->getToken(); 39 | if (!($token instanceof TwoFactorTokenInterface)) { 40 | return; 41 | } 42 | 43 | $event = new TwoFactorAuthenticationEvent($request, $token); 44 | $this->eventDispatcher->dispatch($event, TwoFactorAuthenticationEvents::FORM); 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public static function getSubscribedEvents(): array 51 | { 52 | return [KernelEvents::REQUEST => 'onKernelRequest']; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Security/TwoFactor/IpWhitelist/DefaultIpWhitelistProvider.php: -------------------------------------------------------------------------------- 1 | ipWhitelist; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Security/TwoFactor/IpWhitelist/IpWhitelistProviderInterface.php: -------------------------------------------------------------------------------- 1 | $templateVars 19 | */ 20 | public function __construct( 21 | private readonly Environment $twigEnvironment, 22 | private readonly string $template, 23 | private readonly array $templateVars = [], 24 | ) { 25 | } 26 | 27 | /** 28 | * @param array $templateVars 29 | */ 30 | public function renderForm(Request $request, array $templateVars): Response 31 | { 32 | $content = $this->twigEnvironment->render($this->template, array_merge($this->templateVars, $templateVars)); 33 | $response = new Response(); 34 | $response->setContent($content); 35 | 36 | return $response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/Exception/TwoFactorProviderLogicException.php: -------------------------------------------------------------------------------- 1 | tokenStorage->getToken(); 27 | if (!($token instanceof TwoFactorTokenInterface)) { 28 | throw new UnexpectedTokenException('The security token has to be an instance of TwoFactorTokenInterface.'); 29 | } 30 | 31 | $providerKey = $token->getFirewallName(); 32 | if ($providerKey !== $firewallName) { 33 | throw new LogicException(sprintf('Cannot store preparation state for firewall "%s" in a TwoFactorToken belonging to "%s".', $firewallName, $providerKey)); 34 | } 35 | 36 | return $token->isTwoFactorProviderPrepared($providerName); 37 | } 38 | 39 | public function setTwoFactorProviderPrepared(string $firewallName, string $providerName): void 40 | { 41 | $token = $this->tokenStorage->getToken(); 42 | if (!($token instanceof TwoFactorTokenInterface)) { 43 | throw new UnexpectedTokenException('The security token has to be an instance of TwoFactorTokenInterface.'); 44 | } 45 | 46 | $providerKey = $token->getFirewallName(); 47 | if ($providerKey !== $firewallName) { 48 | throw new LogicException(sprintf('Cannot store preparation state for firewall "%s" in a TwoFactorToken belonging to "%s".', $firewallName, $providerKey)); 49 | } 50 | 51 | $token->setTwoFactorProviderPrepared($providerName); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/TwoFactorFormRendererInterface.php: -------------------------------------------------------------------------------- 1 | $templateVars 16 | */ 17 | public function renderForm(Request $request, array $templateVars): Response; 18 | } 19 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/TwoFactorProviderDecider.php: -------------------------------------------------------------------------------- 1 | getUser(); 22 | 23 | if ($user instanceof PreferredProviderInterface) { 24 | return $user->getPreferredTwoFactorProvider(); 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/TwoFactorProviderDeciderInterface.php: -------------------------------------------------------------------------------- 1 | providerRegistry->getAllProviders() as $providerName => $provider) { 33 | if (!$provider->beginAuthentication($context)) { 34 | continue; 35 | } 36 | 37 | $activeTwoFactorProviders[] = $providerName; 38 | } 39 | 40 | return $activeTwoFactorProviders; 41 | } 42 | 43 | public function beginTwoFactorAuthentication(AuthenticationContextInterface $context): TwoFactorTokenInterface|null 44 | { 45 | $activeTwoFactorProviders = $this->getActiveTwoFactorProviders($context); 46 | 47 | $authenticatedToken = $context->getToken(); 48 | if ($activeTwoFactorProviders) { 49 | $twoFactorToken = $this->twoFactorTokenFactory->create($authenticatedToken, $context->getFirewallName(), $activeTwoFactorProviders); 50 | 51 | $preferredProvider = $this->twoFactorProviderDecider->getPreferredTwoFactorProvider($activeTwoFactorProviders, $twoFactorToken, $context); 52 | 53 | if (null !== $preferredProvider) { 54 | try { 55 | $twoFactorToken->preferTwoFactorProvider($preferredProvider); 56 | } catch (UnknownTwoFactorProviderException) { 57 | // Bad user input 58 | } 59 | } 60 | 61 | return $twoFactorToken; 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/TwoFactorProviderInterface.php: -------------------------------------------------------------------------------- 1 | logger = $logger ?? new NullLogger(); 46 | } 47 | 48 | public function onLogin(AuthenticationEvent $event): void 49 | { 50 | $token = $event->getAuthenticationToken(); 51 | if (!$this->prepareOnLogin || !$this->supports($token)) { 52 | return; 53 | } 54 | 55 | // After login, when the token is a TwoFactorTokenInterface, execute preparation 56 | assert($token instanceof TwoFactorTokenInterface); 57 | $this->twoFactorToken = $token; 58 | } 59 | 60 | public function onAccessDenied(TwoFactorAuthenticationEvent $event): void 61 | { 62 | $token = $event->getToken(); 63 | if (!$this->prepareOnAccessDenied || !$this->supports($token)) { 64 | return; 65 | } 66 | 67 | // Whenever two-factor authentication is required, execute preparation 68 | assert($token instanceof TwoFactorTokenInterface); 69 | $this->twoFactorToken = $token; 70 | } 71 | 72 | public function onTwoFactorForm(TwoFactorAuthenticationEvent $event): void 73 | { 74 | $token = $event->getToken(); 75 | if (!$this->supports($token)) { 76 | return; 77 | } 78 | 79 | // Whenever two-factor authentication form is shown, execute preparation 80 | assert($token instanceof TwoFactorTokenInterface); 81 | $this->twoFactorToken = $token; 82 | } 83 | 84 | public function onKernelResponse(ResponseEvent $event): void 85 | { 86 | if (!$event->isMainRequest()) { 87 | return; 88 | } 89 | 90 | // Unset the token from context. This is important for environments where this instance of the class is reused 91 | // for multiple requests, such as PHP PM. 92 | $twoFactorToken = $this->twoFactorToken; 93 | $this->twoFactorToken = null; 94 | 95 | if (!($twoFactorToken instanceof TwoFactorTokenInterface)) { 96 | return; 97 | } 98 | 99 | $providerName = $twoFactorToken->getCurrentTwoFactorProvider(); 100 | if (null === $providerName) { 101 | return; 102 | } 103 | 104 | $firewallName = $twoFactorToken->getFirewallName(); 105 | 106 | try { 107 | if ($this->preparationRecorder->isTwoFactorProviderPrepared($firewallName, $providerName)) { 108 | $this->logger->info(sprintf('Two-factor provider "%s" was already prepared.', $providerName)); 109 | 110 | return; 111 | } 112 | 113 | $user = $twoFactorToken->getUser(); 114 | $this->providerRegistry->getProvider($providerName)->prepareAuthentication($user); 115 | $this->preparationRecorder->setTwoFactorProviderPrepared($firewallName, $providerName); 116 | $this->logger->info(sprintf('Two-factor provider "%s" prepared.', $providerName)); 117 | } catch (UnexpectedTokenException) { 118 | $this->logger->info(sprintf('Two-factor provider "%s" was not prepared, security token was change within the request.', $providerName)); 119 | } 120 | } 121 | 122 | private function supports(TokenInterface $token): bool 123 | { 124 | return $token instanceof TwoFactorTokenInterface && $token->getFirewallName() === $this->firewallName; 125 | } 126 | 127 | /** 128 | * {@inheritDoc} 129 | */ 130 | public static function getSubscribedEvents(): array 131 | { 132 | return [ 133 | AuthenticationEvents::AUTHENTICATION_SUCCESS => ['onLogin', self::AUTHENTICATION_SUCCESS_LISTENER_PRIORITY], 134 | TwoFactorAuthenticationEvents::REQUIRE => 'onAccessDenied', 135 | TwoFactorAuthenticationEvents::FORM => 'onTwoFactorForm', 136 | KernelEvents::RESPONSE => ['onKernelResponse', self::RESPONSE_LISTENER_PRIORITY], 137 | ]; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Security/TwoFactor/Provider/TwoFactorProviderRegistry.php: -------------------------------------------------------------------------------- 1 | $providers 17 | */ 18 | public function __construct(private readonly iterable $providers) 19 | { 20 | } 21 | 22 | /** 23 | * @return iterable 24 | */ 25 | public function getAllProviders(): iterable 26 | { 27 | return $this->providers; 28 | } 29 | 30 | public function getProvider(string $providerName): TwoFactorProviderInterface 31 | { 32 | foreach ($this->providers as $name => $provider) { 33 | if ($name === $providerName) { 34 | return $provider; 35 | } 36 | } 37 | 38 | throw new UnknownTwoFactorProviderException(sprintf('Two-factor provider "%s" does not exist.', $providerName)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Security/TwoFactor/TwoFactorFirewallConfig.php: -------------------------------------------------------------------------------- 1 | $options 19 | */ 20 | public function __construct( 21 | private readonly array $options, 22 | private readonly string $firewallName, 23 | private readonly HttpUtils $httpUtils, 24 | private readonly RequestDataReader $requestDataReader, 25 | ) { 26 | } 27 | 28 | public function getFirewallName(): string 29 | { 30 | return $this->firewallName; 31 | } 32 | 33 | public function isMultiFactor(): bool 34 | { 35 | return $this->options['multi_factor'] ?? TwoFactorFactory::DEFAULT_MULTI_FACTOR; 36 | } 37 | 38 | public function getAuthCodeParameterName(): string 39 | { 40 | return $this->options['auth_code_parameter_name'] ?? TwoFactorFactory::DEFAULT_AUTH_CODE_PARAMETER_NAME; 41 | } 42 | 43 | public function getTrustedParameterName(): string 44 | { 45 | return $this->options['trusted_parameter_name'] ?? TwoFactorFactory::DEFAULT_TRUSTED_PARAMETER_NAME; 46 | } 47 | 48 | public function isRememberMeSetsTrusted(): bool 49 | { 50 | return $this->options['remember_me_sets_trusted'] ?? TwoFactorFactory::DEFAULT_REMEMBER_ME_SETS_TRUSTED; 51 | } 52 | 53 | public function isCsrfProtectionEnabled(): bool 54 | { 55 | return $this->options['enable_csrf'] ?? TwoFactorFactory::DEFAULT_ENABLE_CSRF; 56 | } 57 | 58 | public function getCsrfParameterName(): string 59 | { 60 | return $this->options['csrf_parameter'] ?? TwoFactorFactory::DEFAULT_CSRF_PARAMETER; 61 | } 62 | 63 | public function getCsrfTokenId(): string 64 | { 65 | return $this->options['csrf_token_id'] ?? TwoFactorFactory::DEFAULT_CSRF_TOKEN_ID; 66 | } 67 | 68 | public function getCsrfHeader(): string|null 69 | { 70 | return $this->options['csrf_header'] ?? null; 71 | } 72 | 73 | public function getAuthFormPath(): string 74 | { 75 | return $this->options['auth_form_path'] ?? TwoFactorFactory::DEFAULT_AUTH_FORM_PATH; 76 | } 77 | 78 | public function getCheckPath(): string 79 | { 80 | return $this->options['check_path'] ?? TwoFactorFactory::DEFAULT_CHECK_PATH; 81 | } 82 | 83 | public function isPostOnly(): bool 84 | { 85 | return $this->options['post_only'] ?? TwoFactorFactory::DEFAULT_POST_ONLY; 86 | } 87 | 88 | public function isAlwaysUseDefaultTargetPath(): bool 89 | { 90 | return $this->options['always_use_default_target_path'] ?? TwoFactorFactory::DEFAULT_ALWAYS_USE_DEFAULT_TARGET_PATH; 91 | } 92 | 93 | public function getDefaultTargetPath(): string 94 | { 95 | return $this->options['default_target_path'] ?? TwoFactorFactory::DEFAULT_TARGET_PATH; 96 | } 97 | 98 | public function isCheckPathRequest(Request $request): bool 99 | { 100 | return (!$this->isPostOnly() || $request->isMethod('POST')) 101 | && $this->httpUtils->checkRequestPath($request, $this->getCheckPath()); 102 | } 103 | 104 | public function isAuthFormRequest(Request $request): bool 105 | { 106 | return $this->httpUtils->checkRequestPath($request, $this->getAuthFormPath()); 107 | } 108 | 109 | public function getAuthCodeFromRequest(Request $request): string 110 | { 111 | return (string) ($this->requestDataReader->getRequestValue($request, $this->getAuthCodeParameterName()) ?? ''); 112 | } 113 | 114 | public function hasTrustedDeviceParameterInRequest(Request $request): bool 115 | { 116 | return (bool) $this->requestDataReader->getRequestValue($request, $this->getTrustedParameterName()); 117 | } 118 | 119 | public function getCsrfTokenFromRequest(Request $request): string 120 | { 121 | $csrfHeaderName = $this->getCsrfHeader(); 122 | if (null !== $csrfHeaderName && $request->headers->has($csrfHeaderName)) { 123 | return (string) $request->headers->get($csrfHeaderName); 124 | } 125 | 126 | return (string) ($this->requestDataReader->getRequestValue($request, $this->getCsrfParameterName()) ?? ''); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Security/TwoFactor/TwoFactorFirewallContext.php: -------------------------------------------------------------------------------- 1 | $firewallConfigs 17 | */ 18 | public function __construct(private readonly array $firewallConfigs) 19 | { 20 | } 21 | 22 | public function getFirewallConfig(string $firewallName): TwoFactorFirewallConfig 23 | { 24 | if (!isset($this->firewallConfigs[$firewallName])) { 25 | throw new InvalidArgumentException(sprintf('Firewall "%s" has no two-factor config.', $firewallName)); 26 | } 27 | 28 | return $this->firewallConfigs[$firewallName]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scheb/2fa-bundle", 3 | "type": "symfony-bundle", 4 | "description": "A generic interface to implement two-factor authentication in Symfony applications", 5 | "keywords": ["2fa", "two-factor", "two-step", "authentication", "symfony"], 6 | "homepage": "https://github.com/scheb/2fa", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Christian Scheb", 11 | "email": "me@christianscheb.de" 12 | } 13 | ], 14 | "require": { 15 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0", 16 | "ext-json": "*", 17 | "symfony/config": "^6.4 || ^7.0", 18 | "symfony/dependency-injection": "^6.4 || ^7.0", 19 | "symfony/event-dispatcher": "^6.4 || ^7.0", 20 | "symfony/framework-bundle": "^6.4 || ^7.0", 21 | "symfony/http-foundation": "^6.4 || ^7.0", 22 | "symfony/http-kernel": "^6.4 || ^7.0", 23 | "symfony/property-access": "^6.4 || ^7.0", 24 | "symfony/security-bundle": "^6.4 || ^7.0", 25 | "symfony/service-contracts": "^2.5|^3", 26 | "symfony/twig-bundle": "^6.4 || ^7.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Scheb\\TwoFactorBundle\\": "" 31 | } 32 | }, 33 | "suggest": { 34 | "scheb/2fa-backup-code": "Emergency codes when you have no access to other methods", 35 | "scheb/2fa-email": "Send codes by email", 36 | "scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)", 37 | "scheb/2fa-google-authenticator": "Google Authenticator support", 38 | "scheb/2fa-trusted-device": "Trusted devices support" 39 | }, 40 | "conflict": { 41 | "scheb/two-factor-bundle": "*" 42 | } 43 | } 44 | --------------------------------------------------------------------------------