Skip to content

Commit e2c5c67

Browse files
Merge branch '7.4' into 8.0
* 7.4: (28 commits) [Messenger] Allow Pheanstalk v8 [TypeInfo] Fix resolving constructor type with templates fix compatibility with RelayCluster 0.12 fix type alias with template resolving fix compatibility with RelayCluster 0.11 and 0.12 [DependencyInjection] Register a custom autoloader to generate `*Config` classes when they don't exist yet [Security] Add security:oidc-token:generate command [PropertyInfo][TypeInfo] Fix resolving constructor type with templates [WebProfilerBundle] ”finish” errored requests Add support for union types on AsEventListener [Console] Update CHANGELOG to reflect attribute name changes for interactive invokable commands bump ext-redis to 6.2 and ext-relay to 0.12 minimum [TypeInfo] Fix type alias with template resolving [Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes bump ext-relay to 0.12+ fix merge [Config] Generate the array-shape of the current node instead of the whole root node in Config classes [HttpFoundation] Deprecate Request::get() in favor of using properties ->attributes, query or request directly fix Relay Cluster 0.12 compatibility [TypeInfo] ArrayShape can resolve key type as callable instead of string ...
2 parents 5a50801 + d12c036 commit e2c5c67

File tree

11 files changed

+341
-55
lines changed

11 files changed

+341
-55
lines changed

