-
-
Notifications
You must be signed in to change notification settings - Fork 404
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #1522 [make:security:custom] create a custom authenticator
- Loading branch information
Showing
6 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony MakerBundle package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\MakerBundle\Maker\Security; | ||
|
||
use Symfony\Bundle\MakerBundle\ConsoleStyle; | ||
use Symfony\Bundle\MakerBundle\DependencyBuilder; | ||
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; | ||
use Symfony\Bundle\MakerBundle\FileManager; | ||
use Symfony\Bundle\MakerBundle\Generator; | ||
use Symfony\Bundle\MakerBundle\InputConfiguration; | ||
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; | ||
use Symfony\Bundle\MakerBundle\Maker\Common\InstallDependencyTrait; | ||
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; | ||
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; | ||
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; | ||
use Symfony\Bundle\MakerBundle\Validator; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\Exception\AuthenticationException; | ||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | ||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | ||
|
||
/** | ||
* @author Jesse Rushlow <jr@rushlow.dev> | ||
* | ||
* @internal | ||
*/ | ||
final class MakeCustomAuthenticator extends AbstractMaker | ||
{ | ||
use InstallDependencyTrait; | ||
|
||
private const SECURITY_CONFIG_PATH = 'config/packages/security.yaml'; | ||
|
||
private ClassNameDetails $authenticatorClassName; | ||
|
||
public function __construct( | ||
private FileManager $fileManager, | ||
private Generator $generator, | ||
) { | ||
} | ||
|
||
public static function getCommandName(): string | ||
{ | ||
return 'make:security:custom'; | ||
} | ||
|
||
public static function getCommandDescription(): string | ||
{ | ||
return 'Create a custom security authenticator.'; | ||
} | ||
|
||
public function configureCommand(Command $command, InputConfiguration $inputConfig): void | ||
{ | ||
$command | ||
->setHelp(file_get_contents(__DIR__.'/../../Resources/help/security/MakeCustom.txt')) | ||
; | ||
} | ||
|
||
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void | ||
{ | ||
$this->installDependencyIfNeeded( | ||
io: $io, | ||
expectedClassToExist: AbstractAuthenticator::class, | ||
composerPackage: 'symfony/security-bundle' | ||
); | ||
|
||
if (!$this->fileManager->fileExists(self::SECURITY_CONFIG_PATH)) { | ||
throw new RuntimeCommandException(sprintf('The file "%s" does not exist. PHP & XML configuration formats are currently not supported.', self::SECURITY_CONFIG_PATH)); | ||
} | ||
|
||
$name = $io->ask( | ||
question: 'What is the class name of the authenticator (e.g. <fg=yellow>CustomAuthenticator</>)', | ||
validator: static function (mixed $answer) { | ||
return Validator::notBlank($answer); | ||
} | ||
); | ||
|
||
$this->authenticatorClassName = $this->generator->createClassNameDetails( | ||
name: $name, | ||
namespacePrefix: 'Security\\', | ||
suffix: 'Authenticator' | ||
); | ||
} | ||
|
||
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void | ||
{ | ||
// Configure security to use custom authenticator | ||
$securityConfig = ($ysm = new YamlSourceManipulator( | ||
$this->fileManager->getFileContents(self::SECURITY_CONFIG_PATH) | ||
))->getData(); | ||
|
||
$securityConfig['security']['firewalls']['main']['custom_authenticators'] = [$this->authenticatorClassName->getFullName()]; | ||
|
||
$ysm->setData($securityConfig); | ||
$generator->dumpFile(self::SECURITY_CONFIG_PATH, $ysm->getContents()); | ||
|
||
// Generate the new authenticator | ||
$useStatements = new UseStatementGenerator([ | ||
Request::class, | ||
Response::class, | ||
TokenInterface::class, | ||
AuthenticationException::class, | ||
AbstractAuthenticator::class, | ||
Passport::class, | ||
JsonResponse::class, | ||
UserBadge::class, | ||
CustomUserMessageAuthenticationException::class, | ||
SelfValidatingPassport::class, | ||
]); | ||
|
||
$generator->generateClass( | ||
className: $this->authenticatorClassName->getFullName(), | ||
templateName: 'security/custom/Authenticator.tpl.php', | ||
variables: [ | ||
'use_statements' => $useStatements, | ||
'class_short_name' => $this->authenticatorClassName->getShortName(), | ||
] | ||
); | ||
|
||
$generator->writeChanges(); | ||
|
||
$this->writeSuccessMessage($io); | ||
} | ||
|
||
public function configureDependencies(DependencyBuilder $dependencies): void | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
The <info>%command.name%</info> command generates a simple custom authenticator | ||
class based off the example provided in: | ||
|
||
<href=https://symfony.com/doc/current/security/custom_authenticator.html>https://symfony.com/doc/current/security/custom_authenticator.html</> | ||
|
||
This will also update your <info>security.yaml</info> for the new custom authenticator. | ||
|
||
<info>php %command.full_name%</info> |
67 changes: 67 additions & 0 deletions
67
src/Resources/skeleton/security/custom/Authenticator.tpl.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<?= "<?php\n" ?> | ||
|
||
namespace <?= $namespace; ?>; | ||
|
||
<?= $use_statements; ?> | ||
|
||
/** | ||
* @see https://symfony.com/doc/current/security/custom_authenticator.html | ||
*/ | ||
class <?= $class_short_name ?> extends AbstractAuthenticator | ||
{ | ||
/** | ||
* Called on every request to decide if this authenticator should be | ||
* used for the request. Returning `false` will cause this authenticator | ||
* to be skipped. | ||
*/ | ||
public function supports(Request $request): ?bool | ||
{ | ||
// return $request->headers->has('X-AUTH-TOKEN'); | ||
} | ||
|
||
public function authenticate(Request $request): Passport | ||
{ | ||
// $apiToken = $request->headers->get('X-AUTH-TOKEN'); | ||
// if (null === $apiToken) { | ||
// The token header was empty, authentication fails with HTTP Status | ||
// Code 401 "Unauthorized" | ||
// throw new CustomUserMessageAuthenticationException('No API token provided'); | ||
// } | ||
|
||
// implement your own logic to get the user identifier from `$apiToken` | ||
// e.g. by looking up a user in the database using its API key | ||
// $userIdentifier = /** ... */; | ||
|
||
// return new SelfValidatingPassport(new UserBadge($userIdentifier)); | ||
} | ||
|
||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | ||
{ | ||
// on success, let the request continue | ||
return null; | ||
} | ||
|
||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | ||
{ | ||
$data = [ | ||
// you may want to customize or obfuscate the message first | ||
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) | ||
|
||
// or to translate this message | ||
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) | ||
]; | ||
|
||
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); | ||
} | ||
|
||
// public function start(Request $request, AuthenticationException $authException = null): Response | ||
// { | ||
// /* | ||
// * If you would like this class to control what happens when an anonymous user accesses a | ||
// * protected page (e.g. redirect to /login), uncomment this method and make this class | ||
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface. | ||
// * | ||
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point | ||
// */ | ||
// } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony MakerBundle package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\MakerBundle\Tests\Maker\Security; | ||
|
||
use Symfony\Bundle\MakerBundle\Maker\Security\MakeCustomAuthenticator; | ||
use Symfony\Bundle\MakerBundle\Test\MakerTestCase; | ||
use Symfony\Bundle\MakerBundle\Test\MakerTestRunner; | ||
|
||
/** | ||
* @author Jesse Rushlow <jr@rushlow.dev> | ||
*/ | ||
class MakeCustomAuthenticatorTest extends MakerTestCase | ||
{ | ||
protected function getMakerClass(): string | ||
{ | ||
return MakeCustomAuthenticator::class; | ||
} | ||
|
||
public function getTestDetails(): \Generator | ||
{ | ||
yield 'generates_custom_authenticator' => [$this->createMakerTest() | ||
->run(function (MakerTestRunner $runner) { | ||
$output = $runner->runMaker([ | ||
'FixtureAuthenticator', // Authenticator Name | ||
]); | ||
|
||
$this->assertStringContainsString('Success', $output); | ||
$fixturePath = \dirname(__DIR__, 2).'/fixtures/security/make-custom-authenticator/expected'; | ||
|
||
$this->assertFileEquals($fixturePath.'/FixtureAuthenticator.php', $runner->getPath('src/Security/FixtureAuthenticator.php')); | ||
|
||
$securityConfig = $runner->readYaml('config/packages/security.yaml'); | ||
|
||
self::assertArrayHasKey('custom_authenticators', $mainFirewall = $securityConfig['security']['firewalls']['main']); | ||
self::assertSame(['App\Security\FixtureAuthenticator'], $mainFirewall['custom_authenticators']); | ||
}), | ||
]; | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
tests/fixtures/security/make-custom-authenticator/expected/FixtureAuthenticator.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
namespace App\Security; | ||
|
||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\Exception\AuthenticationException; | ||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | ||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | ||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | ||
|
||
/** | ||
* @see https://symfony.com/doc/current/security/custom_authenticator.html | ||
*/ | ||
class FixtureAuthenticator extends AbstractAuthenticator | ||
{ | ||
/** | ||
* Called on every request to decide if this authenticator should be | ||
* used for the request. Returning `false` will cause this authenticator | ||
* to be skipped. | ||
*/ | ||
public function supports(Request $request): ?bool | ||
{ | ||
// return $request->headers->has('X-AUTH-TOKEN'); | ||
} | ||
|
||
public function authenticate(Request $request): Passport | ||
{ | ||
// $apiToken = $request->headers->get('X-AUTH-TOKEN'); | ||
// if (null === $apiToken) { | ||
// The token header was empty, authentication fails with HTTP Status | ||
// Code 401 "Unauthorized" | ||
// throw new CustomUserMessageAuthenticationException('No API token provided'); | ||
// } | ||
|
||
// implement your own logic to get the user identifier from `$apiToken` | ||
// e.g. by looking up a user in the database using its API key | ||
// $userIdentifier = /** ... */; | ||
|
||
// return new SelfValidatingPassport(new UserBadge($userIdentifier)); | ||
} | ||
|
||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | ||
{ | ||
// on success, let the request continue | ||
return null; | ||
} | ||
|
||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | ||
{ | ||
$data = [ | ||
// you may want to customize or obfuscate the message first | ||
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), | ||
|
||
// or to translate this message | ||
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) | ||
]; | ||
|
||
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); | ||
} | ||
|
||
// public function start(Request $request, AuthenticationException $authException = null): Response | ||
// { | ||
// /* | ||
// * If you would like this class to control what happens when an anonymous user accesses a | ||
// * protected page (e.g. redirect to /login), uncomment this method and make this class | ||
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface. | ||
// * | ||
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point | ||
// */ | ||
// } | ||
} |