Skip to content

Commit ab12c72

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

File tree

8 files changed

+332
-0
lines changed

8 files changed

+332
-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 <[email protected]>
5964
* @author Jesse Rushlow <[email protected]>
6065
*
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Generator;
17+
use Symfony\Bundle\MakerBundle\InputConfiguration;
18+
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
19+
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
20+
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
21+
use Symfony\Bundle\MakerBundle\Validator;
22+
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Input\InputInterface;
24+
use Symfony\Component\HttpFoundation\JsonResponse;
25+
use Symfony\Component\HttpFoundation\Request;
26+
use Symfony\Component\HttpFoundation\Response;
27+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
28+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
29+
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
30+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
31+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
32+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
33+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
34+
35+
/**
36+
* @author Jesse Rushlow <[email protected]>
37+
*
38+
* @internal
39+
*/
40+
final class MakeCustomAuthenticator extends AbstractMaker
41+
{
42+
private ClassNameDetails $authenticatorClassName;
43+
44+
public function __construct(
45+
private Generator $generator,
46+
) {
47+
}
48+
49+
public static function getCommandName(): string
50+
{
51+
return 'make:security:custom';
52+
}
53+
54+
public static function getCommandDescription(): string
55+
{
56+
return 'Create a custom security authenticator.';
57+
}
58+
59+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
60+
{
61+
$command
62+
->setHelp(file_get_contents(__DIR__.'/../../Resources/help/security/MakeCustom.txt'))
63+
;
64+
}
65+
66+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
67+
{
68+
$name = $io->ask(
69+
question: 'What is the class name of the authenticator (e.g. <fg=yellow>CustomAuthenticator</>)',
70+
validator: static function (mixed $answer) {
71+
return Validator::notBlank($answer);
72+
}
73+
);
74+
75+
$this->authenticatorClassName = $this->generator->createClassNameDetails(
76+
name: $name,
77+
namespacePrefix: 'Security\\',
78+
suffix: 'Authenticator'
79+
);
80+
}
81+
82+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
83+
{
84+
$useStatements = new UseStatementGenerator([
85+
Request::class,
86+
Response::class,
87+
TokenInterface::class,
88+
AuthenticationException::class,
89+
AbstractAuthenticator::class,
90+
Passport::class,
91+
JsonResponse::class,
92+
UserBadge::class,
93+
CustomUserMessageAuthenticationException::class,
94+
SelfValidatingPassport::class,
95+
]);
96+
97+
$generator->generateClass(
98+
className: $this->authenticatorClassName->getFullName(),
99+
templateName: 'security/custom/Authenticator.tpl.php',
100+
variables: [
101+
'use_statements' => $useStatements,
102+
'class_short_name' => $this->authenticatorClassName->getShortName(),
103+
]
104+
);
105+
106+
$generator->writeChanges();
107+
108+
$this->writeSuccessMessage($io);
109+
}
110+
111+
public function configureDependencies(DependencyBuilder $dependencies): void
112+
{
113+
// TODO: Implement configureDependencies() method.
114+
}
115+
}

src/Resources/config/makers.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@
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.generator" />
163+
<tag name="maker.command" />
164+
</service>
165+
161166
<service id="maker.maker.make_webhook" class="Symfony\Bundle\MakerBundle\Maker\MakeWebhook">
162167
<argument type="service" id="maker.file_manager" />
163168
<argument type="service" id="maker.generator" />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@TODO --------------
2+
3+
The <info>%command.name%</info> command generates a controller and twig template
4+
to allow users to login using the form_login authenticator.
5+
6+
The controller name, and logout ability can be customized by answering the
7+
questions asked when running <info>%command.name%</info>.
8+
9+
This will also update your <info>security.yaml</info> for the new authenticator.
10+
11+
<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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
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+
// $this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['login_path']);
44+
// $this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['check_path']);
45+
// $this->assertTrue($securityConfig['security']['firewalls']['main']['form_login']['enable_csrf']);
46+
// $this->assertSame('app_logout', $securityConfig['security']['firewalls']['main']['logout']['path']);
47+
}),
48+
];
49+
}
50+
}
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)