Skip to content

Commit 17610a5

Browse files
committed
[make:security:custom] create a custom authenticator
1 parent d711796 commit 17610a5

File tree

8 files changed

+357
-0
lines changed

8 files changed

+357
-0
lines changed

src/Maker/MakeAuthenticator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Bundle\MakerBundle\FileManager;
2020
use Symfony\Bundle\MakerBundle\Generator;
2121
use Symfony\Bundle\MakerBundle\InputConfiguration;
22+
use Symfony\Bundle\MakerBundle\Maker\Security\MakeCustomAuthenticator;
2223
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
2324
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
2425
use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder;
@@ -54,7 +55,11 @@
5455
use Symfony\Component\Security\Http\Util\TargetPathTrait;
5556
use Symfony\Component\Yaml\Yaml;
5657

58+
trigger_deprecation('symfony/maker-bundle', '1.99999999', 'The "%s" class is deprecated, use "%s" instead.', MakeAuthenticator::class, MakeCustomAuthenticator::class);
59+
5760
/**
61+
* @deprecated since MakerBundle 1.9999999999, use any of the Security/MakeX instead.
62+
*
5863
* @author Ryan Weaver <ryan@symfonycasts.com>
5964
* @author Jesse Rushlow <jr@rushlow.dev>
6065
*
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Maker\Security;
13+
14+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
15+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
16+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
17+
use Symfony\Bundle\MakerBundle\FileManager;
18+
use Symfony\Bundle\MakerBundle\Generator;
19+
use Symfony\Bundle\MakerBundle\InputConfiguration;
20+
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
21+
use Symfony\Bundle\MakerBundle\Maker\Common\InstallDependencyTrait;
22+
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
23+
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
24+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
25+
use Symfony\Bundle\MakerBundle\Validator;
26+
use Symfony\Component\Console\Command\Command;
27+
use Symfony\Component\Console\Input\InputInterface;
28+
use Symfony\Component\HttpFoundation\JsonResponse;
29+
use Symfony\Component\HttpFoundation\Request;
30+
use Symfony\Component\HttpFoundation\Response;
31+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
32+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
33+
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
34+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
35+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
36+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
37+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
38+
39+
/**
40+
* @author Jesse Rushlow <jr@rushlow.dev>
41+
*
42+
* @internal
43+
*/
44+
final class MakeCustomAuthenticator extends AbstractMaker
45+
{
46+
use InstallDependencyTrait;
47+
48+
private const SECURITY_CONFIG_PATH = 'config/packages/security.yaml';
49+
50+
private ClassNameDetails $authenticatorClassName;
51+
52+
public function __construct(
53+
private FileManager $fileManager,
54+
private Generator $generator,
55+
) {
56+
}
57+
58+
public static function getCommandName(): string
59+
{
60+
return 'make:security:custom';
61+
}
62+
63+
public static function getCommandDescription(): string
64+
{
65+
return 'Create a custom security authenticator.';
66+
}
67+
68+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
69+
{
70+
$command
71+
->setHelp(file_get_contents(__DIR__.'/../../Resources/help/security/MakeCustom.txt'))
72+
;
73+
}
74+
75+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
76+
{
77+
$this->installDependencyIfNeeded(
78+
io: $io,
79+
expectedClassToExist: AbstractAuthenticator::class,
80+
composerPackage: 'symfony/security-bundle'
81+
);
82+
83+
if (!$this->fileManager->fileExists(self::SECURITY_CONFIG_PATH)) {
84+
throw new RuntimeCommandException(sprintf('The file "%s" does not exist. PHP & XML configuration formats are currently not supported.', self::SECURITY_CONFIG_PATH));
85+
}
86+
87+
$name = $io->ask(
88+
question: 'What is the class name of the authenticator (e.g. <fg=yellow>CustomAuthenticator</>)',
89+
validator: static function (mixed $answer) {
90+
return Validator::notBlank($answer);
91+
}
92+
);
93+
94+
$this->authenticatorClassName = $this->generator->createClassNameDetails(
95+
name: $name,
96+
namespacePrefix: 'Security\\',
97+
suffix: 'Authenticator'
98+
);
99+
}
100+
101+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
102+
{
103+
// Configure security to use custom authenticator
104+
$securityConfig = ($ysm = new YamlSourceManipulator(
105+
$this->fileManager->getFileContents(self::SECURITY_CONFIG_PATH)
106+
))->getData();
107+
108+
$securityConfig['security']['firewalls']['main']['custom_authenticators'] = [$this->authenticatorClassName->getFullName()];
109+
110+
$ysm->setData($securityConfig);
111+
$generator->dumpFile(self::SECURITY_CONFIG_PATH, $ysm->getContents());
112+
113+
// Generate the new authenticator
114+
$useStatements = new UseStatementGenerator([
115+
Request::class,
116+
Response::class,
117+
TokenInterface::class,
118+
AuthenticationException::class,
119+
AbstractAuthenticator::class,
120+
Passport::class,
121+
JsonResponse::class,
122+
UserBadge::class,
123+
CustomUserMessageAuthenticationException::class,
124+
SelfValidatingPassport::class,
125+
]);
126+
127+
$generator->generateClass(
128+
className: $this->authenticatorClassName->getFullName(),
129+
templateName: 'security/custom/Authenticator.tpl.php',
130+
variables: [
131+
'use_statements' => $useStatements,
132+
'class_short_name' => $this->authenticatorClassName->getShortName(),
133+
]
134+
);
135+
136+
$generator->writeChanges();
137+
138+
$this->writeSuccessMessage($io);
139+
}
140+
141+
public function configureDependencies(DependencyBuilder $dependencies): void
142+
{
143+
}
144+
}

