Skip to content
This repository was archived by the owner on Aug 7, 2023. It is now read-only.

Add support symfony 6 #4

Merged
merged 11 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

## 1.0.2

* Fix: add child node `rules` array config to avoid empty array when using arrayPrototype.

## 1.0.1

* Fix: auto add authenticator when not config.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ security:
main:
stateless: true
istio_jwt_authenticator:
- issuer: issuer_1 # Required
user_identifier_claim: sub #Default is `sub` claim
origin_token_headers: [authorization] #Required at least once of `origin_token_headers`, `origin_token_query_params` or `base64_headers`. Use this option when your Istio JWTRule CRD using `forwardOriginalToken`.
origin_token_query_params: [token] #Use this option when your Istio JWTRule CRD using `forwardOriginalToken` and your JWT token in query param.
base64_headers: [x-istio-jwt-payload] # Use this option when your Istio JWTRule CRD using `outputPayloadToHeader`.
rules:
- issuer: issuer_1 # Required
user_identifier_claim: sub #Default is `sub` claim
origin_token_headers: [authorization] #Required at least once of `origin_token_headers`, `origin_token_query_params` or `base64_headers`. Use this option when your Istio JWTRule CRD using `forwardOriginalToken`.
origin_token_query_params: [token] #Use this option when your Istio JWTRule CRD using `forwardOriginalToken` and your JWT token in query param.
base64_headers: [x-istio-jwt-payload] # Use this option when your Istio JWTRule CRD using `outputPayloadToHeader`.
prefix: "Bearer " #Token prefix of origin token passthrough by default blank ("") if not set.
```

In case your application have multi issuers:
Expand All @@ -69,11 +71,13 @@ In case your application have multi issuers:
main:
stateless: true
istio_jwt_authenticator:
- issuer: issuer_1
origin_token_headers: [authorization]
- issuer: issuer_2
user_identifier_claim: aud
base64_headers: [x-istio-jwt-payload]
rules:
- issuer: issuer_1
origin_token_headers: [authorization]
prefix: "Bearer "
- issuer: issuer_2
user_identifier_claim: aud
base64_headers: [x-istio-jwt-payload]
#....
```

