diff --git a/Controller/IntrospectionController.php b/Controller/IntrospectionController.php index 2c26f681..3e6a1635 100644 --- a/Controller/IntrospectionController.php +++ b/Controller/IntrospectionController.php @@ -13,13 +13,19 @@ namespace FOS\OAuthServerBundle\Controller; +use FOS\OAuthServerBundle\Form\Model\Introspect; +use FOS\OAuthServerBundle\Form\Type\IntrospectionFormType; use FOS\OAuthServerBundle\Model\AccessTokenInterface; use FOS\OAuthServerBundle\Model\RefreshTokenInterface; use FOS\OAuthServerBundle\Model\TokenInterface; use FOS\OAuthServerBundle\Model\TokenManagerInterface; +use FOS\OAuthServerBundle\Security\Authentication\Token\OAuthToken; +use Symfony\Component\Form\FormFactory; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; class IntrospectionController { @@ -38,21 +44,47 @@ class IntrospectionController */ private $refreshTokenManager; + /** + * @var FormFactory + */ + private $formFactory; + + /** + * @var array + */ + private $allowedIntrospectionClients; + public function __construct( TokenStorageInterface $tokenStorage, TokenManagerInterface $accessTokenManager, - TokenManagerInterface $refreshTokenManager + TokenManagerInterface $refreshTokenManager, + FormFactory $formFactory, + array $allowedIntrospectionClients ) { $this->tokenStorage = $tokenStorage; $this->accessTokenManager = $accessTokenManager; $this->refreshTokenManager = $refreshTokenManager; + $this->formFactory = $formFactory; + $this->allowedIntrospectionClients = $allowedIntrospectionClients; } public function introspectAction(Request $request): JsonResponse { - // $clientToken = $this->tokenStorage->getToken(); → use in security + $clientToken = $this->tokenStorage->getToken(); // → use in security + + if (!$clientToken instanceof OAuthToken) { + throw new AccessDeniedException('The introspect endpoint must be behind a secure firewall.'); + } - // TODO security for this endpoint. Probably in the README documentation + $callerToken = $this->accessTokenManager->findTokenByToken($clientToken->getToken()); + + if (!$callerToken) { + throw new AccessDeniedException('The access token must have a valid token.'); + } + + if (!in_array($callerToken->getClientId(), $this->allowedIntrospectionClients)) { + throw new AccessDeniedException('This access token is not autorised to do introspection.'); + } $token = $this->getToken($request); @@ -79,8 +111,9 @@ public function introspectAction(Request $request): JsonResponse */ private function getToken(Request $request) { - $tokenTypeHint = $request->request->get('token_type_hint'); // TODO move in a form type ? can be `access_token`, `refresh_token` See https://tools.ietf.org/html/rfc7009#section-4.1.2 - $tokenString = $request->request->get('token'); // TODO move in a form type ? + $formData = $this->processIntrospectionForm($request); + $tokenString = $formData->token; + $tokenTypeHint = $formData->token_type_hint; $tokenManagerList = []; if (!$tokenTypeHint || 'access_token' === $tokenTypeHint) { @@ -125,4 +158,21 @@ private function getUsername(TokenInterface $token) return $user->getUserName(); } + + private function processIntrospectionForm(Request $request): Introspect + { + $formData = new Introspect(); + $form = $this->formFactory->create(IntrospectionFormType::class, $formData); + $form->handleRequest($request); + + if (!$form->isSubmitted() || !$form->isValid()) { + $errors = $form->getErrors(); + if (count($errors) > 0) { + throw new BadRequestHttpException((string) $errors); + } else { + throw new BadRequestHttpException('Introspection endpoint needs to have at least a "token" form parameter'); + } + } + return $form->getData(); + } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 571fcafd..d2500fea 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -82,6 +82,7 @@ public function getConfigTreeBuilder() $this->addAuthorizeSection($rootNode); $this->addServiceSection($rootNode); $this->addTemplateSection($rootNode); + $this->addIntrospectionSection($rootNode); return $treeBuilder; } @@ -151,4 +152,24 @@ private function addTemplateSection(ArrayNodeDefinition $node) ->end() ; } + + private function addIntrospectionSection(ArrayNodeDefinition $node) + { + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('introspection') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('allowed_clients') + ->useAttributeAsKey('key') + ->treatNullLike([]) + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/DependencyInjection/FOSOAuthServerExtension.php b/DependencyInjection/FOSOAuthServerExtension.php index 1a1fc008..8b84433a 100644 --- a/DependencyInjection/FOSOAuthServerExtension.php +++ b/DependencyInjection/FOSOAuthServerExtension.php @@ -102,7 +102,7 @@ public function load(array $configs, ContainerBuilder $container) $authorizeFormDefinition->setFactory([new Reference('form.factory'), 'createNamed']); } - $this->loadIntrospection($loader); + $this->loadIntrospection($config, $container, $loader); } /** @@ -144,9 +144,12 @@ protected function remapParametersNamespaces(array $config, ContainerBuilder $co } } - protected function loadIntrospection(XmlFileLoader $loader) + protected function loadIntrospection(array $config, ContainerBuilder $container, XmlFileLoader $loader) { $loader->load('introspection.xml'); + + $allowedClients = $config['introspection']['allowed_clients']; + $container->setParameter('fos_oauth_server.introspection.allowed_clients', $allowedClients); } protected function loadAuthorize(array $config, ContainerBuilder $container, XmlFileLoader $loader) diff --git a/Form/Model/Introspect.php b/Form/Model/Introspect.php new file mode 100644 index 00000000..b419ed6a --- /dev/null +++ b/Form/Model/Introspect.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\OAuthServerBundle\Form\Model; + +use Symfony\Component\Validator\Constraints as Assert; + +class Introspect +{ + /** + * @var string + * @Assert\NotBlank() + */ + public $token; + + /** + * @var string + */ + public $token_type_hint; +} diff --git a/Form/Type/IntrospectionFormType.php b/Form/Type/IntrospectionFormType.php new file mode 100644 index 00000000..b169cf5a --- /dev/null +++ b/Form/Type/IntrospectionFormType.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\OAuthServerBundle\Form\Type; + +use FOS\OAuthServerBundle\Form\Model\Introspect; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class IntrospectionFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('token', HiddenType::class); + $builder->add('token_type_hint', ChoiceType::class, [ // can be `access_token`, `refresh_token` See https://tools.ietf.org/html/rfc7009#section-4.1.2 + 'choices' => [ + 'access_token' => 'access_token', + 'refresh_token' => 'refresh_token', + ] + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Introspect::class, + 'csrf_protection' => false, + ]); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return ''; + } +} diff --git a/Resources/config/introspection.xml b/Resources/config/introspection.xml index 7bc972b1..da9d1cd6 100644 --- a/Resources/config/introspection.xml +++ b/Resources/config/introspection.xml @@ -9,6 +9,8 @@ + + %fos_oauth_server.introspection.allowed_clients%