src/Resources/config/makers.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@
158158
<tag name="maker.command" />
159159
</service>
160160

161+
<service id="maker.maker.make_custom_authenticator" class="Symfony\Bundle\MakerBundle\Maker\Security\MakeCustomAuthenticator">
162+
<argument type="service" id="maker.file_manager" />
163+
<argument type="service" id="maker.generator" />
164+
<tag name="maker.command" />
165+
</service>
166+
161167
<service id="maker.maker.make_webhook" class="Symfony\Bundle\MakerBundle\Maker\MakeWebhook">
162168
<argument type="service" id="maker.file_manager" />
163169
<argument type="service" id="maker.generator" />
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The <info>%command.name%</info> command generates a simple custom authenticator
2+
class based off the example provided in:
3+
4+
<href=https://symfony.com/doc/current/security/custom_authenticator.html>https://symfony.com/doc/current/security/custom_authenticator.html</>
5+
6+
This will also update your <info>security.yaml</info> for the new custom authenticator.
7+
8+
<info>php %command.full_name%</info>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace; ?>;
4+
5+
<?= $use_statements; ?>
6+
7+
/**
8+
* @see https://symfony.com/doc/current/security/custom_authenticator.html
9+
*/
10+
class <?= $class_short_name ?> extends AbstractAuthenticator
11+
{
12+
/**
13+
* Called on every request to decide if this authenticator should be
14+
* used for the request. Returning `false` will cause this authenticator
15+
* to be skipped.
16+
*/
17+
public function supports(Request $request): ?bool
18+
{
19+
// return $request->headers->has('X-AUTH-TOKEN');
20+
}
21+
22+
public function authenticate(Request $request): Passport
23+
{
24+
// $apiToken = $request->headers->get('X-AUTH-TOKEN');
25+
// if (null === $apiToken) {
26+
// The token header was empty, authentication fails with HTTP Status
27+
// Code 401 "Unauthorized"
28+
// throw new CustomUserMessageAuthenticationException('No API token provided');
29+
// }
30+
31+
// implement your own logic to get the user identifier from `$apiToken`
32+
// e.g. by looking up a user in the database using its API key
33+
// $userIdentifier = /** ... */;
34+
35+
// return new SelfValidatingPassport(new UserBadge($userIdentifier));
36+
}
37+
38+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
39+
{
40+
// on success, let the request continue
41+
return null;
42+
}
43+
44+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
45+
{
46+
$data = [
47+
// you may want to customize or obfuscate the message first
48+
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
49+
50+
// or to translate this message
51+
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
52+
];
53+
54+
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
55+
}
56+
57+
// public function start(Request $request, AuthenticationException $authException = null): Response
58+
// {
59+
// /*
60+
// * If you would like this class to control what happens when an anonymous user accesses a
61+
// * protected page (e.g. redirect to /login), uncomment this method and make this class
62+
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface.
63+
// *
64+
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
65+
// */
66+
// }
67+
}