AccessToken/FormEncodedBodyExtractor.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ public function __construct(
3434

3535
public function extractAccessToken(Request $request): ?string
3636
{
37-
if (
38-
Request::METHOD_POST !== $request->getMethod()
39-
|| !str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
40-
) {
37+
if ('POST' !== $request->getMethod() || !str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')) {
4138
return null;
4239
}
4340
$parameter = $request->request->get($this->parameter);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony 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\Component\Security\Http\AccessToken\Oidc;
13+
14+
use Jose\Component\Core\Algorithm;
15+
use Jose\Component\Core\AlgorithmManager;
16+
use Jose\Component\Core\JWKSet;
17+
use Jose\Component\Signature\JWSBuilder;
18+
use Jose\Component\Signature\Serializer\CompactSerializer;
19+
use Psr\Clock\ClockInterface;
20+
use Symfony\Component\Clock\Clock;
21+
22+
class OidcTokenGenerator
23+
{
24+
public function __construct(
25+
private readonly AlgorithmManager $algorithmManager,
26+
private readonly JWKSet $jwkset,
27+
private readonly string $audience,
28+
private readonly array $issuers,
29+
private readonly string $claim = 'sub',
30+
private readonly ClockInterface $clock = new Clock(),
31+
) {
32+
}
33+
34+
public function generate(string $userIdentifier, ?string $algorithmAlias = null, ?string $issuer = null, ?int $ttl = null, ?\DateTimeImmutable $notBefore = null): string
35+
{
36+
$algorithm = $this->getAlgorithm($algorithmAlias);
37+
38+
if (!$jwk = $this->jwkset->selectKey('sig', $algorithm)) {
39+
throw new \InvalidArgumentException(\sprintf('No JWK found to sign with "%s" algorithm.', $algorithm->name()));
40+
}
41+
42+
$jwsBuilder = new JWSBuilder($this->algorithmManager);
43+
44+
$now = $this->clock->now();
45+
$payload = [
46+
$this->claim => $userIdentifier,
47+
'iat' => $now->getTimestamp(), # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
48+
'aud' => $this->audience, # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
49+
'iss' => $this->getIssuer($issuer), # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
50+
];
51+
if ($ttl) {
52+
if (0 > $ttl) {
53+
throw new \InvalidArgumentException('Time to live must be a positive integer.');
54+
}
55+
56+
$payload['exp'] = $now->add(new \DateInterval("PT{$ttl}S"))->getTimestamp(); # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
57+
}
58+
if ($notBefore) {
59+
$payload['nbf'] = $notBefore->getTimestamp(); # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
60+
}
61+
62+
$jws = $jwsBuilder
63+
->create()
64+
->withPayload(json_encode($payload, flags: \JSON_THROW_ON_ERROR))
65+
->addSignature($jwk, ['alg' => $algorithm->name()])
66+
->build();
67+
68+
$serializer = new CompactSerializer();
69+
70+
return $serializer->serialize($jws, 0);
71+
}
72+
73+
private function getAlgorithm(?string $alias): Algorithm
74+
{
75+
if ($alias) {
76+
if (!$this->algorithmManager->has($alias)) {
77+
throw new \InvalidArgumentException(sprintf('"%s" is not a valid algorithm. Available algorithms: "%s".', $alias, implode('", "', $this->algorithmManager->list())));
78+
}
79+
return $this->algorithmManager->get($alias);
80+
}
81+
82+
if (1 !== count($list = $this->algorithmManager->list())) {
83+
throw new \InvalidArgumentException(sprintf('Please choose an algorithm. Available algorithms: "%s".', implode('", "', $list)));
84+
}
85+
86+
return $this->algorithmManager->get($list[0]);
87+
}
88+
89+
private function getIssuer(?string $issuer): string
90+
{
91+
if ($issuer) {
92+
if (!in_array($issuer, $this->issuers, true)) {
93+
throw new \InvalidArgumentException(sprintf('"%s" is not a valid issuer. Available issuers: "%s".', $issuer, implode('", "', $this->issuers)));
94+
}
95+
96+
return $issuer;
97+
}
98+
99+
if (1 !== count($this->issuers)) {
100+
throw new \InvalidArgumentException(sprintf('Please choose an issuer. Available issuers: "%s".', implode('", "', $this->issuers)));
101+
}
102+
103+
return $this->issuers[0];
104+
}
105+
}

Authenticator/LoginLinkAuthenticator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function supports(Request $request): ?bool
5151

5252
public function authenticate(Request $request): Passport
5353
{
54-
if (!$username = $request->get('user')) {
54+
if (!$username = $request->query->get('user') ?? (!\in_array($request->getMethod(), ['GET', 'HEAD'], true) ? $request->request->get('user') : null)) {
5555
throw new InvalidLoginLinkAuthenticationException('Missing user from link.');
5656
}
5757

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony 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\Component\Security\Http\Command;
13+
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Completion\CompletionInput;
17+
use Symfony\Component\Console\Completion\CompletionSuggestions;
18+
use Symfony\Component\Console\Completion\Suggestion;
19+
use Symfony\Component\Console\Input\InputArgument;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Input\InputOption;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenGenerator;
24+
25+
#[AsCommand(name: 'security:oidc:generate-token', description: 'Generate an OIDC token for a given user')]
26+
final class OidcTokenGenerateCommand extends Command
27+
{
28+
/** @var array<string, OidcTokenGenerator> */
29+
private array $generators = [];
30+
/** @var array<string, list<string>> */
31+
private array $algorithms;
32+
/** @var array<string, list<string>> */
33+
private array $issuers;
34+
35+
protected function configure(): void
36+
{
37+
$this
38+
->addArgument('user-identifier', InputArgument::REQUIRED, 'User identifier')
39+
->addOption('firewall', null, InputOption::VALUE_REQUIRED, 'Firewall')
40+
->addOption('algorithm', null, InputOption::VALUE_REQUIRED, 'Algorithm name to use to sign')
41+
->addOption('issuer', null, InputOption::VALUE_REQUIRED, 'Set the Issuer claim (iss)')
42+
->addOption('ttl', null, InputOption::VALUE_REQUIRED, 'Set the Expiration Time claim (exp) (time to live in seconds)')
43+
->addOption('not-before', null, InputOption::VALUE_REQUIRED, 'Set the Not Before claim (nbf)')
44+
;
45+
}
46+
47+
48+
/**
49+
* @params array<string, list<string>> $algorithms
50+
* @params array<string, list<string>> $issuers
51+
*/
52+
public function addGenerator(string $firewall, OidcTokenGenerator $oidcTokenGenerator, array $algorithms, array $issuers): void
53+
{
54+
$this->generators[$firewall] = $oidcTokenGenerator;
55+
foreach ($algorithms as $algorithm) {
56+
$this->algorithms[$algorithm] ??= [];
57+
$this->algorithms[$algorithm][] = $firewall;
58+
}
59+
foreach ($issuers as $issuer) {
60+
$this->issuers[$issuer] ??= [];
61+
$this->issuers[$issuer][] = $firewall;
62+
}
63+
}
64+
65+
protected function execute(InputInterface $input, OutputInterface $output): int
66+
{
67+
$generator = $this->getGenerator($input->getOption('firewall'));
68+
$token = $generator->generate(
69+
$input->getArgument('user-identifier'),
70+
$input->getOption('algorithm'),
71+
$input->getOption('issuer'),
72+
$input->getOption('ttl'),
73+
($nbf = $input->getOption('not-before')) ? new \DateTimeImmutable($nbf) : null,
74+
);
75+
76+
$output->writeln($token);
77+
78+
return self::SUCCESS;
79+
}
80+
81+
private function getGenerator(?string $firewall): OidcTokenGenerator
82+
{
83+
if (0 === count($this->generators)) {
84+
throw new \InvalidArgumentException('No OIDC token generator configured.');
85+
}
86+
87+
if ($firewall) {
88+
return $this->generators[$firewall] ?? throw new \InvalidArgumentException(sprintf('Invalid firewall. Available firewalls: "%s".', implode('", "', array_keys($this->generators))));
89+
}
90+
91+
if (1 === count($this->generators)) {
92+
return end($this->generators);
93+
}
94+
95+
throw new \InvalidArgumentException(sprintf('Please choose an firewall. Available firewalls: "%s".', implode('", "', array_keys($this->generators))));
96+
}
97+
98+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
99+
{
100+
if ($input->mustSuggestOptionValuesFor('firewall')) {
101+
$suggestions->suggestValues(array_keys($this->generators));
102+
}
103+
104+
if ($input->mustSuggestOptionValuesFor('algorithm')) {
105+
foreach ($this->algorithms as $algorithm => $firewalls) {
106+
$suggestions->suggestValue(new Suggestion($algorithm, sprintf('Available firewalls: "%s".', implode('", "', $firewalls))));
107+
}
108+
}
109+
110+
if ($input->mustSuggestOptionValuesFor('issuer')) {
111+
foreach ($this->issuers as $issuer => $firewalls) {
112+
$suggestions->suggestValue(new Suggestion($issuer, sprintf('Available firewalls: "%s".', implode('", "', $firewalls))));
113+
}
114+
}
115+
}
116+
}

Firewall/SwitchUserListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function __construct(
6565
public function supports(Request $request): ?bool
6666
{
6767
// usernames can be falsy
68-
$username = $request->get($this->usernameParameter);
68+
$username = $request->query->get($this->usernameParameter) ?? (!\in_array($request->getMethod(), ['GET', 'HEAD'], true) ? $request->request->get($this->usernameParameter) : null);
6969

7070
if (null === $username || '' === $username) {
7171
$username = $request->headers->get($this->usernameParameter);

HttpUtils.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ public function createRequest(Request $request, string $path): Request
8888
$newRequest->attributes->set(SecurityRequestAttributes::LAST_USERNAME, $request->attributes->get(SecurityRequestAttributes::LAST_USERNAME));
8989
}
9090

91-
if ($request->get('_format')) {
92-
$newRequest->attributes->set('_format', $request->get('_format'));
91+
if ($request->attributes->has('_format')) {
92+
$newRequest->attributes->set('_format', $request->attributes->get('_format'));
9393
}
9494
if ($request->getDefaultLocale() !== $request->getLocale()) {
9595
$newRequest->setLocale($request->getLocale());

LoginLink/LoginLinkHandler.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Security\Core\User\UserProviderInterface;
2323
use Symfony\Component\Security\Http\LoginLink\Exception\ExpiredLoginLinkException;
2424
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkException;
25+
use Symfony\Component\Security\Http\ParameterBagUtils;
2526

2627
/**
2728
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -79,16 +80,16 @@ public function createLoginLink(UserInterface $user, ?Request $request = null, ?
7980

8081
public function consumeLoginLink(Request $request): UserInterface
8182
{
82-
$userIdentifier = $request->get('user');
83+
$userIdentifier = ParameterBagUtils::getRequestParameterValue($request, 'user');
8384

84-
if (!$hash = $request->get('hash')) {
85+
if (!$hash = ParameterBagUtils::getRequestParameterValue($request, 'hash')) {
8586
throw new InvalidLoginLinkException('Missing "hash" parameter.');
8687
}
8788
if (!\is_string($hash)) {
8889
throw new InvalidLoginLinkException('Invalid "hash" parameter.');
8990
}
9091

91-
if (!$expires = $request->get('expires')) {
92+
if (!$expires = ParameterBagUtils::getRequestParameterValue($request, 'expires')) {
9293
throw new InvalidLoginLinkException('Missing "expires" parameter.');
9394
}
9495
if (!preg_match('/^\d+$/', $expires)) {

ParameterBagUtils.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ public static function getParameterBagValue(ParameterBag $parameters, string $pa
6262
*/
6363
public static function getRequestParameterValue(Request $request, string $path, array $parameters = []): mixed
6464
{
65+
$get = static fn ($path) => $request->attributes->get($path) ?? $request->query->all()[$path] ?? (!\in_array($request->getMethod(), ['GET', 'HEAD'], true) ? $request->request->all()[$path] ?? null : null);
66+
6567
if (false === $pos = strpos($path, '[')) {
66-
return $parameters[$path] ?? $request->get($path);
68+
return $parameters[$path] ?? $get($path);
6769
}
6870

6971
$root = substr($path, 0, $pos);
7072

71-
if (null === $value = $parameters[$root] ?? $request->get($root)) {
73+
if (null === $value = $parameters[$root] ?? $get($root)) {
7274
return null;
7375
}
7476

@@ -77,7 +79,7 @@ public static function getRequestParameterValue(Request $request, string $path,
7779
try {
7880
$value = self::$propertyAccessor->getValue($value, substr($path, $pos));
7981

80-
if (null === $value && isset($parameters[$root]) && null !== $value = $request->get($root)) {
82+
if (null === $value && isset($parameters[$root]) && null !== $value = $get($root)) {
8183
$value = self::$propertyAccessor->getValue($value, substr($path, $pos));
8284
}
8385

0 commit comments

Comments
 (0)