Expand All @@ -82,15 +86,15 @@ In case your application have multi issuers:
```shell
#!/bin/bash

# Generate mock JWT token forwarded by Istio sidecar
#Generate mock JWT token forwarded by Istio sidecar

payload='{"issuer":"issuer_1", "sub": "test"}';
base64_payload=$(echo -n $payload | base64 -);
origin_token=$(echo "header.$base64_payload.signature");

#You can test authenticate origin token with curl:

curl -H "Authorization: $origin_token" http://localhost/
curl -H "Authorization: Bearer $origin_token" http://localhost/

#Or authenticate base64 payload header:

Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"minimum-stability": "stable",
"require": {
"php": ">=8.0",
"php-istio/jwt-payload-extractor": "^1.0",
"php-istio/jwt-payload-extractor": "^v1.1.1",
"symfony/psr7-pack": "^1.0",
"symfony/security-bundle": "^5.3"
"symfony/security-bundle": "^6.0"
},
"autoload": {
"psr-4": {
Expand All @@ -30,9 +30,9 @@
}
},
"require-dev": {
"symfony/browser-kit": "^5.3",
"symfony/console": "^5.3",
"symfony/framework-bundle": "^5.3",
"symfony/phpunit-bridge": "^5.3"
"symfony/browser-kit": "^6.0",
"symfony/console": "^6.0",
"symfony/framework-bundle": "^6.0",
"symfony/phpunit-bridge": "^6.0"
}
}
10 changes: 5 additions & 5 deletions src/Authenticator/Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

Expand Down Expand Up @@ -56,7 +56,7 @@ public function supports(Request $request): ?bool
return false;
}

public function authenticate(Request $request): PassportInterface
public function authenticate(Request $request): Passport
{
[$userIdentifierClaim, $payload] = $request->attributes->get('_user_identifier_claim_and_payload');
$request->attributes->remove('_user_identifier_claim_and_payload');
Expand Down Expand Up @@ -88,17 +88,17 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
throw $exception;
}

public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
/** @var SelfValidatingPassport $passport */
$payload = $passport->getAttribute('_payload');
$token = parent::createAuthenticatedToken($passport, $firewallName);
$token = parent::createToken($passport, $firewallName);
$token->setAttribute('jwt_payload', $payload);

return $token;
}

public function start(Request $request, AuthenticationException $authException = null)
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new Response('Istio JWT in request\'s missing or invalid.', Response::HTTP_UNAUTHORIZED);
}
Expand Down
134 changes: 75 additions & 59 deletions src/DependencyInjection/Security/AuthenticatorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

use Istio\Symfony\JWTAuthentication\Authenticator\UserIdentifierClaimMapping;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
Expand All @@ -21,74 +20,78 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

final class AuthenticatorFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
final class AuthenticatorFactory implements AuthenticatorFactoryInterface
{
public const PRIORITY = -40;

public function createAuthenticator(
ContainerBuilder $container,
string $firewallName,
array $config,
string $userProviderId
) {
): string|array {
$authenticator = sprintf('security.authenticator.istio_jwt_authenticator.%s', $firewallName);
$definition = new ChildDefinition('istio.jwt_authentication.authenticator');
$definition->replaceArgument(0, $this->createUserIdentifierClaimMappings($container, $authenticator, $config));
$definition->replaceArgument(
0,
$this->createUserIdentifierClaimMappings($container, $authenticator, $config['rules'])
);
$definition->replaceArgument(1, new Reference($userProviderId));
$container->setDefinition($authenticator, $definition);

return $authenticator;
}

public function create(
ContainerBuilder $container,
string $id,
array $config,
string $userProviderId,
?string $defaultEntryPointId
) {
throw new \LogicException('Istio JWT Authentication is not supported when "security.enable_authenticator_manager" is not set to true.');
}

public function getPosition()
{
return 'pre_auth';
}

public function getKey()
public function getKey(): string
{
return 'istio_jwt_authenticator';
}

public function addConfiguration(NodeDefinition $builder)
{
$builder
->cannotBeEmpty()
->fixXmlConfig('origin_token_header')
->fixXmlConfig('origin_token_query_param')
->fixXmlConfig('base64_header')
->arrayPrototype()
->addDefaultsIfNotSet()
->children()
->scalarNode('issuer')
->cannotBeEmpty()
->isRequired()
->end()
->scalarNode('user_identifier_claim')
->cannotBeEmpty()
->defaultValue('sub')
->end()
->arrayNode('origin_token_headers')
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('origin_token_query_params')
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('base64_headers')
->scalarPrototype()
->cannotBeEmpty()
->fixXmlConfig('rule')
->children()
->arrayNode('rules')
->isRequired()
->cannotBeEmpty()
->arrayPrototype()
->fixXmlConfig('origin_token_header')
->fixXmlConfig('origin_token_query_param')
->fixXmlConfig('base64_header')
->addDefaultsIfNotSet()
->children()
->scalarNode('issuer')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('user_identifier_claim')
->cannotBeEmpty()
->defaultValue('sub')
->end()
->arrayNode('origin_token_headers')
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('origin_token_query_params')
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('base64_headers')
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->scalarNode('prefix')
->defaultNull()
->end()
->end()
->end()
->end()
Expand All @@ -98,53 +101,55 @@ public function addConfiguration(NodeDefinition $builder)

private function createUserIdentifierClaimMappings(
ContainerBuilder $container,
string $authenticatorName,
array $config,
string $authenticatorId,
array $rules,
): IteratorArgument {
$extractorIdPrefix = sprintf('%s.payload_extractor', $authenticatorName);
$extractorIdPrefix = sprintf('%s.payload_extractor', $authenticatorId);
$mappings = [];

foreach ($config as $key => $item) {
foreach ($rules as $key => $rule) {
$extractor = null;

if (!empty($item['origin_token_headers'])) {
if (!empty($rule['origin_token_headers'])) {
$extractor = $this->createPayloadExtractor(
$container,
sprintf('%s.origin_token_headers.%s', $extractorIdPrefix, $key),
'istio.jwt_authentication.payload_extractor.origin_token.header',
$item['issuer'],
$item['origin_token_headers']
$rule['issuer'],
$rule['origin_token_headers'],
$rule['prefix']
);
}

if (!empty($item['origin_token_query_params'])) {
if (!empty($rule['origin_token_query_params'])) {
$extractor = $this->createPayloadExtractor(
$container,
sprintf('%s.origin_token_query_params.%s', $extractorIdPrefix, $key),
'istio.jwt_authentication.payload_extractor.origin_token.query_param',
$item['issuer'],
$item['origin_token_query_params']
$rule['issuer'],
$rule['origin_token_query_params'],
$rule['prefix']
);
}

if (!empty($item['base64_headers'])) {
if (!empty($rule['base64_headers'])) {
$extractor = $this->createPayloadExtractor(
$container,
sprintf('%s.base64_headers.%s', $extractorIdPrefix, $key),
'istio.jwt_authentication.payload_extractor.base64_header',
$item['issuer'],
$item['base64_headers']
$rule['issuer'],
$rule['base64_headers']
);
}

if (null === $extractor) {
throw new InvalidConfigurationException(sprintf('`%s`: at least once `origin_token_headers`, `origin_token_query_params`, `base64_headers` should be config when using', $this->getKey()));
}

$mappingId = sprintf('%s.user_identifier_claim_mapping.%s', $authenticatorName, $key);
$mappingId = sprintf('%s.user_identifier_claim_mapping.%s', $authenticatorId, $key);
$mappings[] = new Reference($mappingId);
$mappingDefinition = new Definition(UserIdentifierClaimMapping::class);
$mappingDefinition->setArgument(0, $item['user_identifier_claim']);
$mappingDefinition->setArgument(0, $rule['user_identifier_claim']);
$mappingDefinition->setArgument(1, $extractor);
$container->setDefinition($mappingId, $mappingDefinition);
}
Expand All @@ -157,7 +162,8 @@ private function createPayloadExtractor(
string $id,
string $fromAbstractId,
string $issuer,
array $items
array $items,
?string $prefix = null
): Reference {
$definition = new ChildDefinition('istio.jwt_authentication.payload_extractor.composite');
$container->setDefinition($id, $definition);
Expand All @@ -170,11 +176,21 @@ private function createPayloadExtractor(
$subDefinition = new ChildDefinition($fromAbstractId);
$subDefinition->replaceArgument(0, $issuer);
$subDefinition->replaceArgument(1, $item);

if (null !== $prefix) {
$subDefinition->replaceArgument(2, $prefix);
}

$container->setDefinition($subId, $subDefinition);
}

$definition->setArguments($subExtractors);

return new Reference($id);
}

public function getPriority(): int
{
return self::PRIORITY;
}
}
2 changes: 1 addition & 1 deletion src/JWTAuthenticationBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ public function build(ContainerBuilder $container)
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
$extension->addUserProviderFactory(new StatelessUserProviderFactory());
$extension->addSecurityListenerFactory(new AuthenticatorFactory());
$extension->addAuthenticatorFactory(new AuthenticatorFactory());
}
}
2 changes: 2 additions & 0 deletions src/Resources/config/payload_extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
->factory([ExtractorFactory::class, 'fromOriginTokenHeader'])
->arg(0, abstract_arg('issuer'))
->arg(1, abstract_arg('header name'))
->arg(2, '') // token prefix

->set('istio.jwt_authentication.payload_extractor.origin_token.query_param', OriginTokenExtractor::class)
->abstract()
->factory([ExtractorFactory::class, 'fromOriginTokenQueryParam'])
->arg(0, abstract_arg('issuer'))
->arg(1, abstract_arg('param name'))
->arg(2, '') // token prefix

->set('istio.jwt_authentication.payload_extractor.base64_header', Base64HeaderExtractor::class)
->abstract()
Expand Down
3 changes: 2 additions & 1 deletion src/User/JWTPayloadAwareUserProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@

namespace Istio\Symfony\JWTAuthentication\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

interface JWTPayloadAwareUserProviderInterface extends UserProviderInterface
{
public function loadUserByIdentifier(string $identifier, array $payload = null);
public function loadUserByIdentifier(string $identifier, array $payload = null): UserInterface;
}
Loading