tests/Maker/MakeAuthenticatorTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Symfony\Bundle\MakerBundle\Test\MakerTestRunner;
1717
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
1818

19+
/**
20+
* @group legacy
21+
*/
1922
class MakeAuthenticatorTest extends MakerTestCase
2023
{
2124
protected function getMakerClass(): string
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Tests\Maker\Security;
13+
14+
use Symfony\Bundle\MakerBundle\Maker\Security\MakeCustomAuthenticator;
15+
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
16+
use Symfony\Bundle\MakerBundle\Test\MakerTestRunner;
17+
18+
/**
19+
* @author Jesse Rushlow <jr@rushlow.dev>
20+
*/
21+
class MakeCustomAuthenticatorTest extends MakerTestCase
22+
{
23+
protected function getMakerClass(): string
24+
{
25+
return MakeCustomAuthenticator::class;
26+
}
27+
28+
public function getTestDetails(): \Generator
29+
{
30+
yield 'generates_custom_authenticator' => [$this->createMakerTest()
31+
->run(function (MakerTestRunner $runner) {
32+
$output = $runner->runMaker([
33+
'FixtureAuthenticator', // Authenticator Name
34+
]);
35+
36+
$this->assertStringContainsString('Success', $output);
37+
$fixturePath = \dirname(__DIR__, 2).'/fixtures/security/make-custom-authenticator/expected';
38+
39+
$this->assertFileEquals($fixturePath.'/FixtureAuthenticator.php', $runner->getPath('src/Security/FixtureAuthenticator.php'));
40+
41+
$securityConfig = $runner->readYaml('config/packages/security.yaml');
42+
43+
self::assertArrayHasKey('custom_authenticators', $mainFirewall = $securityConfig['security']['firewalls']['main']);
44+
self::assertSame(['App\Security\FixtureAuthenticator'], $mainFirewall['custom_authenticators']);
45+
}),
46+
];
47+
}
48+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace App\Security;
4+
5+
use Symfony\Component\HttpFoundation\JsonResponse;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
9+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
10+
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
11+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
12+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
13+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
14+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
15+
16+
/**
17+
* @see https://symfony.com/doc/current/security/custom_authenticator.html
18+
*/
19+
class FixtureAuthenticator extends AbstractAuthenticator
20+
{
21+
/**
22+
* Called on every request to decide if this authenticator should be
23+
* used for the request. Returning `false` will cause this authenticator
24+
* to be skipped.
25+
*/
26+
public function supports(Request $request): ?bool
27+
{
28+
// return $request->headers->has('X-AUTH-TOKEN');
29+
}
30+
31+
public function authenticate(Request $request): Passport
32+
{
33+
// $apiToken = $request->headers->get('X-AUTH-TOKEN');
34+
// if (null === $apiToken) {
35+
// The token header was empty, authentication fails with HTTP Status
36+
// Code 401 "Unauthorized"
37+
// throw new CustomUserMessageAuthenticationException('No API token provided');
38+
// }
39+
40+
// implement your own logic to get the user identifier from `$apiToken`
41+
// e.g. by looking up a user in the database using its API key
42+
// $userIdentifier = /** ... */;
43+
44+
// return new SelfValidatingPassport(new UserBadge($userIdentifier));
45+
}
46+
47+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
48+
{
49+
// on success, let the request continue
50+
return null;
51+
}
52+
53+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
54+
{
55+
$data = [
56+
// you may want to customize or obfuscate the message first
57+
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
58+
59+
// or to translate this message
60+
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
61+
];
62+
63+
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
64+
}
65+
66+
// public function start(Request $request, AuthenticationException $authException = null): Response
67+
// {
68+
// /*
69+
// * If you would like this class to control what happens when an anonymous user accesses a
70+
// * protected page (e.g. redirect to /login), uncomment this method and make this class
71+
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface.
72+
// *
73+
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
74+
// */
75+
// }
76+
}

0 commit comments

Comments
 (0)